diff options
Diffstat (limited to 'degesch.c')
-rw-r--r-- | degesch.c | 14473 |
1 files changed, 0 insertions, 14473 deletions
diff --git a/degesch.c b/degesch.c deleted file mode 100644 index 0f8f22b..0000000 --- a/degesch.c +++ /dev/null @@ -1,14473 +0,0 @@ -/* - * degesch.c: a terminal-based IRC client - * - * Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name> - * - * 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 "degesch" - -#include "common.c" -#include "kike-replies.c" - -#include <math.h> -#include <langinfo.h> -#include <locale.h> -#include <pwd.h> -#include <sys/utsname.h> -#include <wchar.h> - -#include <termios.h> -#include <sys/ioctl.h> - -#include <curses.h> -#include <term.h> - -// Literally cancer -#undef lines -#undef columns - -#include <ffi.h> - -#ifdef HAVE_LUA -#include <lua.h> -#include <lualib.h> -#include <lauxlib.h> -#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 - char *(*get_line) (void *input); - /// 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 <readline/readline.h> -#include <readline/history.h> - -#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) -{ - (void) input; - 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-<key> 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 <histedit.h> - -#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 int -input_el__get_termios (int character, int fallback) -{ - if (!g_terminal.initialized) - return fallback; - - cc_t value = g_terminal.termios.c_cc[character]; - if (value == _POSIX_VDISABLE) - return fallback; - return value; -} - -static void -input_el__redisplay (void *input) -{ - // See rl_redisplay() - struct input_el *self = input; - char x[] = { input_el__get_termios (VREPRINT, 'R' - 0x40), 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) -{ - struct input_el *self = input; - const LineInfo *info = el_line (self->editline); - 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 mIRC 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_mirc_escape; ///< Awaiting a mIRC attribute 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 = "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 display a buffer's history", - .type = CONFIG_ITEM_STRING, - .default_ = "\"LESSSECURE=1 less -M -R +Gb\"" }, - { .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 -}; - -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); - - 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 a mIRC-formatted string (auto-resets at boundaries) -// #n cuts the nickname from a string and automatically colours it -// #N is like #n but also appends userhost, if present -// -// #a inserts named attributes (auto-resets) -// #r resets terminal attributes -// #c sets foreground 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_)) -#define FORMATTER_ADD_SIMPLE(self, attribute_) \ - FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = TEXT_ ## attribute_) - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -enum -{ - MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN, - MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE, - MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN, - MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY, -}; - -// We use estimates from the 16 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 char 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 * -formatter_parse_mirc_color (struct formatter *self, const char *s) -{ - if (!isdigit_ascii (*s)) - { - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = -1); - return s; - } - - int fg = *s++ - '0'; - if (isdigit_ascii (*s)) - fg = fg * 10 + (*s++ - '0'); - if (fg < 16) - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); - else - FORMATTER_ADD_ITEM (self, FG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[fg])); - - if (*s != ',' || !isdigit_ascii (s[1])) - return s; - s++; - - int bg = *s++ - '0'; - if (isdigit_ascii (*s)) - bg = bg * 10 + (*s++ - '0'); - if (bg < 16) - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); - else - FORMATTER_ADD_ITEM (self, BG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[bg])); - - return s; -} - -static void -formatter_parse_mirc (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); - } - - switch (c) - { - case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD); break; - case '\x11': /* monospace, N/A */ break; - case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC); break; - case '\x1e': FORMATTER_ADD_SIMPLE (self, CROSSED_OUT); break; - case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break; - case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE); break; - - case '\x03': - s = formatter_parse_mirc_color (self, s); - break; - case '\x0f': - FORMATTER_ADD_RESET (self); - break; - default: - str_append_c (&buf, c); - } - } - - if (buf.len) - FORMATTER_ADD_TEXT (self, buf.str); - - str_free (&buf); - FORMATTER_ADD_RESET (self); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -formatter_parse_nick (struct formatter *self, 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_mirc (self, tmp); - free (tmp); - break; - case 'n': - formatter_parse_nick (self, (s = va_arg (*ap, char *))); - break; - case 'N': - formatter_parse_nick_full (self, (s = va_arg (*ap, char *))); - break; - - case 'a': - FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int)); - break; - case 'c': - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int)); - break; - case 'C': - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int)); - break; - case 'r': - FORMATTER_ADD_RESET (self); - break; - - default: - if (c == '&' && !free_string) - free_string = true; - else if (c) - hard_assert (!"unexpected format specifier"); - else - hard_assert (!"unexpected end of format string"); - goto restart; - } - - if (free_string) - free (s); - return field; -} - -// I was unable to take a pointer of a bare "va_list" when it was passed in -// as a function argument, so it has to be a pointer from the beginning -static void -formatter_addv (struct formatter *self, const char *format, va_list *ap) -{ - struct str buf = str_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_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, "<H>"); - - 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_mirc (&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 - // TODO: also handle actions here - 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); - 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 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 }, -}; - -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 the most basic acceptable algorithm; something like ICU with proper -// locale specification would be needed to make it work better. - -static size_t -wrap_text_for_single_line (const char *text, size_t text_len, - size_t line_len, struct str *output) -{ - 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 (line_len && word_len <= line_len) - { - if (word_len) - { - str_append_data (output, text, word_len); - - text += word_len; - eaten += word_len; - line_len -= word_len; - } - - // Find the next word's end - word_start = text + strspn (text, " "); - word_end = word_start + strcspn (word_start, " "); - word_len = word_end - text; - } - - if (eaten) - // Discard whitespace between words if split - return eaten + (word_start - text); - - // And if that doesn't help, cut the longest valid block of characters - for (const char *p = text; (size_t) (p - text) <= line_len; ) - { - eaten = p - text; - hard_assert (utf8_decode (&p, text_len - eaten) >= 0); - } - str_append_data (output, text, eaten); - return eaten; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static bool -wrap_message (const char *message, - int line_max, struct strv *output, struct error **e) -{ - if (line_max <= 0) - goto error; - - int message_left = strlen (message); - while (message_left > line_max) - { - struct str m = str_make (); - - size_t eaten = wrap_text_for_single_line - (message, message_left, line_max, &m); - if (!eaten) - { - str_free (&m); - goto error; - } - - strv_append_owned (output, str_steal (&m)); - message += eaten; - message_left -= eaten; - } - - if (message_left) - strv_append (output, message); - - return true; - -error: - // Well, that's just weird - error_set (e, - "Message splitting was unsuccessful as there was " - "too little room for UTF-8 characters"); - return false; -} - -/// Automatically splits messages that arrive at other clients with our prefix -/// so that they don't arrive cut off by the server -static bool -irc_autosplit_message (struct server *s, const char *message, - int fixed_part, struct strv *output, struct error **e) -{ - // :<nick>!<user>@<host> <fixed-part><message> - 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 degesch 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", - "[<command> | <option>]", - handle_command_help, 0 }, - { "quit", "Quit the program", - "[<message>]", - handle_command_quit, 0 }, - { "buffer", "Manage buffers", - "<N> | list | clear | move <N> | goto <N or name> | close [<N or name>]", - handle_command_buffer, 0 }, - { "set", "Manage configuration", - "[<option>]", - handle_command_set, 0 }, - { "save", "Save configuration", - NULL, - handle_command_save, 0 }, - { "plugin", "Manage plugins", - "list | load <name> | unload <name>", - handle_command_plugin, 0 }, - - { "alias", "List or set aliases", - "[<name> <definition>]", - handle_command_alias, 0 }, - { "unalias", "Unset aliases", - "<name>...", - handle_command_unalias, 0 }, - - { "msg", "Send message to a nick or channel", - "<target> <message>", - handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG }, - { "query", "Send a private message to a nick", - "<nick> <message>", - handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG }, - { "notice", "Send notice to a nick or channel", - "<target> <message>", - handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG }, - { "squery", "Send a message to a service", - "<service> <message>", - handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG }, - { "ctcp", "Send a CTCP query", - "<target> <tag>", - handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG }, - { "me", "Send a CTCP action", - "<message>", - handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG }, - - { "join", "Join channels", - "[<channel>[,<channel>...]] [<key>[,<key>...]]", - handle_command_join, HANDLER_SERVER }, - { "part", "Leave channels", - "[<channel>[,<channel>...]] [<reason>]", - handle_command_part, HANDLER_SERVER }, - { "cycle", "Rejoin channels", - "[<channel>[,<channel>...]] [<reason>]", - handle_command_cycle, HANDLER_SERVER }, - - { "op", "Give channel operator status", - "<nick>...", - handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "deop", "Remove channel operator status", - "[<nick>...]", - handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "voice", "Give voice", - "<nick>...", - handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "devoice", "Remove voice", - "[<nick>...]", - handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - - { "mode", "Change mode", - "[<channel>] [<mode>...]", - handle_command_mode, HANDLER_SERVER }, - { "topic", "Change topic", - "[<channel>] [<topic>]", - handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "kick", "Kick user from channel", - "[<channel>] <user> [<reason>]", - handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "kickban", "Kick and ban user from channel", - "[<channel>] <user> [<reason>]", - handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "ban", "Ban user from channel", - "[<channel>] [<mask>...]", - handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "unban", "Unban user from channel", - "[<channel>] <mask>...", - handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST }, - { "invite", "Invite user to channel", - "<user>... [<channel>]", - handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST }, - - { "server", "Manage servers", - "list | add <name> | remove <name> | rename <old> <new>", - handle_command_server, 0 }, - { "connect", "Connect to the server", - "[<server>]", - handle_command_connect, 0 }, - { "disconnect", "Disconnect from the server", - "[<server> [<reason>]]", - handle_command_disconnect, 0 }, - - { "list", "List channels and their topic", - "[<channel>[,<channel>...]] [<target>]", - handle_command_list, HANDLER_SERVER }, - { "names", "List users on channel", - "[<channel>[,<channel>...]]", - handle_command_names, HANDLER_SERVER }, - { "who", "List users", - "[<mask> [o]]", - handle_command_who, HANDLER_SERVER }, - { "whois", "Get user information", - "[<target>] <mask>", - handle_command_whois, HANDLER_SERVER }, - { "whowas", "Get user information", - "<user> [<count> [<target>]]", - handle_command_whowas, HANDLER_SERVER }, - - { "motd", "Get the Message of The Day", - "[<target>]", - handle_command_motd, HANDLER_SERVER }, - { "oper", "Authenticate as an IRC operator", - "<name> <password>", - handle_command_oper, HANDLER_SERVER }, - { "kill", "Kick another user from the server", - "<user> <comment>", - handle_command_kill, HANDLER_SERVER }, - { "stats", "Query server statistics", - "[<query> [<target>]]", - handle_command_stats, HANDLER_SERVER }, - { "away", "Set away status", - "[<text>]", - handle_command_away, HANDLER_SERVER }, - { "nick", "Change current nick", - "<nickname>", - handle_command_nick, HANDLER_SERVER }, - { "quote", "Send a raw command to the server", - "<command>", - handle_command_quote, HANDLER_SERVER }, -}; - -static bool -try_handle_command_help_option (struct app_context *ctx, const char *name) -{ - struct config_item *item = - config_item_get (ctx->config.root, name, NULL); - if (!item) - return false; - - struct config_schema *schema = item->schema; - if (!schema) - { - log_global_error (ctx, "#s: #s", "Option not recognized", name); - return true; - } - - log_global_indent (ctx, ""); - log_global_indent (ctx, "Option \"#s\":", name); - log_global_indent (ctx, " Description: #s", - schema->comment ? schema->comment : "(none)"); - log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type)); - log_global_indent (ctx, " Default: #s", - schema->default_ ? schema->default_ : "null"); - - struct str tmp = str_make (); - config_item_write (item, false, &tmp); - log_global_indent (ctx, " Current value: #s", tmp.str); - str_free (&tmp); - return true; -} - -static bool -show_command_list (struct app_context *ctx) -{ - log_global_indent (ctx, ""); - log_global_indent (ctx, "Commands:"); - - int longest = 0; - for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) - { - int len = strlen (g_command_handlers[i].name); - longest = MAX (longest, len); - } - for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) - { - struct command_handler *handler = &g_command_handlers[i]; - log_global_indent (ctx, " #&s", xstrdup_printf - ("%-*s %s", longest, handler->name, handler->description)); - } - return true; -} - -static bool -show_command_help (struct app_context *ctx, struct command_handler *handler) -{ - log_global_indent (ctx, ""); - log_global_indent (ctx, "/#s: #s", handler->name, handler->description); - log_global_indent (ctx, " Arguments: #s", - handler->usage ? handler->usage : "(none)"); - return true; -} - -static bool -handle_command_help (struct handler_args *a) -{ - struct app_context *ctx = a->ctx; - if (!*a->arguments) - return show_command_list (ctx); - - const char *word = cut_word (&a->arguments); - - const char *command = word + (*word == '/'); - for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) - { - struct command_handler *handler = &g_command_handlers[i]; - if (!strcasecmp_ascii (command, handler->name)) - return show_command_help (ctx, handler); - } - - if (try_handle_command_help_option (ctx, word)) - return true; - - if (str_map_find (get_aliases_config (ctx), command)) - log_global_status (ctx, "/#s is an alias", command); - else - log_global_error (ctx, "#s: #s", "No such command or option", word); - return true; -} - -static void -init_user_command_map (struct str_map *map) -{ - *map = str_map_make (NULL); - map->key_xfrm = tolower_ascii_strxfrm; - - for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) - { - struct command_handler *handler = &g_command_handlers[i]; - str_map_set (map, handler->name, handler); - } -} - -static bool -process_user_command (struct app_context *ctx, struct buffer *buffer, - const char *command_name, char *input) -{ - static bool initialized = false; - static struct str_map map; - if (!initialized) - { - init_user_command_map (&map); - initialized = true; - } - - if (try_handle_buffer_goto (ctx, command_name)) - return true; - - struct handler_args args = - { - .ctx = ctx, - .buffer = buffer, - .arguments = input, - }; - - struct command_handler *handler; - if (!(handler = str_map_find (&map, command_name))) - return false; - hard_assert (handler->flags == 0 || (handler->flags & HANDLER_SERVER)); - - if ((handler->flags & HANDLER_SERVER) - && args.buffer->type == BUFFER_GLOBAL) - log_global_error (ctx, "/#s: #s", - command_name, "can't do this from a global buffer"); - else if ((handler->flags & HANDLER_SERVER) - && !irc_is_connected ((args.s = args.buffer->server))) - log_server_error (args.s, args.s->buffer, "Not connected"); - else if ((handler->flags & HANDLER_NEEDS_REG) - && args.s->state != IRC_REGISTERED) - log_server_error (args.s, args.s->buffer, "Not registered"); - else if (((handler->flags & HANDLER_CHANNEL_FIRST) - && !(args.channel_name = - try_get_channel (&args, maybe_cut_word))) - || ((handler->flags & HANDLER_CHANNEL_LAST) - && !(args.channel_name = - try_get_channel (&args, maybe_cut_word_from_end)))) - log_server_error (args.s, args.buffer, "/#s: #s", command_name, - "no channel name given and this buffer is not a channel"); - else if (!handler->handler (&args)) - log_global_error (ctx, - "#s: /#s #s", "Usage", handler->name, handler->usage); - return true; -} - -static const char * -expand_alias_escape (const char *p, const char *arguments, struct str *output) -{ - struct strv words = strv_make (); - cstr_split (arguments, " ", true, &words); - - // TODO: eventually also add support for argument ranges - // - Can use ${0}, ${0:}, ${:0}, ${1:-1} with strtol, dispose of $1 syntax - // (default aliases don't use numeric arguments). - // - Start numbering from zero, since we'd have to figure out what to do - // in case we encounter a zero if we keep the current approach. - // - Ignore the sequence altogether if no closing '}' can be found, - // or if the internal format doesn't fit the above syntax. - if (*p >= '1' && *p <= '9') - { - size_t offset = *p - '1'; - if (offset < words.len) - str_append (output, words.vector[offset]); - } - else if (*p == '*') - str_append (output, arguments); - else if (strchr ("$;", *p)) - str_append_c (output, *p); - else - str_append_printf (output, "$%c", *p); - - strv_free (&words); - return ++p; -} - -static void -expand_alias_definition (const char *definition, const char *arguments, - struct strv *commands) -{ - struct str expanded = str_make (); - bool escape = false; - for (const char *p = definition; *p; p++) - { - if (escape) - { - p = expand_alias_escape (p, arguments, &expanded) - 1; - escape = false; - } - else if (*p == ';') - { - strv_append_owned (commands, str_steal (&expanded)); - expanded = str_make (); - } - else if (*p == '$' && p[1]) - escape = true; - else - str_append_c (&expanded, *p); - } - strv_append_owned (commands, str_steal (&expanded)); -} - -static bool -expand_alias (struct app_context *ctx, - const char *alias_name, char *input, struct strv *commands) -{ - struct config_item *entry = - str_map_find (get_aliases_config (ctx), alias_name); - if (!entry) - return false; - - if (!config_item_type_is_string (entry->type)) - { - log_global_error (ctx, "Error executing `/#s': #s", - alias_name, "alias definition is not a string"); - return false; - } - - expand_alias_definition (entry->value.string.str, input, commands); - return true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -send_message_to_target (struct server *s, - const char *target, char *message, struct buffer *buffer) -{ - if (!irc_is_connected (s)) - log_server_error (s, buffer, "Not connected"); - else - SEND_AUTOSPLIT_PRIVMSG (s, target, message); -} - -static void -send_message_to_buffer (struct app_context *ctx, struct buffer *buffer, - char *message) -{ - hard_assert (buffer != NULL); - - switch (buffer->type) - { - case BUFFER_CHANNEL: - send_message_to_target (buffer->server, - buffer->channel->name, message, buffer); - break; - case BUFFER_PM: - send_message_to_target (buffer->server, - buffer->user->nickname, message, buffer); - break; - default: - log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR, - "This buffer is not a channel"); - } -} - -static bool -process_alias (struct app_context *ctx, struct buffer *buffer, - struct strv *commands, int level) -{ - for (size_t i = 0; i < commands->len; i++) - log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"", - (int) i, commands->vector[i]); - for (size_t i = 0; i < commands->len; i++) - if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level)) - return false; - return true; -} - -static bool -process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer, - char *input, int alias_level) -{ - if (*input != '/' || *++input == '/') - { - send_message_to_buffer (ctx, buffer, input); - return true; - } - - char *name = cut_word (&input); - if (process_user_command (ctx, buffer, name, input)) - return true; - - struct strv commands = strv_make (); - bool result = false; - if (!expand_alias (ctx, name, input, &commands)) - log_global_error (ctx, "#s: /#s", "No such command or alias", name); - else if (alias_level != 0) - log_global_error (ctx, "#s: /#s", "Aliases can't nest", name); - else - result = process_alias (ctx, buffer, &commands, alias_level); - - strv_free (&commands); - return result; -} - -static char * -process_input_hooks (struct app_context *ctx, struct buffer *buffer, - char *input) -{ - uint64_t hash = siphash_wrapper (input, strlen (input)); - LIST_FOR_EACH (struct hook, iter, ctx->input_hooks) - { - struct input_hook *hook = (struct input_hook *) iter; - if (!(input = hook->filter (hook, buffer, input))) - { - log_global_debug (ctx, "Input thrown away by hook"); - return NULL; - } - - uint64_t new_hash = siphash_wrapper (input, strlen (input)); - if (new_hash != hash) - log_global_debug (ctx, "Input transformed to \"#s\"#r", input); - hash = new_hash; - } - return input; -} - -static bool -process_input_utf8 (struct app_context *ctx, struct buffer *buffer, - const char *input, int alias_level) -{ - // Note that this also gets called on expanded aliases, - // which might or might not be desirable (we can forward "alias_level") - char *processed = process_input_hooks (ctx, buffer, xstrdup (input)); - bool result = !processed - || process_input_utf8_posthook (ctx, buffer, processed, alias_level); - free (processed); - return result; -} - -static void -process_input (struct app_context *ctx, char *user_input) -{ - char *input; - if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL))) - print_error ("character conversion failed for: %s", "user input"); - else - { - struct strv lines = strv_make (); - - // XXX: this interprets commands in pasted text - cstr_split (input, "\r\n", false, &lines); - for (size_t i = 0; i < lines.len; i++) - (void) process_input_utf8 (ctx, - ctx->current_buffer, lines.vector[i], 0); - - strv_free (&lines); - } - free (input); -} - -// --- Word completion --------------------------------------------------------- - -// The amount of crap that goes into this is truly insane. -// It's mostly because of Editline's total ignorance of this task. - -static void -completion_init (struct completion *self) -{ - memset (self, 0, sizeof *self); -} - -static void -completion_free (struct completion *self) -{ - free (self->line); - free (self->words); -} - -static void -completion_add_word (struct completion *self, size_t start, size_t end) -{ - if (!self->words) - self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words); - if (self->words_len == self->words_alloc) - self->words = xreallocarray (self->words, - (self->words_alloc <<= 1), sizeof *self->words); - self->words[self->words_len++] = (struct completion_word) { start, end }; -} - -static void -completion_parse (struct completion *self, const char *line, size_t len) -{ - self->line = xstrndup (line, len); - - // The first and the last word may be empty - const char *s = self->line; - while (true) - { - const char *start = s; - size_t word_len = strcspn (s, WORD_BREAKING_CHARS); - const char *end = start + word_len; - s = end + strspn (end, WORD_BREAKING_CHARS); - - completion_add_word (self, start - self->line, end - self->line); - if (s == end) - break; - } -} - -static void -completion_locate (struct completion *self, size_t offset) -{ - size_t i = 0; - for (; i < self->words_len; i++) - if (self->words[i].start > offset) - break; - self->location = i - 1; -} - -static char * -completion_word (struct completion *self, int word) -{ - hard_assert (word >= 0 && word < (int) self->words_len); - return xstrndup (self->line + self->words[word].start, - self->words[word].end - self->words[word].start); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// XXX: this isn't completely right because Unicode, but let's keep it simple. -// At worst it will stop before a combining mark, or fail to compare -// non-ASCII identifiers case-insensitively. - -static size_t -utf8_common_prefix (const char **vector, size_t len) -{ - size_t prefix = 0; - if (!vector || !len) - return 0; - - struct utf8_iter a[len]; - for (size_t i = 0; i < len; i++) - a[i] = utf8_iter_make (vector[i]); - - size_t ch_len; - int32_t ch; - while ((ch = utf8_iter_next (&a[0], &ch_len)) >= 0) - { - for (size_t i = 1; i < len; i++) - { - int32_t other = utf8_iter_next (&a[i], NULL); - if (ch == other) - continue; - // Not bothering with lowercasing non-ASCII - if (ch >= 0x80 || other >= 0x80 - || tolower_ascii (ch) != tolower_ascii (other)) - return prefix; - } - prefix += ch_len; - } - return prefix; -} - -static void -complete_command (struct app_context *ctx, struct completion *data, - const char *word, struct strv *output) -{ - (void) data; - - const char *prefix = ""; - if (*word == '/') - { - word++; - prefix = "/"; - } - - size_t word_len = strlen (word); - for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++) - { - struct command_handler *handler = &g_command_handlers[i]; - if (!strncasecmp_ascii (word, handler->name, word_len)) - strv_append_owned (output, - xstrdup_printf ("%s%s", prefix, handler->name)); - } - - struct str_map_iter iter = str_map_iter_make (get_aliases_config (ctx)); - struct config_item *alias; - while ((alias = str_map_iter_next (&iter))) - { - if (!strncasecmp_ascii (word, iter.link->key, word_len)) - strv_append_owned (output, - xstrdup_printf ("%s%s", prefix, iter.link->key)); - } -} - -static void -complete_option (struct app_context *ctx, struct completion *data, - const char *word, struct strv *output) -{ - (void) data; - - struct strv options = strv_make (); - config_dump (ctx->config.root, &options); - strv_sort (&options); - - // Wildcard expansion is an interesting side-effect - char *mask = xstrdup_printf ("%s*", word); - for (size_t i = 0; i < options.len; i++) - { - char *key = cstr_cut_until (options.vector[i], " "); - if (!fnmatch (mask, key, 0)) - strv_append_owned (output, key); - else - free (key); - } - free (mask); - strv_free (&options); -} - -static void -complete_set_value (struct config_item *item, const char *word, - struct strv *output) -{ - struct str serialized = str_make (); - config_item_write (item, false, &serialized); - if (!strncmp (serialized.str, word, strlen (word))) - strv_append_owned (output, str_steal (&serialized)); - else - str_free (&serialized); -} - -static void -complete_set_value_array (struct config_item *item, const char *word, - struct strv *output) -{ - if (!item->schema || item->schema->type != CONFIG_ITEM_STRING_ARRAY) - return; - - struct strv items = strv_make (); - cstr_split (item->value.string.str, ",", false, &items); - for (size_t i = 0; i < items.len; i++) - { - struct str wrapped = str_make (), serialized = str_make (); - str_append (&wrapped, items.vector[i]); - config_item_write_string (&serialized, &wrapped); - str_free (&wrapped); - - if (!strncmp (serialized.str, word, strlen (word))) - strv_append_owned (output, str_steal (&serialized)); - else - str_free (&serialized); - } - strv_free (&items); -} - -static void -complete_set (struct app_context *ctx, struct completion *data, - const char *word, struct strv *output) -{ - if (data->location == 1) - { - complete_option (ctx, data, word, output); - return; - } - if (data->location != 3) - return; - - char *key = completion_word (data, 1); - struct config_item *item = config_item_get (ctx->config.root, key, NULL); - if (item) - { - char *op = completion_word (data, 2); - if (!strcmp (op, "-=")) complete_set_value_array (item, word, output); - if (!strcmp (op, "=")) complete_set_value (item, word, output); - free (op); - } - free (key); -} - -static void -complete_topic (struct app_context *ctx, struct completion *data, - const char *word, struct strv *output) -{ - (void) data; - - // TODO: make it work in other server-related buffers, too, i.e. when we're - // completing the third word and the second word is a known channel name - struct buffer *buffer = ctx->current_buffer; - if (buffer->type != BUFFER_CHANNEL) - return; - - const char *topic = buffer->channel->topic; - if (topic && !strncasecmp_ascii (word, topic, strlen (word))) - { - // We must prepend the channel name if the topic itself starts - // with something that could be regarded as a channel name - strv_append_owned (output, irc_is_channel (buffer->server, topic) - ? xstrdup_printf ("%s %s", buffer->channel->name, topic) - : xstrdup (topic)); - } -} - -static void -complete_nicknames (struct app_context *ctx, struct completion *data, - const char *word, struct strv *output) -{ - struct buffer *buffer = ctx->current_buffer; - if (buffer->type == BUFFER_SERVER) - { - struct user *self_user = buffer->server->irc_user; - if (self_user) - strv_append (output, self_user->nickname); - } - if (buffer->type != BUFFER_CHANNEL) - return; - - size_t word_len = strlen (word); - LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users) - { - const char *nickname = iter->user->nickname; - if (irc_server_strncmp (buffer->server, word, nickname, word_len)) - continue; - strv_append_owned (output, data->location == 0 - ? xstrdup_printf ("%s:", nickname) - : xstrdup (nickname)); - } -} - -static char ** -complete_word (struct app_context *ctx, struct completion *data, - const char *word) -{ - char *initial = completion_word (data, 0); - - // Start with a placeholder for the longest common prefix - struct strv words = strv_make (); - strv_append_owned (&words, NULL); - - if (data->location == 0 && *initial == '/') - complete_command (ctx, data, word, &words); - else if (data->location >= 1 && !strcmp (initial, "/set")) - complete_set (ctx, data, word, &words); - else if (data->location == 1 && !strcmp (initial, "/help")) - { - complete_command (ctx, data, word, &words); - complete_option (ctx, data, word, &words); - } - else if (data->location == 1 && !strcmp (initial, "/topic")) - { - complete_topic (ctx, data, word, &words); - complete_nicknames (ctx, data, word, &words); - } - else - complete_nicknames (ctx, data, word, &words); - - cstr_set (&initial, NULL); - LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks) - { - struct completion_hook *hook = (struct completion_hook *) iter; - hook->complete (hook, data, word, &words); - } - - if (words.len == 1) - { - // Nothing matched - strv_free (&words); - return NULL; - } - - if (words.len == 2) - { - words.vector[0] = words.vector[1]; - words.vector[1] = NULL; - } - else - { - size_t prefix = utf8_common_prefix - ((const char **) words.vector + 1, words.len - 1); - if (!prefix) - words.vector[0] = xstrdup (word); - else - words.vector[0] = xstrndup (words.vector[1], prefix); - } - return words.vector; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// A special wrapper for iconv_xstrdup() that also fixes indexes into the -/// original string to point to the right location in the output. -/// Thanks, Readline! Without you I would have never needed to deal with this. -static char * -locale_to_utf8 (struct app_context *ctx, const char *locale, - int *indexes[], size_t n_indexes) -{ - mbstate_t state; - memset (&state, 0, sizeof state); - - size_t remaining = strlen (locale) + 1; - const char *p = locale; - - // Reset the shift state, FWIW - (void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL); - - bool fixed[n_indexes]; - memset (fixed, 0, sizeof fixed); - - struct str utf8 = str_make (); - while (true) - { - size_t len = mbrlen (p, remaining, &state); - - // Incomplete multibyte character or illegal sequence (probably) - if (len == (size_t) -2 - || len == (size_t) -1) - { - str_free (&utf8); - return NULL; - } - - // Convert indexes into the multibyte string to UTF-8 - for (size_t i = 0; i < n_indexes; i++) - if (!fixed[i] && *indexes[i] <= p - locale) - { - *indexes[i] = utf8.len; - fixed[i] = true; - } - - // End of string - if (!len) - break; - - // EINVAL (incomplete sequence) should never happen and - // EILSEQ neither because we've already checked for that with mbrlen(). - // E2BIG is what iconv_xstrdup solves. This must succeed. - size_t ch_len; - char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len); - hard_assert (ch != NULL); - str_append_data (&utf8, ch, ch_len); - free (ch); - - p += len; - remaining -= len; - } - return str_steal (&utf8); -} - -static void -utf8_vector_to_locale (struct app_context *ctx, char **vector) -{ - for (; *vector; vector++) - { - char *converted = iconv_xstrdup - (ctx->term_from_utf8, *vector, -1, NULL); - if (!soft_assert (converted)) - converted = xstrdup (""); - - cstr_set (vector, converted); - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// Takes a line in locale-specific encoding and position of a word to complete, -/// returns a vector of matches in locale-specific encoding. -static char ** -make_completions (struct app_context *ctx, char *line, int start, int end) -{ - int *fixes[] = { &start, &end }; - char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes)); - if (!line_utf8) - return NULL; - - hard_assert (start >= 0 && end >= 0 && start <= end); - - struct completion c; - completion_init (&c); - completion_parse (&c, line, strlen (line)); - completion_locate (&c, start); - char *word = xstrndup (line + start, end - start); - char **completions = complete_word (ctx, &c, word); - free (word); - completion_free (&c); - - if (completions) - utf8_vector_to_locale (ctx, completions); - - free (line_utf8); - return completions; -} - -// --- Common code for user actions -------------------------------------------- - -static void -toggle_bracketed_paste (bool enable) -{ - fprintf (stdout, "\x1b[?2004%c", "lh"[enable]); - fflush (stdout); -} - -static void -suspend_terminal (struct app_context *ctx) -{ - // Terminal can get suspended by both backlog helper and SIGTSTP handling - if (ctx->terminal_suspended++ > 0) - return; - - toggle_bracketed_paste (false); - CALL (ctx->input, hide); - poller_fd_reset (&ctx->tty_event); - - CALL_ (ctx->input, prepare, false); -} - -static void -resume_terminal (struct app_context *ctx) -{ - if (--ctx->terminal_suspended > 0) - return; - - update_screen_size (); - CALL_ (ctx->input, prepare, true); - CALL (ctx->input, on_tty_resized); - - toggle_bracketed_paste (true); - // In theory we could just print all unseen messages but this is safer - buffer_print_backlog (ctx, ctx->current_buffer); - // Now it's safe to process any user input - poller_fd_set (&ctx->tty_event, POLLIN); - CALL (ctx->input, show); -} - -static pid_t -spawn_helper_child (struct app_context *ctx) -{ - suspend_terminal (ctx); - pid_t child = fork (); - switch (child) - { - case -1: - { - int saved_errno = errno; - resume_terminal (ctx); - errno = saved_errno; - break; - } - case 0: - // Put the child in a new foreground process group - hard_assert (setpgid (0, 0) != -1); - hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1); - break; - default: - // Make sure of it in the parent as well before continuing - (void) setpgid (child, child); - } - return child; -} - -static void -redraw_screen (struct app_context *ctx) -{ - // If by some circumstance we had the wrong idea - CALL (ctx->input, on_tty_resized); - update_screen_size (); - - CALL (ctx->input, hide); - buffer_print_backlog (ctx, ctx->current_buffer); - CALL (ctx->input, show); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static bool -dump_input_to_file (struct app_context *ctx, char *template, struct error **e) -{ - mode_t mask = umask (S_IXUSR | S_IRWXG | S_IRWXO); - int fd = mkstemp (template); - (void) umask (mask); - - if (fd < 0) - return error_set (e, "%s", strerror (errno)); - - char *input = CALL (ctx->input, get_line); - bool success = xwrite (fd, input, strlen (input), e); - free (input); - - if (!success) - (void) unlink (template); - - xclose (fd); - return success; -} - -static char * -try_dump_input_to_file (struct app_context *ctx) -{ - char *template = resolve_filename - ("input.XXXXXX", resolve_relative_runtime_template); - - struct error *e = NULL; - if (dump_input_to_file (ctx, template, &e)) - return template; - - log_global_error (ctx, "#s: #s", - "Failed to create a temporary file for editing", e->message); - error_free (e); - free (template); - return NULL; -} - -static bool -on_edit_input (int count, int key, void *user_data) -{ - (void) count; - (void) key; - struct app_context *ctx = user_data; - - char *filename; - if (!(filename = try_dump_input_to_file (ctx))) - return false; - - const char *command; - if (!(command = getenv ("VISUAL")) - && !(command = getenv ("EDITOR"))) - command = "vi"; - - hard_assert (!ctx->running_editor); - switch (spawn_helper_child (ctx)) - { - case 0: - execlp (command, command, filename, NULL); - print_error ("%s: %s", - "Failed to launch editor", strerror (errno)); - _exit (EXIT_FAILURE); - case -1: - log_global_error (ctx, "#s: #l", - "Failed to launch editor", strerror (errno)); - free (filename); - break; - default: - ctx->running_editor = true; - ctx->editor_filename = filename; - } - return true; -} - -static void -input_editor_process (struct app_context *ctx) -{ - struct str input = str_make (); - struct error *e = NULL; - if (!read_file (ctx->editor_filename, &input, &e)) - { - log_global_error (ctx, "#s: #s", "Input editing failed", e->message); - error_free (e); - } - else - CALL (ctx->input, clear_line); - - if (!CALL_ (ctx->input, insert, input.str)) - log_global_error (ctx, "#s: #s", "Input editing failed", - "could not re-insert the modified text"); - - str_free (&input); -} - -static void -input_editor_cleanup (struct app_context *ctx) -{ - if (unlink (ctx->editor_filename)) - log_global_error (ctx, "Could not unlink `#s': #l", - ctx->editor_filename, strerror (errno)); - - cstr_set (&ctx->editor_filename, NULL); - ctx->running_editor = false; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -launch_backlog_helper (struct app_context *ctx, int backlog_fd) -{ - hard_assert (!ctx->running_backlog_helper); - switch (spawn_helper_child (ctx)) - { - case 0: - dup2 (backlog_fd, STDIN_FILENO); - execl ("/bin/sh", "/bin/sh", "-c", get_config_string - (ctx->config.root, "behaviour.backlog_helper"), NULL); - print_error ("%s: %s", - "Failed to launch backlog helper", strerror (errno)); - _exit (EXIT_FAILURE); - case -1: - log_global_error (ctx, "#s: #l", - "Failed to launch backlog helper", strerror (errno)); - break; - default: - ctx->running_backlog_helper = true; - } -} - -static bool -display_backlog (struct app_context *ctx, int flush_opts) -{ - FILE *backlog = tmpfile (); - if (!backlog) - { - log_global_error (ctx, "#s: #l", - "Failed to create a temporary file", strerror (errno)); - return false; - } - - if (!get_config_boolean (ctx->config.root, - "behaviour.backlog_helper_strip_formatting")) - flush_opts |= FLUSH_OPT_RAW; - - struct buffer *buffer = ctx->current_buffer; - int until_marker = - (int) buffer->lines_count - (int) buffer->new_messages_count; - for (struct buffer_line *line = buffer->lines; line; line = line->next) - { - if (until_marker-- == 0 - && buffer->new_messages_count != buffer->lines_count) - buffer_print_read_marker (ctx, backlog, flush_opts); - if (buffer_line_will_show_up (buffer, line)) - buffer_line_write_to_backlog (ctx, line, backlog, flush_opts); - } - - // So that it is obvious if the last line in the buffer is not from today - buffer_update_time (ctx, time (NULL), backlog, flush_opts); - - rewind (backlog); - set_cloexec (fileno (backlog)); - launch_backlog_helper (ctx, fileno (backlog)); - fclose (backlog); - return true; -} - -static bool -on_display_backlog (int count, int key, void *user_data) -{ - (void) count; - (void) key; - return display_backlog (user_data, 0); -} - -static bool -on_display_backlog_nowrap (int count, int key, void *user_data) -{ - (void) count; - (void) key; - return display_backlog (user_data, FLUSH_OPT_NOWRAP); -} - -static bool -on_display_full_log (int count, int key, void *user_data) -{ - (void) count; - (void) key; - struct app_context *ctx = user_data; - - char *path = buffer_get_log_path (ctx->current_buffer); - FILE *full_log = fopen (path, "rb"); - free (path); - - if (!full_log) - { - log_global_error (ctx, "Failed to open log file for #s: #l", - ctx->current_buffer->name, strerror (errno)); - return false; - } - - if (ctx->current_buffer->log_file) - // The regular flush will log any error eventually - (void) fflush (ctx->current_buffer->log_file); - - set_cloexec (fileno (full_log)); - launch_backlog_helper (ctx, fileno (full_log)); - fclose (full_log); - return true; -} - -static bool -on_toggle_unimportant (int count, int key, void *user_data) -{ - (void) count; - (void) key; - struct app_context *ctx = user_data; - ctx->current_buffer->hide_unimportant ^= true; - buffer_print_backlog (ctx, ctx->current_buffer); - return true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static bool -on_goto_buffer (int count, int key, void *user_data) -{ - (void) count; - struct app_context *ctx = user_data; - - int n = key - '0'; - if (n < 0 || n > 9) - return false; - - // There's no buffer zero - if (n == 0) - n = 10; - - if (!ctx->last_buffer || buffer_get_index (ctx, ctx->current_buffer) != n) - return buffer_goto (ctx, n); - - // Fast switching between two buffers - buffer_activate (ctx, ctx->last_buffer); - return true; -} - -static bool -on_previous_buffer (int count, int key, void *user_data) -{ - (void) key; - buffer_activate (user_data, buffer_previous (user_data, count)); - return true; -} - -static bool -on_next_buffer (int count, int key, void *user_data) -{ - (void) key; - buffer_activate (user_data, buffer_next (user_data, count)); - return true; -} - -static bool -on_switch_buffer (int count, int key, void *user_data) -{ - (void) count; - (void) key; - struct app_context *ctx = user_data; - - if (!ctx->last_buffer) - return false; - buffer_activate (ctx, ctx->last_buffer); - return true; -} - -static bool -on_goto_highlight (int count, int key, void *user_data) -{ - (void) count; - (void) key; - - struct app_context *ctx = user_data; - struct buffer *iter = ctx->current_buffer;; - do - { - if (!(iter = iter->next)) - iter = ctx->buffers; - if (iter == ctx->current_buffer) - return false; - } - while (!iter->highlighted); - buffer_activate (ctx, iter); - return true; -} - -static bool -on_goto_activity (int count, int key, void *user_data) -{ - (void) count; - (void) key; - - struct app_context *ctx = user_data; - struct buffer *iter = ctx->current_buffer; - do - { - if (!(iter = iter->next)) - iter = ctx->buffers; - if (iter == ctx->current_buffer) - return false; - } - while (iter->new_messages_count == iter->new_unimportant_count); - buffer_activate (ctx, iter); - return true; -} - -static bool -on_move_buffer_left (int count, int key, void *user_data) -{ - (void) key; - - struct app_context *ctx = user_data; - int total = buffer_count (ctx); - int n = buffer_get_index (ctx, ctx->current_buffer) - count; - buffer_move (ctx, ctx->current_buffer, n <= 0 - ? (total + n % total) - : ((n - 1) % total + 1)); - return true; -} - -static bool -on_move_buffer_right (int count, int key, void *user_data) -{ - return on_move_buffer_left (-count, key, user_data); -} - -static bool -on_redraw_screen (int count, int key, void *user_data) -{ - (void) count; - (void) key; - - redraw_screen (user_data); - return true; -} - -static bool -on_insert_attribute (int count, int key, void *user_data) -{ - (void) count; - (void) key; - - struct app_context *ctx = user_data; - ctx->awaiting_mirc_escape = true; - return true; -} - -static bool -on_start_paste_mode (int count, int key, void *user_data) -{ - (void) count; - (void) key; - - struct app_context *ctx = user_data; - ctx->in_bracketed_paste = true; - return true; -} - -static void -input_add_functions (void *user_data) -{ - struct app_context *ctx = user_data; -#define XX(...) CALL_ (ctx->input, register_fn, __VA_ARGS__, ctx); - XX ("previous-buffer", "Previous buffer", on_previous_buffer) - XX ("next-buffer", "Next buffer", on_next_buffer) - XX ("goto-buffer", "Go to buffer", on_goto_buffer) - XX ("switch-buffer", "Switch buffer", on_switch_buffer) - XX ("goto-highlight", "Go to highlight", on_goto_highlight) - XX ("goto-activity", "Go to activity", on_goto_activity) - XX ("move-buffer-left", "Move buffer left", on_move_buffer_left) - XX ("move-buffer-right", "Move buffer right", on_move_buffer_right) - XX ("display-backlog", "Show backlog", on_display_backlog) - XX ("display-backlog-nw", "Non-wrapped log", on_display_backlog_nowrap) - XX ("display-full-log", "Show full log", on_display_full_log) - XX ("toggle-unimportant", "Toggle junk msgs", on_toggle_unimportant) - XX ("edit-input", "Edit input", on_edit_input) - XX ("redraw-screen", "Redraw screen", on_redraw_screen) - XX ("insert-attribute", "mIRC formatting", on_insert_attribute) - XX ("start-paste-mode", "Bracketed paste", on_start_paste_mode) -#undef XX -} - -static void -bind_common_keys (struct app_context *ctx) -{ - struct input *self = ctx->input; - CALL_ (self, bind_control, 'p', "previous-buffer"); - CALL_ (self, bind_control, 'n', "next-buffer"); - - // Redefine M-0 through M-9 to switch buffers - for (int i = 0; i <= 9; i++) - CALL_ (self, bind_meta, '0' + i, "goto-buffer"); - - CALL_ (self, bind_meta, '\t', "switch-buffer"); - CALL_ (self, bind_meta, '!', "goto-highlight"); - CALL_ (self, bind_meta, 'a', "goto-activity"); - CALL_ (self, bind_meta, 'm', "insert-attribute"); - CALL_ (self, bind_meta, 'h', "display-full-log"); - CALL_ (self, bind_meta, 'H', "toggle-unimportant"); - CALL_ (self, bind_meta, 'e', "edit-input"); - - if (key_f5) CALL_ (self, bind, key_f5, "previous-buffer"); - if (key_f6) CALL_ (self, bind, key_f6, "next-buffer"); - if (key_ppage) CALL_ (self, bind, key_ppage, "display-backlog"); - - if (clear_screen) - CALL_ (self, bind_control, 'l', "redraw-screen"); - - CALL_ (self, bind, "\x1b[200~", "start-paste-mode"); -} - -// --- GNU Readline user actions ----------------------------------------------- - -#ifdef HAVE_READLINE - -static int -on_readline_return (int count, int key) -{ - (void) count; - (void) key; - - // Let readline pass the line to our input handler - rl_done = 1; - - struct app_context *ctx = g_ctx; - struct input_rl *self = (struct input_rl *) ctx->input; - - // Hide the line, don't redisplay it - CALL (ctx->input, hide); - input_rl__restore (self); - return 0; -} - -static void -on_readline_input (char *line) -{ - struct app_context *ctx = g_ctx; - struct input_rl *self = (struct input_rl *) ctx->input; - - if (line) - { - if (*line) - add_history (line); - - // readline always erases the input line after returning from here, - // but we don't want that to happen if the command to be executed - // would switch the buffer (we'd keep the already executed command in - // the old buffer and delete any input restored from the new buffer) - strv_append_owned (&ctx->pending_input, line); - poller_idle_set (&ctx->input_event); - } - else - { - // Prevent readline from showing the prompt twice for w/e reason - CALL (ctx->input, hide); - input_rl__restore (self); - - CALL (ctx->input, ding); - } - - if (self->active) - // Readline automatically redisplays it - self->prompt_shown = 1; -} - -static char ** -app_readline_completion (const char *text, int start, int end) -{ - // We will reconstruct that ourselves - (void) text; - - // Don't iterate over filenames and stuff - rl_attempted_completion_over = true; - - return make_completions (g_ctx, rl_line_buffer, start, end); -} - -static int -app_readline_init (void) -{ - struct app_context *ctx = g_ctx; - struct input *self = ctx->input; - - // XXX: maybe use rl_make_bare_keymap() and start from there; - // our dear user could potentionally rig things up in a way that might - // result in some funny unspecified behaviour - - // For vi mode, enabling "show-mode-in-prompt" is recommended as there is - // no easy way to indicate mode changes otherwise. - - rl_add_defun ("send-line", on_readline_return, -1); - bind_common_keys (ctx); - - // Move native history commands - CALL_ (self, bind_meta, 'p', "previous-history"); - CALL_ (self, bind_meta, 'n', "next-history"); - - // We need to hide the prompt and input first - rl_bind_key (RETURN, rl_named_function ("send-line")); - CALL_ (self, bind_control, 'j', "send-line"); - - rl_variable_bind ("completion-ignore-case", "on"); - rl_bind_key (TAB, rl_named_function ("menu-complete")); - if (key_btab) - CALL_ (self, bind, key_btab, "menu-complete-backward"); - return 0; -} - -#endif // HAVE_READLINE - -// --- BSD Editline user actions ----------------------------------------------- - -#ifdef HAVE_EDITLINE - -static unsigned char -on_editline_complete (EditLine *editline, int key) -{ - (void) key; - (void) editline; - - struct app_context *ctx = g_ctx; - - // First prepare what Readline would have normally done for us... - const LineInfo *info_mb = el_line (editline); - int len = info_mb->lastchar - info_mb->buffer; - int point = info_mb->cursor - info_mb->buffer; - char *copy = xstrndup (info_mb->buffer, len); - - // XXX: possibly incorrect wrt. shift state encodings - int el_start = point, el_end = point; - while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1])) - el_start--; - - char **completions = make_completions (ctx, copy, el_start, el_end); - - // XXX: possibly incorrect wrt. shift state encodings - copy[el_end] = '\0'; - int el_len = mbstowcs (NULL, copy + el_start, 0); - free (copy); - - if (!completions) - return CC_REFRESH_BEEP; - - // Remove the original word - el_wdeletestr (editline, el_len); - - // Insert the best match instead - el_insertstr (editline, completions[0]); - bool only_match = !completions[1]; - for (char **p = completions; *p; p++) - free (*p); - free (completions); - - // I'm not sure if Readline's menu-complete can at all be implemented - // with Editline. Spamming the terminal with possible completions - // probably isn't what the user wants and we have no way of detecting - // what the last executed handler was. - if (!only_match) - return CC_REFRESH_BEEP; - - // But if there actually is just one match, finish the word - el_insertstr (editline, " "); - return CC_REFRESH; -} - -static unsigned char -on_editline_return (EditLine *editline, int key) -{ - (void) key; - struct app_context *ctx = g_ctx; - struct input_el *self = (struct input_el *) ctx->input; - - const LineInfoW *info = el_wline (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); - - // XXX: Editline seems to remember its position in history, - // so it's not going to work as you'd expect it to - if (*line) - { - HistEventW ev; - history_w (self->current->history, &ev, H_ENTER, line); - print_debug ("history: %d %ls", ev.num, ev.str); - } - free (line); - - // process_input() expects a multibyte string - const LineInfo *info_mb = el_line (editline); - strv_append_owned (&ctx->pending_input, - xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer)); - poller_idle_set (&ctx->input_event); - - el_cursor (editline, len - point); - el_wdeletestr (editline, len); - return CC_REFRESH; -} - -static void -app_editline_init (struct input_el *self) -{ - // el_set() leaks memory in 20150325 and other versions, we need wchar_t - el_wset (self->editline, EL_ADDFN, - L"send-line", L"Send line", on_editline_return); - el_wset (self->editline, EL_ADDFN, - L"complete", L"Complete word", on_editline_complete); - - struct input *input = &self->super; - input->add_functions (input->user_data); - bind_common_keys (g_ctx); - - // Move native history commands - CALL_ (input, bind_meta, 'p', "ed-prev-history"); - CALL_ (input, bind_meta, 'n', "ed-next-history"); - - // No, editline, it's not supposed to kill the entire line - CALL_ (input, bind_control, 'w', "ed-delete-prev-word"); - // Just what are you doing? - CALL_ (input, bind_control, 'u', "vi-kill-line-prev"); - - // We need to hide the prompt and input first - CALL_ (input, bind, "\n", "send-line"); - - CALL_ (input, bind_control, 'i', "complete"); - - // Source the user's defaults file - el_source (self->editline, NULL); -} - -#endif // HAVE_EDITLINE - -// --- Configuration loading --------------------------------------------------- - -static const char *g_first_time_help[] = -{ - "", - "\x02Welcome to degesch!", - "", - "To get a list of all commands, type \x02/help\x02. To obtain", - "more information on a command or option, simply add it as", - "a parameter, e.g. \x02/help set\x02 or \x02/help behaviour.logging\x02.", - "", - "To switch between buffers, press \x02" - "F5/Ctrl-P\x02 or \x02" "F6/Ctrl-N\x02.", - "", - "Finally, adding a network is as simple as:", - " - \x02/server add freenode\x02", - " - \x02/set servers.freenode.addresses = \"chat.freenode.net\"\x02", - " - \x02/connect freenode\x02", - "", - "That should be enough to get you started. Have fun!", - "" -}; - -static void -show_first_time_help (struct app_context *ctx) -{ - for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++) - log_global_indent (ctx, "#m", g_first_time_help[i]); -} - -const char *g_default_aliases[][2] = -{ - { "c", "/buffer clear" }, { "close", "/buffer close" }, - { "j", "/join $*" }, { "p", "/part $*" }, - { "k", "/kick $*" }, { "kb", "/kickban $*" }, - { "m", "/msg $*" }, { "q", "/query $*" }, - { "n", "/names $*" }, { "t", "/topic $*" }, - { "w", "/who $*" }, { "wi", "/whois $*" }, - { "ww", "/whowas $*" }, -}; - -static void -load_default_aliases (struct app_context *ctx) -{ - struct str_map *aliases = get_aliases_config (ctx); - for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++) - { - const char **pair = g_default_aliases[i]; - str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1])); - } -} - -static void -load_configuration (struct app_context *ctx) -{ - // In theory, we could ensure that only one instance is running by locking - // the configuration file and ensuring here that it exists. This is - // however brittle, as it may be unlinked without the application noticing. - - struct config_item *root = NULL; - struct error *e = NULL; - - char *filename = resolve_filename - (PROGRAM_NAME ".conf", resolve_relative_config_filename); - if (filename) - root = config_read_from_file (filename, &e); - else - log_global_error (ctx, "Configuration file not found"); - free (filename); - - if (e) - { - log_global_error (ctx, "Cannot load configuration: #s", e->message); - log_global_error (ctx, - "Please either fix the configuration file or remove it"); - error_free (e); - exit (EXIT_FAILURE); - } - - if (root) - { - config_load (&ctx->config, root); - log_global_status (ctx, "Configuration loaded"); - } - else - { - show_first_time_help (ctx); - load_default_aliases (ctx); - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -load_servers (struct app_context *ctx) -{ - struct str_map_iter iter = str_map_iter_make (get_servers_config (ctx)); - - struct config_item *subtree; - while ((subtree = str_map_iter_next (&iter))) - { - const char *name = iter.link->key; - const char *err; - if (subtree->type != CONFIG_ITEM_OBJECT) - log_global_error (ctx, "Error in configuration: " - "ignoring server `#s' as it's not an object", name); - else if ((err = check_server_name_for_addition (ctx, name))) - log_global_error (ctx, "Cannot load server `#s': #s", name, err); - else - server_add (ctx, name, subtree); - } -} - -// --- Signals ----------------------------------------------------------------- - -static int g_signal_pipe[2]; ///< A pipe used to signal... signals - -/// Program termination has been requested by a signal -static volatile sig_atomic_t g_termination_requested; -/// The window has changed in size -static volatile sig_atomic_t g_winch_received; - -static void -postpone_signal_handling (char id) -{ - int original_errno = errno; - if (write (g_signal_pipe[1], &id, 1) == -1) - soft_assert (errno == EAGAIN); - errno = original_errno; -} - -static void -signal_superhandler (int signum) -{ - switch (signum) - { - case SIGWINCH: - g_winch_received = true; - postpone_signal_handling ('w'); - break; - case SIGINT: - case SIGTERM: - g_termination_requested = true; - postpone_signal_handling ('t'); - break; - case SIGCHLD: - postpone_signal_handling ('c'); - break; - case SIGTSTP: - postpone_signal_handling ('s'); - break; - default: - hard_assert (!"unhandled signal"); - } -} - -static void -setup_signal_handlers (void) -{ - if (pipe (g_signal_pipe) == -1) - exit_fatal ("%s: %s", "pipe", strerror (errno)); - - set_cloexec (g_signal_pipe[0]); - set_cloexec (g_signal_pipe[1]); - - // So that the pipe cannot overflow; it would make write() block within - // the signal handler, which is something we really don't want to happen. - // The same holds true for read(). - set_blocking (g_signal_pipe[0], false); - set_blocking (g_signal_pipe[1], false); - - signal (SIGPIPE, SIG_IGN); - - // So that we can write to the terminal while we're running a backlog - // helper. This is also inherited by the child so that it doesn't stop - // when it calls tcsetpgrp(). - signal (SIGTTOU, SIG_IGN); - - struct sigaction sa; - sa.sa_flags = SA_RESTART; - sa.sa_handler = signal_superhandler; - sigemptyset (&sa.sa_mask); - - if (sigaction (SIGWINCH, &sa, NULL) == -1 - || sigaction (SIGINT, &sa, NULL) == -1 - || sigaction (SIGTERM, &sa, NULL) == -1 - || sigaction (SIGTSTP, &sa, NULL) == -1 - || sigaction (SIGCHLD, &sa, NULL) == -1) - exit_fatal ("sigaction: %s", strerror (errno)); -} - -// --- I/O event handlers ------------------------------------------------------ - -static bool -try_reap_child (struct app_context *ctx) -{ - int status; - pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED); - - if (zombie == -1) - { - if (errno == ECHILD) return false; - if (errno == EINTR) return true; - exit_fatal ("%s: %s", "waitpid", strerror (errno)); - } - if (!zombie) - return false; - - if (WIFSTOPPED (status)) - { - // We could also send SIGCONT but what's the point - print_debug ("a child has been stopped, killing its process group"); - kill (-zombie, SIGKILL); - return true; - } - - if (ctx->running_backlog_helper) - ctx->running_backlog_helper = false; - else if (!ctx->running_editor) - { - log_global_debug (ctx, "An unknown child has died"); - return true; - } - - hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1); - resume_terminal (ctx); - - if (WIFSIGNALED (status)) - log_global_error (ctx, - "Child died from signal #d", WTERMSIG (status)); - else if (WIFEXITED (status) && WEXITSTATUS (status) != 0) - log_global_error (ctx, - "Child returned status #d", WEXITSTATUS (status)); - else if (ctx->running_editor) - input_editor_process (ctx); - - if (ctx->running_editor) - input_editor_cleanup (ctx); - return true; -} - -static void -on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) -{ - char id = 0; - (void) read (fd->fd, &id, 1); - - // Stop ourselves cleanly, even if it makes little sense to do this - if (id == 's') - { - suspend_terminal (ctx); - kill (getpid (), SIGSTOP); - g_winch_received = true; - resume_terminal (ctx); - } - - // Reap all dead children (since the signal pipe may overflow etc. we run - // waitpid() in a loop to return all the zombies it knows about). - while (try_reap_child (ctx)) - ; - - if (g_termination_requested) - { - g_termination_requested = false; - request_quit (ctx, NULL); - } - if (g_winch_received) - { - g_winch_received = false; - redraw_screen (ctx); - } -} - -static void -process_mirc_escape (const struct pollfd *fd, struct app_context *ctx) -{ - // There's no other way with libedit, as both el_getc() in a function - // handler and CC_ARGHACK would block execution - struct str *buf = &ctx->input_buffer; - str_reserve (buf, 1); - if (read (fd->fd, buf->str + buf->len, 1) != 1) - goto error; - buf->str[++buf->len] = '\0'; - - // XXX: I think this should be global and shared with Readline/libedit - mbstate_t state; - memset (&state, 0, sizeof state); - - size_t len = mbrlen (buf->str, buf->len, &state); - - // Illegal sequence - if (len == (size_t) -1) - goto error; - - // Incomplete multibyte character - if (len == (size_t) -2) - return; - - if (buf->len != 1) - goto error; - switch (buf->str[0]) - { - case 'b' ^ 96: - case 'b': CALL_ (ctx->input, insert, "\x02"); break; - case 'c': CALL_ (ctx->input, insert, "\x03"); break; - case 'i' ^ 96: - case 'i': - case ']': CALL_ (ctx->input, insert, "\x1d"); break; - case 'x' ^ 96: - case 'x': - case '^': CALL_ (ctx->input, insert, "\x1e"); break; - case 'u' ^ 96: - case 'u': - case '_': CALL_ (ctx->input, insert, "\x1f"); break; - case 'v': CALL_ (ctx->input, insert, "\x16"); break; - case 'o': CALL_ (ctx->input, insert, "\x0f"); break; - - default: - goto error; - } - goto done; - -error: - CALL (ctx->input, ding); -done: - str_reset (buf); - ctx->awaiting_mirc_escape = false; -} - -#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted - -static void -process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx) -{ - struct str *buf = &ctx->input_buffer; - str_reserve (buf, 1); - if (read (fd->fd, buf->str + buf->len, 1) != 1) - goto error; - buf->str[++buf->len] = '\0'; - - static const char stop_mark[] = "\x1b[201~"; - static const size_t stop_mark_len = sizeof stop_mark - 1; - if (buf->len < stop_mark_len) - return; - - size_t text_len = buf->len - stop_mark_len; - if (memcmp (buf->str + text_len, stop_mark, stop_mark_len)) - return; - - // Avoid endless flooding of the buffer - if (text_len > BRACKETED_PASTE_LIMIT) - log_global_error (ctx, "Paste trimmed to #d bytes", - (int) (text_len = BRACKETED_PASTE_LIMIT)); - - buf->str[text_len] = '\0'; - if (CALL_ (ctx->input, insert, buf->str)) - goto done; - -error: - CALL (ctx->input, ding); - log_global_error (ctx, "Paste failed"); -done: - str_reset (buf); - ctx->in_bracketed_paste = false; -} - -static void -reset_autoaway (struct app_context *ctx) -{ - // Stop the last one if it's been disabled altogether in the meantime - poller_timer_reset (&ctx->autoaway_tmr); - - // Unset any automated statuses that are active right at this moment - struct str_map_iter iter = str_map_iter_make (&ctx->servers); - struct server *s; - while ((s = str_map_iter_next (&iter))) - { - if (s->autoaway_active - && s->irc_user - && s->irc_user->away) - irc_send (s, "AWAY"); - - s->autoaway_active = false; - } - - // And potentially start a new auto-away timer - int64_t delay = get_config_integer - (ctx->config.root, "behaviour.autoaway_delay"); - if (delay) - poller_timer_set (&ctx->autoaway_tmr, delay * 1000); -} - -static void -on_autoaway_timer (struct app_context *ctx) -{ - // An empty message would unset any away status, so let's ignore that - const char *message = get_config_string - (ctx->config.root, "behaviour.autoaway_message"); - if (!message || !*message) - return; - - struct str_map_iter iter = str_map_iter_make (&ctx->servers); - struct server *s; - while ((s = str_map_iter_next (&iter))) - { - // If the user has already been marked as away, - // don't override his current away status - if (s->irc_user - && s->irc_user->away) - continue; - - irc_send (s, "AWAY :%s", message); - s->autoaway_active = true; - } -} - -static void -on_tty_readable (const struct pollfd *fd, struct app_context *ctx) -{ - if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) - print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); - - if (ctx->awaiting_mirc_escape) - process_mirc_escape (fd, ctx); - else if (ctx->in_bracketed_paste) - process_bracketed_paste (fd, ctx); - else if (!ctx->quitting) - CALL (ctx->input, on_tty_readable); - - // User activity detected, stop current auto-away and start anew; - // since they might have just changed the settings, do this last - reset_autoaway (ctx); -} - -static void -rearm_flush_timer (struct app_context *ctx) -{ - poller_timer_set (&ctx->flush_timer, 60 * 1000); -} - -static void -on_flush_timer (struct app_context *ctx) -{ - // I guess we don't need to do anything more complicated - fflush (NULL); - - // It would be a bit problematic to handle it properly, so do this at least - LIST_FOR_EACH (struct buffer, buffer, ctx->buffers) - { - if (!buffer->log_file || !ferror (buffer->log_file)) - continue; - - // Might be a transient error such as running out of disk space, - // keep notifying of the problem until it disappears - clearerr (buffer->log_file); - log_global (ctx, BUFFER_LINE_ERROR | BUFFER_LINE_SKIP_FILE, - "Log write failure detected for #s", buffer->name); - } - -#ifdef LOMEM - // Lua should normally be reasonable and collect garbage when needed, - // though we can try to push it. This is a reasonable place. - LIST_FOR_EACH (struct plugin, iter, ctx->plugins) - if (iter->vtable->gc) - iter->vtable->gc (iter); -#endif // LOMEM - - rearm_flush_timer (ctx); -} - -static void -rearm_date_change_timer (struct app_context *ctx) -{ - struct tm tm_; - const time_t now = time (NULL); - if (!soft_assert (localtime_r (&now, &tm_))) - return; - - tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0; - tm_.tm_mday++; - tm_.tm_isdst = -1; - - const time_t midnight = mktime (&tm_); - if (!soft_assert (midnight != (time_t) -1)) - return; - poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000); -} - -static void -on_date_change_timer (struct app_context *ctx) -{ - if (ctx->terminal_suspended <= 0) - { - CALL (ctx->input, hide); - buffer_update_time (ctx, time (NULL), stdout, 0); - CALL (ctx->input, show); - } - rearm_date_change_timer (ctx); -} - -static void -on_pending_input (struct app_context *ctx) -{ - poller_idle_reset (&ctx->input_event); - for (size_t i = 0; i < ctx->pending_input.len; i++) - process_input (ctx, ctx->pending_input.vector[i]); - strv_reset (&ctx->pending_input); -} - -static void -init_poller_events (struct app_context *ctx) -{ - ctx->signal_event = poller_fd_make (&ctx->poller, g_signal_pipe[0]); - ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable; - ctx->signal_event.user_data = ctx; - poller_fd_set (&ctx->signal_event, POLLIN); - - ctx->tty_event = poller_fd_make (&ctx->poller, STDIN_FILENO); - ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable; - ctx->tty_event.user_data = ctx; - poller_fd_set (&ctx->tty_event, POLLIN); - - ctx->flush_timer = poller_timer_make (&ctx->poller); - ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer; - ctx->flush_timer.user_data = ctx; - rearm_flush_timer (ctx); - - ctx->date_chg_tmr = poller_timer_make (&ctx->poller); - ctx->date_chg_tmr.dispatcher = (poller_timer_fn) on_date_change_timer; - ctx->date_chg_tmr.user_data = ctx; - rearm_date_change_timer (ctx); - - ctx->autoaway_tmr = poller_timer_make (&ctx->poller); - ctx->autoaway_tmr.dispatcher = (poller_timer_fn) on_autoaway_timer; - ctx->autoaway_tmr.user_data = ctx; - - ctx->prompt_event = poller_idle_make (&ctx->poller); - ctx->prompt_event.dispatcher = (poller_idle_fn) on_refresh_prompt; - ctx->prompt_event.user_data = ctx; - - ctx->input_event = poller_idle_make (&ctx->poller); - ctx->input_event.dispatcher = (poller_idle_fn) on_pending_input; - ctx->input_event.user_data = ctx; -} - -// --- Tests ------------------------------------------------------------------- - -// The application is quite monolithic and can only be partially unit-tested. -// Locale-, terminal- and filesystem-dependent tests are also somewhat tricky. - -#ifdef TESTING - -static struct config_schema g_config_test[] = -{ - { .name = "foo", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" }, - { .name = "bar", .type = CONFIG_ITEM_INTEGER, .default_ = "1" }, - { .name = "foobar", .type = CONFIG_ITEM_STRING, .default_ = "\"x\\x01\"" }, - {} -}; - -static void -test_config (void) -{ - struct config_item *foo = config_item_object (); - config_schema_apply_to_object (g_config_test, foo, NULL); - struct config_item *root = config_item_object (); - str_map_set (&root->value.object, "top", foo); - - struct strv v = strv_make (); - dump_matching_options (root, "*foo*", &v); - hard_assert (v.len == 2); - hard_assert (!strcmp (v.vector[0], "top.foo = off")); - hard_assert (!strcmp (v.vector[1], "top.foobar = \"x\\x01\"")); - strv_free (&v); - - config_item_destroy (root); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -test_aliases (void) -{ - struct strv v = strv_make (); - expand_alias_definition ("/foo; /bar $* $$$;;;$1$2$3$4", "foo bar baz", &v); - hard_assert (v.len == 4); - hard_assert (!strcmp (v.vector[0], "/foo")); - hard_assert (!strcmp (v.vector[1], " /bar foo bar baz $;")); - hard_assert (!strcmp (v.vector[2], "")); - hard_assert (!strcmp (v.vector[3], "foobarbaz")); - strv_free (&v); -} - -static void -test_wrapping (void) -{ - static const char *message = " foo bar foobar fóóbárbáz"; - static const char *split[] = - { " foo", "bar", "foob", "ar", "fó", "ób", "árb", "áz" }; - - struct strv v = strv_make (); - hard_assert (wrap_message (message, 4, &v, NULL)); - hard_assert (v.len == N_ELEMENTS (split)); - for (size_t i = 0; i < N_ELEMENTS (split); i++) - hard_assert (!strcmp (v.vector[i], split[i])); - strv_free (&v); -} - -static void -test_utf8 (void) -{ - static const char *a[] = { "fřoo", "Fřooř", "fřOOŘ" }; - hard_assert (utf8_common_prefix (a, N_ELEMENTS (a)) == 5); - - char *cut_off = xstrdup ("ё\xD0"); - irc_sanitize_cut_off_utf8 (&cut_off); - hard_assert (!strcmp (cut_off, "ё\xEF\xBF\xBD")); - free (cut_off); -} - -int -main (int argc, char *argv[]) -{ - struct test test; - test_init (&test, argc, argv); - test_add_simple (&test, "/config", NULL, test_config); - test_add_simple (&test, "/aliases", NULL, test_aliases); - test_add_simple (&test, "/wrapping", NULL, test_wrapping); - test_add_simple (&test, "/utf8", NULL, test_utf8); - return test_run (&test); -} - -#define main main_shadowed -#endif // TESTING - -// --- Main program ------------------------------------------------------------ - -static const char *g_logo[] = -{ - " __ __ ", - " __/ /___________________/ / ", - " / / , / / , / __/ __/ _ \\ ", - "/ / / __/ / / __/_ / /_/ // / ", - "\\__/\\__/_ /\\__/___/\\__/_//_/ " PROGRAM_VERSION, - " /___/", - "" -}; - -static void -show_logo (struct app_context *ctx) -{ - for (size_t i = 0; i < N_ELEMENTS (g_logo); i++) - log_global_indent (ctx, "#m", g_logo[i]); -} - -static void -format_input_and_die (struct app_context *ctx) -{ - char buf[513]; - while (fgets (buf, sizeof buf, stdin)) - { - struct formatter f = formatter_make (ctx, NULL); - formatter_add (&f, "#m", buf); - formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP); - formatter_free (&f); - } - exit (EXIT_SUCCESS); -} - -int -main (int argc, char *argv[]) -{ - // We include a generated file from kike including this array we don't use; - // let's just keep it there and silence the compiler warning instead - (void) g_default_replies; - - static const struct opt opts[] = - { - { 'h', "help", NULL, 0, "display this help and exit" }, - { 'V', "version", NULL, 0, "output version information and exit" }, - // This is mostly intended for previewing formatted MOTD files - { 'f', "format", NULL, OPT_LONG_ONLY, "format IRC text from stdin" }, - { 0, NULL, NULL, 0, NULL } - }; - - struct opt_handler oh = - opt_handler_make (argc, argv, opts, NULL, "Terminal-based IRC client."); - bool format_mode = false; - - int c; - while ((c = opt_handler_get (&oh)) != -1) - switch (c) - { - case 'h': - opt_handler_usage (&oh, stdout); - exit (EXIT_SUCCESS); - case 'V': - printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); - exit (EXIT_SUCCESS); - case 'f': - format_mode = true; - break; - default: - print_error ("wrong options"); - opt_handler_usage (&oh, stderr); - exit (EXIT_FAILURE); - } - if (optind != argc) - { - opt_handler_usage (&oh, stderr); - exit (EXIT_FAILURE); - } - opt_handler_free (&oh); - - // We only need to convert to and from the terminal encoding - setlocale (LC_CTYPE, ""); - - struct app_context ctx; - app_context_init (&ctx); - g_ctx = &ctx; - - init_openssl (); - - // Bootstrap configuration, so that we can access schema items at all - register_config_modules (&ctx); - config_load (&ctx.config, config_item_object ()); - - // The following part is a bit brittle because of interdependencies - init_colors (&ctx); - if (format_mode) format_input_and_die (&ctx); - init_global_buffer (&ctx); - show_logo (&ctx); - setup_signal_handlers (); - init_poller_events (&ctx); - load_configuration (&ctx); - - // At this moment we can safely call any "on_change" callbacks - config_schema_call_changed (ctx.config.root); - - // Initialize input so that we can switch to new buffers - on_refresh_prompt (&ctx); - ctx.input->add_functions = input_add_functions; - CALL_ (ctx.input, start, argv[0]); - toggle_bracketed_paste (true); - reset_autoaway (&ctx); - - // Finally, we juice the configuration for some servers to create - load_plugins (&ctx); - load_servers (&ctx); - - ctx.polling = true; - while (ctx.polling) - poller_run (&ctx.poller); - - CALL (ctx.input, stop); - - if (get_config_boolean (ctx.config.root, "behaviour.save_on_quit")) - save_configuration (&ctx); - - app_context_free (&ctx); - toggle_bracketed_paste (false); - free_terminal (); - return EXIT_SUCCESS; -} |