aboutsummaryrefslogtreecommitdiff
path: root/degesch.c
diff options
context:
space:
mode:
Diffstat (limited to 'degesch.c')
-rw-r--r--degesch.c14473
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, &current))
- {
- // 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, &current))
- {
- 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, &current))
- print_error ("%s: %s", "localtime_r", strerror (errno));
- else if (!strftime (buf, sizeof buf, "%T", &current))
- 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, &current))
- print_error ("%s: %s", "gmtime_r", strerror (errno));
- else if (!strftime (buf, sizeof buf, "%F %T", &current))
- 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 (&copy, msg->params.vector + 1);
- char *modes = strv_join (&copy, " ");
- strv_free (&copy);
-
- 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 (&copy, 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 (&copy, " "));
- }
-
- strv_free (&copy);
-}
-
-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 (&params, " %s", v->vector[i + k]);
- }
-
- irc_send (s, "%s%s", modes.str, params.str);
-
- str_free (&modes);
- str_free (&params);
- }
-}
-
-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;
-}