/* * xC.c: a terminal-based IRC client * * Copyright (c) 2015 - 2021, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ // A table of all attributes we use for output #define ATTR_TABLE(XX) \ XX( PROMPT, prompt, Terminal attrs for the prompt ) \ XX( RESET, reset, String to reset terminal attributes ) \ XX( DATE_CHANGE, date_change, Terminal attrs for date change ) \ 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 "xC" #include "common.c" #include "xD-replies.c" #include #include #include #include #include #include #include #include #include #include // Literally cancer #undef lines #undef columns #include #ifdef HAVE_LUA #include #include #include #endif // HAVE_LUA // --- Terminal information ---------------------------------------------------- static struct { bool initialized; ///< Terminal is available bool stdout_is_tty; ///< `stdout' is a terminal bool stderr_is_tty; ///< `stderr' is a terminal struct termios termios; ///< Terminal attributes 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 || tcgetattr (tty_fd, &g_terminal.termios)) { del_curterm (cur_term); return false; } // Make sure newlines are output correctly g_terminal.termios.c_oflag |= ONLCR; (void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios); 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); } // --- 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. We use lots // of hacks to get the results we want and need. // // The abstraction is a necessary evil. It's still not 100%, though. /// Some arbitrary limit for the history #define HISTORY_LIMIT 10000 /// Characters that separate words #define WORD_BREAKING_CHARS " \f\n\r\t\v" struct input { struct input_vtable *vtable; ///< Virtual methods void (*add_functions) (void *); ///< Define functions for binding void *user_data; ///< User data for callbacks }; typedef void *input_buffer_t; ///< Pointer alias for input buffers /// Named function that can be bound to a sequence of characters typedef bool (*input_fn) (int count, int key, void *user_data); // A little bit better than tons of forwarder functions in our case #define CALL(self, name) ((self)->vtable->name ((self))) #define CALL_(self, name, ...) ((self)->vtable->name ((self), __VA_ARGS__)) struct input_vtable { /// Start the interface under the given program name void (*start) (void *input, const char *program_name); /// Stop the interface void (*stop) (void *input); /// Prepare or unprepare terminal for our needs void (*prepare) (void *input, bool enabled); /// Destroy the object void (*destroy) (void *input); /// Hide the prompt if shown void (*hide) (void *input); /// Show the prompt if hidden void (*show) (void *input); /// Retrieve current prompt string const char *(*get_prompt) (void *input); /// Change the prompt string; takes ownership void (*set_prompt) (void *input, char *prompt); /// Ring the terminal bell void (*ding) (void *input); /// Create a new input buffer input_buffer_t (*buffer_new) (void *input); /// Destroy an input buffer void (*buffer_destroy) (void *input, input_buffer_t buffer); /// Switch to a different input buffer void (*buffer_switch) (void *input, input_buffer_t buffer); /// Register a function that can be bound to character sequences void (*register_fn) (void *input, const char *name, const char *help, input_fn fn, void *user_data); /// Bind an arbitrary sequence of characters to the given named function void (*bind) (void *input, const char *seq, const char *fn); /// Bind Ctrl+key to the given named function void (*bind_control) (void *input, char key, const char *fn); /// Bind Alt+key to the given named function void (*bind_meta) (void *input, char key, const char *fn); /// Get the current line input and position within char *(*get_line) (void *input, int *position); /// Clear the current line input void (*clear_line) (void *input); /// Insert text at current position bool (*insert) (void *input, const char *text); /// Handle terminal resize void (*on_tty_resized) (void *input); /// Handle terminal input void (*on_tty_readable) (void *input); }; #define INPUT_VTABLE(XX) \ XX (start) XX (stop) XX (prepare) XX (destroy) \ XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \ XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \ XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta) \ XX (get_line) XX (clear_line) XX (insert) \ XX (on_tty_resized) XX (on_tty_readable) // --- GNU Readline ------------------------------------------------------------ #ifdef HAVE_READLINE #include #include #define INPUT_START_IGNORE RL_PROMPT_START_IGNORE #define INPUT_END_IGNORE RL_PROMPT_END_IGNORE struct input_rl_fn { ffi_closure closure; ///< Closure LIST_HEADER (struct input_rl_fn) input_fn callback; ///< Real callback void *user_data; ///< Real callback user data }; struct input_rl_buffer { HISTORY_STATE *history; ///< Saved history state char *saved_line; ///< Saved line content int saved_point; ///< Saved cursor position int saved_mark; ///< Saved mark }; struct input_rl { struct input super; ///< Parent class bool active; ///< Interface has been started char *prompt; ///< The prompt we use int prompt_shown; ///< Whether the prompt is shown now char *saved_line; ///< Saved line content int saved_point; ///< Saved cursor position int saved_mark; ///< Saved mark struct input_rl_fn *fns; ///< Named functions struct input_rl_buffer *current; ///< Current input buffer }; static void input_rl_ding (void *input) { (void) input; rl_ding (); } static const char * input_rl_get_prompt (void *input) { struct input_rl *self = input; return self->prompt; } static void input_rl_set_prompt (void *input, char *prompt) { struct input_rl *self = input; cstr_set (&self->prompt, prompt); if (!self->active || self->prompt_shown <= 0) return; // First reset the prompt to work around a bug in readline rl_set_prompt (""); rl_redisplay (); rl_set_prompt (self->prompt); rl_redisplay (); } static void input_rl_clear_line (void *input) { (void) input; rl_replace_line ("", false); rl_redisplay (); } static void input_rl__erase (struct input_rl *self) { rl_set_prompt (""); input_rl_clear_line (self); } static bool input_rl_insert (void *input, const char *s) { struct input_rl *self = input; rl_insert_text (s); if (self->prompt_shown > 0) rl_redisplay (); // GNU Readline, contrary to Editline, doesn't care about validity return true; } static char * input_rl_get_line (void *input, int *position) { (void) input; if (position) *position = rl_point; return rl_copy_text (0, rl_end); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_rl_bind (void *input, const char *seq, const char *function_name) { (void) input; rl_bind_keyseq (seq, rl_named_function (function_name)); } static void input_rl_bind_meta (void *input, char key, const char *function_name) { // This one seems to actually work char keyseq[] = { '\\', 'e', key, 0 }; input_rl_bind (input, 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_rl_bind_control (void *input, char key, const char *function_name) { char keyseq[] = { '\\', 'C', '-', key, 0 }; input_rl_bind (input, keyseq, function_name); } static void input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data) { (void) cif; struct input_rl_fn *data = user_data; if (!data->callback (*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data)) rl_ding (); *(int *) ret = 0; } static void input_rl_register_fn (void *input, const char *name, const char *help, input_fn callback, void *user_data) { struct input_rl *self = input; (void) help; void *bound_fn = NULL; struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn); hard_assert (data); static ffi_cif cif; static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint }; hard_assert (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK); data->prev = data->next = NULL; data->callback = callback; data->user_data = user_data; hard_assert (ffi_prep_closure_loc (&data->closure, &cif, input_rl__forward, data, bound_fn) == FFI_OK); rl_add_defun (name, (rl_command_func_t *) bound_fn, -1); LIST_PREPEND (self->fns, data); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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_rl_start (void *input, const char *program_name) { struct input_rl *self = input; 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_change_environment = false; rl_basic_word_break_characters = WORD_BREAKING_CHARS; rl_completer_word_break_characters = NULL; rl_attempted_completion_function = app_readline_completion; // We shouldn't produce any duplicates that the library would help us // autofilter, and we don't generally want alphabetic ordering at all rl_sort_completion_matches = false; hard_assert (self->prompt != NULL); // The inputrc is read before any callbacks are called, so we need to // register all functions that our user may want to map up front self->super.add_functions (self->super.user_data); rl_callback_handler_install (self->prompt, on_readline_input); self->prompt_shown = 1; self->active = true; } static void input_rl_stop (void *input) { struct input_rl *self = input; if (self->prompt_shown > 0) input_rl__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; } static void input_rl_prepare (void *input, bool enabled) { (void) input; if (enabled) rl_prep_terminal (true); else rl_deprep_terminal (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 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_rl__save_buffer (struct input_rl *self, struct input_rl_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; rl_replace_line ("", true); if (self->prompt_shown > 0) rl_redisplay (); } static void input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer) { 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); } if (buffer->saved_line) { rl_replace_line (buffer->saved_line, true); rl_point = buffer->saved_point; rl_mark = buffer->saved_mark; cstr_set (&buffer->saved_line, NULL); if (self->prompt_shown > 0) rl_redisplay (); } } static void input_rl_buffer_switch (void *input, input_buffer_t input_buffer) { struct input_rl *self = input; struct input_rl_buffer *buffer = input_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_rl__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_rl__restore_buffer (self, buffer); self->current = buffer; } static void input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self) { free (self->history); free (self->saved_line); free (self); } static void input_rl_buffer_destroy (void *input, input_buffer_t input_buffer) { (void) input; struct input_rl_buffer *buffer = input_buffer; // 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_rl_buffer_switch() 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_rl__buffer_destroy_wo_history (buffer); } static input_buffer_t input_rl_buffer_new (void *input) { (void) input; struct input_rl_buffer *self = xcalloc (1, sizeof *self); return self; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Since {save,restore}_buffer() store history, we can't use them here like we // do with libedit, because then buffer_destroy() can free memory that's still // being used by readline. This situation is bound to happen on quit. static void input_rl__save (struct input_rl *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_rl__restore (struct input_rl *self) { hard_assert (self->saved_line); rl_set_prompt (self->prompt); rl_replace_line (self->saved_line, false); rl_point = self->saved_point; rl_mark = self->saved_mark; cstr_set (&self->saved_line, NULL); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_rl_hide (void *input) { struct input_rl *self = input; if (!self->active || self->prompt_shown-- < 1) return; input_rl__save (self); input_rl__erase (self); } static void input_rl_show (void *input) { struct input_rl *self = input; if (!self->active || ++self->prompt_shown < 1) return; input_rl__restore (self); rl_redisplay (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_rl_on_tty_resized (void *input) { (void) input; // 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_rl_on_tty_readable (void *input) { (void) input; rl_callback_read_char (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_rl_destroy (void *input) { struct input_rl *self = input; free (self->saved_line); LIST_FOR_EACH (struct input_rl_fn, iter, self->fns) ffi_closure_free (iter); free (self->prompt); free (self); } #define XX(a) .a = input_rl_ ## a, static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) }; #undef XX static struct input * input_rl_new (void) { struct input_rl *self = xcalloc (1, sizeof *self); self->super.vtable = &input_rl_vtable; return &self->super; } #define input_new input_rl_new #endif // HAVE_READLINE // --- BSD Editline ------------------------------------------------------------ #ifdef HAVE_EDITLINE #include #define INPUT_START_IGNORE '\x01' #define INPUT_END_IGNORE '\x01' struct input_el_fn { ffi_closure closure; ///< Closure LIST_HEADER (struct input_el_fn) input_fn callback; ///< Real callback void *user_data; ///< Real callback user data wchar_t *name; ///< Function name wchar_t *help; ///< Function help }; struct input_el_buffer { HistoryW *history; ///< The history object wchar_t *saved_line; ///< Saved line content int saved_len; ///< Length of the saved line int saved_point; ///< Saved cursor position }; struct input_el { struct input super; ///< Parent class EditLine *editline; ///< The EditLine object bool active; ///< Are we a thing? char *prompt; ///< The prompt we use int prompt_shown; ///< Whether the prompt is shown now struct input_el_fn *fns; ///< Named functions struct input_el_buffer *current; ///< Current input buffer }; static void app_editline_init (struct input_el *self); static void input_el__redisplay (void *input) { // See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT // so we bind redisplay somewhere else in app_editline_init() struct input_el *self = input; char x[] = { 'q' & 31, 0 }; el_push (self->editline, x); // We have to do this or it gets stuck and nothing is done int count = 0; (void) el_wgets (self->editline, &count); } static char * input_el__make_prompt (EditLine *editline) { struct input_el *self; el_get (editline, EL_CLIENTDATA, &self); if (!self->prompt) return ""; return self->prompt; } static char * input_el__make_empty_prompt (EditLine *editline) { (void) editline; return ""; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_ding (void *input) { // XXX: this isn't probably very portable; // we could use "bell" from terminfo but that creates a dependency (void) input; write (STDOUT_FILENO, "\a", 1); } static const char * input_el_get_prompt (void *input) { struct input_el *self = input; return self->prompt; } static void input_el_set_prompt (void *input, char *prompt) { struct input_el *self = input; cstr_set (&self->prompt, prompt); if (self->prompt_shown > 0) input_el__redisplay (self); } static void input_el_clear_line (void *input) { struct input_el *self = input; 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); input_el__redisplay (self); } static void input_el__erase (struct input_el *self) { el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt); input_el_clear_line (self); } static bool input_el_insert (void *input, const char *s) { struct input_el *self = input; bool success = !*s || !el_insertstr (self->editline, s); if (self->prompt_shown > 0) input_el__redisplay (self); return success; } static char * input_el_get_line (void *input, int *position) { struct input_el *self = input; const LineInfo *info = el_line (self->editline); int point = info->cursor - info->buffer; if (position) *position = point; return xstrndup (info->buffer, info->lastchar - info->buffer); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_bind (void *input, const char *seq, const char *function_name) { struct input_el *self = input; el_set (self->editline, EL_BIND, seq, function_name, NULL); } static void input_el_bind_meta (void *input, char key, const char *function_name) { char keyseq[] = { 'M', '-', key, 0 }; input_el_bind (input, keyseq, function_name); } static void input_el_bind_control (void *input, char key, const char *function_name) { char keyseq[] = { '^', key, 0 }; input_el_bind (input, keyseq, function_name); } static void input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data) { (void) cif; struct input_el_fn *data = user_data; *(unsigned char *) ret = data->callback (1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR; } static wchar_t * ascii_to_wide (const char *ascii) { size_t len = strlen (ascii) + 1; wchar_t *wide = xcalloc (sizeof *wide, len); while (len--) hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80); return wide; } static void input_el_register_fn (void *input, const char *name, const char *help, input_fn callback, void *user_data) { void *bound_fn = NULL; struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn); hard_assert (data); static ffi_cif cif; static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint }; hard_assert (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK); data->user_data = user_data; data->callback = callback; data->name = ascii_to_wide (name); data->help = ascii_to_wide (help); hard_assert (ffi_prep_closure_loc (&data->closure, &cif, input_el__forward, data, bound_fn) == FFI_OK); struct input_el *self = input; el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn); LIST_PREPEND (self->fns, data); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_start (void *input, const char *program_name) { struct input_el *self = input; self->editline = el_init (program_name, stdin, stdout, stderr); el_set (self->editline, EL_CLIENTDATA, self); el_set (self->editline, EL_PROMPT_ESC, input_el__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_el_stop (void *input) { struct input_el *self = input; if (self->prompt_shown > 0) input_el__erase (self); el_end (self->editline); self->editline = NULL; self->active = false; self->prompt_shown = false; } static void input_el_prepare (void *input, bool enabled) { struct input_el *self = input; el_set (self->editline, EL_PREP_TERM, enabled); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el__save_buffer (struct input_el *self, struct input_el_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_el__save (struct input_el *self) { if (self->current) input_el__save_buffer (self, self->current); } static void input_el__restore_buffer (struct input_el *self, struct input_el_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_el__restore (struct input_el *self) { if (self->current) input_el__restore_buffer (self, self->current); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_buffer_switch (void *input, input_buffer_t input_buffer) { struct input_el *self = input; struct input_el_buffer *buffer = input_buffer; if (self->current) input_el__save_buffer (self, self->current); input_el__restore_buffer (self, buffer); el_wset (self->editline, EL_HIST, history, buffer->history); self->current = buffer; } static void input_el_buffer_destroy (void *input, input_buffer_t input_buffer) { (void) input; struct input_el_buffer *buffer = input_buffer; history_wend (buffer->history); free (buffer->saved_line); free (buffer); } static input_buffer_t input_el_buffer_new (void *input) { (void) input; struct input_el_buffer *self = xcalloc (1, sizeof *self); self->history = history_winit (); HistEventW ev; history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT); return self; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_hide (void *input) { struct input_el *self = input; if (!self->active || self->prompt_shown-- < 1) return; input_el__save (self); input_el__erase (self); } static void input_el_show (void *input) { struct input_el *self = input; if (!self->active || ++self->prompt_shown < 1) return; input_el__restore (self); el_set (self->editline, EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE); input_el__redisplay (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_on_tty_resized (void *input) { struct input_el *self = input; el_resize (self->editline); } static void input_el_on_tty_readable (void *input) { // We bind the return key to process it how we need to struct input_el *self = input; // 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; if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */) { el_deletestr (self->editline, 1); input_el__redisplay (self); input_el_ding (self); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_el_destroy (void *input) { struct input_el *self = input; LIST_FOR_EACH (struct input_el_fn, iter, self->fns) { free (iter->name); free (iter->help); ffi_closure_free (iter); } free (self->prompt); free (self); } #define XX(a) .a = input_el_ ## a, static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) }; #undef XX static struct input * input_el_new (void) { struct input_el *self = xcalloc (1, sizeof *self); self->super.vtable = &input_el_vtable; return &self->super; } #define input_new input_el_new #endif // HAVE_EDITLINE // --- Application data -------------------------------------------------------- // All text stored in our data structures is encoded in UTF-8. Or at least // should be--our only ways of retrieving strings are: via the command line // (converted from locale, no room for errors), via the configuration file // (restrictive ASCII grammar for bare words and an internal check for strings), // and via plugins (meticulously validated). // // The only exception is IRC identifiers. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We need a few reference countable objects with support for both strong // and weak references (mainly used for scripted plugins). // // Beware that if you don't own the object, you will most probably want // to keep the weak reference link so that you can get rid of it later. // Also note that you have to make sure the user_data don't leak resources. // // Having a callback is more versatile than just nulling out a pointer. /// Callback just before a reference counted object is destroyed typedef void (*destroy_cb_fn) (void *object, void *user_data); struct weak_ref_link { LIST_HEADER (struct weak_ref_link) destroy_cb_fn on_destroy; ///< Called when object is destroyed void *user_data; ///< User data }; static struct weak_ref_link * weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data) { struct weak_ref_link *link = xcalloc (1, sizeof *link); link->on_destroy = cb; link->user_data = user_data; LIST_PREPEND (*list, link); return link; } static void weak_unref (struct weak_ref_link **list, struct weak_ref_link **link) { if (*link) LIST_UNLINK (*list, *link); free (*link); *link = NULL; } #define REF_COUNTABLE_HEADER \ size_t ref_count; /**< Reference count */ \ struct weak_ref_link *weak_refs; /**< To remove any weak references */ #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; \ LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \ { \ iter->on_destroy (self, iter->user_data); \ free (iter); \ } \ name ## _destroy (self); \ } \ \ static struct weak_ref_link * \ name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \ { return weak_ref (&self->weak_refs, cb, user_data); } \ \ static void \ name ## _weak_unref (struct name *self, struct weak_ref_link **link) \ { weak_unref (&self->weak_refs, link); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Simple introspection framework to simplify exporting stuff to Lua, since // there is a lot of it. While not fully automated, at least we have control // over which fields are exported. enum ispect_type { ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING, ISPECT_STR, ///< "struct str" ISPECT_STR_MAP, ///< "struct str_map" ISPECT_REF ///< Weakly referenced object }; struct ispect_field { const char *name; ///< Name of the field size_t offset; ///< Offset in the structure enum ispect_type type; ///< Type of the field enum ispect_type subtype; ///< STR_MAP subtype struct ispect_field *fields; ///< REF target fields bool is_list; ///< REF target is a list }; #define ISPECT(object, field, type) \ { #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false }, #define ISPECT_REF(object, field, is_list, ref_type) \ { #field, offsetof (struct object, field), ISPECT_REF, 0, \ g_##ref_type##_ispect, is_list }, #define ISPECT_MAP(object, field, subtype) \ { #field, offsetof (struct object, field), ISPECT_STR_MAP, \ ISPECT_##subtype, NULL, false }, #define ISPECT_MAP_REF(object, field, is_list, ref_type) \ { #field, offsetof (struct object, field), ISPECT_STR_MAP, \ ISPECT_REF, g_##ref_type##_ispect, is_list }, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct user_channel { LIST_HEADER (struct user_channel) struct channel *channel; ///< Reference to channel }; static struct user_channel * user_channel_new (struct channel *channel) { struct user_channel *self = xcalloc (1, sizeof *self); self->channel = channel; 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 bool away; ///< User is away struct user_channel *channels; ///< Channels the user is on (with us) }; static struct ispect_field g_user_ispect[] = { ISPECT( user, nickname, STRING ) ISPECT( user, away, BOOL ) {} }; static struct user * user_new (char *nickname) { struct user *self = xcalloc (1, sizeof *self); self->ref_count = 1; self->nickname = nickname; return self; } static void user_destroy (struct user *self) { free (self->nickname); LIST_FOR_EACH (struct user_channel, iter, self->channels) user_channel_destroy (iter); free (self); } REF_COUNTABLE_METHODS (user) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct channel_user { LIST_HEADER (struct channel_user) struct user *user; ///< Reference to user char *prefixes; ///< Ordered @+... characters }; static struct channel_user * channel_user_new (struct user *user, const char *prefixes) { struct channel_user *self = xcalloc (1, sizeof *self); self->user = user; self->prefixes = xstrdup (prefixes); return self; } static void channel_user_destroy (struct channel_user *self) { user_unref (self->user); free (self->prefixes); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We keep references to channels in their buffers, // and weak references in their users and the name lookup table. struct channel { REF_COUNTABLE_HEADER struct server *s; ///< Server 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 strv names_buf; ///< Buffer for RPL_NAMREPLY size_t users_len; ///< User count bool left_manually; ///< Don't rejoin on reconnect bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES }; static struct ispect_field g_channel_ispect[] = { ISPECT( channel, name, STRING ) ISPECT( channel, topic, STRING ) ISPECT( channel, no_param_modes, STR ) ISPECT_MAP( channel, param_modes, STRING ) ISPECT( channel, users_len, SIZE ) ISPECT( channel, left_manually, BOOL ) {} }; static struct channel * channel_new (struct server *s, char *name) { struct channel *self = xcalloc (1, sizeof *self); self->ref_count = 1; self->s = s; self->name = name; self->no_param_modes = str_make (); self->param_modes = str_map_make (free); self->names_buf = strv_make (); 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 && !self->users_len); strv_free (&self->names_buf); free (self); } REF_COUNTABLE_METHODS (channel) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum formatter_item_type { FORMATTER_ITEM_END, ///< Sentinel value for arrays FORMATTER_ITEM_TEXT, ///< Text FORMATTER_ITEM_ATTR, ///< Formatting attributes FORMATTER_ITEM_FG_COLOR, ///< Foreground colour FORMATTER_ITEM_BG_COLOR, ///< Background colour FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration }; struct formatter_item { enum formatter_item_type type : 16; ///< Type of this item int attribute : 16; ///< Attribute ID int color; ///< Colour char *text; ///< String }; static void formatter_item_free (struct formatter_item *self) { free (self->text); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct formatter { struct app_context *ctx; ///< Application context struct server *s; ///< Server struct formatter_item *items; ///< Items size_t items_len; ///< Items used size_t items_alloc; ///< Items allocated }; static struct formatter formatter_make (struct app_context *ctx, struct server *s) { struct formatter self = { .ctx = ctx, .s = s }; self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16)); return self; } static void formatter_free (struct formatter *self) { for (size_t i = 0; i < self->items_len; i++) formatter_item_free (&self->items[i]); free (self->items); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam }; struct buffer_line { LIST_HEADER (struct buffer_line) int flags; ///< Flags time_t when; ///< Time of the event struct formatter_item items[]; ///< Line data }; /// Create a new buffer line stealing all data from the provided formatter struct buffer_line * buffer_line_new (struct formatter *f) { // We make space for one more item that gets initialized to all zeros, // meaning FORMATTER_ITEM_END (because it's the first value in the enum) size_t items_size = f->items_len * sizeof *f->items; struct buffer_line *self = xcalloc (1, sizeof *self + items_size + sizeof *self->items); memcpy (self->items, f->items, items_size); // We've stolen pointers from the formatter, let's destroy it altogether free (f->items); memset (f, 0, sizeof *f); return self; } static void buffer_line_destroy (struct buffer_line *self) { for (struct formatter_item *iter = self->items; iter->type; iter++) formatter_item_free (iter); 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) REF_COUNTABLE_HEADER enum buffer_type type; ///< Type of the buffer char *name; ///< The name of the buffer struct input *input; ///< API for "input_data" input_buffer_t 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 new_messages_count; ///< # messages since last left unsigned new_unimportant_count; ///< How much of that is unimportant bool highlighted; ///< We've been highlighted bool hide_unimportant; ///< Hide unimportant messages 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 ispect_field g_server_ispect[]; static struct ispect_field g_buffer_ispect[] = { ISPECT( buffer, name, STRING ) ISPECT( buffer, new_messages_count, UINT ) ISPECT( buffer, new_unimportant_count, UINT ) ISPECT( buffer, highlighted, BOOL ) ISPECT( buffer, hide_unimportant, BOOL ) ISPECT_REF( buffer, server, false, server ) ISPECT_REF( buffer, channel, false, channel ) ISPECT_REF( buffer, user, false, user ) {} }; static struct buffer * buffer_new (struct input *input, enum buffer_type type, char *name) { struct buffer *self = xcalloc (1, sizeof *self); self->ref_count = 1; self->input = input; self->input_data = CALL (input, buffer_new); self->type = type; self->name = name; return self; } static void buffer_destroy (struct buffer *self) { free (self->name); if (self->input_data) { #ifdef HAVE_READLINE // FIXME: can't really free "history" contents from here, as we cannot // be sure that the user interface pointer is valid and usable input_rl__buffer_destroy_wo_history (self->input_data); #else // ! HAVE_READLINE CALL_ (self->input, buffer_destroy, self->input_data); #endif // ! HAVE_READLINE } 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); } REF_COUNTABLE_METHODS (buffer) #define buffer_ref do_not_use_dangerous // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The only real purpose of this is to abstract away TLS struct transport { /// Initialize the transport bool (*init) (struct server *s, const char *hostname, struct error **e); /// Destroy the user data pointer void (*cleanup) (struct server *s); /// The underlying socket may have become readable, update `read_buffer' enum socket_io_result (*try_read) (struct server *s); /// The underlying socket may have become writeable, flush `write_buffer' enum socket_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 { REF_COUNTABLE_HEADER 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 struct socks_connector *socks_conn; ///< SOCKS connection establisher unsigned reconnect_attempt; ///< Number of reconnect attempt 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 struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate // 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 autoaway_active; ///< Autoaway is currently active struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS bool cap_echo_message; ///< Whether the server echoes messages bool cap_away_notify; ///< Whether we get AWAY notifications bool cap_sasl; ///< Whether SASL is available // 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_extban_prefix; ///< EXTBAN prefix or \0 char *irc_extban_types; ///< EXTBAN types 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 struct ispect_field g_server_ispect[] = { ISPECT( server, name, STRING ) ISPECT( server, state, INT ) ISPECT( server, reconnect_attempt, UINT ) ISPECT( server, manual_disconnect, BOOL ) ISPECT( server, irc_user_host, STRING ) ISPECT( server, autoaway_active, BOOL ) ISPECT( server, cap_echo_message, BOOL ) ISPECT_REF( server, buffer, false, buffer ) // TODO: either rename the underlying field or fix the plugins { "user", offsetof (struct server, irc_user), ISPECT_REF, 0, g_user_ispect, false }, { "user_mode", offsetof (struct server, irc_user_mode), ISPECT_STR, 0, NULL, false }, {} }; static void on_irc_timeout (void *user_data); static void on_irc_ping_timeout (void *user_data); static void on_irc_autojoin_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_extban_prefix = 0; self->irc_extban_types = 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_extban_types); 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 struct server * server_new (struct poller *poller) { struct server *self = xcalloc (1, sizeof *self); self->ref_count = 1; self->socket = -1; self->read_buffer = str_make (); self->write_buffer = str_make (); self->state = IRC_DISCONNECTED; self->timeout_tmr = poller_timer_make (poller); self->timeout_tmr.dispatcher = on_irc_timeout; self->timeout_tmr.user_data = self; self->ping_tmr = poller_timer_make (poller); self->ping_tmr.dispatcher = on_irc_ping_timeout; self->ping_tmr.user_data = self; self->reconnect_tmr = poller_timer_make (poller); self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect; self->reconnect_tmr.user_data = self; self->autojoin_tmr = poller_timer_make (poller); self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout; self->autojoin_tmr.user_data = self; self->irc_users = str_map_make (NULL); self->irc_users.key_xfrm = irc_strxfrm; self->irc_channels = str_map_make (NULL); self->irc_channels.key_xfrm = irc_strxfrm; self->irc_buffer_map = str_map_make (NULL); self->irc_buffer_map.key_xfrm = irc_strxfrm; self->irc_user_mode = str_make (); self->cap_ls_buf = strv_make (); server_init_specifics (self); return self; } static void server_destroy (struct server *self) { free (self->name); if (self->connector) { connector_free (self->connector); free (self->connector); } if (self->socks_conn) { socks_connector_free (self->socks_conn); free (self->socks_conn); } if (self->transport && self->transport->cleanup) self->transport->cleanup (self); if (self->socket != -1) { poller_fd_reset (&self->socket_event); xclose (self->socket); } str_free (&self->read_buffer); str_free (&self->write_buffer); poller_timer_reset (&self->ping_tmr); poller_timer_reset (&self->timeout_tmr); poller_timer_reset (&self->reconnect_tmr); poller_timer_reset (&self->autojoin_tmr); 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); strv_free (&self->cap_ls_buf); server_free_specifics (self); free (self); } REF_COUNTABLE_METHODS (server) #define server_ref do_not_use_dangerous // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct plugin { LIST_HEADER (struct plugin) char *name; ///< Name of the plugin struct plugin_vtable *vtable; ///< Methods }; struct plugin_vtable { /// Collect garbage void (*gc) (struct plugin *self); /// Unregister and free the plugin including all relevant resources void (*free) (struct plugin *self); }; static void plugin_destroy (struct plugin *self) { self->vtable->free (self); free (self->name); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // This is a bit ugly since insertion is O(n) and the need to get rid of the // specific type because of list macros, however I don't currently posses any // strictly better, ordered data structure struct hook { LIST_HEADER (struct hook) int priority; ///< The lesser the sooner }; static struct hook * hook_insert (struct hook *list, struct hook *item) { // Corner cases: list is empty or we precede everything if (!list || item->priority < list->priority) { LIST_PREPEND (list, item); return list; } // Otherwise fast-forward to the last entry that precedes us struct hook *before = list; while (before->next && before->next->priority < item->priority) before = before->next; // And link ourselves in between it and its successor if ((item->next = before->next)) item->next->prev = item; before->next = item; item->prev = before; return list; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct input_hook { struct hook super; ///< Common hook fields /// Takes over the ownership of "input", returns either NULL if input /// was thrown away, or a possibly modified version of it char *(*filter) (struct input_hook *self, struct buffer *buffer, char *input); }; struct irc_hook { struct hook super; ///< Common hook fields /// Takes over the ownership of "message", returns either NULL if message /// was thrown away, or a possibly modified version of it char *(*filter) (struct irc_hook *self, struct server *server, char *message); }; struct prompt_hook { struct hook super; ///< Common hook fields /// Returns what the prompt should look like right now based on other state char *(*make) (struct prompt_hook *self); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct completion_word { size_t start; ///< Offset to start of word size_t end; ///< Offset to end of word }; struct completion { char *line; ///< The line which is being completed struct completion_word *words; ///< Word locations size_t words_len; ///< Number of words size_t words_alloc; ///< Number of words allocated size_t location; ///< Which word is being completed }; struct completion_hook { struct hook super; ///< Common hook fields /// Tries to add possible completions of "word" to "output" void (*complete) (struct completion_hook *self, struct completion *data, const char *word, struct strv *output); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct app_context { 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 bool show_all_prefixes; ///< Show all prefixes before nicks bool word_wrapping; ///< Enable simple word wrapping 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_timer autoaway_tmr; ///< Autoaway timer 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 struct str_map buffers_by_name; ///< Buffers by name unsigned backlog_limit; ///< Limit for buffer lines 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 struct input *input; ///< User interface struct poller_idle prompt_event; ///< Deferred prompt refresh struct poller_idle input_event; ///< Pending input event struct strv pending_input; ///< Pending input lines int *nick_palette; ///< A 256-colour palette for nicknames size_t nick_palette_len; ///< Number of entries in nick_palette bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape bool in_bracketed_paste; ///< User is pasting some content struct str input_buffer; ///< Buffered pasted content bool running_backlog_helper; ///< Running a backlog helper bool running_editor; ///< Running editor for the input char *editor_filename; ///< The file being edited by user int terminal_suspended; ///< Terminal suspension level struct plugin *plugins; ///< Loaded plugins struct hook *input_hooks; ///< Input hooks struct hook *irc_hooks; ///< IRC hooks struct hook *prompt_hooks; ///< Prompt hooks struct hook *completion_hooks; ///< Autocomplete hooks } *g_ctx; static struct ispect_field g_ctx_ispect[] = { ISPECT_MAP_REF( app_context, servers, false, server ) ISPECT_REF( app_context, buffers, true, buffer ) ISPECT_REF( app_context, global_buffer, false, buffer ) ISPECT_REF( app_context, current_buffer, false, buffer ) {} }; static int * filter_color_cube_for_acceptable_nick_colors (size_t *len) { // This is a pure function and we don't use threads, static storage is fine static int table[6 * 6 * 6]; size_t len_counter = 0; for (int x = 0; x < (int) N_ELEMENTS (table); x++) { int r = x / 36; int g = (x / 6) % 6; int b = (x % 6); // The first step is 95/255, the rest are 40/255, // as an approximation we can double the first step double linear_R = pow ((r + !!r) / 6., 2.2); double linear_G = pow ((g + !!g) / 6., 2.2); double linear_B = pow ((b + !!b) / 6., 2.2); // Use the relative luminance of colours within the cube to filter // colours that look okay-ish on terminals with both black and white // backgrounds (use the test-nick-colors script to calibrate) double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B; if (Y >= .25 && Y <= .4) table[len_counter++] = 16 + x; } *len = len_counter; return table; } static bool app_iconv_open (iconv_t *target, const char *to, const char *from) { if (ICONV_ACCEPTS_TRANSLIT) { char *to_real = xstrdup_printf ("%s//TRANSLIT", to); *target = iconv_open (to_real, from); free (to_real); } else *target = iconv_open (to, from); return *target != (iconv_t) -1; } static void app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); self->config = config_make (); poller_init (&self->poller); self->servers = str_map_make ((str_map_free_fn) server_unref); self->servers.key_xfrm = tolower_ascii_strxfrm; self->buffers_by_name = str_map_make (NULL); self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm; // So that we don't lose the logo shortly after startup self->backlog_limit = 1000; self->last_displayed_msg_time = time (NULL); char *native = nl_langinfo (CODESET); if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8") || !app_iconv_open (&self->term_to_utf8, "UTF-8", native)) exit_fatal ("creating the UTF-8 conversion object failed: %s", strerror (errno)); self->input = input_new (); self->input->user_data = self; self->pending_input = strv_make (); self->input_buffer = str_make (); self->nick_palette = filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len); } static void app_context_free (struct app_context *self) { // Plugins can try to use of the other fields when destroyed LIST_FOR_EACH (struct plugin, iter, self->plugins) plugin_destroy (iter); 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 // We can use the user interface here; see buffer_destroy() CALL_ (self->input, buffer_destroy, iter->input_data); iter->input_data = NULL; #endif // HAVE_READLINE buffer_unref (iter); } str_map_free (&self->buffers_by_name); str_map_free (&self->servers); poller_free (&self->poller); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); CALL (self->input, destroy); strv_free (&self->pending_input); str_free (&self->input_buffer); free (self->editor_filename); } static void refresh_prompt (struct app_context *ctx) { // XXX: the need for this conditional could probably be resolved // by some clever reordering if (ctx->prompt_event.poller) poller_idle_set (&ctx->prompt_event); } // --- Configuration ----------------------------------------------------------- static void on_config_debug_mode_change (struct config_item *item) { g_debug_mode = item->value.boolean; } static void on_config_show_all_prefixes_change (struct config_item *item) { struct app_context *ctx = item->user_data; ctx->show_all_prefixes = item->value.boolean; refresh_prompt (ctx); } static void on_config_backlog_limit_change (struct config_item *item); 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) TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 (iscntrl_ascii (c)) { 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 }, // XXX: if we add support for new capabilities, the value stays unchanged { .name = "capabilities", .comment = "Capabilities to use if supported by server", .type = CONFIG_ITEM_STRING_ARRAY, .validate = config_validate_nonjunk_string, .default_ = "\"multi-prefix,invite-notify,server-time,echo-message," "message-tags,away-notify,cap-notify,chghost\"" }, { .name = "tls", .comment = "Whether to use TLS", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" }, { .name = "tls_cert", .comment = "Client TLS certificate (PEM)", .type = CONFIG_ITEM_STRING }, { .name = "tls_verify", .comment = "Whether to verify certificates", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "tls_ca_file", .comment = "OpenSSL CA bundle file", .type = CONFIG_ITEM_STRING }, { .name = "tls_ca_path", .comment = "OpenSSL CA bundle path", .type = CONFIG_ITEM_STRING }, { .name = "tls_ciphers", .comment = "OpenSSL cipher preference list", .type = CONFIG_ITEM_STRING, .default_ = "\"DEFAULT:!MEDIUM:!LOW\"" }, { .name = "autoconnect", .comment = "Connect automatically on startup", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "autojoin", .comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")", .type = CONFIG_ITEM_STRING_ARRAY, .validate = config_validate_nonjunk_string }, { .name = "command", .comment = "Command to execute after a successful connect", .type = CONFIG_ITEM_STRING }, { .name = "command_delay", .comment = "Delay between executing \"command\" and joining channels", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "0" }, { .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 = "show_all_prefixes", .comment = "Show all prefixes in front of nicknames", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off", .on_change = on_config_show_all_prefixes_change }, { .name = "word_wrapping", .comment = "Enable simple word wrapping in buffers", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on", .on_change = on_config_word_wrapping_change }, { .name = "editor_command", .comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\"", .type = CONFIG_ITEM_STRING }, { .name = "process_pasted_text", .comment = "Normalize newlines and quote the command prefix in pastes", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "date_change_line", .comment = "Input to strftime(3) for the date change line", .type = CONFIG_ITEM_STRING, .default_ = "\"%F\"" }, { .name = "read_marker_char", .comment = "The character to use for the read marker line", .type = CONFIG_ITEM_STRING, .default_ = "\"-\"", .validate = config_validate_nonjunk_string }, { .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 }, { .name = "backlog_limit", .comment = "Maximum number of lines stored in the backlog", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "1000", .on_change = on_config_backlog_limit_change }, { .name = "backlog_helper", .comment = "Shell command to page buffer history (args: name [path])", .type = CONFIG_ITEM_STRING, .default_ = "`name=$(echo \"$1\" | sed 's/[%?:.]/\\\\&/g'); " "prompt='?f%F:'$name'. ?db- page %db?L of %D. .(?eEND:?PB%PB\\%..)'; " "LESSSECURE=1 less +Gb -Ps\"$prompt\" \"${2:--R}\"`" }, { .name = "backlog_helper_strip_formatting", .comment = "Strip formatting from backlog helper input", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" }, { .name = "reconnect_delay_growing", .comment = "Growing factor for reconnect delay", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "2" }, { .name = "reconnect_delay_max", .comment = "Maximum reconnect delay in seconds", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "600" }, { .name = "autoaway_message", .comment = "Automated away message", .type = CONFIG_ITEM_STRING, .default_ = "\"I'm not here right now\"" }, { .name = "autoaway_delay", .comment = "Delay from the last keypress in seconds", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "1800" }, { .name = "plugin_autoload", .comment = "Plugins to automatically load on start", .type = CONFIG_ITEM_STRING_ARRAY, .validate = config_validate_nonjunk_string }, {} }; 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, "plugins", 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 struct str_map * get_plugins_config (struct app_context *ctx) { return &config_item_get (ctx->config.root, "plugins", NULL)->value.object; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void serialize_configuration (struct config_item *root, 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 (root, true, output); } // --- Terminal output --------------------------------------------------------- /// Default colour pair #define COLOR_DEFAULT -1 /// Bright versions of the basic colour set #define COLOR_BRIGHT(x) (COLOR_ ## x + 8) /// Builds a colour pair for 256-colour terminals with a 16-colour backup value #define COLOR_256(name, c256) \ (((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16)) 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; CALL (ctx->input, hide); print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); fputs ("\n", stream); CALL (ctx->input, show); } static ssize_t attr_by_name (const char *name) { 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 (name, table[i])) return i; return -1; } static void on_config_attribute_change (struct config_item *item) { struct app_context *ctx = item->user_data; ssize_t id = attr_by_name (item->schema->name); if (id != -1) { cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL ? ctx->attrs_defaults[id] : item->value.string.str)); } } 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 (DATE_CHANGE, enter_bold_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 = have_ti ? xstrdup_printf ("%s%s%s", g_terminal.color_set_fg[COLOR_YELLOW], g_terminal.color_set_bg[COLOR_MAGENTA], enter_bold_mode) : NULL; INIT_ATTR (HIGHLIGHT, highlight); free (highlight); #undef INIT_ATTR // This prevents formatters from obtaining an attribute printer function if (!have_ti) { 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 colour pairs. enum { TEXT_BOLD = 1 << 0, TEXT_ITALIC = 1 << 1, TEXT_UNDERLINE = 1 << 2, TEXT_INVERSE = 1 << 3, TEXT_BLINK = 1 << 4, TEXT_CROSSED_OUT = 1 << 5, TEXT_MONOSPACE = 1 << 6 }; struct attr_printer { char **attrs; ///< Named attributes FILE *stream; ///< Output stream bool dirty; ///< Attributes are set }; #define ATTR_PRINTER_INIT(ctx, stream) { ctx->attrs, stream, true } static void attr_printer_filtered_puts (FILE *stream, const char *attr) { for (; *attr; attr++) { // sgr/set_attributes and sgr0/exit_attribute_mode like to enable or // disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R` // does not skip over these characters and it screws up word wrapping if (*attr == 14 /* SO */ || *attr == 15 /* SI */) continue; // Trivially skip delay sequences intended to be processed by tputs() const char *end = NULL; if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>'))) attr = end; else fputc (*attr, stream); } } static void attr_printer_tputs (struct attr_printer *self, const char *attr) { terminal_printer_fn printer = get_attribute_printer (self->stream); if (printer) tputs (attr, 1, printer); else // We shouldn't really do this but we need it to output formatting // to the backlog helper--it should be SGR-only attr_printer_filtered_puts (self->stream, attr); } static void attr_printer_reset (struct attr_printer *self) { if (self->dirty) attr_printer_tputs (self, self->attrs[ATTR_RESET]); self->dirty = false; } static void attr_printer_apply_named (struct attr_printer *self, int attribute) { attr_printer_reset (self); if (attribute != ATTR_RESET) { attr_printer_tputs (self, self->attrs[attribute]); self->dirty = true; } } // NOTE: commonly terminals have: // 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK) // 16 colours (okayish, we have the full basic range guaranteed) // 88 colours (the same plus a 4^3 RGB cube and a few shades of grey) // 256 colours (best, like above but with a larger cube and more grey) /// Interpolate from the 256-colour palette to the 88-colour one static int attr_printer_256_to_88 (int color) { // These colours are the same everywhere if (color < 16) return color; // 24 -> 8 extra shades of grey 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 attr_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; } // Fall-through case 16: return c16; case 88: return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256); case 256: return c256 <= 0 ? c16 : c256; default: // Unsupported palette return -1; } } static void attr_printer_apply (struct attr_printer *self, int text_attrs, int wanted_fg, int wanted_bg) { bool fg_is_bright; int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright); bool bg_is_bright; int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright); bool have_inverse = !!(text_attrs & TEXT_INVERSE); if (have_inverse) { bool tmp = fg_is_bright; fg_is_bright = bg_is_bright; bg_is_bright = tmp; } // In 8 colour mode, some terminals don't support bright backgrounds. // However, we can make use of the fact that the brightness change caused // by the bold attribute is retained when inverting the colours. // This has the downside of making the text bold when it's not supposed // to be, and we still can't make both colours bright, so it's more of // an interesting hack rather than anything else. if (!fg_is_bright && bg_is_bright && have_inverse) text_attrs |= TEXT_BOLD; else if (!fg_is_bright && bg_is_bright && !have_inverse && fg >= 0 && bg >= 0) { // As long as none of the colours is the default, we can swap them int tmp = fg; fg = bg; bg = tmp; text_attrs |= TEXT_BOLD | TEXT_INVERSE; } else { // This often works, however... if (fg_is_bright) text_attrs |= TEXT_BOLD; // this turns out to be annoying if implemented "correctly" if (bg_is_bright) text_attrs |= TEXT_BLINK; } attr_printer_reset (self); // TEXT_MONOSPACE is unimplemented, for obvious reasons if (text_attrs) attr_printer_tputs (self, tparm (set_attributes, 0, // standout text_attrs & TEXT_UNDERLINE, text_attrs & TEXT_INVERSE, text_attrs & TEXT_BLINK, 0, // dim text_attrs & TEXT_BOLD, 0, // blank 0, // protect 0)); // acs if ((text_attrs & TEXT_ITALIC) && enter_italics_mode) attr_printer_tputs (self, enter_italics_mode); char *smxx = NULL; if ((text_attrs & TEXT_CROSSED_OUT) && (smxx = tigetstr ("smxx")) && smxx != (char *) -1) attr_printer_tputs (self, smxx); if (fg >= 0) attr_printer_tputs (self, g_terminal.color_set_fg[fg]); if (bg >= 0) attr_printer_tputs (self, g_terminal.color_set_bg[bg]); 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 cstr_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)); } // Message targets can be prefixed by a character filtering their targets static const char * irc_skip_statusmsg (struct server *s, const char *target) { return target + (*target && strchr (s->irc_statusmsg, *target)); } static bool irc_is_extban (struct server *s, const char *target) { // Some servers have a prefix, and some support negation if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix) return false; if (*target == '~') target++; // XXX: we don't know if it's supposed to have an argument, or not return *target && strchr (s->irc_extban_types, *target++) && strchr (":\0", *target); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // As of 2020, 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 (const char *text) { if (!text) return NULL; // XXX: the validation may be unnecessarily harsh, could do with a lenient // first pass, then replace any errors with the replacement character size_t len = strlen (text) + 1; if (utf8_validate (text, len)) return xstrdup (text); // Windows 1252 redefines several silly C1 control characters as glyphs static const char c1[32][4] = { "\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92", "\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1", "\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9", "\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f", "\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c", "\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94", "\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba", "\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8", }; struct str s = str_make (); for (const char *p = text; *p; p++) { int c = *(unsigned char *) p; if (c < 0x80) str_append_c (&s, c); else if (c < 0xA0) str_append (&s, c1[c & 0x1f]); else str_append_data (&s, (char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2); } return str_steal (&s); } // --- 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 // #l inserts a locale-encoded string // // #S inserts a string from the server with unknown encoding // #m inserts an IRC-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 colour // #C sets background colour // // 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); if (self->items_len == self->items_alloc) self->items = xreallocarray (self->items, sizeof *self->items, (self->items_alloc <<= 1)); self->items[self->items_len++] = template_; } #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_)) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 colour terminal palette, or the 256 colour 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), }; // https://modern.ircdocs.horse/formatting.html // http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html static const int16_t g_extra_to_256[100 - 16] = { 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, 196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198, 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1 }; static const char * irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg) { if (!isdigit_ascii (*s)) { *fg = *bg = 99; return s; } *fg = *s++ - '0'; if (isdigit_ascii (*s)) *fg = *fg * 10 + (*s++ - '0'); if (*s != ',' || !isdigit_ascii (s[1])) return s; s++; *bg = *s++ - '0'; if (isdigit_ascii (*s)) *bg = *bg * 10 + (*s++ - '0'); return s; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct irc_char_attrs { uint8_t fg, bg; ///< {Fore,back}ground colour or 99 uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK uint8_t starts_at_boundary; ///< Possible to split here? }; static void irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out) { soft_assert (attrs->fg < 100 && attrs->bg < 100); if (attrs->fg != 99 || attrs->bg != 99) { str_append_printf (out, "\x03%u", attrs->fg); if (attrs->bg != 99) str_append_printf (out, ",%02u", attrs->bg); } if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02'); if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d'); if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f'); if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16'); if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e'); if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11'); } static int irc_parse_attribute (char c) { switch (c) { case '\x02' /* ^B */: return TEXT_BOLD; case '\x11' /* ^Q */: return TEXT_MONOSPACE; case '\x16' /* ^V */: return TEXT_INVERSE; case '\x1d' /* ^] */: return TEXT_ITALIC; case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT; case '\x1f' /* ^_ */: return TEXT_UNDERLINE; case '\x0f' /* ^O */: return -1; } return 0; } // The text needs to be NUL-terminated, and a valid UTF-8 string static struct irc_char_attrs * irc_analyze_text (const char *text, size_t len) { struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs), blank = { .fg = 99, .bg = 99, .starts_at_boundary = true }, next = blank, cur = next; for (size_t i = 0; i != len; cur = next) { const char *start = text; hard_assert (utf8_decode (&text, len - i) >= 0); int attribute = irc_parse_attribute (*start); if (*start == '\x03') text = irc_parse_mirc_color (text, &next.fg, &next.bg); else if (attribute > 0) next.attributes ^= attribute; else if (attribute < 0) next = blank; while (start++ != text) { attrs[i++] = cur; cur.starts_at_boundary = false; } } return attrs; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static const char * formatter_parse_mirc_color (struct formatter *self, const char *s) { uint8_t fg = 255, bg = 255; s = irc_parse_mirc_color (s, &fg, &bg); if (fg < 16) FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); else if (fg < 100) FORMATTER_ADD_ITEM (self, FG_COLOR, .color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16])); if (bg < 16) FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); else if (bg < 100) FORMATTER_ADD_ITEM (self, BG_COLOR, .color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16])); return s; } static void formatter_parse_message (struct formatter *self, const char *s) { FORMATTER_ADD_RESET (self); struct str buf = str_make (); unsigned char c; while ((c = *s++)) { if (buf.len && c < 0x20) { FORMATTER_ADD_TEXT (self, buf.str); str_reset (&buf); } int attribute = irc_parse_attribute (c); if (c == '\x03') s = formatter_parse_mirc_color (self, s); else if (attribute > 0) FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute); else if (attribute < 0) FORMATTER_ADD_RESET (self); else 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, const char *s) { // For outgoing messages; maybe we should add a special #t for them // which would also make us not cut off the userhost part, ever if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s))) { char *tmp = irc_to_utf8 (s); FORMATTER_ADD_TEXT (self, tmp); free (tmp); return; } char *nick = irc_cut_nickname (s); int color = siphash_wrapper (nick, strlen (nick)) % 7; // Never use the black colour, could become transparent on black terminals; // white is similarly excluded from the range if (color == COLOR_BLACK) color = (uint16_t) -1; // Use a colour from the 256-colour cube if available color |= self->ctx->nick_palette[siphash_wrapper (nick, strlen (nick)) % self->ctx->nick_palette_len] << 16; // We always use the default colour for ourselves if (self->s && irc_is_this_us (self->s, nick)) color = -1; FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color); char *x = irc_to_utf8 (nick); free (nick); FORMATTER_ADD_TEXT (self, x); free (x); // Need to reset the colour afterwards FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); } static void formatter_parse_nick_full (struct formatter *self, const 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 (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 'l': if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8, (s = va_arg (*ap, char *)), -1, NULL))) print_error ("character conversion failed for: %s", "output"); else str_append (buf, tmp); free (tmp); break; case 'S': tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); str_append (buf, tmp); free (tmp); break; case 'm': tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); formatter_parse_message (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_make (); 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); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct line_char_attrs { short named; ///< Named attribute or -1 short text; ///< Text attributes int fg; ///< Foreground colour (-1 for default) int bg; ///< Background colour (-1 for default) }; // We can get rid of the linked list and do this in one allocation (use strlen() // for the upper bound)--since we only prepend and/or replace characters, add // a member to specify the prepended character and how many times to repeat it. // Tabs may nullify the wide character but it's not necessary. // // This would be slighly more optimal but it would also set the algorithm in // stone and complicate flushing. struct line_char { LIST_HEADER (struct line_char) wchar_t wide; ///< The character as a wchar_t int width; ///< Width of the character in cells struct line_char_attrs attrs; ///< Attributes }; static struct line_char * line_char_new (wchar_t wc) { struct line_char *self = xcalloc (1, sizeof *self); self->width = wcwidth ((self->wide = wc)); // Typically various control characters if (self->width < 0) self->width = 0; self->attrs.bg = self->attrs.fg = -1; self->attrs.named = ATTR_RESET; return self; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct line_wrap_mark { struct line_char *start; ///< First character int used; ///< Display cells used }; static void line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c) { if (!mark->start) mark->start = c; mark->used += c->width; } struct line_wrap_state { struct line_char *result; ///< Head of result struct line_char *result_tail; ///< Tail of result int line_used; ///< Line length before marks int line_max; ///< Maximum line length struct line_wrap_mark chunk; ///< All buffered text struct line_wrap_mark overflow; ///< Overflowing text }; static void line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before) { struct line_char *nl = line_char_new (L'\n'); LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start); s->line_used = before->used; } static void line_wrap_flush (struct line_wrap_state *s, bool force_split) { if (!s->overflow.start) s->line_used += s->chunk.used; else if (force_split || s->chunk.used > s->line_max) { #ifdef WRAP_UNNECESSARILY // When the line wraps at the end of the screen and a background colour // is set, the terminal paints the entire new line with that colour. // Explicitly inserting a newline with the default attributes fixes it. line_wrap_flush_split (s, &s->overflow); #else // Splitting here breaks link searching mechanisms in some terminals, // though, so we make a trade-off and let the chunk wrap naturally. // Fuck terminals, really. s->line_used = s->overflow.used; #endif } else // Print the chunk in its entirety on a new line line_wrap_flush_split (s, &s->chunk); memset (&s->chunk, 0, sizeof s->chunk); memset (&s->overflow, 0, sizeof s->overflow); } static void line_wrap_nl (struct line_wrap_state *s) { line_wrap_flush (s, true); struct line_char *nl = line_char_new (L'\n'); LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl); s->line_used = 0; } static void line_wrap_tab (struct line_wrap_state *s, struct line_char *c) { line_wrap_flush (s, true); if (s->line_used >= s->line_max) line_wrap_nl (s); // Compute the number of characters needed to get to the next tab stop int tab_width = ((s->line_used + 8) & ~7) - s->line_used; // On overflow just fill the rest of the line with spaces if (s->line_used + tab_width > s->line_max) tab_width = s->line_max - s->line_used; s->line_used += tab_width; while (tab_width--) { struct line_char *space = line_char_new (L' '); space->attrs = c->attrs; LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space); } } static void line_wrap_push_char (struct line_wrap_state *s, struct line_char *c) { // Note that when processing whitespace here, any non-WS chunk has already // been flushed, and thus it matters little if we flush with force split if (wcschr (L"\r\f\v", c->wide)) /* Skip problematic characters */; else if (c->wide == L'\n') line_wrap_nl (s); else if (c->wide == L'\t') line_wrap_tab (s, c); else goto use_as_is; free (c); return; use_as_is: if (s->overflow.start || s->line_used + s->chunk.used + c->width > s->line_max) { if (s->overflow.used + c->width > s->line_max) { #ifdef WRAP_UNNECESSARILY // If the overflow overflows, restart on a new line line_wrap_nl (s); #else // See line_wrap_flush(), we would end up on a new line anyway line_wrap_flush (s, true); s->line_used = 0; #endif } else line_wrap_mark_push (&s->overflow, c); } line_wrap_mark_push (&s->chunk, c); LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c); } /// Basic word wrapping that respects wcwidth(3) and expands tabs. /// Besides making text easier to read, it also fixes the problem with /// formatting spilling over the entire new line on line wrap. static struct line_char * line_wrap (struct line_char *line, int max_width) { struct line_wrap_state s = { .line_max = max_width }; bool last_was_word_char = false; LIST_FOR_EACH (struct line_char, c, line) { // Act on the right boundary of (\s*\S+) chunks bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide); if (last_was_word_char && !this_is_word_char) line_wrap_flush (&s, false); last_was_word_char = this_is_word_char; LIST_UNLINK (line, c); line_wrap_push_char (&s, c); } // Make sure to process the last word and return the modified list line_wrap_flush (&s, false); return s.result; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct exploder { struct app_context *ctx; ///< Application context struct line_char *result; ///< Result struct line_char *result_tail; ///< Tail of result struct line_char_attrs attrs; ///< Current attributes }; static bool explode_formatter_attr (struct exploder *self, struct formatter_item *item) { switch (item->type) { case FORMATTER_ITEM_ATTR: self->attrs.named = item->attribute; self->attrs.text = 0; self->attrs.fg = -1; self->attrs.bg = -1; return true; case FORMATTER_ITEM_SIMPLE: self->attrs.named = -1; self->attrs.text ^= item->attribute; return true; case FORMATTER_ITEM_FG_COLOR: self->attrs.named = -1; self->attrs.fg = item->color; return true; case FORMATTER_ITEM_BG_COLOR: self->attrs.named = -1; self->attrs.bg = item->color; return true; default: return false; } } static void explode_text (struct exploder *self, const char *text) { // Throw away any potentially harmful control characters first struct str filtered = str_make (); for (const char *p = text; *p; p++) if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p)) str_append_c (&filtered, *p); size_t term_len = 0; char *term = iconv_xstrdup (self->ctx->term_from_utf8, filtered.str, filtered.len + 1, &term_len); str_free (&filtered); mbstate_t ps; memset (&ps, 0, sizeof ps); wchar_t wch; size_t len, processed = 0; while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps))) { hard_assert (len != (size_t) -2 && len != (size_t) -1); processed += len; struct line_char *c = line_char_new (wch); c->attrs = self->attrs; LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c); } free (term); } static struct line_char * formatter_to_chars (struct formatter *formatter) { struct exploder self = { .ctx = formatter->ctx }; self.attrs.fg = self.attrs.bg = self.attrs.named = -1; int attribute_ignore = 0; for (size_t i = 0; i < formatter->items_len; i++) { struct formatter_item *iter = &formatter->items[i]; if (iter->type == FORMATTER_ITEM_TEXT) explode_text (&self, iter->text); else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR) attribute_ignore += iter->attribute; else if (attribute_ignore <= 0 && !explode_formatter_attr (&self, iter)) hard_assert (!"unhandled formatter item type"); } return self.result; } enum { FLUSH_OPT_RAW = (1 << 0), ///< Print raw attributes FLUSH_OPT_NOWRAP = (1 << 1) ///< Do not wrap }; /// The input is a bunch of wide characters--respect shift state encodings static void formatter_putc (struct line_char *c, FILE *stream) { static mbstate_t mb; char buf[MB_LEN_MAX] = {}; size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb); if (len != (size_t) -1 && len) fwrite (buf, len - !c, 1, stream); free (c); } static void formatter_flush (struct formatter *self, FILE *stream, int flush_opts) { struct line_char *line = formatter_to_chars (self); bool is_tty = !!get_attribute_printer (stream); if (!is_tty && !(flush_opts & FLUSH_OPT_RAW)) { LIST_FOR_EACH (struct line_char, c, line) formatter_putc (c, stream); formatter_putc (NULL, stream); return; } if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP)) line = line_wrap (line, g_terminal.columns); struct attr_printer state = ATTR_PRINTER_INIT (self->ctx, stream); struct line_char_attrs attrs = {}; // Won't compare equal to anything LIST_FOR_EACH (struct line_char, c, line) { if (attrs.fg != c->attrs.fg || attrs.bg != c->attrs.bg || attrs.named != c->attrs.named || attrs.text != c->attrs.text) { formatter_putc (NULL, stream); attrs = c->attrs; if (attrs.named != -1) attr_printer_apply_named (&state, attrs.named); else attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); } formatter_putc (c, stream); } formatter_putc (NULL, stream); attr_printer_reset (&state); } // --- Buffers ----------------------------------------------------------------- static void buffer_pop_excess_lines (struct app_context *ctx, struct buffer *self) { int to_delete = (int) self->lines_count - (int) ctx->backlog_limit; while (to_delete-- > 0 && self->lines) { struct buffer_line *excess = self->lines; LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, excess); buffer_line_destroy (excess); self->lines_count--; } } static void on_config_backlog_limit_change (struct config_item *item) { struct app_context *ctx = item->user_data; ctx->backlog_limit = MIN (item->value.integer, INT_MAX); LIST_FOR_EACH (struct buffer, iter, ctx->buffers) buffer_pop_excess_lines (ctx, iter); } static void buffer_update_time (struct app_context *ctx, time_t now, FILE *stream, int flush_opts) { 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[64] = ""; const char *format = get_config_string (ctx->config.root, "behaviour.date_change_line"); if (!strftime (buf, sizeof buf, format, ¤t)) { print_error ("%s: %s", "strftime", strerror (errno)); return; } struct formatter f = formatter_make (ctx, NULL); formatter_add (&f, "#a#s\n", ATTR_DATE_CHANGE, buf); formatter_flush (&f, stream, flush_opts); // Flush the trailing formatting reset item fflush (stream); formatter_free (&f); } static void buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output, int flush_opts) { 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); for (struct formatter_item *iter = line->items; iter->type; iter++) formatter_add_item (f, *iter); formatter_add (f, "\n"); formatter_flush (f, output, flush_opts); formatter_free (f); } static void buffer_line_write_time (struct formatter *f, struct buffer_line *line, FILE *stream, int flush_opts) { // Normal timestamps don't include the date, make sure the user won't be // confused as to when an event has happened buffer_update_time (f->ctx, line->when, stream, flush_opts); 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); } #define buffer_line_will_show_up(buffer, line) \ (!(buffer)->hide_unimportant || !((line)->flags & BUFFER_LINE_UNIMPORTANT)) static void buffer_line_display (struct app_context *ctx, struct buffer *buffer, struct buffer_line *line, bool is_external) { if (!buffer_line_will_show_up (buffer, line)) return; CALL (ctx->input, hide); struct formatter f = formatter_make (ctx, NULL); buffer_line_write_time (&f, line, stdout, 0); // 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); } buffer_line_flush (line, &f, stdout, 0); // Flush the trailing formatting reset item fflush (stdout); CALL (ctx->input, show); } static void buffer_line_write_to_backlog (struct app_context *ctx, struct buffer_line *line, FILE *log_file, int flush_opts) { struct formatter f = formatter_make (ctx, NULL); buffer_line_write_time (&f, line, log_file, flush_opts); buffer_line_flush (line, &f, log_file, flush_opts); } 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_make (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); // The target is not a terminal, thus it won't wrap in spite of the 0 buffer_line_flush (line, &f, log_file, 0); } 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 (f); line->flags = flags; // TODO: allow providing custom time (IRCv3.2 server-time) line->when = time (NULL); buffer_pop_excess_lines (ctx, buffer); 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); bool unseen_pm = buffer->type == BUFFER_PM && buffer != ctx->current_buffer && !(flags & BUFFER_LINE_UNIMPORTANT); bool important = (flags & BUFFER_LINE_HIGHLIGHT) || unseen_pm; if (ctx->beep_on_highlight && important) // XXX: this may disturb any other foreground process CALL (ctx->input, ding); 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; bool displayed = true; if (ctx->terminal_suspended > 0) // Another process is using the terminal displayed = false; else if (buffer == ctx->current_buffer) buffer_line_display (ctx, buffer, line, false); else if (!ctx->isolate_buffers && can_leak) buffer_line_display (ctx, buffer, line, true); else displayed = false; // Advance the unread marker in active buffers but don't create a new one if (!displayed || (buffer == ctx->current_buffer && buffer->new_messages_count)) { buffer->new_messages_count++; if (flags & BUFFER_LINE_UNIMPORTANT) buffer->new_unimportant_count++; buffer->highlighted |= important; } if (!displayed) 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_make (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 ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ "You are now known as #n", (new_)) #define log_nick(s, buffer, old, new_) \ log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ "#n is now known as #n", (old), (new_)) #define log_chghost_self(s, buffer, new_) \ log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ "You are now #N", (new_)) #define log_chghost(s, buffer, old, new_) \ log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \ "#N is now #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_outcoming_orphan_action(s, target, text) \ log_server_status ((s), (s)->buffer, "MSG(#n): #a*#r #m", (target), \ ATTR_ACTION, (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 char * buffer_get_log_path (struct buffer *buffer) { struct str path = str_make (); 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); str_append_c (&path, '/'); make_log_filename (buffer->name, &path); str_append (&path, ".log"); return str_steal (&path); } static void buffer_open_log_file (struct app_context *ctx, struct buffer *buffer) { if (!ctx->logging || buffer->log_file) return; // TODO: should we try to reopen files wrt. case mapping? // - Need to read the whole directory and look for matches: // irc_server_strcmp(buffer->s, d_name, make_log_filename()) // remember to strip the ".log" suffix from d_name, case-sensitively. // - The tolower_ascii() in make_log_filename() is a perfect overlap, // it may stay as-is. // - buffer_get_log_path() will need to return a FILE *, // or an error that includes the below message. char *path = buffer_get_log_path (buffer); if (!(buffer->log_file = fopen (path, "ab"))) log_global_error (ctx, "Couldn't open log file `#s': #l", path, strerror (errno)); else set_cloexec (fileno (buffer->log_file)); 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); // Normally this doesn't cause changes in the prompt but a prompt hook // could decide to show some information for all buffers nonetheless 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); CALL_ (ctx->input, buffer_destroy, 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; if (buffer->type == BUFFER_SERVER) buffer->server->buffer = NULL; str_map_set (&ctx->buffers_by_name, buffer->name, NULL); LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); buffer_unref (buffer); refresh_prompt (ctx); } static void buffer_print_read_marker (struct app_context *ctx, FILE *stream, int flush_opts) { struct formatter f = formatter_make (ctx, NULL); const int timestamp_width = 8; // hardcoded to %T right now, simple const char *marker_char = get_config_string (ctx->config.root, "behaviour.read_marker_char"); // We could turn this off on FLUSH_OPT_NOWRAP, however our default pager // wraps lines for us even if we don't do it ourselves, and thus there's // no need to worry about inconsistency. if (*marker_char) { struct str s = str_make (); for (int i = 0; i < timestamp_width; i++) str_append (&s, marker_char); formatter_add (&f, "#a#s#r", ATTR_TIMESTAMP, s.str); str_reset (&s); for (int i = timestamp_width; i < g_terminal.columns; i++) str_append (&s, marker_char); formatter_add (&f, "#a#s#r\n", ATTR_READ_MARKER, s.str); str_free (&s); } else formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER); formatter_flush (&f, stream, flush_opts); // Flush the trailing formatting reset item fflush (stream); formatter_free (&f); } static void buffer_print_backlog (struct app_context *ctx, struct buffer *buffer) { // The prompt can take considerable time to redraw CALL (ctx->input, hide); // Simulate curses-like fullscreen buffers if the terminal allows it if (g_terminal.initialized && clear_screen) { terminal_printer_fn printer = get_attribute_printer (stdout); tputs (clear_screen, 1, printer); if (cursor_to_ll) tputs (cursor_to_ll, 1, printer); else if (row_address) tputs (tparm (row_address, g_terminal.lines - 1, 0, 0, 0, 0, 0, 0, 0, 0), 1, printer); else if (cursor_address) tputs (tparm (cursor_address, g_terminal.lines - 1, 0, 0, 0, 0, 0, 0, 0, 0), 1, printer); fflush (stdout); // We should update "last_displayed_msg_time" here just to be sure // that the first date marker, if necessary, is shown, but in practice // the value should always be from today when this function is called } else { char *buffer_name_localized = iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL); print_status ("%s", buffer_name_localized); free (buffer_name_localized); } // That is, minus the readline prompt (taking at least one line) int display_limit = MAX (10, g_terminal.lines - 1); int to_display = 0; struct buffer_line *line; for (line = buffer->lines_tail; line; line = line->prev) { to_display++; if (buffer_line_will_show_up (buffer, line)) display_limit--; if (!line->prev || display_limit <= 0) break; } // Once we've found where we want to start with the backlog, print it int until_marker = to_display - (int) buffer->new_messages_count; for (; line; line = line->next) { if (until_marker-- == 0 && buffer->new_messages_count != buffer->lines_count) buffer_print_read_marker (ctx, stdout, 0); buffer_line_display (ctx, buffer, line, 0); } // So that it is obvious if the last line in the buffer is not from today buffer_update_time (ctx, time (NULL), stdout, 0); refresh_prompt (ctx); CALL (ctx->input, show); } static void buffer_activate (struct app_context *ctx, struct buffer *buffer) { if (ctx->current_buffer == buffer) return; // This is the only place where the unread messages marker // and highlight indicator are reset if (ctx->current_buffer) { ctx->current_buffer->new_messages_count = 0; ctx->current_buffer->new_unimportant_count = 0; ctx->current_buffer->highlighted = false; } buffer_print_backlog (ctx, buffer); CALL_ (ctx->input, buffer_switch, 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; 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); cstr_set (&buffer->name, xstrdup (new_name)); buffer_close_log_file (buffer); buffer_open_log_file (ctx, buffer); // 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_count (struct app_context *ctx) { int total = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) total++; return total; } static void buffer_move (struct app_context *ctx, struct buffer *buffer, int n) { hard_assert (n >= 1 && n <= buffer_count (ctx)); LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); struct buffer *following = ctx->buffers; while (--n && following) following = following->next; LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following); refresh_prompt (ctx); } 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 buffer_remove_safe (struct app_context *ctx, struct buffer *buffer) { if (buffer == ctx->current_buffer) buffer_activate (ctx, ctx->last_buffer ? ctx->last_buffer : buffer_next (ctx, 1)); buffer_remove (ctx, buffer); } static void init_global_buffer (struct app_context *ctx) { struct buffer *global = ctx->global_buffer = buffer_new (ctx->input, BUFFER_GLOBAL, xstrdup (PROGRAM_NAME)); buffer_add (ctx, global); buffer_activate (ctx, global); } // --- Users, channels --------------------------------------------------------- static char * irc_make_buffer_name (struct server *s, const char *target) { if (!target) return xstrdup (s->name); // XXX: this may be able to trigger the uniqueness assertion with non-UTF-8 char *target_utf8 = irc_to_utf8 (target); char *result = xstrdup_printf ("%s.%s", s->name, target_utf8); free (target_utf8); return result; } 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 (nickname); (void) user_weak_ref (user, irc_user_on_destroy, s); 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 (s->ctx->input, BUFFER_PM, irc_make_buffer_name (s, nickname)); buffer->server = s; buffer->user = user; str_map_set (&s->irc_buffer_map, user->nickname, buffer); buffer_add (s->ctx, buffer); return buffer; } static void irc_get_channel_user_prefix (struct server *s, struct channel_user *channel_user, struct str *output) { if (s->ctx->show_all_prefixes) str_append (output, channel_user->prefixes); else if (channel_user->prefixes[0]) str_append_c (output, channel_user->prefixes[0]); } static bool irc_channel_is_joined (struct channel *channel) { // TODO: find a better way of checking if we're on a channel return !!channel->users_len; } // 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 (channel); LIST_PREPEND (user->channels, user_channel); struct channel_user *channel_user = channel_user_new (user, prefixes); LIST_PREPEND (channel->users, channel_user); channel->users_len++; } 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); } // TODO: poll the away status for users we don't share a channel with. // It might or might not be worth to auto-set this on with RPL_AWAY. if (!user->channels && user != channel->s->irc_user) user->away = false; // Then just unlink the user from the channel LIST_UNLINK (channel->users, channel_user); channel_user_destroy (channel_user); channel->users_len--; } 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 (s, name); (void) channel_weak_ref (channel, irc_channel_on_destroy, s); 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) { strv_reset (&channel->names_buf); channel->show_names_after_who = false; 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 channel *channel, struct buffer *buffer) { struct server *s = channel->s; 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; s->irc_users = str_map_make (NULL); s->irc_channels = str_map_make (NULL); s->irc_buffer_map = str_map_make (NULL); 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; iter = str_map_iter_make (&old_users); while ((user = str_map_iter_next (&iter))) irc_try_readd_user (s, user, str_map_find (&old_buffer_map, user->nickname)); iter = str_map_iter_make (&old_channels); while ((channel = str_map_iter_next (&iter))) irc_try_readd_channel (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); poller_timer_reset (&s->autojoin_tmr); } static void irc_reset_connection_timeouts (struct server *s) { poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000); poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000); poller_timer_reset (&s->reconnect_tmr); } static int64_t irc_get_reconnect_delay (struct server *s) { int64_t delay = get_config_integer (s->config, "reconnect_delay"); int64_t delay_factor = get_config_integer (s->ctx->config.root, "behaviour.reconnect_delay_growing"); for (unsigned i = 0; i < s->reconnect_attempt; i++) { if (delay_factor && delay > INT64_MAX / delay_factor) break; delay *= delay_factor; } int64_t delay_max = get_config_integer (s->ctx->config.root, "behaviour.reconnect_delay_max"); return MIN (delay, delay_max); } 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; // XXX: maybe add a state for when a connect is queued? hard_assert (s->state == IRC_DISCONNECTED); int64_t delay = irc_get_reconnect_delay (s); s->reconnect_attempt++; 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_process_sent_message (const struct irc_message *msg, struct server *s); 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_make (); str_append_vprintf (&str, format, ap); va_end (ap); log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str); struct irc_message msg; irc_parse_message (&msg, str.str); irc_process_sent_message (&msg, s); irc_free_message (&msg); 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) // XXX: we get ENOTCONN with OpenSSL (not plain) when a localhost // server is aborted, why? strace says read 0, write 31, shutdown -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) { if (s->connector) connector_free (s->connector); free (s->connector); s->connector = NULL; if (s->socks_conn) socks_connector_free (s->socks_conn); free (s->socks_conn); s->socks_conn = NULL; // Not connecting anymore s->state = IRC_DISCONNECTED; } static void try_finish_quit (struct app_context *ctx) { if (!ctx->quitting) return; bool disconnected_all = true; struct str_map_iter iter = str_map_iter_make (&ctx->servers); 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 irc_destroy_transport (struct server *s) { if (s->transport && s->transport->cleanup) s->transport->cleanup (s); s->transport = NULL; poller_fd_reset (&s->socket_event); xclose (s->socket); s->socket = -1; s->state = IRC_DISCONNECTED; 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_make (&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); cstr_set (&s->irc_user_host, NULL); strv_reset (&s->cap_ls_buf); s->cap_away_notify = false; s->cap_echo_message = false; s->cap_sasl = 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_make (&s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, "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 { s->reconnect_attempt = 0; 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)); // It can take a very long time for sending QUIT to take effect if (s->manual_disconnect) { log_server_error (s, s->buffer, "#s: #s", "Disconnected from server", "connection torn down early per user request"); irc_disconnect (s); return; } if (reason) irc_send (s, "QUIT :%s", reason); else // TODO: make the default QUIT message customizable // -> global/per server/both? // -> implement it with an output hook in a plugin? irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION); s->manual_disconnect = true; irc_shutdown (s); } static void request_quit (struct app_context *ctx, const char *message) { if (!ctx->quitting) { log_global_status (ctx, "Shutting down"); ctx->quitting = true; // Disable the user interface CALL (ctx->input, hide); } struct str_map_iter iter = str_map_iter_make (&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_initiate_disconnect (s, message); else if (s->state == IRC_CONNECTING) irc_destroy_connector (s); } try_finish_quit (ctx); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void on_irc_ping_timeout (void *user_data) { struct server *s = user_data; log_server_error (s, s->buffer, "#s: #s", "Disconnected from server", "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)); } static void on_irc_autojoin_timeout (void *user_data) { struct server *s = user_data; // Since we may not have information from RPL_ISUPPORT yet, // it's our safest bet to send the channels one at a time struct str_map joins_sent = str_map_make (NULL); // We don't know the casemapping yet either, however ASCII should do joins_sent.key_xfrm = tolower_ascii_strxfrm; // First join autojoin channels in their given order const char *autojoin = get_config_string (s->config, "autojoin"); if (autojoin) { struct strv v = strv_make (); cstr_split (autojoin, ",", true, &v); for (size_t i = 0; i < v.len; i++) { irc_send (s, "JOIN %s", v.vector[i]); str_map_set (&joins_sent, v.vector[i], (void *) 1); } strv_free (&v); } // Then also rejoin any channels from the last disconnect struct str_map_iter iter = str_map_iter_make (&s->irc_channels); struct channel *channel; while ((channel = str_map_iter_next (&iter))) { struct str target = str_make (); str_append (&target, channel->name); const char *key; if ((key = str_map_find (&channel->param_modes, "k"))) str_append_printf (&target, " %s", key); // When a channel is both autojoined and rejoined, both keys are tried if (!channel->left_manually && !str_map_find (&joins_sent, target.str)) irc_send (s, "JOIN %s", target.str); str_free (&target); } str_map_free (&joins_sent); } // --- Server I/O -------------------------------------------------------------- static char * irc_process_hooks (struct server *s, char *input) { log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, input); uint64_t hash = siphash_wrapper (input, strlen (input)); LIST_FOR_EACH (struct hook, iter, s->ctx->irc_hooks) { struct irc_hook *hook = (struct irc_hook *) iter; if (!(input = hook->filter (hook, s, input))) { log_server_debug (s, "#a>= #s#r", ATTR_JOIN, "thrown away by hook"); return NULL; } // The old input may get freed, so we compare against a hash of it uint64_t new_hash = siphash_wrapper (input, strlen (input)); if (new_hash != hash) log_server_debug (s, "#a>= \"#S\"#r", ATTR_JOIN, input); hash = new_hash; } return input; } static void irc_process_message (const struct irc_message *msg, struct server *s); static void irc_process_buffer_custom (struct server *s, struct str *buf) { const char *start = buf->str, *end = start + buf->len; for (const char *p = start; p + 1 < end; p++) { // Split the input on newlines if (p[0] != '\r' || p[1] != '\n') continue; char *processed = irc_process_hooks (s, xstrndup (start, p - start)); start = p + 2; if (!processed) continue; struct irc_message msg; irc_parse_message (&msg, processed); irc_process_message (&msg, s); irc_free_message (&msg); free (processed); } str_remove_slice (buf, 0, start - buf->str); } static enum socket_io_result irc_try_read (struct server *s) { enum socket_io_result result = s->transport->try_read (s); if (s->read_buffer.len >= (1 << 20)) { // XXX: this is stupid; if anything, count it in dependence of time; // we could make transport_tls_try_read() limit the immediate amount // of data read like socket_io_try_read() does and remove this check log_server_error (s, s->buffer, "The IRC server seems to spew out data frantically"); return SOCKET_IO_ERROR; } if (s->read_buffer.len) irc_process_buffer_custom (s, &s->read_buffer); return result; } static enum socket_io_result irc_try_write (struct server *s) { enum socket_io_result result = s->transport->try_write (s); if (result == SOCKET_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 socket_io_result read_result; enum socket_io_result write_result; if ((read_result = irc_try_read (s)) == SOCKET_IO_ERROR || (write_result = irc_try_write (s)) == SOCKET_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 == SOCKET_IO_EOF || write_result == SOCKET_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 == SOCKET_IO_EOF) return false; if (read_result == SOCKET_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 socket_io_result transport_plain_try_read (struct server *s) { enum socket_io_result result = socket_io_try_read (s->socket, &s->read_buffer); if (result == SOCKET_IO_ERROR) print_debug ("%s: %s", __func__, strerror (errno)); return result; } static enum socket_io_result transport_plain_try_write (struct server *s) { enum socket_io_result result = socket_io_try_write (s->socket, &s->write_buffer); if (result == SOCKET_IO_ERROR) print_debug ("%s: %s", __func__, strerror (errno)); return result; } 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, }; // --- TLS transport ----------------------------------------------------------- struct transport_tls_data { SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL connection bool ssl_rx_want_tx; ///< SSL_read() wants to write bool ssl_tx_want_rx; ///< SSL_write() wants to read }; /// The index in SSL_CTX user data for a reference to the server static int g_transport_tls_data_index = -1; static int transport_tls_verify_callback (int preverify_ok, X509_STORE_CTX *ctx) { SSL *ssl = X509_STORE_CTX_get_ex_data (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ()); struct server *s = SSL_CTX_get_ex_data (SSL_get_SSL_CTX (ssl), g_transport_tls_data_index); X509 *cert = X509_STORE_CTX_get_current_cert (ctx); char *subject = X509_NAME_oneline (X509_get_subject_name (cert), NULL, 0); char *issuer = X509_NAME_oneline (X509_get_issuer_name (cert), NULL, 0); log_server_status (s, s->buffer, "Certificate subject: #s", subject); log_server_status (s, s->buffer, "Certificate issuer: #s", issuer); if (!preverify_ok) { log_server_error (s, s->buffer, "Certificate verification failed: #s", X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx))); } free (subject); free (issuer); return preverify_ok; } static bool transport_tls_init_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path, struct error **e) { ERR_clear_error (); if (file || path) { if (SSL_CTX_load_verify_locations (ssl_ctx, file, path)) return true; return error_set (e, "%s: %s", "Failed to set locations for the CA certificate bundle", xerr_describe_error ()); } if (!SSL_CTX_set_default_verify_paths (ssl_ctx)) return error_set (e, "%s: %s", "Couldn't load the default CA certificate bundle", xerr_describe_error ()); return true; } static bool transport_tls_init_ca (struct server *s, SSL_CTX *ssl_ctx, struct error **e) { const char *ca_file = get_config_string (s->config, "tls_ca_file"); const char *ca_path = get_config_string (s->config, "tls_ca_path"); char *full_ca_file = ca_file ? resolve_filename (ca_file, resolve_relative_config_filename) : NULL; char *full_ca_path = ca_path ? resolve_filename (ca_path, resolve_relative_config_filename) : NULL; bool ok = false; if (ca_file && !full_ca_file) error_set (e, "Couldn't find the CA bundle file"); else if (ca_path && !full_ca_path) error_set (e, "Couldn't find the CA bundle path"); else ok = transport_tls_init_ca_set (ssl_ctx, full_ca_file, full_ca_path, e); free (full_ca_file); free (full_ca_path); return ok; } static bool transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e) { bool verify = get_config_boolean (s->config, "tls_verify"); SSL_CTX_set_verify (ssl_ctx, verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, transport_tls_verify_callback); if (g_transport_tls_data_index == -1) g_transport_tls_data_index = SSL_CTX_get_ex_new_index (0, "server", NULL, NULL, NULL); SSL_CTX_set_ex_data (ssl_ctx, g_transport_tls_data_index, s); const char *ciphers = get_config_string (s->config, "tls_ciphers"); if (ciphers && !SSL_CTX_set_cipher_list (ssl_ctx, ciphers)) log_server_error (s, s->buffer, "Failed to select any cipher from the cipher list"); SSL_CTX_set_mode (ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); // Disable deprecated protocols (see RFC 7568) SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); // This seems to consume considerable amounts of memory while not giving // that much in return; in addition to that, I'm not sure about security // (see RFC 7525, section 3.3) #ifdef SSL_OP_NO_COMPRESSION SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_COMPRESSION); #endif // SSL_OP_NO_COMPRESSION #ifdef LOMEM SSL_CTX_set_mode (ssl_ctx, SSL_MODE_RELEASE_BUFFERS); #endif // LOMEM struct error *error = NULL; if (!transport_tls_init_ca (s, ssl_ctx, &error)) { if (verify) { error_propagate (e, error); return false; } // Just 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 *tls_cert = get_config_string (s->config, "tls_cert"); if (!tls_cert) return true; ERR_clear_error (); bool result = false; char *path = resolve_filename (tls_cert, resolve_relative_config_filename); if (!path) error_set (e, "%s: %s", "Cannot open file", tls_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 TLS client certificate failed", xerr_describe_error ()); else result = true; free (path); return result; } static bool transport_tls_init (struct server *s, const char *hostname, struct error **e) { ERR_clear_error (); struct error *error = 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, &error)) goto error_ssl_2; SSL *ssl = SSL_new (ssl_ctx); if (!ssl) goto error_ssl_2; 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); error = NULL; } SSL_set_connect_state (ssl); if (!SSL_set_fd (ssl, s->socket)) goto error_ssl_3; // Enable SNI, FWIW; literal IP addresses aren't allowed struct in6_addr dummy; if (!inet_pton (AF_INET, hostname, &dummy) && !inet_pton (AF_INET6, hostname, &dummy)) SSL_set_tlsext_host_name (ssl, hostname); struct transport_tls_data *data = xcalloc (1, sizeof *data); data->ssl_ctx = ssl_ctx; data->ssl = ssl; // Forces a handshake even if neither side wants to transmit data data->ssl_rx_want_tx = true; s->transport_data = data; return true; error_ssl_3: SSL_free (ssl); error_ssl_2: SSL_CTX_free (ssl_ctx); error_ssl_1: if (!error) error_set (&error, "%s: %s", "Could not initialize TLS", xerr_describe_error ()); error_propagate (e, error); 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 socket_io_result transport_tls_try_read (struct server *s) { struct transport_tls_data *data = s->transport_data; if (data->ssl_tx_want_rx) return SOCKET_IO_OK; struct str *buf = &s->read_buffer; data->ssl_rx_want_tx = false; while (true) { ERR_clear_error (); str_reserve (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 SOCKET_IO_EOF; case SSL_ERROR_WANT_READ: return SOCKET_IO_OK; case SSL_ERROR_WANT_WRITE: data->ssl_rx_want_tx = true; return SOCKET_IO_OK; case XSSL_ERROR_TRY_AGAIN: continue; default: LOG_FUNC_FAILURE ("SSL_read", error_info); return SOCKET_IO_ERROR; } } } static enum socket_io_result transport_tls_try_write (struct server *s) { struct transport_tls_data *data = s->transport_data; if (data->ssl_rx_want_tx) return SOCKET_IO_OK; struct str *buf = &s->write_buffer; data->ssl_tx_want_rx = false; while (buf->len) { ERR_clear_error (); 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 SOCKET_IO_EOF; case SSL_ERROR_WANT_WRITE: return SOCKET_IO_OK; case SSL_ERROR_WANT_READ: data->ssl_tx_want_rx = true; return SOCKET_IO_OK; case XSSL_ERROR_TRY_AGAIN: continue; default: LOG_FUNC_FAILURE ("SSL_write", error_info); return SOCKET_IO_ERROR; } } return SOCKET_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 errno = 0; struct passwd *pwd = getpwuid (geteuid ()); if (!pwd) { return error_set (e, "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 strv v = strv_make (); cstr_split (get_config_string (s->config, "nicks"), ",", true, &v); char *result = NULL; if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len) result = xstrdup (v.vector[s->nick_counter++]); if ((size_t) s->nick_counter >= v.len) // Exhausted all nicknames s->nick_counter = -1; strv_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 capability negotiation, with up to 3.2 features; // at worst the server will ignore this or send a harmless error message irc_send (s, "CAP LS 302"); 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, const char *hostname) { struct app_context *ctx = s->ctx; // Most of our output comes from the user one full command at a time and we // use output buffering, so it makes a lot of sense to avoid these delays int yes = 1; soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes) != -1); set_blocking (socket, false); s->socket = socket; s->transport = get_config_boolean (s->config, "tls") ? &g_transport_tls : &g_transport_plain; struct error *e = NULL; if (s->transport->init && !s->transport->init (s, hostname, &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; s->socket_event = poller_fd_make (&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); } /// Unwrap IPv6 addresses in format_host_port_pair() format static void irc_split_host_port (char *s, char **host, char **port) { *host = s; *port = "6667"; char *right_bracket = strchr (s, ']'); if (s[0] == '[' && right_bracket) { *right_bracket = '\0'; *host = s + 1; s = right_bracket + 1; } char *colon = strchr (s, ':'); if (colon) { *colon = '\0'; *port = colon + 1; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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, const char *hostname) { struct server *s = user_data; char *hostname_copy = xstrdup (hostname); irc_destroy_connector (s); irc_finish_connection (s, socket, hostname_copy); free (hostname_copy); } static void irc_setup_connector (struct server *s, const struct strv *addresses) { struct connector *connector = xmalloc (sizeof *connector); connector_init (connector, &s->ctx->poller); s->connector = connector; 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; for (size_t i = 0; i < addresses->len; i++) { char *host, *port; irc_split_host_port (addresses->vector[i], &host, &port); connector_add_target (connector, host, port); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: see if we can further merge code for the two connectors, for example // by making SOCKS 4A and 5 mere plugins for the connector, or by using // a virtual interface common to them both (seems more likely) static void irc_on_socks_connecting (void *user_data, const char *address, const char *via, const char *version) { struct server *s = user_data; log_server_status (s, s->buffer, "Connecting to #s via #s (#s)...", address, via, version); } static bool irc_setup_connector_socks (struct server *s, const struct strv *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"); if (!socks_host) return false; struct socks_connector *connector = xmalloc (sizeof *connector); socks_connector_init (connector, &s->ctx->poller); s->socks_conn = connector; connector->user_data = s; connector->on_connecting = irc_on_socks_connecting; connector->on_error = irc_on_connector_error; connector->on_connected = irc_on_connector_connected; connector->on_failure = irc_on_connector_failure; for (size_t i = 0; i < addresses->len; i++) { char *host, *port; irc_split_host_port (addresses->vector[i], &host, &port); if (!socks_connector_add_target (connector, host, port, e)) return false; } char *service = xstrdup_printf ("%" PRIi64, socks_port_int); socks_connector_run (connector, socks_host, service, get_config_string (s->config, "socks_username"), get_config_string (s->config, "socks_password")); free (service); // The SOCKS connector can have already failed; we mustn't return true then if (!s->socks_conn) return error_set (e, "SOCKS connection failed"); return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 strv servers = strv_make (); cstr_split (addresses, ",", true, &servers); struct error *e = NULL; if (!irc_setup_connector_socks (s, &servers, &e) && !e) irc_setup_connector (s, &servers); strv_free (&servers); if (e) { irc_destroy_connector (s); log_server_error (s, s->buffer, "#s", e->message); error_free (e); irc_queue_reconnect (s); } else if (s->state != IRC_CONNECTED) s->state = IRC_CONNECTING; } // --- Input prompt ------------------------------------------------------------ static void make_unseen_prefix (struct app_context *ctx, struct str *active_buffers) { size_t buffer_no = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { buffer_no++; if (!(iter->new_messages_count - iter->new_unimportant_count) || iter == ctx->current_buffer) continue; if (active_buffers->len) str_append_c (active_buffers, ','); if (iter->highlighted) str_append_c (active_buffers, '!'); str_append_printf (active_buffers, "%zu", buffer_no); } } 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_make (&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_registered (struct buffer *buffer, struct str *output) { struct server *s = buffer->server; if (buffer->type == BUFFER_CHANNEL) { struct channel_user *channel_user = irc_channel_get_user (buffer->channel, s->irc_user); if (channel_user) irc_get_channel_user_prefix (s, channel_user, output); } 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_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 make_server_postfix_registered (buffer, output); } static void make_prompt (struct app_context *ctx, struct str *output) { LIST_FOR_EACH (struct hook, iter, ctx->prompt_hooks) { struct prompt_hook *hook = (struct prompt_hook *) iter; char *made = hook->make (hook); if (made) { str_append (output, made); free (made); return; } } struct buffer *buffer = ctx->current_buffer; if (!buffer) return; str_append_c (output, '['); struct str active_buffers = str_make (); 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); // We remember old modes, don't show them while we're not on the channel if (buffer->type == BUFFER_CHANNEL && buffer->channel->users_len) { struct str modes = str_make (); make_chanmode_postfix (buffer->channel, &modes); if (modes.len) str_append_printf (output, "(+%s)", modes.str); str_free (&modes); str_append_printf (output, "{%zu}", buffer->channel->users_len); } if (buffer->hide_unimportant) str_append (output, ""); if (buffer != ctx->global_buffer) make_server_postfix (buffer, output); str_append_c (output, ']'); str_append_c (output, ' '); } static void input_maybe_set_prompt (struct input *self, char *new_prompt) { // Fix libedit's expectations to see a non-control character following // the end mark (see prompt.c and literal.c) by cleaning this up for (char *p = new_prompt; *p; ) if (p[0] == INPUT_END_IGNORE && p[1] == INPUT_START_IGNORE) memmove (p, p + 2, strlen (p + 2) + 1); else p++; // Redisplay can be an expensive operation const char *prompt = CALL (self, get_prompt); if (prompt && !strcmp (new_prompt, prompt)) free (new_prompt); else CALL_ (self, set_prompt, new_prompt); } static void on_refresh_prompt (struct app_context *ctx) { poller_idle_reset (&ctx->prompt_event); bool have_attributes = !!get_attribute_printer (stdout); struct str prompt = str_make (); make_prompt (ctx, &prompt); // libedit has a weird bug where it misapplies ignores when they're not // followed by anything else, so let's try to move a trailing space, // which will at least fix the default prompt. const char *attributed_suffix = ""; #ifdef HAVE_EDITLINE if (have_attributes && prompt.len && prompt.str[prompt.len - 1] == ' ') { prompt.str[--prompt.len] = 0; attributed_suffix = " "; } // Also enable a uniform interface for prompt hooks by assuming it uses // GNU Readline escapes: turn this into libedit's almost-flip-flop for (size_t i = 0; i < prompt.len; i++) if (prompt.str[i] == '\x01' || prompt.str[i] == '\x02') prompt.str[i] = INPUT_START_IGNORE /* == INPUT_END_IGNORE */; #endif // HAVE_EDITLINE char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL); str_free (&prompt); if (have_attributes) { // XXX: to be completely correct, we should use tputs, but we cannot input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s", INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], INPUT_END_IGNORE, localized, INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], INPUT_END_IGNORE, attributed_suffix)); free (localized); } else input_maybe_set_prompt (ctx->input, localized); } // --- Helpers ----------------------------------------------------------------- static struct buffer * irc_get_buffer_for_message (struct server *s, const struct irc_message *msg, const char *target) { // TODO: display such messages differently target = irc_skip_statusmsg (s, 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); // This is weird if (!channel) return NULL; } else if (!buffer) { // Outgoing messages needn't have a prefix, no buffer associated if (!msg->prefix) return NULL; // 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; // Strip formatting from the message so that it doesn't interfere // with nickname detection (colour sequences in particular) struct formatter f = formatter_make (s->ctx, NULL); formatter_parse_message (&f, message); struct str stripped = str_make (); for (size_t i = 0; i < f.items_len; i++) { if (f.items[i].type == FORMATTER_ITEM_TEXT) str_append (&stripped, f.items[i].text); } formatter_free (&f); // Well, this is rather crude but it should make most users happy. // We could do this in proper Unicode but that's two more conversions per // message when both the nickname and the message are likely valid UTF-8. char *copy = str_steal (&stripped); cstr_transform (copy, s->irc_tolower); char *nick = xstrdup (s->irc_user->nickname); cstr_transform (nick, s->irc_tolower); // Special characters allowed in nicknames by RFC 2812: []\`_^{|} and - // Also excluded from the ASCII: common user channel prefixes: +%@&~ // XXX: why did I exclude those? It won't match when IRC newbies use them. 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 char * irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target) { struct str prefix = str_make (); if (user && irc_is_channel (s, (target = irc_skip_statusmsg (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))) irc_get_channel_user_prefix (s, channel_user, &prefix); } return str_steal (&prefix); } // --- 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 unsigned changes; ///< Count of all changes unsigned usermode_changes; ///< Count of all usermode changes }; /// 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; // Translate mode character to user prefix character const char *all_prefixes = self->s->irc_chanuser_prefixes; const char *all_modes = self->s->irc_chanuser_modes; const char *mode = strchr (all_modes, self->mode_char); hard_assert (mode && (size_t) (mode - all_modes) < strlen (all_prefixes)); char prefix = all_prefixes[mode - all_modes]; char **prefixes = &channel_user->prefixes; char *pos = strchr (*prefixes, prefix); if (self->adding == !!pos) return; if (self->adding) { // Add the new mode prefix while retaining the right order struct str buf = str_make (); for (const char *p = all_prefixes; *p; p++) if (*p == prefix || strchr (*prefixes, *p)) str_append_c (&buf, *p); cstr_set (prefixes, str_steal (&buf)); } else memmove (pos, pos + 1, strlen (pos)); } 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) { self->changes++; if (strchr (self->s->irc_chanuser_modes, self->mode_char)) { self->usermode_changes++; 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; } /// Returns whether the change has only affected channel user modes static bool irc_handle_mode_channel (struct channel *channel, char **params) { struct mode_processor p = { .s = channel->s, .channel = channel }; mode_processor_run (&p, params, mode_processor_apply_channel); return p.changes == p.usermode_changes; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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); } // --- Output processing ------------------------------------------------------- // Both user and plugins can send whatever the heck they want to, // we need to parse it back so that it's evident what's happening static void irc_handle_sent_cap (struct server *s, const struct irc_message *msg) { if (msg->params.len < 1) return; const char *subcommand = msg->params.vector[0]; const char *args = (msg->params.len > 1) ? msg->params.vector[1] : ""; if (!strcasecmp_ascii (subcommand, "REQ")) log_server_status (s, s->buffer, "#s: #S", "Capabilities requested", args); } static void irc_handle_sent_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 && soft_assert (s->irc_user)) log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str); else log_outcoming_orphan_notice (s, target, text->str); } static void irc_handle_sent_notice (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2 || s->cap_echo_message) return; // This ignores empty messages which we should not normally send struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]); LIST_FOR_EACH (struct ctcp_chunk, iter, chunks) { if (iter->is_extended) log_ctcp_reply (s, msg->params.vector[0], xstrdup_printf ("%s %s", iter->tag.str, iter->text.str)); else irc_handle_sent_notice_text (s, msg, &iter->text); } ctcp_destroy (chunks); } static void irc_handle_sent_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 && soft_assert (s->irc_user)) { char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target); if (is_action) log_outcoming_action (s, buffer, s->irc_user->nickname, text->str); else log_outcoming_privmsg (s, buffer, prefixes, s->irc_user->nickname, text->str); free (prefixes); } else if (is_action) log_outcoming_orphan_action (s, target, text->str); else log_outcoming_orphan_privmsg (s, target, text->str); } static void irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2 || s->cap_echo_message) return; // This ignores empty messages which we should not normally send // and the server is likely going to reject with an error reply 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_sent_privmsg_text (s, msg, &iter->text, false); else if (!strcmp (iter->tag.str, "ACTION")) irc_handle_sent_privmsg_text (s, msg, &iter->text, true); else log_ctcp_query (s, msg->params.vector[0], iter->tag.str); } ctcp_destroy (chunks); } static struct irc_handler { const char *name; void (*handler) (struct server *s, const struct irc_message *msg); } g_irc_sent_handlers[] = { // This list needs to stay sorted { "CAP", irc_handle_sent_cap }, { "NOTICE", irc_handle_sent_notice }, { "PRIVMSG", irc_handle_sent_privmsg }, }; 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 void irc_process_sent_message (const struct irc_message *msg, struct server *s) { // The server is free to reject even a matching prefix // XXX: even though no prefix should normally be present, this is racy if (msg->prefix && !irc_is_this_us (s, msg->prefix)) return; struct irc_handler key = { .name = msg->command }; struct irc_handler *handler = bsearch (&key, g_irc_sent_handlers, N_ELEMENTS (g_irc_sent_handlers), sizeof key, irc_handler_cmp_by_name); if (handler) handler->handler (s, msg); } // --- Input handling ---------------------------------------------------------- static void irc_handle_authenticate (struct server *s, const struct irc_message *msg) { if (msg->params.len < 1) return; // Empty challenge -> empty response for e.g. SASL EXTERNAL, // abort anything else as it doesn't make much sense to let the user do it if (!strcmp (msg->params.vector[0], "+")) irc_send (s, "AUTHENTICATE +"); else irc_send (s, "AUTHENTICATE *"); } static void irc_handle_away (struct server *s, const struct irc_message *msg) { if (!msg->prefix) return; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); free (nickname); // Let's allow the server to make us away if (user) user->away = !!msg->params.len; } static void irc_process_cap_ls (struct server *s) { log_server_status (s, s->buffer, "#s: #&S", "Capabilities supported", strv_join (&s->cap_ls_buf, " ")); struct strv chosen = strv_make (); struct strv use = strv_make (); cstr_split (get_config_string (s->config, "capabilities"), ",", true, &use); // Filter server capabilities for ones we can make use of for (size_t i = 0; i < s->cap_ls_buf.len; i++) { const char *cap = s->cap_ls_buf.vector[i]; size_t cap_name_len = strcspn (cap, "="); for (size_t k = 0; k < use.len; k++) if (!strncasecmp_ascii (use.vector[k], cap, cap_name_len)) strv_append_owned (&chosen, xstrndup (cap, cap_name_len)); } strv_reset (&s->cap_ls_buf); char *chosen_str = strv_join (&chosen, " "); strv_free (&chosen); strv_free (&use); // XXX: with IRCv3.2, this may end up being too long for one message, // and we need to be careful with CAP END. One probably has to count // the number of sent CAP REQ vs the number of received CAP ACK/NAK. if (s->state == IRC_CONNECTED) irc_send (s, "CAP REQ :%s", chosen_str); free (chosen_str); } static void irc_toggle_cap (struct server *s, const char *cap, bool active) { if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active; if (!strcasecmp_ascii (cap, "away-notify")) s->cap_away_notify = active; if (!strcasecmp_ascii (cap, "sasl")) s->cap_sasl = active; } static void irc_try_finish_cap_negotiation (struct server *s) { // It does not make sense to do this post-registration, although it would // not hurt either, as the server must ignore it in that case if (s->state == IRC_CONNECTED) irc_send (s, "CAP END"); } static void irc_handle_cap (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; struct strv v = strv_make (); const char *args = ""; if (msg->params.len > 2) cstr_split ((args = msg->params.vector[2]), " ", true, &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++; } irc_toggle_cap (s, cap, active); } if (s->cap_sasl && s->transport == &g_transport_tls) irc_send (s, "AUTHENTICATE EXTERNAL"); else irc_try_finish_cap_negotiation (s); } else if (!strcasecmp_ascii (subcommand, "NAK")) { log_server_error (s, s->buffer, "#s: #S", "Capabilities not acknowledged", args); irc_try_finish_cap_negotiation (s); } else if (!strcasecmp_ascii (subcommand, "DEL")) { log_server_error (s, s->buffer, "#s: #S", "Capabilities deleted", args); for (size_t i = 0; i < v.len; i++) irc_toggle_cap (s, v.vector[i], false); } else if (!strcasecmp_ascii (subcommand, "LS")) { if (msg->params.len > 3 && !strcmp (args, "*")) cstr_split (msg->params.vector[3], " ", true, &s->cap_ls_buf); else { strv_append_vector (&s->cap_ls_buf, v.vector); irc_process_cap_ls (s); } } strv_free (&v); } static void irc_handle_chghost (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); free (nickname); if (!user) return; char *new_prefix = xstrdup_printf ("%s!%s@%s", user->nickname, msg->params.vector[0], msg->params.vector[1]); if (irc_is_this_us (s, msg->prefix)) { cstr_set (&s->irc_user_host, xstrdup_printf ("%s@%s", msg->params.vector[0], msg->params.vector[1])); log_chghost_self (s, s->buffer, new_prefix); // Log a message in all open buffers on this server struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) log_chghost_self (s, buffer, new_prefix); } else { // Log a message in any PM buffer struct buffer *buffer = str_map_find (&s->irc_buffer_map, user->nickname); if (buffer) log_chghost (s, buffer, msg->prefix, new_prefix); // Log a message in all channels the user is in LIST_FOR_EACH (struct user_channel, iter, user->channels) { buffer = str_map_find (&s->irc_buffer_map, iter->channel->name); hard_assert (buffer != NULL); log_chghost (s, buffer, msg->prefix, new_prefix); } } free (new_prefix); } static void irc_handle_error (struct server *s, const struct irc_message *msg) { if (msg->params.len < 1) return; log_server_error (s, s->buffer, "#m", msg->params.vector[0]); } 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); // We've joined a new channel if (!channel) { // This is weird, ignoring if (!irc_is_this_us (s, msg->prefix)) return; buffer = buffer_new (s->ctx->input, BUFFER_CHANNEL, irc_make_buffer_name (s, 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); char *input = CALL_ (s->ctx->input, get_line, NULL); if (!*input) buffer_activate (s->ctx, buffer); else buffer->highlighted = true; free (input); } if (irc_is_this_us (s, msg->prefix)) { // Reset the field so that we rejoin the channel after reconnecting channel->left_manually = false; // Request the channel mode as we don't get it automatically str_reset (&channel->no_param_modes); str_map_clear (&channel->param_modes); irc_send (s, "MODE %s", channel_name); if ((channel->show_names_after_who = s->cap_away_notify)) irc_send (s, "WHO %s", channel_name); } // 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, BUFFER_LINE_UNIMPORTANT, "#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); // It would be 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_make (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_kill (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 *comment = msg->params.vector[1]; if (irc_is_this_us (s, target)) log_server_status (s, s->buffer, "You've been killed by #n (#m)", msg->prefix, comment); } 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 strv copy = strv_make (); strv_append_vector (©, msg->params.vector + 1); char *modes = strv_join (©, " "); strv_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); int flags = 0; if (channel && irc_handle_mode_channel (channel, msg->params.vector + 1)) // This is 90% automode spam, let's not let it steal attention, // maybe this behaviour should be configurable though flags = BUFFER_LINE_UNIMPORTANT; if (buffer) { log_server (s, buffer, BUFFER_LINE_STATUS | flags, "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); } 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_make (&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); cstr_set (&user->nickname, xstrdup (new_nickname)); } 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); // It would be 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_make (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, BUFFER_LINE_UNIMPORTANT, &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_make (); 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); 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)) { if (s->cap_echo_message) log_ctcp_query (s, target, chunk->tag.str); if (!irc_is_this_us (s, target)) return; } struct formatter f = formatter_make (s->ctx, s); formatter_add (&f, "CTCP requested by #n", msg->prefix); if (irc_is_channel (s, irc_skip_statusmsg (s, target))) formatter_add (&f, " (to #S)", target); formatter_add (&f, ": #S", chunk->tag.str); log_formatter (s->ctx, s->buffer, BUFFER_LINE_STATUS, &f); char *nickname = irc_cut_nickname (msg->prefix); if (!strcmp (chunk->tag.str, "CLIENTINFO")) irc_send_ctcp_reply (s, nickname, "CLIENTINFO %s %s %s %s", "PING", "VERSION", "TIME", "CLIENTINFO"); else if (!strcmp (chunk->tag.str, "PING")) irc_send_ctcp_reply (s, nickname, "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, nickname, "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, nickname, "TIME %s", buf); } free (nickname); } 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); char *prefixes = irc_get_privmsg_prefix (s, str_map_find (&s->irc_users, nickname), target); // Make autocomplete offer recent speakers first on partial matches // (note that freshly joined users also move to the front) struct user *user; struct channel_user *channel_user; if (!irc_is_this_us (s, msg->prefix) && buffer->channel && (user = str_map_find (&s->irc_users, nickname)) && (channel_user = irc_channel_get_user (buffer->channel, user))) { LIST_UNLINK (buffer->channel->users, channel_user); LIST_PREPEND (buffer->channel->users, channel_user); } // 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); free (prefixes); } 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_make (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, BUFFER_LINE_UNIMPORTANT, &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_tagmsg (struct server *s, const struct irc_message *msg) { // TODO: here we can process "typing" tags, once we find this useful (void) s; (void) msg; } 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); // It would be weird for this to be false if (channel) cstr_set (&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 void irc_handle_wallops (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *message = msg->params.vector[0]; log_server (s, s->buffer, 0, "<#n> #m", msg->prefix, message); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static struct irc_handler g_irc_handlers[] = { // This list needs to stay sorted { "AUTHENTICATE", irc_handle_authenticate }, { "AWAY", irc_handle_away }, { "CAP", irc_handle_cap }, { "CHGHOST", irc_handle_chghost }, { "ERROR", irc_handle_error }, { "INVITE", irc_handle_invite }, { "JOIN", irc_handle_join }, { "KICK", irc_handle_kick }, { "KILL", irc_handle_kill }, { "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 }, { "TAGMSG", irc_handle_tagmsg }, { "TOPIC", irc_handle_topic }, { "WALLOPS", irc_handle_wallops }, }; 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)) { cstr_set (&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 strv v = strv_make (); cstr_split (m, " ", true, &v); for (size_t i = 0; i < v.len; i++) if (irc_try_parse_word_for_userhost (s, v.vector[i])) break; strv_free (&v); } static bool process_input_utf8 (struct app_context *, struct buffer *, const char *, int); static void on_autoaway_timer (struct app_context *ctx); 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); cstr_set (&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) // TODO: maybe rather always use RPL_ISUPPORT NICKLEN & USERLEN & HOSTLEN // since we don't seem to follow any subsequent changes in userhost; // unrealircd sends RPL_HOSTHIDDEN (396), which has an optional user part, // and there is also CAP CHGHOST which /may/ send it to ourselves irc_send (s, "USERHOST %s", s->irc_user->nickname); // A little hack that reinstates auto-away status when we get disconnected if (s->autoaway_active) on_autoaway_timer (s->ctx); const char *command = get_config_string (s->config, "command"); if (command) { log_server_debug (s, "Executing \"#s\"", command); process_input_utf8 (s->ctx, s->buffer, command, 0); } int64_t command_delay = get_config_integer (s->config, "command_delay"); log_server_debug (s, "Autojoining channels in #&s seconds...", xstrdup_printf ("%" PRId64, command_delay)); poller_timer_set (&s->autojoin_tmr, command_delay * 1000); } 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 strv v = strv_make (); cstr_split (response, " ", true, &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)) cstr_set (&s->irc_user_host, xstrdup (userhost)); } strv_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? } 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) cstr_split (nicks, " ", true, &channel->names_buf); else log_server_status (s, s->buffer, "Users on #S: #S", channel_name, nicks); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct channel_user_sort_entry { struct server *s; ///< Server (because of the qsort API) struct channel_user *channel_user; ///< Channel user }; static int channel_user_sort_entry_cmp (const void *entry_a, const void *entry_b) { const struct channel_user_sort_entry *a = entry_a; const struct channel_user_sort_entry *b = entry_b; struct server *s = a->s; // First order by the most significant channel user prefix const char *prio_a = strchr (s->irc_chanuser_prefixes, a->channel_user->prefixes[0]); const char *prio_b = strchr (s->irc_chanuser_prefixes, b->channel_user->prefixes[0]); // Put unrecognized prefixes at the end of the list if (prio_a || prio_b) { if (!prio_a) return 1; if (!prio_b) return -1; if (prio_a != prio_b) return prio_a - prio_b; } return irc_server_strcmp (s, a->channel_user->user->nickname, b->channel_user->user->nickname); } static void irc_sort_channel_users (struct channel *channel) { size_t n_users = channel->users_len; struct channel_user_sort_entry entries[n_users], *p = entries; LIST_FOR_EACH (struct channel_user, iter, channel->users) { p->s = channel->s; p->channel_user = iter; p++; } qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp); channel->users = NULL; while (p-- != entries) LIST_PREPEND (channel->users, p->channel_user); } static char * make_channel_users_list (struct channel *channel) { size_t n_users = channel->users_len; struct channel_user_sort_entry entries[n_users], *p = entries; LIST_FOR_EACH (struct channel_user, iter, channel->users) { p->s = channel->s; p->channel_user = iter; p++; } qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp); // Make names of users that are away italicised, constructing a formatter // and adding a new attribute seems like unnecessary work struct str list = str_make (); for (size_t i = 0; i < n_users; i++) { struct channel_user *channel_user = entries[i].channel_user; if (channel_user->user->away) str_append_c (&list, '\x1d'); irc_get_channel_user_prefix (channel->s, channel_user, &list); str_append (&list, channel_user->user->nickname); if (channel_user->user->away) str_append_c (&list, '\x1d'); str_append_c (&list, ' '); } if (list.len) list.str[--list.len] = '\0'; return str_steal (&list); } static void irc_sync_channel_user (struct channel *channel, const char *nickname, const char *prefixes) { struct user *user = irc_get_or_make_user (channel->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[0] != prefixes[0]) cstr_set (&channel_user->prefixes, xstrdup (prefixes)); } static void irc_process_names_finish (struct channel *channel) { struct server *s = channel->s; struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name); if (buffer) { log_server_status (channel->s, buffer, "Users on #S: #&m", channel->name, make_channel_users_list (channel)); } } static void irc_process_names (struct channel *channel) { struct str_map present = str_map_make (NULL); present.key_xfrm = channel->s->irc_strxfrm; // Either that, or there is no other inhabitant, and sorting does nothing bool we_have_just_joined = channel->users_len == 1; struct strv *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, channel->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 (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); strv_reset (&channel->names_buf); if (we_have_just_joined) irc_sort_channel_users (channel); if (!channel->show_names_after_who) irc_process_names_finish (channel); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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_make (&s->irc_channels); struct channel *channel; while ((channel = str_map_iter_next (&iter))) irc_process_names (channel); } else if (channel) irc_process_names (channel); } static bool irc_handle_rpl_whoreply (struct server *s, const struct irc_message *msg) { if (msg->params.len < 7) return false; // Sequence: channel, user, host, server, nick, chars const char *channel_name = msg->params.vector[1]; const char *nickname = msg->params.vector[5]; const char *chars = msg->params.vector[6]; struct channel *channel = str_map_find (&s->irc_channels, channel_name); struct user *user = str_map_find (&s->irc_users, nickname); // This makes sense to set only with the away-notify capability so far. if (!channel || !channel->show_names_after_who) return false; // We track ourselves by other means and we can't track PM-only users yet. if (user && user != s->irc_user && user->channels) user->away = *chars == 'G'; return true; } static bool irc_handle_rpl_endofwho (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return false; const char *target = msg->params.vector[1]; struct channel *channel = str_map_find (&s->irc_channels, target); if (!channel || !channel->show_names_after_who) return false; irc_process_names_finish (channel); channel->show_names_after_who = false; return true; } 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); if (channel) cstr_set (&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); if (channel) { str_reset (&channel->no_param_modes); str_map_clear (&channel->param_modes); irc_handle_mode_channel (channel, msg->params.vector + 1); } // XXX: do we want to log a message? } 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); 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); 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 *nickname = msg->params.vector[1]; const char *channel_name = 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; cstr_set (&s->irc_chanuser_modes, xstrndup (modes, n_prefixes)); cstr_set (&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) { cstr_set (&s->irc_chantypes, xstrdup (value)); } static void irc_handle_isupport_idchan (struct server *s, char *value) { struct str prefixes = str_make (); struct strv v = strv_make (); cstr_split (value, ",", true, &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); } strv_free (&v); cstr_set (&s->irc_idchan_prefixes, str_steal (&prefixes)); } static void irc_handle_isupport_statusmsg (struct server *s, char *value) { cstr_set (&s->irc_statusmsg, xstrdup (value)); } static void irc_handle_isupport_extban (struct server *s, char *value) { s->irc_extban_prefix = 0; if (*value && *value != ',') s->irc_extban_prefix = *value++; cstr_set (&s->irc_extban_types, xstrdup (*value == ',' ? ++value : "")); } static void irc_handle_isupport_chanmodes (struct server *s, char *value) { struct strv v = strv_make (); cstr_split (value, ",", true, &v); if (v.len >= 4) { cstr_set (&s->irc_chanmodes_list, xstrdup (v.vector[0])); cstr_set (&s->irc_chanmodes_param_always, xstrdup (v.vector[1])); cstr_set (&s->irc_chanmodes_param_when_set, xstrdup (v.vector[2])); cstr_set (&s->irc_chanmodes_param_never, xstrdup (v.vector[3])); } strv_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 ("EXTBAN", irc_handle_isupport_extban); 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_make (); 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 strv copy = strv_make (); strv_append_vector (©, msg->params.vector + !!msg->params.len); struct buffer *buffer = s->buffer; int flags = BUFFER_LINE_STATUS; 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; // Auto-away spams server buffers with activity case IRC_RPL_NOWAWAY: flags |= BUFFER_LINE_UNIMPORTANT; if (s->irc_user) s->irc_user->away = true; break; case IRC_RPL_UNAWAY: flags |= BUFFER_LINE_UNIMPORTANT; if (s->irc_user) s->irc_user->away = false; break; case IRC_RPL_WHOREPLY: if (irc_handle_rpl_whoreply (s, msg)) buffer = NULL; break; case IRC_RPL_ENDOFWHO: if (irc_handle_rpl_endofwho (s, msg)) buffer = NULL; break; case IRC_ERR_NICKLOCKED: case IRC_RPL_SASLSUCCESS: case IRC_ERR_SASLFAIL: case IRC_ERR_SASLTOOLONG: case IRC_ERR_SASLABORTED: case IRC_ERR_SASLALREADY: irc_try_finish_cap_negotiation (s); break; case IRC_RPL_LIST: 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, flags, "#&m", strv_join (©, " ")); } strv_free (©); } static void irc_sanitize_cut_off_utf8 (char **line) { // A variation on utf8_validate(), we need to detect the -2 return const char *p = *line, *end = strchr (p, 0); int32_t codepoint; while ((codepoint = utf8_decode (&p, end - p)) >= 0 && utf8_validate_cp (codepoint)) ; if (codepoint != -2) return; struct str fixed_up = str_make (); str_append_data (&fixed_up, *line, p - *line); str_append (&fixed_up, "\xEF\xBF\xBD" /* U+FFFD */); cstr_set (line, str_steal (&fixed_up)); } static void irc_process_message (const struct irc_message *msg, struct server *s) { if (msg->params.len) irc_sanitize_cut_off_utf8 (&msg->params.vector[msg->params.len - 1]); // TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec()) // -> change all calls to log_{server,nick,chghost,outcoming,ctcp}*() // to take an extra numeric argument specifying time 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); // Better always make sure everything is in sync rather than care about // each case explicitly whether anything might have changed refresh_prompt (s->ctx); } // --- Message autosplitting magic --------------------------------------------- // This is a rather basic 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, struct irc_char_attrs *attrs, size_t text_len, size_t target_len, struct str *output) { size_t 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 (target_len && word_len <= target_len) { if (word_len) { str_append_data (output, text, word_len); text += word_len; eaten += word_len; target_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 for (size_t i = 1; i <= text_len && i <= target_len; i++) if (i == text_len || attrs[i].starts_at_boundary) eaten = i; str_append_data (output, text, eaten); return eaten; } // In practice, this should never fail at all, although it's not guaranteed static bool wrap_message (const char *message, int line_max, struct strv *output, struct error **e) { size_t message_left = strlen (message), i = 0; struct irc_char_attrs *attrs = irc_analyze_text (message, message_left); struct str m = str_make (); if (line_max <= 0) goto error; while (m.len + message_left > (size_t) line_max) { size_t eaten = wrap_text_for_single_line (message + i, attrs + i, message_left, line_max - m.len, &m); if (!eaten) goto error; strv_append_owned (output, str_steal (&m)); m = str_make (); i += eaten; if (!(message_left -= eaten)) break; irc_serialize_char_attrs (attrs + i, &m); if (m.len >= (size_t) line_max) { print_debug ("formatting continuation too long"); str_reset (&m); } } if (message_left) strv_append_owned (output, xstrdup_printf ("%s%s", m.str, message + i)); free (attrs); str_free (&m); return true; error: free (attrs); str_free (&m); return error_set (e, "Message splitting was unsuccessful as there was " "too little room for UTF-8 characters"); } /// 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 strv *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) strv_append (output, message); else if (!wrap_message (message, space_in_one_message, output, e)) return false; return true; } static void send_autosplit_message (struct server *s, const char *command, const char *target, const char *message, const char *prefix, const char *suffix) { struct buffer *buffer = str_map_find (&s->irc_buffer_map, target); int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1 + strlen (prefix) + strlen (suffix); // We might also want to preserve attributes across splits but // that would make this code a lot more complicated struct strv lines = strv_make (); struct error *e = NULL; if (!irc_autosplit_message (s, message, fixed_part, &lines, &e)) { log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message); error_free (e); } else { for (size_t i = 0; i < lines.len; i++) irc_send (s, "%s %s :%s%s%s", command, target, prefix, lines.vector[i], suffix); } strv_free (&lines); } #define SEND_AUTOSPLIT_ACTION(s, target, message) \ send_autosplit_message ((s), "PRIVMSG", (target), (message), \ "\x01" "ACTION ", "\x01") #define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \ send_autosplit_message ((s), "PRIVMSG", (target), (message), "", "") #define SEND_AUTOSPLIT_NOTICE(s, target, message) \ send_autosplit_message ((s), "NOTICE", (target), (message), "", "") // --- Configuration dumper ---------------------------------------------------- struct config_dump_data { struct strv path; ///< Levels struct strv *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 str_map_iter iter = str_map_iter_make (&object->value.object); struct config_item *child; while ((child = str_map_iter_next (&iter))) { strv_append_owned (&data->path, iter.link->key); config_dump_item (child, data); strv_steal (&data->path, data->path.len - 1); } } 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; } // Currently there's no reason for us to dump unknown items struct config_schema *schema = item->schema; if (!schema) return; struct str line = str_make (); if (data->path.len) str_append (&line, data->path.vector[0]); for (size_t i = 1; i < data->path.len; i++) str_append_printf (&line, ".%s", data->path.vector[i]); struct str value = str_make (); config_item_write (item, false, &value); // Don't bother writing out null values everywhere 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); strv_append_owned (data->output, str_steal (&line)); } static void config_dump (struct config_item *root, struct strv *output) { struct config_dump_data data; data.path = strv_make (); data.output = output; config_dump_item (root, &data); hard_assert (!data.path.len); strv_free (&data.path); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int strv_sort_cb (const void *a, const void *b) { return strcmp (*(const char **) a, *(const char **) b); } static void strv_sort (struct strv *self) { qsort (self->vector, self->len, sizeof *self->vector, strv_sort_cb); } static void dump_matching_options (struct config_item *root, const char *mask, struct strv *output) { config_dump (root, output); strv_sort (output); // Filter out results by wildcard matching for (size_t i = 0; i < output->len; i++) { // Yeah, I know char *key = cstr_cut_until (output->vector[i], " "); if (fnmatch (mask, key, 0)) strv_remove (output, i--); free (key); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void save_configuration (struct app_context *ctx) { struct str data = str_make (); serialize_configuration (ctx->config.root, &data); struct error *e = NULL; char *filename = write_configuration_file (NULL, &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 (iscntrl_ascii (*p) || *p == '.') return false; return true; } static const char * check_server_name_for_addition (struct app_context *ctx, const char *name) { if (!strcasecmp_ascii (name, ctx->global_buffer->name)) return "name collides with the global buffer"; if (str_map_find (&ctx->servers, name)) return "server already exists"; if (!validate_server_name (name)) return "invalid server name"; return NULL; } 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 = server_new (&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 (ctx->input, BUFFER_SERVER, irc_make_buffer_name (s, NULL)); 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); } } static void server_remove (struct app_context *ctx, struct server *s) { hard_assert (!irc_is_connected (s)); if (s->buffer) buffer_remove_safe (ctx, s->buffer); struct str_map_unset_iter iter = str_map_unset_iter_make (&s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_unset_iter_next (&iter))) buffer_remove_safe (ctx, buffer); str_map_unset_iter_free (&iter); hard_assert (!s->buffer); hard_assert (!s->irc_buffer_map.len); hard_assert (!s->irc_channels.len); soft_assert (!s->irc_users.len); str_map_set (get_servers_config (ctx), s->name, NULL); s->config = NULL; // This actually destroys the server as it's owned by the map str_map_set (&ctx->servers, s->name, NULL); } static void server_rename (struct app_context *ctx, struct server *s, const char *new_name) { hard_assert (!str_map_find (&ctx->servers, new_name)); str_map_set (&ctx->servers, new_name, str_map_steal (&ctx->servers, s->name)); struct str_map *servers = get_servers_config (ctx); str_map_set (servers, new_name, str_map_steal (servers, s->name)); cstr_set (&s->name, xstrdup (new_name)); buffer_rename (ctx, s->buffer, new_name); struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) { // TODO: creation of buffer names should be centralized -> replace // calls to buffer_rename() and manual setting of buffer names // with something like buffer_autorename() -- just mind the mess // in irc_handle_nick(), which can hopefully be simplified char *x = NULL; switch (buffer->type) { case BUFFER_PM: x = xstrdup_printf ("%s.%s", s->name, buffer->user->nickname); break; case BUFFER_CHANNEL: x = xstrdup_printf ("%s.%s", s->name, buffer->channel->name); break; default: hard_assert (!"unexpected type of server-related buffer"); } buffer_rename (ctx, buffer, x); free (x); } } // --- Plugins ----------------------------------------------------------------- /// Returns the basename of the plugin's name without any extensions, /// or NULL if the name isn't suitable (starts with a dot) static char * plugin_config_name (struct plugin *self) { const char *begin = self->name; for (const char *p = begin; *p; ) if (*p++ == '/') begin = p; size_t len = strcspn (begin, "."); if (!len) return NULL; // XXX: we might also allow arbitrary strings as object keys (except dots) char *copy = xstrndup (begin, len); for (char *p = copy; *p; p++) if (!config_tokenizer_is_word_char (*p)) *p = '_'; return copy; } // --- Lua --------------------------------------------------------------------- // Each plugin has its own Lua state object, so that a/ they don't disturb each // other and b/ unloading a plugin releases all resources. // // References to internal objects (buffers, servers) are all weak. #ifdef HAVE_LUA struct lua_plugin { struct plugin super; ///< The structure we're deriving struct app_context *ctx; ///< Application context lua_State *L; ///< Lua state for the main thread struct lua_schema_item *schemas; ///< Registered schema items }; static void lua_plugin_gc (struct plugin *self_) { struct lua_plugin *self = (struct lua_plugin *) self_; lua_gc (self->L, LUA_GCCOLLECT, 0 /* Lua 5.3 required, 5.4 varargs */); } static void lua_plugin_free (struct plugin *self_) { struct lua_plugin *self = (struct lua_plugin *) self_; lua_close (self->L); } struct plugin_vtable lua_plugin_vtable = { .gc = lua_plugin_gc, .free = lua_plugin_free, }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The registry can be used as a cache for weakly referenced objects static bool lua_cache_get (lua_State *L, void *object) { lua_rawgetp (L, LUA_REGISTRYINDEX, object); if (lua_isnil (L, -1)) { lua_pop (L, 1); return false; } return true; } static void lua_cache_store (lua_State *L, void *object, int index) { lua_pushvalue (L, index); lua_rawsetp (L, LUA_REGISTRYINDEX, object); } static void lua_cache_invalidate (lua_State *L, void *object) { lua_pushnil (L); lua_rawsetp (L, LUA_REGISTRYINDEX, object); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Append a traceback to all errors so that we can later extract it static int lua_plugin_error_handler (lua_State *L) { luaL_traceback (L, L, luaL_checkstring (L, 1), 1); return 1; } static bool lua_plugin_process_error (struct lua_plugin *self, const char *message, struct error **e) { struct strv v = strv_make (); cstr_split (message, "\n", true, &v); if (v.len < 2) error_set (e, "%s", message); else { error_set (e, "%s", v.vector[0]); log_global_debug (self->ctx, "Lua: plugin \"#s\": #s", self->super.name, v.vector[1]); for (size_t i = 2; i < v.len; i++) log_global_debug (self->ctx, " #s", v.vector[i]); } strv_free (&v); return false; } /// Call a Lua function and process errors using our special error handler static bool lua_plugin_call (struct lua_plugin *self, int n_params, int n_results, struct error **e) { // XXX: this may eventually be called from a thread, then this is wrong lua_State *L = self->L; // We need to pop the error handler at the end lua_pushcfunction (L, lua_plugin_error_handler); int error_handler_idx = -n_params - 2; lua_insert (L, error_handler_idx); if (!lua_pcall (L, n_params, n_results, error_handler_idx)) { lua_remove (L, -n_results - 1); return true; } (void) lua_plugin_process_error (self, lua_tostring (L, -1), e); lua_pop (L, 2); return false; } /// Convenience function; replaces the "original" string or produces an error static bool lua_plugin_handle_string_filter_result (struct lua_plugin *self, char **original, bool utf8, struct error **e) { lua_State *L = self->L; if (lua_isnil (L, -1)) { cstr_set (original, NULL); return true; } if (!lua_isstring (L, -1)) return error_set (e, "must return either a string or nil"); size_t len; const char *processed = lua_tolstring (L, -1, &len); if (utf8 && !utf8_validate (processed, len)) return error_set (e, "must return valid UTF-8"); // Only replace the string if it's different if (strcmp (processed, *original)) cstr_set (original, xstrdup (processed)); return true; } static const char * lua_plugin_check_utf8 (lua_State *L, int arg) { size_t len; const char *s = luaL_checklstring (L, arg, &len); luaL_argcheck (L, utf8_validate (s, len), arg, "must be valid UTF-8"); return s; } static void lua_plugin_log_error (struct lua_plugin *self, const char *where, struct error *error) { log_global_error (self->ctx, "Lua: plugin \"#s\": #s: #s", self->super.name, where, error->message); error_free (error); } /// Pop "n" values from the stack into a table, using their indexes as keys static void lua_plugin_pack (lua_State *L, int n) { lua_createtable (L, n, 0); lua_insert (L, -n - 1); for (int i = n; i; i--) lua_rawseti (L, -i - 1, i); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void lua_plugin_kv (lua_State *L, const char *key, const char *value) { lua_pushstring (L, value); lua_setfield (L, -2, key); } static void lua_plugin_push_message (lua_State *L, const struct irc_message *msg) { lua_createtable (L, 0, 4); lua_createtable (L, msg->tags.len, 0); struct str_map_iter iter = str_map_iter_make (&msg->tags); const char *value; while ((value = str_map_iter_next (&iter))) lua_plugin_kv (L, iter.link->key, value); lua_setfield (L, -2, "tags"); // TODO: parse the prefix further? if (msg->prefix) lua_plugin_kv (L, "prefix", msg->prefix); if (msg->command) lua_plugin_kv (L, "command", msg->command); lua_createtable (L, msg->params.len, 0); for (size_t i = 0; i < msg->params.len; i++) { lua_pushstring (L, msg->params.vector[i]); lua_rawseti (L, -2, i + 1); } lua_setfield (L, -2, "params"); } static int lua_plugin_parse (lua_State *L) { struct irc_message msg; irc_parse_message (&msg, luaL_checkstring (L, 1)); lua_plugin_push_message (L, &msg); irc_free_message (&msg); return 1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Lua code can use weakly referenced wrappers for internal objects. typedef struct weak_ref_link * (*lua_weak_ref_fn) (void *object, destroy_cb_fn cb, void *user_data); typedef void (*lua_weak_unref_fn) (void *object, struct weak_ref_link **link); struct lua_weak_info { const char *name; ///< Metatable name struct ispect_field *ispect; ///< Introspection data lua_weak_ref_fn ref; ///< Weak link invalidator lua_weak_unref_fn unref; ///< Weak link generator }; struct lua_weak { struct lua_plugin *plugin; ///< The plugin we belong to struct lua_weak_info *info; ///< Introspection data void *object; ///< The object struct weak_ref_link *weak_ref; ///< A weak reference link }; static void lua_weak_invalidate (void *object, void *user_data) { struct lua_weak *wrapper = user_data; wrapper->object = NULL; wrapper->weak_ref = NULL; // This can in theory call the GC, order isn't arbitrary here lua_cache_invalidate (wrapper->plugin->L, object); } static void lua_weak_push (lua_State *L, struct lua_plugin *plugin, void *object, struct lua_weak_info *info) { if (!object) { lua_pushnil (L); return; } if (lua_cache_get (L, object)) return; struct lua_weak *wrapper = lua_newuserdata (L, sizeof *wrapper); luaL_setmetatable (L, info->name); wrapper->plugin = plugin; wrapper->info = info; wrapper->object = object; wrapper->weak_ref = NULL; if (info->ref) wrapper->weak_ref = info->ref (object, lua_weak_invalidate, wrapper); lua_cache_store (L, object, -1); } static int lua_weak_gc (lua_State *L, const struct lua_weak_info *info) { struct lua_weak *wrapper = luaL_checkudata (L, 1, info->name); if (wrapper->object) { lua_cache_invalidate (L, wrapper->object); if (info->unref) info->unref (wrapper->object, &wrapper->weak_ref); wrapper->object = NULL; } return 0; } static struct lua_weak * lua_weak_deref (lua_State *L, const struct lua_weak_info *info) { struct lua_weak *weak = luaL_checkudata (L, 1, info->name); luaL_argcheck (L, weak->object, 1, "dead reference used"); return weak; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #define LUA_WEAK_DECLARE(id, metatable_id) \ static struct lua_weak_info lua_ ## id ## _info = \ { \ .name = metatable_id, \ .ispect = g_ ## id ## _ispect, \ .ref = (lua_weak_ref_fn) id ## _weak_ref, \ .unref = (lua_weak_unref_fn) id ## _weak_unref, \ }; #define XLUA_USER_METATABLE "user" ///< Identifier for Lua metatable #define XLUA_CHANNEL_METATABLE "channel" ///< Identifier for Lua metatable #define XLUA_BUFFER_METATABLE "buffer" ///< Identifier for Lua metatable #define XLUA_SERVER_METATABLE "server" ///< Identifier for Lua metatable LUA_WEAK_DECLARE (user, XLUA_USER_METATABLE) LUA_WEAK_DECLARE (channel, XLUA_CHANNEL_METATABLE) LUA_WEAK_DECLARE (buffer, XLUA_BUFFER_METATABLE) LUA_WEAK_DECLARE (server, XLUA_SERVER_METATABLE) // The global context is kind of fake and doesn't have any ref-counting, // however it's still very much an object static struct lua_weak_info lua_ctx_info = { .name = PROGRAM_NAME, .ispect = g_ctx_ispect, }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_user_gc (lua_State *L) { return lua_weak_gc (L, &lua_user_info); } static int lua_user_get_channels (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_user_info); struct user *user = wrapper->object; int i = 1; lua_newtable (L); LIST_FOR_EACH (struct user_channel, iter, user->channels) { lua_weak_push (L, wrapper->plugin, iter->channel, &lua_channel_info); lua_rawseti (L, -2, i++); } return 1; } static luaL_Reg lua_user_table[] = { { "__gc", lua_user_gc }, { "get_channels", lua_user_get_channels }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_channel_gc (lua_State *L) { return lua_weak_gc (L, &lua_channel_info); } static int lua_channel_get_users (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_channel_info); struct channel *channel = wrapper->object; int i = 1; lua_newtable (L); LIST_FOR_EACH (struct channel_user, iter, channel->users) { lua_createtable (L, 0, 2); lua_weak_push (L, wrapper->plugin, iter->user, &lua_user_info); lua_setfield (L, -2, "user"); lua_plugin_kv (L, "prefixes", iter->prefixes); lua_rawseti (L, -2, i++); } return 1; } static luaL_Reg lua_channel_table[] = { { "__gc", lua_channel_gc }, { "get_users", lua_channel_get_users }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_buffer_gc (lua_State *L) { return lua_weak_gc (L, &lua_buffer_info); } static int lua_buffer_log (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info); struct buffer *buffer = wrapper->object; const char *message = lua_plugin_check_utf8 (L, 2); log_full (wrapper->plugin->ctx, buffer->server, buffer, BUFFER_LINE_STATUS, "#s", message); return 0; } static int lua_buffer_execute (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info); struct buffer *buffer = wrapper->object; const char *line = lua_plugin_check_utf8 (L, 2); process_input_utf8 (wrapper->plugin->ctx, buffer, line, 0); return 0; } static luaL_Reg lua_buffer_table[] = { { "__gc", lua_buffer_gc }, { "log", lua_buffer_log }, { "execute", lua_buffer_execute }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_server_gc (lua_State *L) { return lua_weak_gc (L, &lua_server_info); } static int lua_server_get_state (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info); struct server *server = wrapper->object; switch (server->state) { case IRC_DISCONNECTED: lua_pushstring (L, "disconnected"); break; case IRC_CONNECTING: lua_pushstring (L, "connecting"); break; case IRC_CONNECTED: lua_pushstring (L, "connected"); break; case IRC_REGISTERED: lua_pushstring (L, "registered"); break; case IRC_CLOSING: lua_pushstring (L, "closing"); break; case IRC_HALF_CLOSED: lua_pushstring (L, "half_closed"); break; } return 1; } static int lua_server_send (lua_State *L) { struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info); struct server *server = wrapper->object; irc_send (server, "%s", luaL_checkstring (L, 2)); return 0; } static luaL_Reg lua_server_table[] = { { "__gc", lua_server_gc }, { "get_state", lua_server_get_state }, { "send", lua_server_send }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #define XLUA_HOOK_METATABLE "hook" ///< Identifier for the Lua metatable enum lua_hook_type { XLUA_HOOK_DEFUNCT, ///< No longer functional XLUA_HOOK_INPUT, ///< Input hook XLUA_HOOK_IRC, ///< IRC hook XLUA_HOOK_PROMPT, ///< Prompt hook XLUA_HOOK_COMPLETION, ///< Autocomplete }; struct lua_hook { struct lua_plugin *plugin; ///< The plugin we belong to enum lua_hook_type type; ///< Type of the hook int ref_callback; ///< Reference to the callback union { struct hook hook; ///< Hook base structure struct input_hook input_hook; ///< Input hook struct irc_hook irc_hook; ///< IRC hook struct prompt_hook prompt_hook; ///< IRC hook struct completion_hook c_hook; ///< Autocomplete hook } data; ///< Hook data }; static int lua_hook_unhook (lua_State *L) { struct lua_hook *hook = luaL_checkudata (L, 1, XLUA_HOOK_METATABLE); switch (hook->type) { case XLUA_HOOK_INPUT: LIST_UNLINK (hook->plugin->ctx->input_hooks, &hook->data.hook); break; case XLUA_HOOK_IRC: LIST_UNLINK (hook->plugin->ctx->irc_hooks, &hook->data.hook); break; case XLUA_HOOK_PROMPT: LIST_UNLINK (hook->plugin->ctx->prompt_hooks, &hook->data.hook); refresh_prompt (hook->plugin->ctx); break; case XLUA_HOOK_COMPLETION: LIST_UNLINK (hook->plugin->ctx->completion_hooks, &hook->data.hook); break; default: hard_assert (!"invalid hook type"); case XLUA_HOOK_DEFUNCT: break; } luaL_unref (L, LUA_REGISTRYINDEX, hook->ref_callback); hook->ref_callback = LUA_REFNIL; // The hook no longer has to stay alive hook->type = XLUA_HOOK_DEFUNCT; lua_cache_invalidate (L, hook); return 0; } // The hook dies either when the plugin requests it or at plugin unload static luaL_Reg lua_hook_table[] = { { "unhook", lua_hook_unhook }, { "__gc", lua_hook_unhook }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static char * lua_input_hook_filter (struct input_hook *self, struct buffer *buffer, char *input) { struct lua_hook *hook = CONTAINER_OF (self, struct lua_hook, data.input_hook); struct lua_plugin *plugin = hook->plugin; lua_State *L = plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback); lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook lua_weak_push (L, plugin, buffer, &lua_buffer_info); // 2: buffer lua_pushstring (L, input); // 3: input struct error *e = NULL; if (lua_plugin_call (plugin, 3, 1, &e)) { lua_plugin_handle_string_filter_result (plugin, &input, true, &e); lua_pop (L, 1); } if (e) lua_plugin_log_error (plugin, "input hook", e); return input; } static char * lua_irc_hook_filter (struct irc_hook *self, struct server *s, char *message) { struct lua_hook *hook = CONTAINER_OF (self, struct lua_hook, data.irc_hook); struct lua_plugin *plugin = hook->plugin; lua_State *L = plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback); lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook lua_weak_push (L, plugin, s, &lua_server_info); // 2: server lua_pushstring (L, message); // 3: message struct error *e = NULL; if (lua_plugin_call (plugin, 3, 1, &e)) { lua_plugin_handle_string_filter_result (plugin, &message, false, &e); lua_pop (L, 1); } if (e) lua_plugin_log_error (plugin, "IRC hook", e); return message; } static char * lua_prompt_hook_make (struct prompt_hook *self) { struct lua_hook *hook = CONTAINER_OF (self, struct lua_hook, data.prompt_hook); struct lua_plugin *plugin = hook->plugin; lua_State *L = plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback); lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook struct error *e = NULL; char *prompt = xstrdup (""); if (lua_plugin_call (plugin, 1, 1, &e)) { lua_plugin_handle_string_filter_result (plugin, &prompt, true, &e); lua_pop (L, 1); } if (e) lua_plugin_log_error (plugin, "prompt hook", e); return prompt; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void lua_plugin_push_completion (lua_State *L, struct completion *data) { lua_createtable (L, 0, 3); lua_pushstring (L, data->line); lua_setfield (L, -2, "line"); lua_createtable (L, data->words_len, 0); for (size_t i = 0; i < data->words_len; i++) { lua_pushlstring (L, data->line + data->words[i].start, data->words[i].end - data->words[i].start); lua_rawseti (L, -2, i + 1); } lua_setfield (L, -2, "words"); lua_pushinteger (L, data->location); lua_setfield (L, -2, "location"); } static bool lua_completion_hook_process_value (lua_State *L, struct strv *output, struct error **e) { if (lua_type (L, -1) != LUA_TSTRING) { return error_set (e, "%s: %s", "invalid type", lua_typename (L, lua_type (L, -1))); } size_t len; const char *value = lua_tolstring (L, -1, &len); if (!utf8_validate (value, len)) return error_set (e, "must be valid UTF-8"); strv_append (output, value); return true; } static bool lua_completion_hook_process (lua_State *L, struct strv *output, struct error **e) { if (lua_isnil (L, -1)) return true; if (!lua_istable (L, -1)) return error_set (e, "must return either a table or nil"); bool success = true; for (lua_Integer i = 1; success && lua_rawgeti (L, -1, i); i++) if ((success = lua_completion_hook_process_value (L, output, e))) lua_pop (L, 1); lua_pop (L, 1); return success; } static void lua_completion_hook_complete (struct completion_hook *self, struct completion *data, const char *word, struct strv *output) { struct lua_hook *hook = CONTAINER_OF (self, struct lua_hook, data.c_hook); struct lua_plugin *plugin = hook->plugin; lua_State *L = plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback); lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook lua_plugin_push_completion (L, data); // 2: data lua_weak_push (L, plugin, plugin->ctx->current_buffer, &lua_buffer_info); lua_setfield (L, -2, "buffer"); lua_pushstring (L, word); // 3: word struct error *e = NULL; if (lua_plugin_call (plugin, 3, 1, &e)) { lua_completion_hook_process (L, output, &e); lua_pop (L, 1); } if (e) lua_plugin_log_error (plugin, "autocomplete hook", e); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static struct lua_hook * lua_plugin_push_hook (lua_State *L, struct lua_plugin *plugin, int callback_index, enum lua_hook_type type, int priority) { luaL_checktype (L, callback_index, LUA_TFUNCTION); struct lua_hook *hook = lua_newuserdata (L, sizeof *hook); luaL_setmetatable (L, XLUA_HOOK_METATABLE); memset (hook, 0, sizeof *hook); hook->data.hook.priority = priority; hook->type = type; hook->plugin = plugin; lua_pushvalue (L, callback_index); hook->ref_callback = luaL_ref (L, LUA_REGISTRYINDEX); // Make sure the hook doesn't get garbage collected and return it lua_cache_store (L, hook, -1); return hook; } static int lua_plugin_hook_input (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); struct lua_hook *hook = lua_plugin_push_hook (L, plugin, 1, XLUA_HOOK_INPUT, luaL_optinteger (L, 2, 0)); hook->data.input_hook.filter = lua_input_hook_filter; plugin->ctx->input_hooks = hook_insert (plugin->ctx->input_hooks, &hook->data.hook); return 1; } static int lua_plugin_hook_irc (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); struct lua_hook *hook = lua_plugin_push_hook (L, plugin, 1, XLUA_HOOK_IRC, luaL_optinteger (L, 2, 0)); hook->data.irc_hook.filter = lua_irc_hook_filter; plugin->ctx->irc_hooks = hook_insert (plugin->ctx->irc_hooks, &hook->data.hook); return 1; } static int lua_plugin_hook_prompt (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); struct lua_hook *hook = lua_plugin_push_hook (L, plugin, 1, XLUA_HOOK_PROMPT, luaL_optinteger (L, 2, 0)); hook->data.prompt_hook.make = lua_prompt_hook_make; plugin->ctx->prompt_hooks = hook_insert (plugin->ctx->prompt_hooks, &hook->data.hook); refresh_prompt (plugin->ctx); return 1; } static int lua_plugin_hook_completion (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); struct lua_hook *hook = lua_plugin_push_hook (L, plugin, 1, XLUA_HOOK_COMPLETION, luaL_optinteger (L, 2, 0)); hook->data.c_hook.complete = lua_completion_hook_complete; plugin->ctx->completion_hooks = hook_insert (plugin->ctx->completion_hooks, &hook->data.hook); return 1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #define XLUA_SCHEMA_METATABLE "schema" ///< Identifier for the Lua metatable struct lua_schema_item { LIST_HEADER (struct lua_schema_item) struct lua_plugin *plugin; ///< The plugin we belong to struct config_item *item; ///< The item managed by the schema struct config_schema schema; ///< Schema itself int ref_validate; ///< Reference to "validate" callback int ref_on_change; ///< Reference to "on_change" callback }; static void lua_schema_item_discard (struct lua_schema_item *self) { if (self->item) { self->item->schema = NULL; self->item->user_data = NULL; self->item = NULL; LIST_UNLINK (self->plugin->schemas, self); } // Now that we've disconnected from the item, allow garbage collection lua_cache_invalidate (self->plugin->L, self); } static int lua_schema_item_gc (lua_State *L) { struct lua_schema_item *self = luaL_checkudata (L, 1, XLUA_SCHEMA_METATABLE); lua_schema_item_discard (self); free ((char *) self->schema.name); free ((char *) self->schema.comment); free ((char *) self->schema.default_); luaL_unref (L, LUA_REGISTRYINDEX, self->ref_validate); luaL_unref (L, LUA_REGISTRYINDEX, self->ref_on_change); return 0; } static luaL_Reg lua_schema_table[] = { { "__gc", lua_schema_item_gc }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Unfortunately this has the same problem as JSON libraries in that Lua /// cannot store null values in containers (it has no distinct "undefined" type) static void lua_plugin_push_config_item (lua_State *L, const struct config_item *item) { switch (item->type) { case CONFIG_ITEM_NULL: lua_pushnil (L); break; case CONFIG_ITEM_OBJECT: { lua_createtable (L, 0, item->value.object.len); struct str_map_iter iter = str_map_iter_make (&item->value.object); struct config_item *child; while ((child = str_map_iter_next (&iter))) { lua_plugin_push_config_item (L, child); lua_setfield (L, -2, iter.link->key); } break; } case CONFIG_ITEM_BOOLEAN: lua_pushboolean (L, item->value.boolean); break; case CONFIG_ITEM_INTEGER: lua_pushinteger (L, item->value.integer); break; case CONFIG_ITEM_STRING: case CONFIG_ITEM_STRING_ARRAY: lua_pushlstring (L, item->value.string.str, item->value.string.len); break; } } static bool lua_schema_item_validate (const struct config_item *item, struct error **e) { struct lua_schema_item *self = item->user_data; if (self->ref_validate == LUA_REFNIL) return true; lua_State *L = self->plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_validate); lua_plugin_push_config_item (L, item); // The callback can make use of error("...", 0) to produce nice messages return lua_plugin_call (self->plugin, 1, 0, e); } static void lua_schema_item_on_change (struct config_item *item) { struct lua_schema_item *self = item->user_data; if (self->ref_on_change == LUA_REFNIL) return; lua_State *L = self->plugin->L; lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_change); lua_plugin_push_config_item (L, item); struct error *e = NULL; if (!lua_plugin_call (self->plugin, 1, 0, &e)) lua_plugin_log_error (self->plugin, "schema on_change", e); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_plugin_decode_config_item_type (const char *type) { if (!strcmp (type, "null")) return CONFIG_ITEM_NULL; if (!strcmp (type, "object")) return CONFIG_ITEM_OBJECT; if (!strcmp (type, "boolean")) return CONFIG_ITEM_BOOLEAN; if (!strcmp (type, "integer")) return CONFIG_ITEM_INTEGER; if (!strcmp (type, "string")) return CONFIG_ITEM_STRING; if (!strcmp (type, "string_array")) return CONFIG_ITEM_STRING_ARRAY; return -1; } static bool lua_plugin_check_field (lua_State *L, int idx, const char *name, int expected, bool optional) { int found = lua_getfield (L, idx, name); if (found == expected) return true; if (optional && found == LUA_TNIL) return false; const char *message = optional ? "invalid field \"%s\" (found: %s, expected: %s or nil)" : "invalid or missing field \"%s\" (found: %s, expected: %s)"; return luaL_error (L, message, name, lua_typename (L, found), lua_typename (L, expected)); } static int lua_plugin_add_config_schema (lua_State *L, struct lua_plugin *plugin, struct config_item *subtree, const char *name) { struct config_item *item = str_map_find (&subtree->value.object, name); // This should only ever happen because of a conflict with another plugin; // this is the price we pay for simplicity if (item && item->schema) { struct lua_schema_item *owner = item->user_data; return luaL_error (L, "conflicting schema item: %s (owned by: %s)", name, owner->plugin->super.name); } // Create and initialize a full userdata wrapper for the schema item struct lua_schema_item *self = lua_newuserdata (L, sizeof *self); luaL_setmetatable (L, XLUA_SCHEMA_METATABLE); memset (self, 0, sizeof *self); self->plugin = plugin; self->ref_on_change = LUA_REFNIL; self->ref_validate = LUA_REFNIL; struct config_schema *schema = &self->schema; schema->name = xstrdup (name); schema->comment = NULL; schema->default_ = NULL; schema->type = CONFIG_ITEM_NULL; schema->on_change = lua_schema_item_on_change; schema->validate = lua_schema_item_validate; // Try to update the defaults with values provided by the plugin int values = lua_absindex (L, -2); (void) lua_plugin_check_field (L, values, "type", LUA_TSTRING, false); int item_type = schema->type = lua_plugin_decode_config_item_type (lua_tostring (L, -1)); if (item_type == -1) return luaL_error (L, "invalid type of schema item"); if (lua_plugin_check_field (L, values, "comment", LUA_TSTRING, true)) schema->comment = xstrdup (lua_tostring (L, -1)); if (lua_plugin_check_field (L, values, "default", LUA_TSTRING, true)) schema->default_ = xstrdup (lua_tostring (L, -1)); lua_pop (L, 3); (void) lua_plugin_check_field (L, values, "on_change", LUA_TFUNCTION, true); self->ref_on_change = luaL_ref (L, LUA_REGISTRYINDEX); (void) lua_plugin_check_field (L, values, "validate", LUA_TFUNCTION, true); self->ref_validate = luaL_ref (L, LUA_REGISTRYINDEX); // Try to install the created schema item into our configuration struct error *warning = NULL, *e = NULL; item = config_schema_initialize_item (&self->schema, subtree, self, &warning, &e); if (warning) { log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s", plugin->super.name, warning->message); error_free (warning); } if (e) { const char *error = lua_pushstring (L, e->message); error_free (e); return luaL_error (L, "%s", error); } self->item = item; LIST_PREPEND (plugin->schemas, self); // On the stack there should be the schema table and the resulting object; // we need to make sure Lua doesn't GC the second and get rid of them both lua_cache_store (L, self, -1); lua_pop (L, 2); return 0; } static int lua_plugin_setup_config (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); luaL_checktype (L, 1, LUA_TTABLE); struct app_context *ctx = plugin->ctx; char *config_name = plugin_config_name (&plugin->super); if (!config_name) return luaL_error (L, "unsuitable plugin name"); struct str_map *plugins = get_plugins_config (ctx); struct config_item *subtree = str_map_find (plugins, config_name); if (!subtree || subtree->type != CONFIG_ITEM_OBJECT) str_map_set (plugins, config_name, (subtree = config_item_object ())); free (config_name); LIST_FOR_EACH (struct lua_schema_item, iter, plugin->schemas) lua_schema_item_discard (iter); // Load all schema items and apply them to the plugin's subtree lua_pushnil (L); while (lua_next (L, 1)) { if (lua_type (L, -2) != LUA_TSTRING || lua_type (L, -1) != LUA_TTABLE) return luaL_error (L, "%s: %s -> %s", "invalid types", lua_typename (L, lua_type (L, -2)), lua_typename (L, lua_type (L, -1))); lua_plugin_add_config_schema (L, plugin, subtree, lua_tostring (L, -2)); } // Let the plugin read out configuration via on_change callbacks config_schema_call_changed (subtree); return 0; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Identifier for the Lua metatable #define XLUA_CONNECTION_METATABLE "connection" struct lua_connection { struct lua_plugin *plugin; ///< The plugin we belong to struct poller_fd socket_event; ///< Socket is ready int socket_fd; ///< Underlying connected socket bool got_eof; ///< Half-closed by remote host bool closing; ///< We're closing the connection struct str read_buffer; ///< Read buffer struct str write_buffer; ///< Write buffer }; static void lua_connection_update_poller (struct lua_connection *self) { poller_fd_set (&self->socket_event, self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN); } static int lua_connection_send (lua_State *L) { struct lua_connection *self = luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE); if (self->socket_fd == -1) return luaL_error (L, "connection has been closed"); size_t len; const char *s = luaL_checklstring (L, 2, &len); str_append_data (&self->write_buffer, s, len); lua_connection_update_poller (self); return 0; } static void lua_connection_discard (struct lua_connection *self) { if (self->socket_fd != -1) { poller_fd_reset (&self->socket_event); xclose (self->socket_fd); self->socket_fd = -1; str_free (&self->read_buffer); str_free (&self->write_buffer); } // Connection is dead, we don't need to hold onto any resources anymore lua_cache_invalidate (self->plugin->L, self); } static int lua_connection_close (lua_State *L) { struct lua_connection *self = luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE); if (self->socket_fd != -1) { self->closing = true; // NOTE: this seems to do nothing on Linux (void) shutdown (self->socket_fd, SHUT_RD); // Right now we want to wait until all data is flushed to the socket // and can't call close() here immediately -- a rewrite to use async // would enable the user to await on either :send() or :flush(); // a successful send() doesn't necessarily mean anything though if (!self->write_buffer.len) lua_connection_discard (self); } return 0; } static int lua_connection_gc (lua_State *L) { lua_connection_discard (luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE)); return 0; } static luaL_Reg lua_connection_table[] = { { "send", lua_connection_send }, { "close", lua_connection_close }, { "__gc", lua_connection_gc }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_connection_check_fn (lua_State *L) { lua_plugin_check_field (L, 1, luaL_checkstring (L, 2), LUA_TFUNCTION, true); return 1; } // We need to run it in a protected environment because of lua_getfield() static bool lua_connection_cb_lookup (struct lua_connection *self, const char *name, struct error **e) { lua_State *L = self->plugin->L; lua_pushcfunction (L, lua_connection_check_fn); hard_assert (lua_cache_get (L, self)); lua_pushstring (L, name); return lua_plugin_call (self->plugin, 2, 1, e); } // Ideally lua_connection_cb_lookup() would return a ternary value static bool lua_connection_eat_nil (struct lua_connection *self) { if (lua_toboolean (self->plugin->L, -1)) return false; lua_pop (self->plugin->L, 1); return true; } static bool lua_connection_invoke_on_data (struct lua_connection *self, struct error **e) { if (!lua_connection_cb_lookup (self, "on_data", e)) return false; if (lua_connection_eat_nil (self)) return true; lua_pushlstring (self->plugin->L, self->read_buffer.str, self->read_buffer.len); return lua_plugin_call (self->plugin, 1, 0, e); } static bool lua_connection_invoke_on_eof (struct lua_connection *self, struct error **e) { if (!lua_connection_cb_lookup (self, "on_eof", e)) return false; if (lua_connection_eat_nil (self)) return true; return lua_plugin_call (self->plugin, 0, 0, e); } static bool lua_connection_invoke_on_error (struct lua_connection *self, const char *error, struct error **e) { // XXX: not sure if ignoring errors after :close() is always desired; // code might want to make sure that data are transferred successfully if (!self->closing && lua_connection_cb_lookup (self, "on_error", e) && !lua_connection_eat_nil (self)) { lua_pushstring (self->plugin->L, error); lua_plugin_call (self->plugin, 1, 0, e); } return false; } static bool lua_connection_try_read (struct lua_connection *self, struct error **e) { // Avoid the read call when it's obviously not going to return any data // and would only cause unwanted invocation of callbacks if (self->closing || self->got_eof) return true; enum socket_io_result read_result = socket_io_try_read (self->socket_fd, &self->read_buffer); const char *error = strerror (errno); // Dispatch any data that we got before an EOF or any error if (self->read_buffer.len) { if (!lua_connection_invoke_on_data (self, e)) return false; str_reset (&self->read_buffer); } if (read_result == SOCKET_IO_EOF) { if (!lua_connection_invoke_on_eof (self, e)) return false; self->got_eof = true; } if (read_result == SOCKET_IO_ERROR) return lua_connection_invoke_on_error (self, error, e); return true; } static bool lua_connection_try_write (struct lua_connection *self, struct error **e) { enum socket_io_result write_result = socket_io_try_write (self->socket_fd, &self->write_buffer); const char *error = strerror (errno); if (write_result == SOCKET_IO_ERROR) return lua_connection_invoke_on_error (self, error, e); return !self->closing || self->write_buffer.len; } static void lua_connection_on_ready (const struct pollfd *pfd, struct lua_connection *self) { (void) pfd; // Hold a reference so that it doesn't get collected on close() hard_assert (lua_cache_get (self->plugin->L, self)); struct error *e = NULL; bool keep = lua_connection_try_read (self, &e) && lua_connection_try_write (self, &e); if (e) lua_plugin_log_error (self->plugin, "network I/O", e); if (keep) lua_connection_update_poller (self); else lua_connection_discard (self); lua_pop (self->plugin->L, 1); } static struct lua_connection * lua_plugin_push_connection (struct lua_plugin *plugin, int socket_fd) { lua_State *L = plugin->L; struct lua_connection *self = lua_newuserdata (L, sizeof *self); luaL_setmetatable (L, XLUA_CONNECTION_METATABLE); memset (self, 0, sizeof *self); self->plugin = plugin; set_blocking (socket_fd, false); self->socket_event = poller_fd_make (&plugin->ctx->poller, (self->socket_fd = socket_fd)); self->socket_event.dispatcher = (poller_fd_fn) lua_connection_on_ready; self->socket_event.user_data = self; poller_fd_set (&self->socket_event, POLLIN); self->read_buffer = str_make (); self->write_buffer = str_make (); // Make sure the connection doesn't get garbage collected and return it lua_cache_store (L, self, -1); return self; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The script can create as many wait channels as wanted. They only actually // do anything once they get yielded to the main lua_resume() call. /// Identifier for the Lua metatable #define XLUA_WCHANNEL_METATABLE "wchannel" struct lua_wait_channel { LIST_HEADER (struct lua_wait_channel) struct lua_task *task; ///< The task we're active in /// Check if the event is ready and eventually push values to the thread; /// the channel then may release any resources bool (*check) (struct lua_wait_channel *self); /// Release all resources held by the subclass void (*cleanup) (struct lua_wait_channel *self); }; static int lua_wchannel_gc (lua_State *L) { struct lua_wait_channel *self = luaL_checkudata (L, 1, XLUA_WCHANNEL_METATABLE); if (self->cleanup) self->cleanup (self); return 0; } static luaL_Reg lua_wchannel_table[] = { { "__gc", lua_wchannel_gc }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // A task encapsulates a thread so that wait channels yielded from its main // function get waited upon by the event loop #define XLUA_TASK_METATABLE "task" ///< Identifier for the Lua metatable struct lua_task { LIST_HEADER (struct lua_task) struct lua_plugin *plugin; ///< The plugin we belong to lua_State *thread; ///< Lua thread struct lua_wait_channel *active; ///< Channels we're waiting on struct poller_idle idle; ///< Idle job }; static void lua_task_unregister_channels (struct lua_task *self) { LIST_FOR_EACH (struct lua_wait_channel, iter, self->active) { iter->task = NULL; LIST_UNLINK (self->active, iter); lua_cache_invalidate (self->plugin->L, iter); } } static void lua_task_cancel_internal (struct lua_task *self) { if (self->thread) { lua_cache_invalidate (self->plugin->L, self->thread); self->thread = NULL; } lua_task_unregister_channels (self); poller_idle_reset (&self->idle); // The task no longer has to stay alive lua_cache_invalidate (self->plugin->L, self); } static int lua_task_cancel (lua_State *L) { struct lua_task *self = luaL_checkudata (L, 1, XLUA_TASK_METATABLE); // We could also yield and make lua_task_resume() check "self->thread", // however the main issue here is that the script should just return luaL_argcheck (L, L != self->thread, 1, "cannot cancel task from within itself"); lua_task_cancel_internal (self); return 0; } #define lua_task_wakeup(self) poller_idle_set (&(self)->idle) static bool lua_task_schedule (struct lua_task *self, int n, struct error **e) { lua_State *L = self->thread; for (int i = -1; -i <= n; i--) { struct lua_wait_channel *channel = luaL_testudata (L, i, XLUA_WCHANNEL_METATABLE); if (!channel) return error_set (e, "bad argument #%d to yield: %s", -i + n + 1, "tasks can only yield wait channels"); if (channel->task) return error_set (e, "bad argument #%d to yield: %s", -i + n + 1, "wait channels can only be active in one task at most"); } for (int i = -1; -i <= n; i--) { // Quietly ignore duplicate channels struct lua_wait_channel *channel = lua_touserdata (L, i); if (channel->task) continue; // By going in reverse the list ends up in the right order channel->task = self; LIST_PREPEND (self->active, channel); lua_cache_store (self->plugin->L, channel, i); } lua_pop (L, n); // There doesn't have to be a single channel // We can also be waiting on a channel that is already ready lua_task_wakeup (self); return true; } static void lua_task_resume (struct lua_task *self, int index) { lua_State *L = self->thread; bool waiting_on_multiple = self->active && self->active->next; // Since we've ended the wait, we don't need to hold on to them anymore lua_task_unregister_channels (self); // On the first run we also have the main function on the stack, // before any initial arguments int n = lua_gettop (L) - (lua_status (L) == LUA_OK); // Pack the values in a table and prepend the index of the channel, so that // the caller doesn't need to care about the number of return values if (waiting_on_multiple) { lua_plugin_pack (L, n); lua_pushinteger (L, index); lua_insert (L, -2); n = 2; } #if LUA_VERSION_NUM >= 504 int nresults = 0; int res = lua_resume (L, NULL, n, &nresults); #else int res = lua_resume (L, NULL, n); int nresults = lua_gettop (L); #endif struct error *error = NULL; if (res == LUA_YIELD) { // AFAIK we don't get any good error context information from here if (lua_task_schedule (self, nresults, &error)) return; } // For simplicity ignore any results from successful returns else if (res != LUA_OK) { luaL_traceback (L, L, lua_tostring (L, -1), 0 /* or 1? */); lua_plugin_process_error (self->plugin, lua_tostring (L, -1), &error); lua_pop (L, 2); } if (error) lua_plugin_log_error (self->plugin, "task", error); lua_task_cancel_internal (self); } static void lua_task_check (struct lua_task *self) { poller_idle_reset (&self->idle); lua_Integer i = 0; LIST_FOR_EACH (struct lua_wait_channel, iter, self->active) { i++; if (iter->check (iter)) { lua_task_resume (self, i); return; } } if (!self->active) lua_task_resume (self, i); } // The task dies either when it finishes, it is cancelled, or at plugin unload static luaL_Reg lua_task_table[] = { { "cancel", lua_task_cancel }, { "__gc", lua_task_cancel }, { NULL, NULL } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct lua_wait_timer { struct lua_wait_channel super; ///< The structure we're deriving struct poller_timer timer; ///< Timer event bool expired; ///< Whether the timer has expired }; static bool lua_wait_timer_check (struct lua_wait_channel *wchannel) { struct lua_wait_timer *self = CONTAINER_OF (wchannel, struct lua_wait_timer, super); return self->super.task && self->expired; } static void lua_wait_timer_cleanup (struct lua_wait_channel *wchannel) { struct lua_wait_timer *self = CONTAINER_OF (wchannel, struct lua_wait_timer, super); poller_timer_reset (&self->timer); } static void lua_wait_timer_dispatch (struct lua_wait_timer *self) { self->expired = true; if (self->super.task) lua_task_wakeup (self->super.task); } static int lua_plugin_push_wait_timer (struct lua_plugin *plugin, lua_State *L, lua_Integer timeout) { struct lua_wait_timer *self = lua_newuserdata (L, sizeof *self); luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE); memset (self, 0, sizeof *self); self->super.check = lua_wait_timer_check; self->super.cleanup = lua_wait_timer_cleanup; self->timer = poller_timer_make (&plugin->ctx->poller); self->timer.dispatcher = (poller_timer_fn) lua_wait_timer_dispatch; self->timer.user_data = self; if (timeout) poller_timer_set (&self->timer, timeout); else self->expired = true; return 1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct lua_wait_dial { struct lua_wait_channel super; ///< The structure we're deriving struct lua_plugin *plugin; ///< The plugin we belong to struct connector connector; ///< Connector object bool active; ///< Whether the connector is alive struct lua_connection *connection; ///< Established connection char *hostname; ///< Target hostname char *last_error; ///< Connecting error, if any }; static bool lua_wait_dial_check (struct lua_wait_channel *wchannel) { struct lua_wait_dial *self = CONTAINER_OF (wchannel, struct lua_wait_dial, super); lua_State *L = self->super.task->thread; if (self->connection) { // FIXME: this way the connection may leak -- we pass the value to the // task manager on the stack and forget about it but still leave the // connection in the cache. That is because right now, when Lua code // sets up callbacks in the connection object and returns, it might // get otherwise GC'd since nothing else keeps referencing it. // By rewriting lua_connection using async, tasks and wait channels // would hold a reference, allowing us to remove it from the cache. lua_cache_get (L, self->connection); lua_pushstring (L, self->hostname); self->connection = NULL; } else if (self->last_error) { lua_pushnil (L); lua_pushnil (L); lua_pushstring (L, self->last_error); } else return false; return true; } static void lua_wait_dial_cancel (struct lua_wait_dial *self) { if (self->active) { connector_free (&self->connector); self->active = false; } } static void lua_wait_dial_cleanup (struct lua_wait_channel *wchannel) { struct lua_wait_dial *self = CONTAINER_OF (wchannel, struct lua_wait_dial, super); lua_wait_dial_cancel (self); if (self->connection) lua_connection_discard (self->connection); free (self->hostname); free (self->last_error); } static void lua_wait_dial_on_connected (void *user_data, int socket, const char *hostname) { struct lua_wait_dial *self = user_data; if (self->super.task) lua_task_wakeup (self->super.task); self->connection = lua_plugin_push_connection (self->plugin, socket); // TODO: use the hostname for SNI once TLS is implemented self->hostname = xstrdup (hostname); lua_wait_dial_cancel (self); } static void lua_wait_dial_on_failure (void *user_data) { struct lua_wait_dial *self = user_data; if (self->super.task) lua_task_wakeup (self->super.task); lua_wait_dial_cancel (self); } static void lua_wait_dial_on_error (void *user_data, const char *error) { struct lua_wait_dial *self = user_data; cstr_set (&self->last_error, xstrdup (error)); } static int lua_plugin_push_wait_dial (struct lua_plugin *plugin, lua_State *L, const char *host, const char *service) { struct lua_wait_dial *self = lua_newuserdata (L, sizeof *self); luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE); memset (self, 0, sizeof *self); self->super.check = lua_wait_dial_check; self->super.cleanup = lua_wait_dial_cleanup; struct connector *connector = &self->connector; connector_init (connector, &plugin->ctx->poller); connector_add_target (connector, host, service); connector->on_connected = lua_wait_dial_on_connected; connector->on_connecting = NULL; connector->on_error = lua_wait_dial_on_error; connector->on_failure = lua_wait_dial_on_failure; connector->user_data = self; self->plugin = plugin; self->active = true; return 1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_async_go (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); luaL_checktype (L, 1, LUA_TFUNCTION); lua_State *thread = lua_newthread (L); lua_cache_store (L, thread, -1); lua_pop (L, 1); // Move the main function w/ arguments to the thread lua_xmove (L, thread, lua_gettop (L)); struct lua_task *task = lua_newuserdata (L, sizeof *task); luaL_setmetatable (L, XLUA_TASK_METATABLE); memset (task, 0, sizeof *task); task->plugin = plugin; task->thread = thread; task->idle = poller_idle_make (&plugin->ctx->poller); task->idle.dispatcher = (poller_idle_fn) lua_task_check; task->idle.user_data = task; poller_idle_set (&task->idle); // Make sure the task doesn't get garbage collected and return it lua_cache_store (L, task, -1); return 1; } static int lua_async_timer_ms (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); lua_Integer timeout = luaL_checkinteger (L, 1); if (timeout < 0) luaL_argerror (L, 1, "timeout mustn't be negative"); return lua_plugin_push_wait_timer (plugin, L, timeout); } static int lua_async_dial (lua_State *L) { struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); return lua_plugin_push_wait_dial (plugin, L, luaL_checkstring (L, 1), luaL_checkstring (L, 2)); } static luaL_Reg lua_async_library[] = { { "go", lua_async_go }, { "timer_ms", lua_async_timer_ms }, { "dial", lua_async_dial }, { NULL, NULL }, }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int lua_plugin_get_screen_size (lua_State *L) { lua_pushinteger (L, g_terminal.lines); lua_pushinteger (L, g_terminal.columns); return 2; } static int lua_ctx_gc (lua_State *L) { return lua_weak_gc (L, &lua_ctx_info); } static luaL_Reg lua_plugin_library[] = { // These are global functions: { "parse", lua_plugin_parse }, { "hook_input", lua_plugin_hook_input }, { "hook_irc", lua_plugin_hook_irc }, { "hook_prompt", lua_plugin_hook_prompt }, { "hook_completion", lua_plugin_hook_completion }, { "setup_config", lua_plugin_setup_config }, // And these are methods: { "get_screen_size", lua_plugin_get_screen_size }, { "__gc", lua_ctx_gc }, { NULL, NULL }, }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void * lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size) { (void) ud; (void) o_size; if (n_size) return realloc (ptr, n_size); free (ptr); return NULL; } static int lua_plugin_panic (lua_State *L) { // XXX: we might be able to do something better print_fatal ("Lua panicked: %s", lua_tostring (L, -1)); lua_close (L); exit (EXIT_FAILURE); return 0; } static void lua_plugin_push_ref (lua_State *L, struct lua_plugin *self, void *object, struct ispect_field *field) { // We create a mapping on object type registration hard_assert (lua_rawgetp (L, LUA_REGISTRYINDEX, field->fields)); struct lua_weak_info *info = lua_touserdata (L, -1); lua_pop (L, 1); if (!field->is_list) { lua_weak_push (L, self, object, info); return; } // As a rule in this codebase, these fields are right at the top of structs struct list_header { LIST_HEADER (void) }; int i = 1; lua_newtable (L); LIST_FOR_EACH (struct list_header, iter, object) { lua_weak_push (L, self, iter, info); lua_rawseti (L, -2, i++); } } static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self, const char *key, void *p, struct ispect_field *field); static void lua_plugin_push_struct (lua_State *L, struct lua_plugin *self, enum ispect_type type, void *value, struct ispect_field *field) { if (type == ISPECT_STR) { const struct str *s = value; lua_pushlstring (L, s->str, s->len); return; } if (type == ISPECT_STR_MAP) { struct str_map_iter iter = str_map_iter_make (value); void *value; lua_newtable (L); while ((value = str_map_iter_next (&iter))) lua_plugin_push_map_field (L, self, iter.link->key, value, field); return; } hard_assert (!"unhandled introspection object type"); } static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self, const char *key, void *p, struct ispect_field *field) { // That would mean maps in maps ad infinitum hard_assert (field->subtype != ISPECT_STR_MAP); intptr_t n = (intptr_t) p; switch (field->subtype) { // Here the types are generally casted to a void pointer case ISPECT_BOOL: lua_pushboolean (L, (bool ) n); break; case ISPECT_INT: lua_pushinteger (L, (int ) n); break; case ISPECT_UINT: lua_pushinteger (L, (unsigned ) n); break; case ISPECT_SIZE: lua_pushinteger (L, (size_t ) n); break; case ISPECT_STRING: lua_pushstring (L, p); break; case ISPECT_REF: lua_plugin_push_ref (L, self, p, field); break; default: lua_plugin_push_struct (L, self, field->subtype, p, field); } lua_setfield (L, -2, key); } static bool lua_plugin_property_get_ispect (lua_State *L, const char *property_name) { struct lua_weak_info *info = lua_touserdata (L, lua_upvalueindex (1)); if (!info || !info->ispect) return false; struct lua_weak *weak = luaL_checkudata (L, 1, info->name); // TODO: I think we can do better than this, maybe binary search at least? struct ispect_field *field; for (field = info->ispect; field->name; field++) if (!strcmp (property_name, field->name)) break; if (!field->name) return false; struct lua_plugin *self = weak->plugin; void *p = (uint8_t *) weak->object + field->offset; switch (field->type) { // Here the types are really what's under the pointer case ISPECT_BOOL: lua_pushboolean (L, *(bool *) p); break; case ISPECT_INT: lua_pushinteger (L, *(int *) p); break; case ISPECT_UINT: lua_pushinteger (L, *(unsigned *) p); break; case ISPECT_SIZE: lua_pushinteger (L, *(size_t *) p); break; case ISPECT_STRING: lua_pushstring (L, *(char **) p); break; case ISPECT_REF: lua_plugin_push_ref (L, self, *(void **) p, field); break; default: lua_plugin_push_struct (L, self, field->type, p, field); } return true; } static int lua_plugin_property_get (lua_State *L) { luaL_checktype (L, 1, LUA_TUSERDATA); const char *property_name = luaL_checkstring (L, 2); // Either it's directly present in the metatable if (luaL_getmetafield (L, 1, property_name)) return 1; // Or we try to find and eventually call a getter method char *getter_name = xstrdup_printf ("get_%s", property_name); bool found = luaL_getmetafield (L, 1, getter_name); free (getter_name); if (found) { lua_pushvalue (L, 1); lua_call (L, 1, 1); return 1; } // Maybe we can find it via introspection if (lua_plugin_property_get_ispect (L, property_name)) return 1; // Or we look for a property set by the user (__gc cannot be overriden) if (lua_getuservalue (L, 1) != LUA_TTABLE) lua_pushnil (L); else lua_getfield (L, -1, property_name); return 1; } static int lua_plugin_property_set (lua_State *L) { luaL_checktype (L, 1, LUA_TUSERDATA); const char *property_name = luaL_checkstring (L, 2); luaL_checkany (L, 3); // We use the associated value to store user-defined properties int type = lua_getuservalue (L, 1); if (type == LUA_TNIL) { lua_pop (L, 1); lua_newtable (L); lua_pushvalue (L, -1); lua_setuservalue (L, 1); } else if (type != LUA_TTABLE) return luaL_error (L, "associated value is not a table"); // Beware that we do not check for conflicts here; // if Lua code writes a conflicting field, it is effectively ignored lua_pushvalue (L, 3); lua_setfield (L, -2, property_name); return 0; } static void lua_plugin_add_accessors (lua_State *L, struct lua_weak_info *info) { // Emulate properties for convenience lua_pushlightuserdata (L, info); lua_pushcclosure (L, lua_plugin_property_get, 1); lua_setfield (L, -2, "__index"); lua_pushcfunction (L, lua_plugin_property_set); lua_setfield (L, -2, "__newindex"); } static void lua_plugin_reg_meta (lua_State *L, const char *name, luaL_Reg *fns) { luaL_newmetatable (L, name); luaL_setfuncs (L, fns, 0); lua_plugin_add_accessors (L, NULL); lua_pop (L, 1); } static void lua_plugin_reg_weak (lua_State *L, struct lua_weak_info *info, luaL_Reg *fns) { // Create a mapping from the object type (info->ispect) back to metadata // so that we can figure out what to create from ISPECT_REF fields lua_pushlightuserdata (L, info); lua_rawsetp (L, LUA_REGISTRYINDEX, info->ispect); luaL_newmetatable (L, info->name); luaL_setfuncs (L, fns, 0); lua_plugin_add_accessors (L, info); lua_pop (L, 1); } static struct plugin * lua_plugin_load (struct app_context *ctx, const char *filename, struct error **e) { lua_State *L = lua_newstate (lua_plugin_alloc, NULL); if (!L) { error_set (e, "Lua initialization failed"); return NULL; } lua_atpanic (L, lua_plugin_panic); luaL_openlibs (L); struct lua_plugin *plugin = xcalloc (1, sizeof *plugin); plugin->super.name = xstrdup (filename); plugin->super.vtable = &lua_plugin_vtable; plugin->ctx = ctx; plugin->L = L; luaL_checkversion (L); // Register the xC library as a singleton with "plugin" as an upvalue // (mostly historical, but rather convenient) luaL_newmetatable (L, lua_ctx_info.name); lua_pushlightuserdata (L, plugin); luaL_setfuncs (L, lua_plugin_library, 1); lua_plugin_add_accessors (L, &lua_ctx_info); // Add the asynchronous library underneath lua_newtable (L); lua_pushlightuserdata (L, plugin); luaL_setfuncs (L, lua_async_library, 1); lua_setfield (L, -2, "async"); lua_pop (L, 1); lua_weak_push (L, plugin, ctx, &lua_ctx_info); lua_setglobal (L, lua_ctx_info.name); // Create metatables for our objects lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE, lua_hook_table); lua_plugin_reg_weak (L, &lua_user_info, lua_user_table); lua_plugin_reg_weak (L, &lua_channel_info, lua_channel_table); lua_plugin_reg_weak (L, &lua_buffer_info, lua_buffer_table); lua_plugin_reg_weak (L, &lua_server_info, lua_server_table); lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_table); lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table); lua_plugin_reg_meta (L, XLUA_TASK_METATABLE, lua_task_table); lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE, lua_wchannel_table); struct error *error = NULL; if (luaL_loadfile (L, filename)) error_set (e, "%s: %s", "Lua", lua_tostring (L, -1)); else if (!lua_plugin_call (plugin, 0, 0, &error)) { error_set (e, "%s: %s", "Lua", error->message); error_free (error); } else return &plugin->super; plugin_destroy (&plugin->super); return NULL; } #endif // HAVE_LUA // --- Plugins ----------------------------------------------------------------- typedef struct plugin *(*plugin_load_fn) (struct app_context *ctx, const char *filename, struct error **e); // We can potentially add support for other scripting languages if so desired, // however this possibility is just a byproduct of abstraction static plugin_load_fn g_plugin_loaders[] = { #ifdef HAVE_LUA lua_plugin_load, #endif // HAVE_LUA }; static struct plugin * plugin_load_from_filename (struct app_context *ctx, const char *filename, struct error **e) { struct plugin *plugin = NULL; struct error *error = NULL; for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++) if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error) break; if (error) error_propagate (e, error); else if (!plugin) { error_set (e, "no plugin handler for \"%s\"", filename); return NULL; } return plugin; } static struct plugin * plugin_find (struct app_context *ctx, const char *name) { LIST_FOR_EACH (struct plugin, iter, ctx->plugins) if (!strcmp (name, iter->name)) return iter; return NULL; } static char * plugin_resolve_relative_filename (const char *filename) { struct strv paths = strv_make (); get_xdg_data_dirs (&paths); char *result = resolve_relative_filename_generic (&paths, PROGRAM_NAME "/plugins/", filename); strv_free (&paths); return result; } static struct plugin * plugin_load_by_name (struct app_context *ctx, const char *name, struct error **e) { struct plugin *plugin = plugin_find (ctx, name); if (plugin) { error_set (e, "plugin already loaded"); return NULL; } // As a side effect, a plugin can be loaded multiple times by giving // various relative or non-relative paths to the function; this is not // supposed to be fool-proof though, that requires other mechanisms char *filename = resolve_filename (name, plugin_resolve_relative_filename); if (!filename) { error_set (e, "file not found"); return NULL; } plugin = plugin_load_from_filename (ctx, filename, e); free (filename); return plugin; } static void plugin_load (struct app_context *ctx, const char *name) { struct error *e = NULL; struct plugin *plugin = plugin_load_by_name (ctx, name, &e); if (plugin) { // FIXME: this way the real name isn't available to the plugin on load, // which has effect on e.g. plugin_config_name() cstr_set (&plugin->name, xstrdup (name)); log_global_status (ctx, "Plugin \"#s\" loaded", name); LIST_PREPEND (ctx->plugins, plugin); } else { log_global_error (ctx, "Can't load plugin \"#s\": #s", name, e->message); error_free (e); } } static void plugin_unload (struct app_context *ctx, const char *name) { struct plugin *plugin = plugin_find (ctx, name); if (!plugin) log_global_error (ctx, "Can't unload plugin \"#s\": #s", name, "plugin not loaded"); else { log_global_status (ctx, "Plugin \"#s\" unloaded", name); LIST_UNLINK (ctx->plugins, plugin); plugin_destroy (plugin); } } static void load_plugins (struct app_context *ctx) { const char *plugins = get_config_string (ctx->config.root, "behaviour.plugin_autoload"); if (plugins) { struct strv v = strv_make (); cstr_split (plugins, ",", true, &v); for (size_t i = 0; i < v.len; i++) plugin_load (ctx, v.vector[i]); strv_free (&v); } } // --- 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 // Contrary to maybe_cut_word(), we ignore all whitespace at the end 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))) return buffer; // Basic case insensitive partial matching -- at most one buffer can match int n_matches = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { char *string = xstrdup (iter->name); char *pattern = xstrdup_printf ("*%s*", word); for (char *p = string; *p; p++) *p = tolower_ascii (*p); for (char *p = pattern; *p; p++) *p = tolower_ascii (*p); if (!fnmatch (pattern, string, 0)) { n_matches++; buffer = iter; } free (string); free (pattern); } return n_matches == 1 ? buffer : NULL; } 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) { struct str s = str_make (); int new = iter->new_messages_count - iter->new_unimportant_count; if (new && iter != ctx->current_buffer) str_append_printf (&s, " (%d%s)", new, &"!"[!iter->highlighted]); log_global_indent (ctx, " [#d] #s#&s", i++, iter->name, str_steal (&s)); } } 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); struct channel *channel; if ((channel = str_map_find (&s->irc_channels, channel_name))) channel->left_manually = true; } static bool handle_buffer_goto (struct app_context *ctx, struct handler_args *a) { if (!*a->arguments) return false; const char *which = cut_word (&a->arguments); struct buffer *buffer = try_decode_buffer (ctx, which); if (buffer) buffer_activate (ctx, buffer); else log_global_error (ctx, "#s: #s", "No such buffer", which); return true; } 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_channel_is_joined (buffer->channel)) part_channel (buffer->server, buffer->channel->name, ""); buffer_remove_safe (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; if (request == 0 || request > (unsigned long) buffer_count (ctx)) { log_global_error (ctx, "#s: #s", "Can't move buffer", "requested position is out of range"); return true; } buffer_move (ctx, a->buffer, request); 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; bool result = true; if (!*action || !strcasecmp_ascii (action, "list")) show_buffers_list (ctx); else if (!strcasecmp_ascii (action, "clear")) { buffer_clear (a->buffer); buffer_print_backlog (ctx, a->buffer); } else if (!strcasecmp_ascii (action, "move")) result = handle_buffer_move (ctx, a); else if (!strcasecmp_ascii (action, "goto")) result = handle_buffer_goto (ctx, a); else if (!strcasecmp_ascii (action, "close")) handle_buffer_close (ctx, a); else result = false; return result; } static bool handle_command_set_add (struct strv *items, const struct strv *values, struct error **e) { for (size_t i = 0; i < values->len; i++) { const char *value = values->vector[i]; if (strv_find (items, values->vector[i]) != -1) return error_set (e, "already present in the array: %s", value); strv_append (items, value); } return true; } static bool handle_command_set_remove (struct strv *items, const struct strv *values, struct error **e) { for (size_t i = 0; i < values->len; i++) { const char *value = values->vector[i]; ssize_t i = strv_find (items, value); if (i == -1) return error_set (e, "not present in the array: %s", value); strv_remove (items, i); } return true; } static bool handle_command_set_modify (struct config_item *item, const char *value, bool add, struct error **e) { struct strv items = strv_make (); if (item->type != CONFIG_ITEM_NULL) cstr_split (item->value.string.str, ",", false, &items); if (items.len == 1 && !*items.vector[0]) strv_reset (&items); struct strv values = strv_make (); cstr_split (value, ",", false, &values); bool result = add ? handle_command_set_add (&items, &values, e) : handle_command_set_remove (&items, &values, e); if (result) { char *changed = strv_join (&items, ","); struct str tmp = { .str = changed, .len = strlen (changed) }; result = config_item_set_from (item, config_item_string_array (&tmp), e); free (changed); } strv_free (&items); strv_free (&values); 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) config_item_set_from (item, config_item_clone (new_), &e); else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY) error_set (&e, "not a string array"); else handle_command_set_modify (item, new_->value.string.str, add, &e); if (e) { log_global_error (ctx, "Failed to set option \"#s\": #s", key, e->message); error_free (e); } else { struct strv tmp = strv_make (); dump_matching_options (ctx->config.root, key, &tmp); log_global_status (ctx, "Option changed: #s", tmp.vector[0]); strv_free (&tmp); } } static bool handle_command_set_assign (struct app_context *ctx, struct strv *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 = cstr_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 strv all = strv_make (); dump_matching_options (ctx->config.root, 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); strv_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 void show_plugin_list (struct app_context *ctx) { log_global_indent (ctx, ""); log_global_indent (ctx, "Plugins:"); LIST_FOR_EACH (struct plugin, iter, ctx->plugins) log_global_indent (ctx, " #s", iter->name); } static bool handle_command_plugin (struct handler_args *a) { char *action = cut_word (&a->arguments); if (!*action || !strcasecmp_ascii (action, "list")) show_plugin_list (a->ctx); else if (!strcasecmp_ascii (action, "load")) { if (!*a->arguments) return false; plugin_load (a->ctx, cut_word (&a->arguments)); } else if (!strcasecmp_ascii (action, "unload")) { if (!*a->arguments) return false; plugin_unload (a->ctx, cut_word (&a->arguments)); } else return false; 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_make (aliases); struct config_item *alias; while ((alias = str_map_iter_next (&iter))) { struct str definition = str_make (); 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_make (); 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, irc_skip_statusmsg (a->s, target))) log_server_error (a->s, a->s->buffer, "Cannot query a channel"); else if (!*a->arguments) buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target)); 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_squery (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 irc_send (a->s, "SQUERY %s :%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); cstr_transform (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); 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) { request_quit (a->ctx, *a->arguments ? a->arguments : NULL); 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"); else if (irc_channel_is_joined (a->buffer->channel)) 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 bool handle_command_part (struct handler_args *a) { if (irc_is_channel (a->s, a->arguments)) { struct strv v = strv_make (); cstr_split (cut_word (&a->arguments), ",", true, &v); for (size_t i = 0; i < v.len; i++) part_channel (a->s, v.vector[i], a->arguments); strv_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"); else if (!irc_channel_is_joined (a->buffer->channel)) 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 strv v = strv_make (); cstr_split (cut_word (&a->arguments), ",", true, &v); for (size_t i = 0; i < v.len; i++) cycle_channel (a->s, v.vector[i], a->arguments); strv_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"); else if (!irc_channel_is_joined (a->buffer->channel)) 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 start the topic with whitespace // FIXME: there's no way to unset the topic; // we could adopt the Tcl style of "-switches" with "--" sentinels, // or we could accept "strings" in the config format 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 strv *v) { size_t n; for (size_t i = 0; i < v->len; i += n) { struct str modes = str_make (); struct str params = str_make (); 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 strv v = strv_make (); cstr_split (a->arguments, " ", true, &v); // XXX: this may be a bit too trivial; we could also map 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, "!@*?") || irc_is_extban (a->s, target)) continue; v.vector[i] = xstrdup_printf ("%s!*@*", target); free (target); } mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v); strv_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 strv v = strv_make (); cstr_split (a->arguments, " ", true, &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); strv_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); s->reconnect_attempt = 0; 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 (poller_timer_is_active (&s->reconnect_tmr)) { log_server_status (s, s->buffer, "Connecting aborted"); poller_timer_reset (&s->reconnect_tmr); } 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 bool 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_make (&ctx->servers); struct server *s; while ((s = str_map_iter_next (&iter))) log_global_indent (ctx, " #s", s->name); return true; } static bool handle_server_add (struct handler_args *a) { if (!*a->arguments) return false; struct app_context *ctx = a->ctx; char *name = cut_word (&a->arguments); const char *err; if ((err = check_server_name_for_addition (ctx, name))) log_global_error (ctx, "Cannot create server `#s': #s", name, err); else { server_add_new (ctx, name); log_global_status (ctx, "Server added: #s", name); } return true; } static bool handle_server_remove (struct handler_args *a) { struct app_context *ctx = a->ctx; struct server *s = NULL; if (!(s = resolve_server (ctx, a, "server"))) return true; if (irc_is_connected (s)) log_server_error (s, s->buffer, "Can't remove a connected server"); else { char *name = xstrdup (s->name); server_remove (ctx, s); log_global_status (ctx, "Server removed: #s", name); free (name); } return true; } static bool handle_server_rename (struct handler_args *a) { struct app_context *ctx = a->ctx; if (!*a->arguments) return false; char *old_name = cut_word (&a->arguments); if (!*a->arguments) return false; char *new_name = cut_word (&a->arguments); struct server *s; const char *err; if (!(s = str_map_find (&ctx->servers, old_name))) log_global_error (ctx, "/#s: #s: #s", "server", "no such server", old_name); else if ((err = check_server_name_for_addition (ctx, new_name))) log_global_error (ctx, "Cannot rename server to `#s': #s", new_name, err); else { server_rename (ctx, s, new_name); log_global_status (ctx, "Server renamed: #s to #s", old_name, new_name); } return true; } static bool handle_command_server (struct handler_args *a) { if (!*a->arguments) return show_servers_list (a->ctx); char *action = cut_word (&a->arguments); if (!strcasecmp_ascii (action, "list")) return show_servers_list (a->ctx); if (!strcasecmp_ascii (action, "add")) return handle_server_add (a); if (!strcasecmp_ascii (action, "remove")) return handle_server_remove (a); if (!strcasecmp_ascii (action, "rename")) return handle_server_rename (a); return false; } 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 neither 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_kill (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (*a->arguments) irc_send (a->s, "KILL %s :%s", target, a->arguments); else irc_send (a->s, "KILL %s", target); 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) { const char *targets = a->arguments; if (!*targets) { if (adding) return false; targets = a->s->irc_user->nickname; } struct strv v = strv_make (); cstr_split (targets, " ", true, &v); mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v); strv_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 (oper, "OPER") 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", "[ |