xK/xC.c
Přemysl Eric Janouch 7ba17a0161
All checks were successful
Alpine 3.21 Success
Arch Linux AUR Success
OpenBSD 7.6 Success
Make the relay acknowledge all received commands
To that effect, bump liberty and the xC relay protocol version.
Relay events have been reordered to improve forward compatibility.

Also prevent use-after-free when serialization fails.

xP now slightly throttles activity notifications,
and indicates when there are unacknowledged commands.
2025-05-10 12:08:51 +02:00

16166 lines
445 KiB
C

/*
* xC.c: a terminal-based IRC client
*
* Copyright (c) 2015 - 2024, 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( 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
{
ATTR_RESET,
#define XX(x, y, z) ATTR_ ## x,
ATTR_TABLE (XX)
#undef XX
ATTR_COUNT
};
// User data for logger functions to enable formatted logging
#define print_fatal_data ((void *) ATTR_ERROR)
#define print_error_data ((void *) ATTR_ERROR)
#define print_warning_data ((void *) ATTR_WARNING)
#include "config.h"
#define PROGRAM_NAME "xC"
// fmemopen
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include "common.c"
#include "xD-replies.c"
#include "xC-proto.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);
/// Switch to a different input buffer
void (*buffer_switch) (void *input, input_buffer_t);
/// Return all history lines in the locale encoding
struct strv (*buffer_get_history) (void *input, input_buffer_t);
/// Add a history line in the locale encoding
void (*buffer_add_history) (void *input, input_buffer_t, const char *);
/// Register a function that can be bound to character sequences
void (*register_fn) (void *input,
const char *name, const char *help, input_fn fn, void *user_data);
/// Bind an arbitrary sequence of characters to the given named function
void (*bind) (void *input, const char *seq, const char *fn);
/// Bind Ctrl+key to the given named function
void (*bind_control) (void *input, char key, const char *fn);
/// Bind Alt+key to the given named function
void (*bind_meta) (void *input, char key, const char *fn);
/// Get the current line input and position within
char *(*get_line) (void *input, int *position);
/// Clear the current line input
void (*clear_line) (void *input);
/// Insert text at current position
bool (*insert) (void *input, const char *text);
/// Handle terminal resize
void (*on_tty_resized) (void *input);
/// Handle terminal input
void (*on_tty_readable) (void *input);
};
#define INPUT_VTABLE(XX) \
XX (start) XX (stop) XX (prepare) XX (destroy) \
XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \
XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \
XX (buffer_get_history) XX (buffer_add_history) \
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, int *position)
{
(void) input;
if (position) *position = rl_point;
return rl_copy_text (0, rl_end);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_bind (void *input, const char *seq, const char *function_name)
{
(void) input;
rl_bind_keyseq (seq, rl_named_function (function_name));
}
static void
input_rl_bind_meta (void *input, char key, const char *function_name)
{
// This one seems to actually work
char keyseq[] = { '\\', 'e', key, 0 };
input_rl_bind (input, keyseq, function_name);
#if 0
// While this one only fucks up UTF-8
// Tested with urxvt and xterm, on Debian Jessie/Arch, default settings
// \M-<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;
// Readline >= 8.0 otherwise prints spurious newlines on EOF.
if (RL_VERSION_MAJOR >= 8)
rl_variable_bind ("enable-bracketed-paste", "off");
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 struct strv
input_rl_buffer_get_history (void *input, input_buffer_t input_buffer)
{
(void) input;
struct input_rl_buffer *buffer = input_buffer;
HIST_ENTRY **p =
buffer->history ? buffer->history->entries : history_list();
struct strv v = strv_make ();
while (p && *p)
strv_append (&v, (*p++)->line);
return v;
}
static void
input_rl_buffer_add_history (void *input, input_buffer_t input_buffer,
const char *line)
{
(void) input;
struct input_rl_buffer *buffer = input_buffer;
// For inactive buffers, we'd have to either alloc_history_entry(),
// construe a timestamp, and manually insert it into saved HISTORY_STATEs,
// or temporarily switch histories.
if (!buffer->history)
{
bool at_end = where_history () == history_length;
add_history (line);
if (at_end)
next_history ();
}
}
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 void
input_el__redisplay (void *input)
{
// See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT
// so we bind redisplay somewhere else in app_editline_init()
struct input_el *self = input;
wchar_t x[] = { L'q' & 31, 0 };
el_wpush (self->editline, x);
// We have to do this or it gets stuck and nothing is done
int dummy_count = 0;
(void) el_wgets (self->editline, &dummy_count);
}
static char *
input_el__make_prompt (EditLine *editline)
{
struct input_el *self = NULL;
el_get (editline, EL_CLIENTDATA, &self);
if (!self->prompt)
return "";
return self->prompt;
}
static char *
input_el__make_empty_prompt (EditLine *editline)
{
(void) editline;
return "";
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_ding (void *input)
{
// XXX: this isn't probably very portable;
// we could use "bell" from terminfo but that creates a dependency
(void) input;
write (STDOUT_FILENO, "\a", 1);
}
static const char *
input_el_get_prompt (void *input)
{
struct input_el *self = input;
return self->prompt;
}
static void
input_el_set_prompt (void *input, char *prompt)
{
struct input_el *self = input;
cstr_set (&self->prompt, prompt);
if (self->prompt_shown > 0)
input_el__redisplay (self);
}
static void
input_el_clear_line (void *input)
{
struct input_el *self = input;
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
input_el__redisplay (self);
}
static void
input_el__erase (struct input_el *self)
{
el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
input_el_clear_line (self);
}
static bool
input_el_insert (void *input, const char *s)
{
struct input_el *self = input;
bool success = !*s || !el_insertstr (self->editline, s);
if (self->prompt_shown > 0)
input_el__redisplay (self);
return success;
}
static char *
input_el_get_line (void *input, int *position)
{
struct input_el *self = input;
const LineInfo *info = el_line (self->editline);
int point = info->cursor - info->buffer;
if (position) *position = point;
return xstrndup (info->buffer, info->lastchar - info->buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_bind (void *input, const char *seq, const char *function_name)
{
struct input_el *self = input;
el_set (self->editline, EL_BIND, seq, function_name, NULL);
}
static void
input_el_bind_meta (void *input, char key, const char *function_name)
{
char keyseq[] = { 'M', '-', key, 0 };
input_el_bind (input, keyseq, function_name);
}
static void
input_el_bind_control (void *input, char key, const char *function_name)
{
char keyseq[] = { '^', key, 0 };
input_el_bind (input, keyseq, function_name);
}
static void
input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
{
(void) cif;
struct input_el_fn *data = user_data;
*(unsigned char *) ret = data->callback
(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
}
static wchar_t *
ascii_to_wide (const char *ascii)
{
size_t len = strlen (ascii) + 1;
wchar_t *wide = xcalloc (sizeof *wide, len);
while (len--)
hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
return wide;
}
static void
input_el_register_fn (void *input,
const char *name, const char *help, input_fn callback, void *user_data)
{
void *bound_fn = NULL;
struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
hard_assert (data);
static ffi_cif cif;
static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
hard_assert (ffi_prep_cif
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
data->user_data = user_data;
data->callback = callback;
data->name = ascii_to_wide (name);
data->help = ascii_to_wide (help);
hard_assert (ffi_prep_closure_loc (&data->closure,
&cif, input_el__forward, data, bound_fn) == FFI_OK);
struct input_el *self = input;
el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
LIST_PREPEND (self->fns, data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_start (void *input, const char *program_name)
{
struct input_el *self = input;
self->editline = el_init (program_name, stdin, stdout, stderr);
el_set (self->editline, EL_CLIENTDATA, self);
el_set (self->editline, EL_PROMPT_ESC,
input_el__make_prompt, INPUT_START_IGNORE);
el_set (self->editline, EL_SIGNAL, false);
el_set (self->editline, EL_UNBUFFERED, true);
el_set (self->editline, EL_EDITOR, "emacs");
app_editline_init (self);
self->prompt_shown = 1;
self->active = true;
}
static void
input_el_stop (void *input)
{
struct input_el *self = input;
if (self->prompt_shown > 0)
input_el__erase (self);
el_end (self->editline);
self->editline = NULL;
self->active = false;
self->prompt_shown = false;
}
static void
input_el_prepare (void *input, bool enabled)
{
struct input_el *self = input;
el_set (self->editline, EL_PREP_TERM, enabled);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
{
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
wchar_t *line = xcalloc (len + 1, sizeof *info->buffer);
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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Editline keeping its own history position (look for "eventno" there).
// This is the only sane way of resetting it.
static void
input_el__start_over (struct input_el *self)
{
wchar_t x[] = { L'c' & 31, 0 };
el_wpush (self->editline, x);
int dummy_count = 0;
(void) el_wgets (self->editline, &dummy_count);
}
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->active)
return;
if (self->current)
input_el__save_buffer (self, self->current);
self->current = buffer;
el_wset (self->editline, EL_HIST, history, buffer->history);
// We only know how to reset the history position to be at the end.
input_el__start_over (self);
input_el__restore_buffer (self, buffer);
}
static struct strv
input_el_buffer_get_history (void *input, input_buffer_t input_buffer)
{
(void) input;
struct input_el_buffer *buffer = input_buffer;
struct strv v = strv_make ();
HistEventW ev;
if (history_w (buffer->history, &ev, H_LAST) < 0)
return v;
do
{
size_t len = wcstombs (NULL, ev.str, 0);
if (len++ == (size_t) -1)
continue;
char *mb = xmalloc (len);
mb[wcstombs (mb, ev.str, len)] = 0;
strv_append_owned (&v, mb);
}
while (history_w (buffer->history, &ev, H_PREV) >= 0);
return v;
}
static void
input_el_buffer_add_history (void *input, input_buffer_t input_buffer,
const char *line)
{
(void) input;
struct input_el_buffer *buffer = input_buffer;
// When currently iterating history, this makes editline's internal
// history pointer wrongly point to a newer entry.
size_t len = mbstowcs (NULL, line, 0);
if (len++ != (size_t) -1)
{
wchar_t *wc = xcalloc (len, sizeof *wc);
wc[mbstowcs (wc, line, len)] = 0;
HistEventW ev;
(void) history_w (buffer->history, &ev, H_ENTER, wc);
free (wc);
}
}
static void
input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
{
struct input_el *self = input;
struct input_el_buffer *buffer = input_buffer;
if (self->active && self->current == buffer)
{
el_wset (self->editline, EL_HIST, history, NULL);
self->current = NULL;
}
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 el_wgets() */)
{
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.
// ~~~ Scripting support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 },
// ~~~ Chat ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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)
// ~~~ Attribute utilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum
{
TEXT_BOLD = 1 << 0,
TEXT_ITALIC = 1 << 1,
TEXT_UNDERLINE = 1 << 2,
TEXT_INVERSE = 1 << 3,
TEXT_BLINK = 1 << 4,
TEXT_CROSSED_OUT = 1 << 5,
TEXT_MONOSPACE = 1 << 6
};
// Similar to code in liberty-tui.c.
struct attrs
{
short fg; ///< Foreground (256-colour cube or -1)
short bg; ///< Background (256-colour cube or -1)
unsigned attrs; ///< TEXT_* mask
};
/// Decode attributes in the value using a subset of the git config format,
/// ignoring all errors since it doesn't affect functionality
static struct attrs
attrs_decode (const char *value)
{
struct strv v = strv_make ();
cstr_split (value, " ", true, &v);
int colors = 0;
struct attrs attrs = { -1, -1, 0 };
for (char **it = v.vector; *it; it++)
{
char *end = NULL;
long n = strtol (*it, &end, 10);
if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
{
if (colors == 0) attrs.fg = n;
if (colors == 1) attrs.bg = n;
colors++;
}
else if (!strcmp (*it, "bold")) attrs.attrs |= TEXT_BOLD;
else if (!strcmp (*it, "italic")) attrs.attrs |= TEXT_ITALIC;
else if (!strcmp (*it, "ul")) attrs.attrs |= TEXT_UNDERLINE;
else if (!strcmp (*it, "reverse")) attrs.attrs |= TEXT_INVERSE;
else if (!strcmp (*it, "blink")) attrs.attrs |= TEXT_BLINK;
else if (!strcmp (*it, "strike")) attrs.attrs |= TEXT_CROSSED_OUT;
}
strv_free (&v);
return attrs;
}
// ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum formatter_item_type
{
FORMATTER_ITEM_END, ///< Sentinel value for arrays
FORMATTER_ITEM_TEXT, ///< Text
FORMATTER_ITEM_ATTR, ///< Formatting attributes
FORMATTER_ITEM_FG_COLOR, ///< Foreground colour
FORMATTER_ITEM_BG_COLOR, ///< Background colour
FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
};
struct formatter_item
{
enum formatter_item_type type : 16; ///< Type of this item
int attribute : 16; ///< Attribute ID or a TEXT_* mask
int color; ///< Colour ([256 << 16] | 16)
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
bool clean; ///< Assume ATTR_RESET
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, .clean = true };
self.items = xcalloc ((self.items_alloc = 16), sizeof *self.items);
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_SKIP_FILE = 1 << 0, ///< Don't log this to file
BUFFER_LINE_UNIMPORTANT = 1 << 1, ///< Joins, parts, similar spam
BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this
};
// NOTE: This sequence must match up with xC-proto, only one lower.
enum buffer_line_rendition
{
BUFFER_LINE_BARE, ///< Unadorned
BUFFER_LINE_INDENT, ///< Just indent the line
BUFFER_LINE_STATUS, ///< Status message
BUFFER_LINE_ERROR, ///< Error message
BUFFER_LINE_JOIN, ///< Join arrow
BUFFER_LINE_PART, ///< Part arrow
BUFFER_LINE_ACTION, ///< Highlighted asterisk
};
struct buffer_line
{
LIST_HEADER (struct buffer_line)
unsigned flags; ///< Functional flags
enum buffer_line_rendition r; ///< What the line should look like
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
// ~~~ Relay ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct client
{
LIST_HEADER (struct client)
struct app_context *ctx; ///< Application context
// TODO: Convert this all to TLS, and only TLS, with required client cert.
// That means replacing plumbing functions with the /other/ set from xD.
int socket_fd; ///< The TCP socket
struct str read_buffer; ///< Unprocessed input
struct str write_buffer; ///< Output yet to be sent out
uint32_t event_seq; ///< Outgoing message counter
bool initialized; ///< Initial sync took place
bool closing; ///< We're closing the connection
struct poller_fd socket_event; ///< The socket can be read/written to
};
static struct client *
client_new (void)
{
struct client *self = xcalloc (1, sizeof *self);
self->socket_fd = -1;
self->read_buffer = str_make ();
self->write_buffer = str_make ();
return self;
}
static void
client_destroy (struct client *self)
{
if (!soft_assert (self->socket_fd == -1))
xclose (self->socket_fd);
str_free (&self->read_buffer);
str_free (&self->write_buffer);
free (self);
}
static void client_kill (struct client *c);
// ~~~ Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 shut down 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_modes; ///< Our current user modes
char *irc_user_host; ///< Our current user@host
bool autoaway_active; ///< Autoaway is currently active
struct strv outstanding_joins; ///< JOINs we expect a response to
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_modes),
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_modes = str_make ();
self->outstanding_joins = strv_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_modes);
free (self->irc_user_host);
strv_free (&self->outstanding_joins);
strv_free (&self->cap_ls_buf);
server_free_specifics (self);
free (self);
}
REF_COUNTABLE_METHODS (server)
#define server_ref do_not_use_dangerous
// ~~~ Scripting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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);
};
// ~~~ Main context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct app_context
{
/// Default terminal attributes
struct attrs theme_defaults[ATTR_COUNT];
// Configuration:
struct config config; ///< Program configuration
struct attrs theme[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_fd relay_event; ///< New relay connection available
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
struct poller_timer date_chg_tmr; ///< Print a date change
struct poller_timer autoaway_tmr; ///< Autoaway timer
struct poller poller; ///< Manages polled descriptors
bool quitting; ///< User requested quitting
bool polling; ///< The event loop is running
// Buffers:
struct buffer *buffers; ///< All our buffers in order
struct buffer *buffers_tail; ///< The tail of our buffers
struct buffer *global_buffer; ///< The global buffer
struct buffer *current_buffer; ///< The current buffer
struct buffer *last_buffer; ///< Last used buffer
struct str_map buffers_by_name; ///< Buffers by name
unsigned backlog_limit; ///< Limit for buffer lines
time_t last_displayed_msg_time; ///< Time of last displayed message
// Terminal:
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
struct input *input; ///< User interface
struct poller_idle prompt_event; ///< Deferred prompt refresh
struct poller_idle input_event; ///< Pending input event
struct strv pending_input; ///< Pending input lines
int *nick_palette; ///< A 256-colour palette for nicknames
size_t nick_palette_len; ///< Number of entries in nick_palette
bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape
bool in_bracketed_paste; ///< User is pasting some content
struct str input_buffer; ///< Buffered pasted content
bool running_pager; ///< Running a pager for buffer history
bool running_editor; ///< Running editor for the input
char *editor_filename; ///< The file being edited by user
int terminal_suspended; ///< Terminal suspension level
// Relay:
int relay_fd; ///< Listening socket FD
struct client *clients; ///< Our relay clients
/// A single message buffer to prepare all outcoming messages within
struct relay_event_message relay_message;
// Plugins:
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);
self->relay_fd = -1;
}
static void
app_context_relay_stop (struct app_context *self)
{
if (self->relay_fd != -1)
{
poller_fd_reset (&self->relay_event);
xclose (self->relay_fd);
self->relay_fd = -1;
}
}
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);
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);
app_context_relay_stop (self);
LIST_FOR_EACH (struct client, c, self->clients)
client_kill (c);
relay_event_message_free (&self->relay_message);
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_relay_bind_change (struct config_item *item);
static void on_config_backlog_limit_change (struct config_item *item);
static void on_config_theme_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 const 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 const struct config_schema g_config_general[] =
{
{ .name = "autosave",
.comment = "Save configuration automatically after each change",
.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 = "logging",
.comment = "Log buffer contents to file",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_logging_change },
{ .name = "plugin_autoload",
.comment = "Plugins to automatically load on start",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_nonjunk_string },
{ .name = "relay_bind",
.comment = "Address to bind to for a user interface relay point",
.type = CONFIG_ITEM_STRING,
.validate = config_validate_nonjunk_string,
.on_change = on_config_relay_bind_change },
// Buffer history:
{ .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 = "pager",
.comment = "Shell command to page buffer history (args: name [path])",
.type = CONFIG_ITEM_STRING,
.default_ = "`name=$(echo \"$1\" | sed 's/[%?:.]/\\\\&/g'); "
"prompt='?f%F:'$name'. ?db- page %db?L of %D. .(?eEND:?PB%PB\\%..)'; "
"LESSSECURE=1 less +Gb -Ps\"$prompt\" \"${2:--R}\"`" },
{ .name = "pager_strip_formatting",
.comment = "Strip terminal formatting from pager input",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off" },
// Output adjustments:
{ .name = "beep_on_highlight",
.comment = "Ring the bell when highlighted or on a new invisible PM",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on",
.on_change = on_config_beep_on_highlight_change },
{ .name = "date_change_line",
.comment = "Input to strftime(3) for the date change line",
.type = CONFIG_ITEM_STRING,
.default_ = "\"%F\"" },
{ .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 = "read_marker_char",
.comment = "The character to use for the read marker line",
.type = CONFIG_ITEM_STRING,
.default_ = "\"-\"",
.validate = config_validate_nonjunk_string },
{ .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 },
// User input:
{ .name = "editor",
.comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\", "
"nano/micro/kakoune: \"nano/micro/kak +%L:%C %F\"",
.type = CONFIG_ITEM_STRING },
{ .name = "process_pasted_text",
.comment = "Normalize newlines and quote the command prefix in pastes",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
// Pan-server configuration:
{ .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 = "reconnect_delay_growing",
.comment = "Growth factor for the 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" },
{}
};
static const struct config_schema g_config_theme[] =
{
#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \
.on_change = on_config_theme_change },
ATTR_TABLE (XX)
#undef XX
{}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_config_general (struct config_item *subtree, void *user_data)
{
config_schema_apply_to_object (g_config_general, subtree, user_data);
}
static void
load_config_theme (struct config_item *subtree, void *user_data)
{
config_schema_apply_to_object (g_config_theme, 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, "general", load_config_general, ctx);
config_register_module (config, "theme", load_config_theme, 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;
}
// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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.
struct attr_printer
{
struct attrs *attrs; ///< Named attributes
FILE *stream; ///< Output stream
bool dirty; ///< Attributes are set
};
#define ATTR_PRINTER_INIT(attrs, stream) { 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 pager--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, exit_attribute_mode);
self->dirty = false;
}
// NOTE: commonly terminals have:
// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
// 16 colours (okayish, we have the full basic range guaranteed)
// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey)
// 256 colours (best, like above but with a larger cube and more grey)
/// Interpolate from the 256-colour palette to the 88-colour one
static int
attr_printer_256_to_88 (int color)
{
// These colours are the same everywhere
if (color < 16)
return color;
// 24 -> 8 extra shades of grey
if (color >= 232)
return 80 + (color - 232) / 3;
// 6 * 6 * 6 cube -> 4 * 4 * 4 cube
int x[6] = { 0, 1, 1, 2, 2, 3 };
int index = color - 16;
return 16 +
( x[ index / 36 ] << 8
| x[(index / 6) % 6 ] << 4
| x[(index % 6) ] );
}
static int
attr_printer_decode_color (int color, bool *is_bright)
{
int16_t c16 = color; hard_assert (c16 < 16);
int16_t c256 = color >> 16; hard_assert (c256 < 256);
*is_bright = false;
switch (max_colors)
{
case 8:
if (c16 >= 8)
{
c16 -= 8;
*is_bright = true;
}
// Fall-through
case 16:
return c16;
case 88:
return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256);
case 256:
return c256 <= 0 ? c16 : c256;
default:
// Unsupported palette
return -1;
}
}
static void
attr_printer_apply (struct attr_printer *self,
int text_attrs, int wanted_fg, int wanted_bg)
{
bool fg_is_bright;
int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright);
bool bg_is_bright;
int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright);
bool have_inverse = !!(text_attrs & TEXT_INVERSE);
if (have_inverse)
{
bool tmp = fg_is_bright;
fg_is_bright = bg_is_bright;
bg_is_bright = tmp;
}
// In 8 colour mode, some terminals don't support bright backgrounds.
// However, we can make use of the fact that the brightness change caused
// by the bold attribute is retained when inverting the colours.
// This has the downside of making the text bold when it's not supposed
// to be, and we still can't make both colours bright, so it's more of
// an interesting hack rather than anything else.
if (!fg_is_bright && bg_is_bright && have_inverse)
text_attrs |= TEXT_BOLD;
else if (!fg_is_bright && bg_is_bright
&& !have_inverse && fg >= 0 && bg >= 0)
{
// As long as none of the colours is the default, we can swap them
int tmp = fg; fg = bg; bg = tmp;
text_attrs |= TEXT_BOLD | TEXT_INVERSE;
}
else
{
// This often works, however...
if (fg_is_bright) text_attrs |= TEXT_BOLD;
// this turns out to be annoying if implemented "correctly"
if (bg_is_bright) text_attrs |= TEXT_BLINK;
}
attr_printer_reset (self);
// TEXT_MONOSPACE is unimplemented, for obvious reasons
if (text_attrs)
attr_printer_tputs (self, tparm (set_attributes,
0, // standout
text_attrs & TEXT_UNDERLINE,
text_attrs & TEXT_INVERSE,
text_attrs & TEXT_BLINK,
0, // dim
text_attrs & TEXT_BOLD,
0, // blank
0, // protect
0)); // acs
if ((text_attrs & TEXT_ITALIC) && enter_italics_mode)
attr_printer_tputs (self, enter_italics_mode);
char *smxx = NULL;
if ((text_attrs & TEXT_CROSSED_OUT)
&& (smxx = tigetstr ("smxx")) && smxx != (char *) -1)
attr_printer_tputs (self, smxx);
if (fg >= 0)
attr_printer_tputs (self, g_terminal.color_set_fg[fg]);
if (bg >= 0)
attr_printer_tputs (self, g_terminal.color_set_bg[bg]);
self->dirty = true;
}
static void
attr_printer_apply_named (struct attr_printer *self, int attribute)
{
attr_printer_reset (self);
if (attribute == ATTR_RESET)
return;
// See the COLOR_256 macro or attr_printer_decode_color().
struct attrs *a = &self->attrs[attribute];
attr_printer_apply (self, a->attrs,
a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)),
a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF)));
self->dirty = true;
}
// ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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;
struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream);
if (printer)
attr_printer_apply_named (&state, attribute);
vfprintf (stream, fmt, ap);
if (printer)
attr_printer_reset (&state);
}
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);
}
// ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static ssize_t
attr_by_name (const char *name)
{
static const char *table[ATTR_COUNT] =
{
NULL,
#define XX(x, y, z) [ATTR_ ## x] = #y,
ATTR_TABLE (XX)
#undef XX
};
for (size_t i = 1; i < N_ELEMENTS (table); i++)
if (!strcmp (name, table[i]))
return i;
return -1;
}
static void
on_config_theme_change (struct config_item *item)
{
struct app_context *ctx = item->user_data;
ssize_t id = attr_by_name (item->schema->name);
if (id != -1)
{
// TODO: There should be a validator.
ctx->theme[id] = item->type == CONFIG_ITEM_NULL
? ctx->theme_defaults[id]
: attrs_decode (item->value.string.str);
}
}
static void
init_colors (struct app_context *ctx)
{
bool have_ti = init_terminal ();
#define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \
ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ }
INIT_ATTR (PROMPT, -1, -1, TEXT_BOLD);
INIT_ATTR (RESET, -1, -1, 0);
INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD);
INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1, 0);
INIT_ATTR (WARNING, COLOR_YELLOW, -1, 0);
INIT_ATTR (ERROR, COLOR_RED, -1, 0);
INIT_ATTR (EXTERNAL, COLOR_WHITE, -1, 0);
INIT_ATTR (TIMESTAMP, COLOR_WHITE, -1, 0);
INIT_ATTR (HIGHLIGHT, COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD);
INIT_ATTR (ACTION, COLOR_RED, -1, 0);
INIT_ATTR (USERHOST, COLOR_CYAN, -1, 0);
INIT_ATTR (JOIN, COLOR_GREEN, -1, 0);
INIT_ATTR (PART, COLOR_RED, -1, 0);
#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;
}
// --- 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 in an unknown encoding
// #m inserts an IRC-formatted string (auto-resets at boundaries)
// #n cuts the nickname from a string and automatically colours it
// #N is like #n but also appends userhost, if present
//
// #a inserts named attributes (auto-resets)
// #r resets terminal attributes
// #c sets foreground colour
// #C sets background colour
//
// Modifiers:
// & free() the string argument after using it
static void
formatter_add_item (struct formatter *self, struct formatter_item template_)
{
// Auto-resetting tends to create unnecessary items,
// which also end up being relayed to frontends, so filter them out.
bool reset =
template_.type == FORMATTER_ITEM_ATTR &&
template_.attribute == ATTR_RESET;
if (self->clean && reset)
return;
self->clean = reset ||
(self->clean && template_.type == FORMATTER_ITEM_TEXT);
if (template_.text)
template_.text = xstrdup (template_.text);
if (self->items_len == self->items_alloc)
self->items = xreallocarray
(self->items, sizeof *self->items, (self->items_alloc <<= 1));
self->items[self->items_len++] = template_;
}
#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
#define FORMATTER_ADD_RESET(self) \
FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
#define FORMATTER_ADD_TEXT(self, text_) \
FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum
{
MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN,
MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE,
MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN,
MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY,
};
// We use estimates from the 16 colour terminal palette, or the 256 colour cube,
// which is not always available. The mIRC orange colour is only in the cube.
static const int g_mirc_to_terminal[] =
{
[MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231),
[MIRC_BLACK] = COLOR_256 (BLACK, 16),
[MIRC_BLUE] = COLOR_256 (BLUE, 19),
[MIRC_GREEN] = COLOR_256 (GREEN, 34),
[MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196),
[MIRC_RED] = COLOR_256 (RED, 124),
[MIRC_PURPLE] = COLOR_256 (MAGENTA, 127),
[MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214),
[MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226),
[MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46),
[MIRC_CYAN] = COLOR_256 (CYAN, 37),
[MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51),
[MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21),
[MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201),
[MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244),
[MIRC_L_GRAY] = COLOR_256 (WHITE, 252),
};
// https://modern.ircdocs.horse/formatting.html
// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html
static const int16_t g_extra_to_256[100 - 16] =
{
52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198,
203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1
};
static const char *
irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg)
{
if (!isdigit_ascii (*s))
{
*fg = *bg = 99;
return s;
}
*fg = *s++ - '0';
if (isdigit_ascii (*s))
*fg = *fg * 10 + (*s++ - '0');
if (*s != ',' || !isdigit_ascii (s[1]))
return s;
s++;
*bg = *s++ - '0';
if (isdigit_ascii (*s))
*bg = *bg * 10 + (*s++ - '0');
return s;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct irc_char_attrs
{
uint8_t fg, bg; ///< {Fore,back}ground colour or 99
uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK
uint8_t starts_at_boundary; ///< Possible to split here?
};
static void
irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out)
{
soft_assert (attrs->fg < 100 && attrs->bg < 100);
if (attrs->fg != 99 || attrs->bg != 99)
{
str_append_printf (out, "\x03%u", attrs->fg);
if (attrs->bg != 99)
str_append_printf (out, ",%02u", attrs->bg);
}
if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02');
if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d');
if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f');
if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16');
if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e');
if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11');
}
static int
irc_parse_attribute (char c)
{
switch (c)
{
case '\x02' /* ^B */: return TEXT_BOLD;
case '\x11' /* ^Q */: return TEXT_MONOSPACE;
case '\x16' /* ^V */: return TEXT_INVERSE;
case '\x1d' /* ^] */: return TEXT_ITALIC;
case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT;
case '\x1f' /* ^_ */: return TEXT_UNDERLINE;
case '\x0f' /* ^O */: return -1;
}
return 0;
}
// The text needs to be NUL-terminated, and a valid UTF-8 string
static struct irc_char_attrs *
irc_analyze_text (const char *text, size_t len)
{
struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs),
blank = { .fg = 99, .bg = 99, .starts_at_boundary = true },
next = blank, cur = next;
for (size_t i = 0; i != len; cur = next)
{
const char *start = text;
hard_assert (utf8_decode (&text, len - i) >= 0);
int attribute = irc_parse_attribute (*start);
if (*start == '\x03')
text = irc_parse_mirc_color (text, &next.fg, &next.bg);
else if (attribute > 0)
next.attributes ^= attribute;
else if (attribute < 0)
next = blank;
while (start++ != text)
{
attrs[i++] = cur;
cur.starts_at_boundary = false;
}
}
return attrs;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *
formatter_parse_mirc_color (struct formatter *self, const char *s)
{
uint8_t fg = 255, bg = 255;
s = irc_parse_mirc_color (s, &fg, &bg);
if (fg < 16)
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
else if (fg < 100)
FORMATTER_ADD_ITEM (self, FG_COLOR,
.color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16]));
if (bg < 16)
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
else if (bg < 100)
FORMATTER_ADD_ITEM (self, BG_COLOR,
.color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16]));
return s;
}
static void
formatter_parse_message (struct formatter *self, const char *s)
{
FORMATTER_ADD_RESET (self);
struct str buf = str_make ();
unsigned char c;
while ((c = *s++))
{
if (buf.len && c < 0x20)
{
FORMATTER_ADD_TEXT (self, buf.str);
str_reset (&buf);
}
int attribute = irc_parse_attribute (c);
if (c == '\x03')
s = formatter_parse_mirc_color (self, s);
else if (attribute > 0)
FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute);
else if (attribute < 0)
FORMATTER_ADD_RESET (self);
else
str_append_c (&buf, c);
}
if (buf.len)
FORMATTER_ADD_TEXT (self, buf.str);
str_free (&buf);
FORMATTER_ADD_RESET (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
formatter_parse_nick (struct formatter *self, const char *s)
{
// For outgoing messages; maybe we should add a special #t for them
// which would also make us not cut off the userhost part, ever
if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s)))
{
char *tmp = irc_to_utf8 (s);
FORMATTER_ADD_TEXT (self, tmp);
free (tmp);
return;
}
char *nick = irc_cut_nickname (s);
int color = siphash_wrapper (nick, strlen (nick)) % 7;
// Never use the black colour, could become transparent on black terminals;
// white is similarly excluded from the range
if (color == COLOR_BLACK)
color = (uint16_t) -1;
// Use a colour from the 256-colour cube if available
color |= self->ctx->nick_palette[siphash_wrapper (nick,
strlen (nick)) % self->ctx->nick_palette_len] << 16;
// We always use the default colour for ourselves
if (self->s && irc_is_this_us (self->s, nick))
color = -1;
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
char *x = irc_to_utf8 (nick);
free (nick);
FORMATTER_ADD_TEXT (self, x);
free (x);
// Need to reset the colour afterwards
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
}
static void
formatter_parse_nick_full (struct formatter *self, const char *s)
{
formatter_parse_nick (self, s);
const char *userhost;
if (!(userhost = irc_find_userhost (s)))
return;
FORMATTER_ADD_TEXT (self, " (");
FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);
char *x = irc_to_utf8 (userhost);
FORMATTER_ADD_TEXT (self, x);
free (x);
FORMATTER_ADD_RESET (self);
FORMATTER_ADD_TEXT (self, ")");
}
static const char *
formatter_parse_field (struct formatter *self,
const char *field, struct str *buf, va_list *ap)
{
bool free_string = false;
char *s = NULL;
char *tmp = NULL;
int c;
restart:
switch ((c = *field++))
{
// We can push boring text content to the caller's buffer
// and let it flush the buffer only when it's actually needed
case 'd':
tmp = xstrdup_printf ("%d", va_arg (*ap, int));
str_append (buf, tmp);
free (tmp);
break;
case 's':
str_append (buf, (s = va_arg (*ap, char *)));
break;
case 'l':
if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
(s = va_arg (*ap, char *)), -1, NULL)))
print_error ("character conversion failed for: %s", "output");
else
str_append (buf, tmp);
free (tmp);
break;
case 'S':
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
str_append (buf, tmp);
free (tmp);
break;
case 'm':
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
formatter_parse_message (self, tmp);
free (tmp);
break;
case 'n':
formatter_parse_nick (self, (s = va_arg (*ap, char *)));
break;
case 'N':
formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
break;
case 'a':
FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
break;
case 'c':
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
break;
case 'C':
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
break;
case 'r':
FORMATTER_ADD_RESET (self);
break;
default:
if (c == '&' && !free_string)
free_string = true;
else if (c)
hard_assert (!"unexpected format specifier");
else
hard_assert (!"unexpected end of format string");
goto restart;
}
if (free_string)
free (s);
return field;
}
// I was unable to take a pointer of a bare "va_list" when it was passed in
// as a function argument, so it has to be a pointer from the beginning
static void
formatter_addv (struct formatter *self, const char *format, va_list *ap)
{
struct str buf = str_make ();
while (*format)
{
if (*format != '#' || *++format == '#')
{
str_append_c (&buf, *format++);
continue;
}
if (buf.len)
{
FORMATTER_ADD_TEXT (self, buf.str);
str_reset (&buf);
}
format = formatter_parse_field (self, format, &buf, ap);
}
if (buf.len)
FORMATTER_ADD_TEXT (self, buf.str);
str_free (&buf);
}
static void
formatter_add (struct formatter *self, const char *format, ...)
{
va_list ap;
va_start (ap, format);
formatter_addv (self, format, &ap);
va_end (ap);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct line_char_attrs
{
short named; ///< Named attribute or -1
short text; ///< Text attributes
int fg; ///< Foreground colour (-1 for default)
int bg; ///< Background colour (-1 for default)
};
// We can get rid of the linked list and do this in one allocation (use strlen()
// for the upper bound)--since we only prepend and/or replace characters, add
// a member to specify the prepended character and how many times to repeat it.
// Tabs may nullify the wide character but it's not necessary.
//
// This would be slighly more optimal but it would also set the algorithm in
// stone and complicate flushing.
struct line_char
{
LIST_HEADER (struct line_char)
wchar_t wide; ///< The character as a wchar_t
int width; ///< Width of the character in cells
struct line_char_attrs attrs; ///< Attributes
};
static struct line_char *
line_char_new (wchar_t wc)
{
struct line_char *self = xcalloc (1, sizeof *self);
self->width = wcwidth ((self->wide = wc));
// Typically various control characters
if (self->width < 0)
self->width = 0;
self->attrs.bg = self->attrs.fg = -1;
self->attrs.named = ATTR_RESET;
return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct line_wrap_mark
{
struct line_char *start; ///< First character
int used; ///< Display cells used
};
static void
line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c)
{
if (!mark->start)
mark->start = c;
mark->used += c->width;
}
struct line_wrap_state
{
struct line_char *result; ///< Head of result
struct line_char *result_tail; ///< Tail of result
int line_used; ///< Line length before marks
int line_max; ///< Maximum line length
struct line_wrap_mark chunk; ///< All buffered text
struct line_wrap_mark overflow; ///< Overflowing text
};
static void
line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before)
{
struct line_char *nl = line_char_new (L'\n');
LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start);
s->line_used = before->used;
}
static void
line_wrap_flush (struct line_wrap_state *s, bool force_split)
{
if (!s->overflow.start)
s->line_used += s->chunk.used;
else if (force_split || s->chunk.used > s->line_max)
{
#ifdef WRAP_UNNECESSARILY
// When the line wraps at the end of the screen and a background colour
// is set, the terminal paints the entire new line with that colour.
// Explicitly inserting a newline with the default attributes fixes it.
line_wrap_flush_split (s, &s->overflow);
#else
// Splitting here breaks link searching mechanisms in some terminals,
// though, so we make a trade-off and let the chunk wrap naturally.
// Fuck terminals, really.
s->line_used = s->overflow.used;
#endif
}
else
// Print the chunk in its entirety on a new line
line_wrap_flush_split (s, &s->chunk);
memset (&s->chunk, 0, sizeof s->chunk);
memset (&s->overflow, 0, sizeof s->overflow);
}
static void
line_wrap_nl (struct line_wrap_state *s)
{
line_wrap_flush (s, true);
struct line_char *nl = line_char_new (L'\n');
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl);
s->line_used = 0;
}
static void
line_wrap_tab (struct line_wrap_state *s, struct line_char *c)
{
line_wrap_flush (s, true);
if (s->line_used >= s->line_max)
line_wrap_nl (s);
// Compute the number of characters needed to get to the next tab stop
int tab_width = ((s->line_used + 8) & ~7) - s->line_used;
// On overflow just fill the rest of the line with spaces
if (s->line_used + tab_width > s->line_max)
tab_width = s->line_max - s->line_used;
s->line_used += tab_width;
while (tab_width--)
{
struct line_char *space = line_char_new (L' ');
space->attrs = c->attrs;
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space);
}
}
static void
line_wrap_push_char (struct line_wrap_state *s, struct line_char *c)
{
// Note that when processing whitespace here, any non-WS chunk has already
// been flushed, and thus it matters little if we flush with force split
if (wcschr (L"\r\f\v", c->wide))
/* Skip problematic characters */;
else if (c->wide == L'\n')
line_wrap_nl (s);
else if (c->wide == L'\t')
line_wrap_tab (s, c);
else
goto use_as_is;
free (c);
return;
use_as_is:
if (s->overflow.start
|| s->line_used + s->chunk.used + c->width > s->line_max)
{
if (s->overflow.used + c->width > s->line_max)
{
#ifdef WRAP_UNNECESSARILY
// If the overflow overflows, restart on a new line
line_wrap_nl (s);
#else
// See line_wrap_flush(), we would end up on a new line anyway
line_wrap_flush (s, true);
s->line_used = 0;
#endif
}
else
line_wrap_mark_push (&s->overflow, c);
}
line_wrap_mark_push (&s->chunk, c);
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c);
}
/// Basic word wrapping that respects wcwidth(3) and expands tabs.
/// Besides making text easier to read, it also fixes the problem with
/// formatting spilling over the entire new line on line wrap.
static struct line_char *
line_wrap (struct line_char *line, int max_width)
{
struct line_wrap_state s = { .line_max = max_width };
bool last_was_word_char = false;
LIST_FOR_EACH (struct line_char, c, line)
{
// Act on the right boundary of (\s*\S+) chunks
bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide);
if (last_was_word_char && !this_is_word_char)
line_wrap_flush (&s, false);
last_was_word_char = this_is_word_char;
LIST_UNLINK (line, c);
line_wrap_push_char (&s, c);
}
// Make sure to process the last word and return the modified list
line_wrap_flush (&s, false);
return s.result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct exploder
{
struct app_context *ctx; ///< Application context
struct line_char *result; ///< Result
struct line_char *result_tail; ///< Tail of result
struct line_char_attrs attrs; ///< Current attributes
};
static bool
explode_formatter_attr (struct exploder *self, struct formatter_item *item)
{
switch (item->type)
{
case FORMATTER_ITEM_ATTR:
self->attrs.named = item->attribute;
self->attrs.text = 0;
self->attrs.fg = -1;
self->attrs.bg = -1;
return true;
case FORMATTER_ITEM_SIMPLE:
self->attrs.named = -1;
self->attrs.text ^= item->attribute;
return true;
case FORMATTER_ITEM_FG_COLOR:
self->attrs.named = -1;
self->attrs.fg = item->color;
return true;
case FORMATTER_ITEM_BG_COLOR:
self->attrs.named = -1;
self->attrs.bg = item->color;
return true;
default:
return false;
}
}
static void
explode_text (struct exploder *self, const char *text)
{
// Throw away any potentially harmful control characters first
struct str filtered = str_make ();
for (const char *p = text; *p; p++)
if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p))
str_append_c (&filtered, *p);
size_t term_len = 0, processed = 0, len;
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;
while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
{
hard_assert (len != (size_t) -2 && len != (size_t) -1);
hard_assert ((processed += len) <= term_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->theme, 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 (&state, attrs.text, attrs.fg, attrs.bg);
else
attr_printer_apply_named (&state, attrs.named);
}
formatter_putc (c, stream);
}
formatter_putc (NULL, stream);
attr_printer_reset (&state);
}
// --- Relay output ------------------------------------------------------------
static void
client_kill (struct client *c)
{
struct app_context *ctx = c->ctx;
poller_fd_reset (&c->socket_event);
xclose (c->socket_fd);
c->socket_fd = -1;
LIST_UNLINK (ctx->clients, c);
client_destroy (c);
}
static void
client_update_poller (struct client *c, const struct pollfd *pfd)
{
// In case of closing without any data in the write buffer,
// we don't actually need to be able to write to the socket,
// but the condition should be quick to satisfy.
int new_events = POLLIN;
if (c->write_buffer.len || c->closing)
new_events |= POLLOUT;
hard_assert (new_events != 0);
if (!pfd || pfd->events != new_events)
poller_fd_set (&c->socket_event, new_events);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
relay_send (struct client *c)
{
struct relay_event_message *m = &c->ctx->relay_message;
m->event_seq = c->event_seq++;
if (!c->initialized || c->closing || c->socket_fd == -1)
return;
// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
size_t frame_len_pos = c->write_buffer.len, frame_len = 0;
str_pack_u32 (&c->write_buffer, 0);
if (!relay_event_message_serialize (m, &c->write_buffer)
|| (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
{
print_error ("serialization failed, killing client");
// We can't kill the client immediately,
// because more relay_send() calls may follow.
c->write_buffer.len = frame_len_pos;
c->closing = true;
}
else
{
uint32_t len = htonl (frame_len);
memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
}
client_update_poller (c, NULL);
}
static void
relay_broadcast_except (struct app_context *ctx, struct client *exception)
{
LIST_FOR_EACH (struct client, c, ctx->clients)
if (c != exception)
relay_send (c);
}
#define relay_broadcast(ctx) relay_broadcast_except ((ctx), NULL)
static struct relay_event_message *
relay_prepare (struct app_context *ctx)
{
struct relay_event_message *m = &ctx->relay_message;
relay_event_message_free (m);
memset (m, 0, sizeof *m);
return m;
}
static void
relay_prepare_ping (struct app_context *ctx)
{
relay_prepare (ctx)->data.event = RELAY_EVENT_PING;
}
static union relay_item_data *
relay_translate_formatter (struct app_context *ctx, union relay_item_data *p,
const struct formatter_item *i)
{
// XXX: See attr_printer_decode_color(), this is a footgun.
int16_t c16 = i->color;
int16_t c256 = i->color >> 16;
unsigned attrs = i->attribute;
switch (i->type)
{
case FORMATTER_ITEM_TEXT:
p->text.text = str_from_cstr (i->text);
(p++)->kind = RELAY_ITEM_TEXT;
break;
case FORMATTER_ITEM_FG_COLOR:
p->fg_color.color = c256 <= 0 ? c16 : c256;
(p++)->kind = RELAY_ITEM_FG_COLOR;
break;
case FORMATTER_ITEM_BG_COLOR:
p->bg_color.color = c256 <= 0 ? c16 : c256;
(p++)->kind = RELAY_ITEM_BG_COLOR;
break;
case FORMATTER_ITEM_ATTR:
(p++)->kind = RELAY_ITEM_RESET;
if ((c256 = ctx->theme[i->attribute].fg) >= 0)
{
p->fg_color.color = c256;
(p++)->kind = RELAY_ITEM_FG_COLOR;
}
if ((c256 = ctx->theme[i->attribute].bg) >= 0)
{
p->bg_color.color = c256;
(p++)->kind = RELAY_ITEM_BG_COLOR;
}
attrs = ctx->theme[i->attribute].attrs;
// Fall-through
case FORMATTER_ITEM_SIMPLE:
if (attrs & TEXT_BOLD)
(p++)->kind = RELAY_ITEM_FLIP_BOLD;
if (attrs & TEXT_ITALIC)
(p++)->kind = RELAY_ITEM_FLIP_ITALIC;
if (attrs & TEXT_UNDERLINE)
(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE;
if (attrs & TEXT_INVERSE)
(p++)->kind = RELAY_ITEM_FLIP_INVERSE;
if (attrs & TEXT_CROSSED_OUT)
(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT;
if (attrs & TEXT_MONOSPACE)
(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE;
break;
default:
break;
}
return p;
}
static union relay_item_data *
relay_items (struct app_context *ctx, const struct formatter_item *items,
uint32_t *len)
{
size_t items_len = 0;
for (size_t i = 0; items[i].type; i++)
items_len++;
// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a;
for (const struct formatter_item *i = items; items_len--; i++)
p = relay_translate_formatter (ctx, p, i);
*len = p - a;
return a;
}
static void
relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
struct buffer_line *line, bool leak_to_active)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_line *e = &m->data.buffer_line;
e->event = RELAY_EVENT_BUFFER_LINE;
e->buffer_name = str_from_cstr (buffer->name);
e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT);
e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT);
e->rendition = 1 + line->r;
e->when = line->when * 1000;
e->leak_to_active = leak_to_active;
e->items = relay_items (ctx, line->items, &e->items_len);
}
static void
relay_prepare_channel_buffer_update (struct app_context *ctx,
struct buffer *buffer, struct relay_buffer_context_channel *e)
{
struct channel *channel = buffer->channel;
struct formatter f = formatter_make (ctx, buffer->server);
if (channel->topic)
formatter_add (&f, "#m", channel->topic);
FORMATTER_ADD_ITEM (&f, END);
e->topic = relay_items (ctx, f.items, &e->topic_len);
formatter_free (&f);
// As in make_prompt(), conceal the last known channel modes.
// XXX: This should use irc_channel_is_joined().
if (!channel->users_len)
return;
struct str modes = str_make ();
str_append_str (&modes, &channel->no_param_modes);
struct str params = str_make ();
struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
const char *param;
while ((param = str_map_iter_next (&iter)))
{
str_append_c (&modes, iter.link->key[0]);
str_append_c (&params, ' ');
str_append (&params, param);
}
str_append_str (&modes, &params);
str_free (&params);
char *modes_utf8 = irc_to_utf8 (modes.str);
str_free (&modes);
e->modes = str_from_cstr (modes_utf8);
free (modes_utf8);
}
static void
relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_update *e = &m->data.buffer_update;
e->event = RELAY_EVENT_BUFFER_UPDATE;
e->buffer_name = str_from_cstr (buffer->name);
e->hide_unimportant = buffer->hide_unimportant;
struct str *server_name = NULL;
switch (buffer->type)
{
case BUFFER_GLOBAL:
e->context.kind = RELAY_BUFFER_KIND_GLOBAL;
break;
case BUFFER_SERVER:
e->context.kind = RELAY_BUFFER_KIND_SERVER;
server_name = &e->context.server.server_name;
break;
case BUFFER_CHANNEL:
e->context.kind = RELAY_BUFFER_KIND_CHANNEL;
server_name = &e->context.channel.server_name;
relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel);
break;
case BUFFER_PM:
e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE;
server_name = &e->context.private_message.server_name;
break;
}
if (server_name)
*server_name = str_from_cstr (buffer->server->name);
}
static void
relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
e->event = RELAY_EVENT_BUFFER_STATS;
e->buffer_name = str_from_cstr (buffer->name);
e->new_messages = MIN (UINT32_MAX,
buffer->new_messages_count - buffer->new_unimportant_count);
e->new_unimportant_messages = MIN (UINT32_MAX,
buffer->new_unimportant_count);
e->highlighted = buffer->highlighted;
}
static void
relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer,
const char *new_name)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_rename *e = &m->data.buffer_rename;
e->event = RELAY_EVENT_BUFFER_RENAME;
e->buffer_name = str_from_cstr (buffer->name);
e->new = str_from_cstr (new_name);
}
static void
relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_remove *e = &m->data.buffer_remove;
e->event = RELAY_EVENT_BUFFER_REMOVE;
e->buffer_name = str_from_cstr (buffer->name);
}
static void
relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_activate *e = &m->data.buffer_activate;
e->event = RELAY_EVENT_BUFFER_ACTIVATE;
e->buffer_name = str_from_cstr (buffer->name);
}
static void
relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer,
const char *input)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_input *e = &m->data.buffer_input;
e->event = RELAY_EVENT_BUFFER_INPUT;
e->buffer_name = str_from_cstr (buffer->name);
e->text = str_from_cstr (input);
}
static void
relay_prepare_buffer_clear (struct app_context *ctx,
struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_clear *e = &m->data.buffer_clear;
e->event = RELAY_EVENT_BUFFER_CLEAR;
e->buffer_name = str_from_cstr (buffer->name);
}
enum relay_server_state
relay_server_state_for_server (struct server *s)
{
switch (s->state)
{
case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED;
case IRC_CONNECTING: return RELAY_SERVER_STATE_CONNECTING;
case IRC_CONNECTED: return RELAY_SERVER_STATE_CONNECTED;
case IRC_REGISTERED: return RELAY_SERVER_STATE_REGISTERED;
case IRC_CLOSING:
case IRC_HALF_CLOSED: return RELAY_SERVER_STATE_DISCONNECTING;
}
return 0;
}
static void
relay_prepare_server_update (struct app_context *ctx, struct server *s)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_server_update *e = &m->data.server_update;
e->event = RELAY_EVENT_SERVER_UPDATE;
e->server_name = str_from_cstr (s->name);
e->data.state = relay_server_state_for_server (s);
if (s->state == IRC_REGISTERED)
{
char *user_utf8 = irc_to_utf8 (s->irc_user->nickname);
e->data.registered.user = str_from_cstr (user_utf8);
free (user_utf8);
char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str);
e->data.registered.user_modes = str_from_cstr (user_modes_utf8);
free (user_modes_utf8);
}
}
static void
relay_prepare_server_rename (struct app_context *ctx, struct server *s,
const char *new_name)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_server_rename *e = &m->data.server_rename;
e->event = RELAY_EVENT_SERVER_RENAME;
e->server_name = str_from_cstr (s->name);
e->new = str_from_cstr (new_name);
}
static void
relay_prepare_server_remove (struct app_context *ctx, struct server *s)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_server_remove *e = &m->data.server_remove;
e->event = RELAY_EVENT_SERVER_REMOVE;
e->server_name = str_from_cstr (s->name);
}
static void
relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_error *e = &m->data.error;
e->event = RELAY_EVENT_ERROR;
e->command_seq = seq;
e->error = str_from_cstr (message);
}
static struct relay_event_data_response *
relay_prepare_response (struct app_context *ctx, uint32_t seq)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_response *e = &m->data.response;
e->event = RELAY_EVENT_RESPONSE;
e->command_seq = seq;
return e;
}
// --- 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, "general.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)
{
switch (line->r)
{
case BUFFER_LINE_BARE: break;
case BUFFER_LINE_INDENT: formatter_add (f, " "); break;
case BUFFER_LINE_STATUS: formatter_add (f, " - "); break;
case BUFFER_LINE_ERROR: formatter_add (f, "#a=!=#r ", ATTR_ERROR); break;
case BUFFER_LINE_JOIN: formatter_add (f, "#a-->#r ", ATTR_JOIN); break;
case BUFFER_LINE_PART: formatter_add (f, "#a<--#r ", ATTR_PART); break;
case BUFFER_LINE_ACTION: formatter_add (f, " #a*#r ", ATTR_ACTION); break;
}
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,
unsigned flags, enum buffer_line_rendition r, struct formatter *f)
{
if (!buffer)
buffer = ctx->global_buffer;
struct buffer_line *line = buffer_line_new (f);
line->flags = flags;
line->r = r;
// 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 = !ctx->isolate_buffers;
bool leak_to_active = buffer != ctx->current_buffer && can_leak;
relay_prepare_buffer_line (ctx, buffer, line, leak_to_active);
relay_broadcast (ctx);
bool visible = (buffer == ctx->current_buffer || leak_to_active)
&& ctx->terminal_suspended <= 0;
// Advance the unread marker but don't create a new one
if (!visible || buffer->new_messages_count)
{
buffer->new_messages_count++;
if ((flags & BUFFER_LINE_UNIMPORTANT) || leak_to_active)
buffer->new_unimportant_count++;
}
if (visible)
buffer_line_display (ctx, buffer, line, leak_to_active);
else
{
buffer->highlighted |= important;
refresh_prompt (ctx);
}
}
static void
log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
unsigned flags, enum buffer_line_rendition r, 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, r, &f);
va_end (ap);
}
#define log_global(ctx, flags, r, ...) \
log_full ((ctx), NULL, (ctx)->global_buffer, (flags), (r), __VA_ARGS__)
#define log_server(s, buffer, flags, r, ...) \
log_full ((s)->ctx, (s), (buffer), (flags), (r), __VA_ARGS__)
#define log_global_status(ctx, ...) \
log_global ((ctx), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_global_error(ctx, ...) \
log_global ((ctx), 0, BUFFER_LINE_ERROR, __VA_ARGS__)
#define log_global_indent(ctx, ...) \
log_global ((ctx), 0, BUFFER_LINE_INDENT, __VA_ARGS__)
#define log_server_status(s, buffer, ...) \
log_server ((s), (buffer), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_server_error(s, buffer, ...) \
log_server ((s), (buffer), 0, BUFFER_LINE_ERROR, __VA_ARGS__)
#define log_global_debug(ctx, ...) \
BLOCK_START \
if (g_debug_mode) \
log_global ((ctx), 0, 0, "(*) " __VA_ARGS__); \
BLOCK_END
#define log_server_debug(s, ...) \
BLOCK_START \
if (g_debug_mode) \
log_server ((s), (s)->buffer, 0, 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_UNIMPORTANT, BUFFER_LINE_STATUS, \
"You are now known as #n", (new_))
#define log_nick(s, buffer, old, new_) \
log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
"#n is now known as #n", (old), (new_))
#define log_chghost_self(s, buffer, new_) \
log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
"You are now #N", (new_))
#define log_chghost(s, buffer, old, new_) \
log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS, \
"#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, 0, "<#s#n> #m", (prefixes), (who), (text))
#define log_outcoming_action(s, buffer, who, text) \
log_server ((s), (buffer), 0, BUFFER_LINE_ACTION, "#n #m", (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 ((s), (s)->buffer, 0, BUFFER_LINE_STATUS, \
"MSG(#n): #m", (target), (text))
#define log_outcoming_orphan_action(s, target, text) \
log_server ((s), (s)->buffer, 0, BUFFER_LINE_ACTION, \
"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, '/');
// FIXME: This mixes up character encodings.
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 `#l': #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));
relay_prepare_buffer_update (ctx, buffer);
relay_broadcast (ctx);
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);
relay_prepare_buffer_remove (ctx, buffer);
relay_broadcast (ctx);
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, "general.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)
{
// Buffers can be activated, or their lines modified, as automatic actions.
if (ctx->terminal_suspended)
return;
// 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;
relay_prepare_buffer_activate (ctx, buffer);
relay_broadcast (ctx);
// 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, 0, 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;
// 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;
// And since there is no log_*() call, send them to relays manually
buffer->highlighted |= merged->highlighted;
LIST_FOR_EACH (struct buffer_line, line, start)
{
if (buffer->new_messages_count)
{
buffer->new_messages_count++;
if (line->flags & BUFFER_LINE_UNIMPORTANT)
buffer->new_unimportant_count++;
}
relay_prepare_buffer_line (ctx, buffer, line, false);
relay_broadcast (ctx);
}
log_full (ctx, NULL, buffer, BUFFER_LINE_SKIP_FILE, BUFFER_LINE_STATUS,
"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);
relay_prepare_buffer_rename (ctx, buffer, new_name);
relay_broadcast (ctx);
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 app_context *ctx, struct buffer *buffer)
{
relay_prepare_buffer_clear (ctx, buffer);
relay_broadcast (ctx);
LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
buffer_line_destroy (iter);
buffer->lines = buffer->lines_tail = NULL;
buffer->lines_count = 0;
}
static void
buffer_toggle_unimportant (struct app_context *ctx, struct buffer *buffer)
{
buffer->hide_unimportant ^= true;
relay_prepare_buffer_update (ctx, buffer);
relay_broadcast (ctx);
if (buffer == ctx->current_buffer)
buffer_print_backlog (ctx, buffer);
}
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);
char *target_utf8 = irc_to_utf8 (target);
char *name = xstrdup_printf ("%s.%s", s->name, target_utf8);
free (target_utf8);
struct buffer *conflict = buffer_by_name (s->ctx, name);
if (!conflict)
return name;
hard_assert (conflict->server == s);
// Fix up any conflicts. Note that while parentheses aren't allowed
// in IRC nicknames, they may occur in channel names.
int i = 0;
char *unique = xstrdup_printf ("%s(%d)", name, ++i);
while (buffer_by_name (s->ctx, unique))
cstr_set (&unique, xstrdup_printf ("%s(%d)", name, ++i));
free (name);
return unique;
}
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 void
irc_channel_broadcast_buffer_update (const struct channel *channel)
{
struct server *s = channel->s;
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
if (buffer)
{
relay_prepare_buffer_update (s->ctx, buffer);
relay_broadcast (s->ctx);
}
}
static void
irc_channel_set_topic (struct channel *channel, const char *topic)
{
cstr_set (&channel->topic, xstrdup (topic));
irc_channel_broadcast_buffer_update (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);
// Send empty channel modes.
irc_channel_broadcast_buffer_update (channel);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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, "general.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, "general.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_set_state (struct server *s, enum server_state state)
{
s->state = state;
relay_prepare_server_update (s->ctx, s);
relay_broadcast (s->ctx);
refresh_prompt (s->ctx);
}
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;
irc_set_state (s, 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
irc_set_state (s, 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
irc_set_state (s, 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;
irc_set_state (s, 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_modes);
cstr_set (&s->irc_user_host, NULL);
strv_reset (&s->outstanding_joins);
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_UNIMPORTANT, BUFFER_LINE_STATUS,
"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);
}
}
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");
irc_set_state (s, 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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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++)
{
const char *port = "6667",
*host = tokenize_host_port (addresses->vector[i], &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++)
{
const char *port = "6667",
*host = tokenize_host_port (addresses->vector[i], &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)
irc_set_state (s, 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);
const 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_modes.len)
str_append_printf (output, "(%s)", s->irc_user_modes.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
&& irc_channel_is_joined (buffer->channel))
{
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);
// XXX: to be completely correct, we should use tputs, but we cannot
if (have_attributes)
{
char buf[16384] = "";
FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb");
struct attr_printer state = { ctx->theme, memfp, false };
fputc (INPUT_START_IGNORE, memfp);
attr_printer_apply_named (&state, ATTR_PROMPT);
fputc (INPUT_END_IGNORE, memfp);
fputs (localized, memfp);
free (localized);
fputc (INPUT_START_IGNORE, memfp);
attr_printer_reset (&state);
fputc (INPUT_END_IGNORE, memfp);
fputs (attributed_suffix, memfp);
fclose (memfp);
input_maybe_set_prompt (ctx->input, xstrdup (buf));
}
else
input_maybe_set_prompt (ctx->input, localized);
}
// --- Helpers -----------------------------------------------------------------
static struct buffer *
irc_get_buffer_for_message (struct server *s,
const struct irc_message *msg, const char *target)
{
// TODO: display such messages differently
target = irc_skip_statusmsg (s, target);
struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
if (irc_is_channel (s, target))
{
struct channel *channel = str_map_find (&s->irc_channels, target);
hard_assert (channel || !buffer);
// This is weird
if (!channel)
return NULL;
}
else if (!buffer)
{
// Outgoing messages needn't have a prefix, no buffer associated
if (!msg->prefix)
return NULL;
// Don't make user buffers for servers (they can send NOTICEs)
if (!irc_find_userhost (msg->prefix))
return s->buffer;
char *nickname = irc_cut_nickname (msg->prefix);
if (irc_is_this_us (s, target))
buffer = irc_get_or_make_user_buffer (s, nickname);
free (nickname);
// With the IRCv3.2 echo-message capability, we can receive messages
// as they are delivered to the target; in that case we return NULL
// and the caller should check the origin
}
return buffer;
}
static bool
irc_is_highlight (struct server *s, const char *message)
{
// This may be called by notices before even successfully registering
if (!s->irc_user)
return false;
// Strip formatting from the message so that it doesn't interfere
// with nickname detection (colour sequences in particular)
struct formatter f = formatter_make (s->ctx, NULL);
formatter_parse_message (&f, message);
struct str stripped = str_make ();
for (size_t i = 0; i < f.items_len; i++)
{
if (f.items[i].type == FORMATTER_ITEM_TEXT)
str_append (&stripped, f.items[i].text);
}
formatter_free (&f);
// Well, this is rather crude but it should make most users happy.
// We could do this in proper Unicode but that's two more conversions per
// message when both the nickname and the message are likely valid UTF-8.
char *copy = str_steal (&stripped);
cstr_transform (copy, s->irc_tolower);
char *nick = xstrdup (s->irc_user->nickname);
cstr_transform (nick, s->irc_tolower);
// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
const char *delimiters = "\t\n\v\f\r !\"#$%&'()*+,./:;<=>?@~";
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);
if (p.changes == p.usermode_changes)
return true;
irc_channel_broadcast_buffer_update (channel);
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
mode_processor_apply_user (struct mode_processor *self)
{
mode_processor_toggle (self, &self->s->irc_user_modes);
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);
relay_prepare_server_update (s->ctx, s);
relay_broadcast (s->ctx);
}
// --- 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_join (struct server *s, const struct irc_message *msg)
{
if (msg->params.len < 1)
return;
if (strcmp (msg->params.vector[0], "0"))
cstr_split (msg->params.vector[0], ",", true, &s->outstanding_joins);
}
static void
irc_handle_sent_notice_text (struct server *s,
const struct irc_message *msg, struct str *text)
{
const char *target = msg->params.vector[0];
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
if (buffer && soft_assert (s->irc_user))
log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str);
else
log_outcoming_orphan_notice (s, target, text->str);
}
static void
irc_handle_sent_notice (struct server *s, const struct irc_message *msg)
{
if (msg->params.len < 2 || s->cap_echo_message)
return;
// This ignores empty messages which we should not normally send
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
{
if (iter->is_extended)
log_ctcp_reply (s, msg->params.vector[0],
xstrdup_printf ("%s %s", iter->tag.str, iter->text.str));
else
irc_handle_sent_notice_text (s, msg, &iter->text);
}
ctcp_destroy (chunks);
}
static void
irc_handle_sent_privmsg_text (struct server *s,
const struct irc_message *msg, struct str *text, bool is_action)
{
const char *target = msg->params.vector[0];
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
if (buffer && soft_assert (s->irc_user))
{
char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target);
if (is_action)
log_outcoming_action (s, buffer, s->irc_user->nickname, text->str);
else
log_outcoming_privmsg (s, buffer,
prefixes, s->irc_user->nickname, text->str);
free (prefixes);
}
else if (is_action)
log_outcoming_orphan_action (s, target, text->str);
else
log_outcoming_orphan_privmsg (s, target, text->str);
}
static void
irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg)
{
if (msg->params.len < 2 || s->cap_echo_message)
return;
// This ignores empty messages which we should not normally send
// and the server is likely going to reject with an error reply anyway
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
{
if (!iter->is_extended)
irc_handle_sent_privmsg_text (s, msg, &iter->text, false);
else if (!strcmp (iter->tag.str, "ACTION"))
irc_handle_sent_privmsg_text (s, msg, &iter->text, true);
else
log_ctcp_query (s, msg->params.vector[0], iter->tag.str);
}
ctcp_destroy (chunks);
}
static struct irc_handler
{
const char *name;
void (*handler) (struct server *s, const struct irc_message *msg);
}
g_irc_sent_handlers[] =
{
// This list needs to stay sorted
{ "CAP", irc_handle_sent_cap },
{ "JOIN", irc_handle_sent_join },
{ "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 bool
irc_satisfy_join (struct server *s, const char *target)
{
// This queue could use some garbage collection,
// but it's unlikely to pose problems.
for (size_t i = 0; i < s->outstanding_joins.len; i++)
if (!irc_server_strcmp (s, target, s->outstanding_joins.vector[i]))
{
strv_remove (&s->outstanding_joins, i);
return true;
}
return false;
}
static void
irc_handle_join (struct server *s, const struct irc_message *msg)
{
if (!msg->prefix || msg->params.len < 1)
return;
// TODO: RFC 2812 doesn't guarantee that the argument isn't a target list.
const char *channel_name = msg->params.vector[0];
if (!irc_is_channel (s, channel_name))
return;
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
hard_assert (channel || !buffer);
// We've joined a new channel
if (!channel)
{
// This is weird, ignoring
if (!irc_is_this_us (s, msg->prefix))
return;
buffer = buffer_new (s->ctx->input,
BUFFER_CHANNEL, irc_make_buffer_name (s, channel_name));
buffer->server = s;
buffer->channel = channel =
irc_make_channel (s, xstrdup (channel_name));
str_map_set (&s->irc_buffer_map, channel->name, buffer);
buffer_add (s->ctx, buffer);
char *input = CALL_ (s->ctx->input, get_line, NULL);
if (irc_satisfy_join (s, channel_name) && !*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, BUFFER_LINE_JOIN,
"#N #a#s#r #S", 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, "#N #a#s#r #n",
msg->prefix, ATTR_PART, "has kicked", target);
if (message)
formatter_add (&f, " (#m)", message);
log_formatter (s->ctx, buffer, 0, BUFFER_LINE_PART, &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, flags, BUFFER_LINE_STATUS,
"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 everything 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 = irc_make_buffer_name (s, new_nickname);
buffer_rename (s->ctx, pm_buffer, x);
free (x);
}
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));
// Finally broadcast the event to relay clients and secondary buffers
if (irc_is_this_us (s, new_nickname))
{
relay_prepare_server_update (s->ctx, s);
relay_broadcast (s->ctx);
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);
}
}
}
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_HIGHLIGHT, BUFFER_LINE_STATUS,
"#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, "#N #a#s#r #S",
msg->prefix, ATTR_PART, "has left", channel_name);
if (message)
formatter_add (&f, " (#m)", message);
log_formatter (s->ctx, buffer,
BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &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, 0, 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, BUFFER_LINE_ACTION,
"#a#S#r #m", ATTR_HIGHLIGHT, nickname, text->str);
else
log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, 0,
"#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, "#N #a#s#r", prefix, ATTR_PART, "has quit");
if (reason)
formatter_add (&f, " (#m)", reason);
log_formatter (s->ctx, buffer,
BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &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)
irc_channel_set_topic (channel, topic);
if (buffer)
{
log_server (s, buffer, 0, BUFFER_LINE_STATUS, "#n #s \"#m\"",
msg->prefix, "has changed the topic to", topic);
}
}
static void
irc_handle_wallops (struct server *s, const struct irc_message *msg)
{
if (!msg->prefix || msg->params.len < 1)
return;
const char *message = msg->params.vector[0];
log_server (s, s->buffer, 0, 0, "<#n> #m", msg->prefix, message);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct irc_handler g_irc_handlers[] =
{
// This list needs to stay sorted
{ "AUTHENTICATE", irc_handle_authenticate },
{ "AWAY", irc_handle_away },
{ "CAP", irc_handle_cap },
{ "CHGHOST", irc_handle_chghost },
{ "ERROR", irc_handle_error },
{ "INVITE", irc_handle_invite },
{ "JOIN", irc_handle_join },
{ "KICK", irc_handle_kick },
{ "KILL", irc_handle_kill },
{ "MODE", irc_handle_mode },
{ "NICK", irc_handle_nick },
{ "NOTICE", irc_handle_notice },
{ "PART", irc_handle_part },
{ "PING", irc_handle_ping },
{ "PRIVMSG", irc_handle_privmsg },
{ "QUIT", irc_handle_quit },
{ "TAGMSG", irc_handle_tagmsg },
{ "TOPIC", irc_handle_topic },
{ "WALLOPS", irc_handle_wallops },
};
static bool
irc_try_parse_word_for_userhost (struct server *s, const char *word)
{
regex_t re;
int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
if (!soft_assert (!err))
return false;
regmatch_t matches[2];
bool result = false;
if (!regexec (&re, word, 2, matches, 0))
{
cstr_set (&s->irc_user_host, xstrndup (word + matches[1].rm_so,
matches[1].rm_eo - matches[1].rm_so));
result = true;
}
regfree (&re);
return result;
}
static void
irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
{
struct strv v = strv_make ();
cstr_split (m, " ", true, &v);
for (size_t i = 0; i < v.len; i++)
if (irc_try_parse_word_for_userhost (s, v.vector[i]))
break;
strv_free (&v);
}
static void process_input
(struct app_context *, struct buffer *, const char *);
static bool process_input_line
(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_modes);
cstr_set (&s->irc_user_host, NULL);
irc_set_state (s, IRC_REGISTERED);
// 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);
(void) process_input (s->ctx, s->buffer, command);
}
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_modes);
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)
irc_channel_set_topic (channel, 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_adjust_motd (char **motd)
{
// Heuristic, force MOTD to be monospace in graphical frontends.
if (!strchr (*motd, '\x11'))
{
struct str s = str_make ();
str_append_c (&s, '\x11');
for (const char *p = *motd; *p; p++)
{
str_append_c (&s, *p);
if (*p == '\x0f')
str_append_c (&s, '\x11');
}
cstr_set (motd, str_steal (&s));
}
}
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 = 0;
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_MOTDSTART:
case IRC_RPL_MOTD:
if (copy.len)
irc_adjust_motd (&copy.vector[0]);
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 < 2)
break;
struct buffer *x;
if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
buffer = x;
// A JOIN request should be split at commas,
// then for each element produce either a JOIN response, or a numeric.
(void) irc_satisfy_join (s, msg->params.vector[1]);
}
if (buffer)
{
// Join the parameter vector back and send it to the server buffer
log_server (s, buffer, flags, BUFFER_LINE_STATUS,
"#&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 a rather basic algorithm; something like ICU with proper
// locale specification would be needed to make it work better.
static size_t
wrap_text_for_single_line (const char *text, struct irc_char_attrs *attrs,
size_t text_len, size_t target_len, struct str *output)
{
size_t eaten = 0;
// First try going word by word
const char *word_start;
const char *word_end = text + strcspn (text, " ");
size_t word_len = word_end - text;
while (target_len && word_len <= target_len)
{
if (word_len)
{
str_append_data (output, text, word_len);
text += word_len;
eaten += word_len;
target_len -= word_len;
}
// Find the next word's end
word_start = text + strspn (text, " ");
word_end = word_start + strcspn (word_start, " ");
word_len = word_end - text;
}
if (eaten)
// Discard whitespace between words if split
return eaten + (word_start - text);
// And if that doesn't help, cut the longest valid block of characters
for (size_t i = 1; i <= text_len && i <= target_len; i++)
if (i == text_len || attrs[i].starts_at_boundary)
eaten = i;
str_append_data (output, text, eaten);
return eaten;
}
// In practice, this should never fail at all, although it's not guaranteed
static bool
wrap_message (const char *message,
int line_max, struct strv *output, struct error **e)
{
size_t message_left = strlen (message), i = 0;
struct irc_char_attrs *attrs = irc_analyze_text (message, message_left);
struct str m = str_make ();
if (line_max <= 0)
goto error;
while (m.len + message_left > (size_t) line_max)
{
size_t eaten = wrap_text_for_single_line
(message + i, attrs + i, message_left, line_max - m.len, &m);
if (!eaten)
goto error;
strv_append_owned (output, str_steal (&m));
m = str_make ();
i += eaten;
if (!(message_left -= eaten))
break;
irc_serialize_char_attrs (attrs + i, &m);
if (m.len >= (size_t) line_max)
{
print_debug ("formatting continuation too long");
str_reset (&m);
}
}
if (message_left)
strv_append_owned (output,
xstrdup_printf ("%s%s", m.str, message + i));
free (attrs);
str_free (&m);
return true;
error:
free (attrs);
str_free (&m);
return error_set (e,
"Message splitting was unsuccessful as there was "
"too little room for UTF-8 characters");
}
/// Automatically splits messages that arrive at other clients with our prefix
/// so that they don't arrive cut off by the server
static bool
irc_autosplit_message (struct server *s, const char *message,
int fixed_part, struct strv *output, struct error **e)
{
// :<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;
// Multiline messages can be triggered through hooks and plugins.
struct strv lines = strv_make ();
cstr_split (message, "\r\n", false, &lines);
bool success = true;
for (size_t i = 0; i < lines.len; i++)
{
// We don't always have the full info for message splitting.
if (!space_in_one_message)
strv_append (output, lines.vector[i]);
else if (!(success =
wrap_message (lines.vector[i], space_in_one_message, output, e)))
break;
}
strv_free (&lines);
return success;
}
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);
// "COMMAND target * :prefix*suffix"
int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
+ strlen (prefix) + strlen (suffix);
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
const 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;
relay_prepare_server_update (ctx, s);
relay_broadcast (ctx);
// 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);
relay_prepare_server_remove (ctx, s);
relay_broadcast (ctx);
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));
relay_prepare_server_rename (ctx, s, new_name);
relay_broadcast (ctx);
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)))
{
char *x = NULL;
switch (buffer->type)
{
case BUFFER_PM:
x = irc_make_buffer_name (s, buffer->user->nickname);
break;
case BUFFER_CHANNEL:
x = irc_make_buffer_name (s, 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,
0, 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);
(void) process_input_line (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 const char *
lua_server_state_to_string (enum server_state state)
{
switch (state)
{
case IRC_DISCONNECTED: return "disconnected";
case IRC_CONNECTING: return "connecting";
case IRC_CONNECTED: return "connected";
case IRC_REGISTERED: return "registered";
case IRC_CLOSING: return "closing";
case IRC_HALF_CLOSED: return "half-closed";
}
return "?";
}
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;
lua_pushstring (L, lua_server_state_to_string (server->state));
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_plugin_measure (lua_State *L)
{
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
const char *line = lua_plugin_check_utf8 (L, 1);
size_t term_len = 0, processed = 0, width = 0, len;
char *term = iconv_xstrdup (plugin->ctx->term_from_utf8,
(char *) line, strlen (line) + 1, &term_len);
mbstate_t ps;
memset (&ps, 0, sizeof ps);
wchar_t wch;
while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
{
hard_assert (len != (size_t) -2 && len != (size_t) -1);
hard_assert ((processed += len) <= term_len);
int wch_width = wcwidth (wch);
width += MAX (0, wch_width);
}
free (term);
lua_pushinteger (L, width);
return 1;
}
static int
lua_ctx_gc (lua_State *L)
{
return lua_weak_gc (L, &lua_ctx_info);
}
static luaL_Reg lua_plugin_library[] =
{
// These are pseudo-global functions:
{ "measure", lua_plugin_measure },
{ "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:
// Note that this only returns the height when used through an accessor.
{ "get_screen_size", lua_plugin_get_screen_size },
{ "__gc", lua_ctx_gc },
{ NULL, NULL },
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void *
lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
{
(void) ud;
(void) o_size;
if (n_size)
return realloc (ptr, n_size);
free (ptr);
return NULL;
}
static int
lua_plugin_panic (lua_State *L)
{
// XXX: we might be able to do something better
print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
lua_close (L);
exit (EXIT_FAILURE);
return 0;
}
static void
lua_plugin_push_ref (lua_State *L, struct lua_plugin *self, void *object,
struct ispect_field *field)
{
// We create a mapping on object type registration
hard_assert (lua_rawgetp (L, LUA_REGISTRYINDEX, field->fields));
struct lua_weak_info *info = lua_touserdata (L, -1);
lua_pop (L, 1);
if (!field->is_list)
{
lua_weak_push (L, self, object, info);
return;
}
// As a rule in this codebase, these fields are right at the top of structs
struct list_header { LIST_HEADER (void) };
int i = 1;
lua_newtable (L);
LIST_FOR_EACH (struct list_header, iter, object)
{
lua_weak_push (L, self, iter, info);
lua_rawseti (L, -2, i++);
}
}
static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
const char *key, void *p, struct ispect_field *field);
static void
lua_plugin_push_struct (lua_State *L, struct lua_plugin *self,
enum ispect_type type, void *value, struct ispect_field *field)
{
if (type == ISPECT_STR)
{
const struct str *s = value;
lua_pushlstring (L, s->str, s->len);
return;
}
if (type == ISPECT_STR_MAP)
{
struct str_map_iter iter = str_map_iter_make (value);
void *value;
lua_newtable (L);
while ((value = str_map_iter_next (&iter)))
lua_plugin_push_map_field (L, self, iter.link->key, value, field);
return;
}
hard_assert (!"unhandled introspection object type");
}
static void
lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
const char *key, void *p, struct ispect_field *field)
{
// That would mean maps in maps ad infinitum
hard_assert (field->subtype != ISPECT_STR_MAP);
intptr_t n = (intptr_t) p;
switch (field->subtype)
{
// Here the types are generally casted to a void pointer
case ISPECT_BOOL: lua_pushboolean (L, (bool ) n); break;
case ISPECT_INT: lua_pushinteger (L, (int ) n); break;
case ISPECT_UINT: lua_pushinteger (L, (unsigned ) n); break;
case ISPECT_SIZE: lua_pushinteger (L, (size_t ) n); break;
case ISPECT_STRING: lua_pushstring (L, p); break;
case ISPECT_REF: lua_plugin_push_ref (L, self, p, field); break;
default: lua_plugin_push_struct (L, self, field->subtype, p, field);
}
lua_setfield (L, -2, key);
}
static bool
lua_plugin_property_get_ispect (lua_State *L, const char *property_name)
{
struct lua_weak_info *info = lua_touserdata (L, lua_upvalueindex (1));
if (!info || !info->ispect)
return false;
struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
// TODO: I think we can do better than this, maybe binary search at least?
struct ispect_field *field;
for (field = info->ispect; field->name; field++)
if (!strcmp (property_name, field->name))
break;
if (!field->name)
return false;
struct lua_plugin *self = weak->plugin;
void *p = (uint8_t *) weak->object + field->offset;
switch (field->type)
{
// Here the types are really what's under the pointer
case ISPECT_BOOL: lua_pushboolean (L, *(bool *) p); break;
case ISPECT_INT: lua_pushinteger (L, *(int *) p); break;
case ISPECT_UINT: lua_pushinteger (L, *(unsigned *) p); break;
case ISPECT_SIZE: lua_pushinteger (L, *(size_t *) p); break;
case ISPECT_STRING: lua_pushstring (L, *(char **) p); break;
case ISPECT_REF: lua_plugin_push_ref (L, self, *(void **) p, field); break;
default: lua_plugin_push_struct (L, self, field->type, p, field);
}
return true;
}
static int
lua_plugin_property_get (lua_State *L)
{
luaL_checktype (L, 1, LUA_TUSERDATA);
const char *property_name = luaL_checkstring (L, 2);
// Either it's directly present in the metatable
if (luaL_getmetafield (L, 1, property_name))
return 1;
// Or we try to find and eventually call a getter method
char *getter_name = xstrdup_printf ("get_%s", property_name);
bool found = luaL_getmetafield (L, 1, getter_name);
free (getter_name);
if (found)
{
lua_pushvalue (L, 1);
lua_call (L, 1, 1);
return 1;
}
// Maybe we can find it via introspection
if (lua_plugin_property_get_ispect (L, property_name))
return 1;
// Or we look for a property set by the user (__gc cannot be overriden)
if (lua_getuservalue (L, 1) != LUA_TTABLE)
lua_pushnil (L);
else
lua_getfield (L, -1, property_name);
return 1;
}
static int
lua_plugin_property_set (lua_State *L)
{
luaL_checktype (L, 1, LUA_TUSERDATA);
const char *property_name = luaL_checkstring (L, 2);
luaL_checkany (L, 3);
// We use the associated value to store user-defined properties
int type = lua_getuservalue (L, 1);
if (type == LUA_TNIL)
{
lua_pop (L, 1);
lua_newtable (L);
lua_pushvalue (L, -1);
lua_setuservalue (L, 1);
}
else if (type != LUA_TTABLE)
return luaL_error (L, "associated value is not a table");
// Beware that we do not check for conflicts here;
// if Lua code writes a conflicting field, it is effectively ignored
lua_pushvalue (L, 3);
lua_setfield (L, -2, property_name);
return 0;
}
static void
lua_plugin_add_accessors (lua_State *L, struct lua_weak_info *info)
{
// Emulate properties for convenience
lua_pushlightuserdata (L, info);
lua_pushcclosure (L, lua_plugin_property_get, 1);
lua_setfield (L, -2, "__index");
lua_pushcfunction (L, lua_plugin_property_set);
lua_setfield (L, -2, "__newindex");
}
static void
lua_plugin_reg_meta (lua_State *L, const char *name, luaL_Reg *fns)
{
luaL_newmetatable (L, name);
luaL_setfuncs (L, fns, 0);
lua_plugin_add_accessors (L, NULL);
lua_pop (L, 1);
}
static void
lua_plugin_reg_weak (lua_State *L, struct lua_weak_info *info, luaL_Reg *fns)
{
// Create a mapping from the object type (info->ispect) back to metadata
// so that we can figure out what to create from ISPECT_REF fields
lua_pushlightuserdata (L, info);
lua_rawsetp (L, LUA_REGISTRYINDEX, info->ispect);
luaL_newmetatable (L, info->name);
luaL_setfuncs (L, fns, 0);
lua_plugin_add_accessors (L, info);
lua_pop (L, 1);
}
static struct plugin *
lua_plugin_load (struct app_context *ctx, const char *filename,
struct error **e)
{
lua_State *L = lua_newstate (lua_plugin_alloc, NULL);
if (!L)
{
error_set (e, "Lua initialization failed");
return NULL;
}
lua_atpanic (L, lua_plugin_panic);
luaL_openlibs (L);
struct lua_plugin *plugin = xcalloc (1, sizeof *plugin);
plugin->super.name = xstrdup (filename);
plugin->super.vtable = &lua_plugin_vtable;
plugin->ctx = ctx;
plugin->L = L;
luaL_checkversion (L);
// Register the xC library as a singleton with "plugin" as an upvalue
// (mostly historical, but rather convenient)
luaL_newmetatable (L, lua_ctx_info.name);
lua_pushlightuserdata (L, plugin);
luaL_setfuncs (L, lua_plugin_library, 1);
lua_plugin_add_accessors (L, &lua_ctx_info);
// Add the asynchronous library underneath
lua_newtable (L);
lua_pushlightuserdata (L, plugin);
luaL_setfuncs (L, lua_async_library, 1);
lua_setfield (L, -2, "async");
lua_pop (L, 1);
lua_weak_push (L, plugin, ctx, &lua_ctx_info);
lua_setglobal (L, lua_ctx_info.name);
// Create metatables for our objects
lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE, lua_hook_table);
lua_plugin_reg_weak (L, &lua_user_info, lua_user_table);
lua_plugin_reg_weak (L, &lua_channel_info, lua_channel_table);
lua_plugin_reg_weak (L, &lua_buffer_info, lua_buffer_table);
lua_plugin_reg_weak (L, &lua_server_info, lua_server_table);
lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_table);
lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
lua_plugin_reg_meta (L, XLUA_TASK_METATABLE, lua_task_table);
lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE, lua_wchannel_table);
struct error *error = NULL;
if (luaL_loadfile (L, filename))
error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
else if (!lua_plugin_call (plugin, 0, 0, &error))
{
error_set (e, "%s: %s", "Lua", error->message);
error_free (error);
}
else
return &plugin->super;
plugin_destroy (&plugin->super);
return NULL;
}
#endif // HAVE_LUA
// --- Plugins -----------------------------------------------------------------
typedef struct plugin *(*plugin_load_fn)
(struct app_context *ctx, const char *filename, struct error **e);
// We can potentially add support for other scripting languages if so desired,
// however this possibility is just a byproduct of abstraction
static plugin_load_fn g_plugin_loaders[] =
{
#ifdef HAVE_LUA
lua_plugin_load,
#endif // HAVE_LUA
};
static struct plugin *
plugin_load_from_filename (struct app_context *ctx, const char *filename,
struct error **e)
{
struct plugin *plugin = NULL;
struct error *error = NULL;
for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++)
if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error)
break;
if (error)
error_propagate (e, error);
else if (!plugin)
{
error_set (e, "no plugin handler for \"%s\"", filename);
return NULL;
}
return plugin;
}
static struct plugin *
plugin_find (struct app_context *ctx, const char *name)
{
LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
if (!strcmp (name, iter->name))
return iter;
return NULL;
}
static char *
plugin_resolve_relative_filename (const char *filename)
{
struct strv paths = strv_make ();
get_xdg_data_dirs (&paths);
strv_append (&paths, PROJECT_DATADIR);
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, "general.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 (ctx, a->buffer);
if (a->buffer == ctx->current_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 bool
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);
return false;
}
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);
return true;
}
static bool
handle_command_set_assign
(struct app_context *ctx, struct strv *all, char *arguments)
{
hard_assert (all->len > 0);
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;
}
bool changed = false;
for (size_t i = 0; i < all->len; i++)
{
char *key = cstr_cut_until (all->vector[i], " ");
if (handle_command_set_assign_item (ctx, key, new_, add, remove))
changed = true;
free (key);
}
config_item_destroy (new_);
if (changed && get_config_boolean (ctx->config.root, "general.autosave"))
save_configuration (ctx);
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
handle_command_relay (struct handler_args *a)
{
if (*a->arguments)
return false;
int len = 0;
LIST_FOR_EACH (struct client, c, a->ctx->clients)
len++;
if (a->ctx->relay_fd == -1)
log_global_status (a->ctx, "The relay is not enabled");
else
log_global_status (a->ctx, "The relay has #d clients", len);
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);
char *name = cut_word (&a->arguments);
if (!*a->arguments)
return false;
if (*name == '/')
name++;
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_away (struct handler_args *a)
{
if (*a->arguments)
irc_send (a->s, "AWAY :%s", a->arguments);
else
irc_send (a->s, "AWAY");
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")
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 },
{ "relay", "Show relay information",
NULL,
handle_command_relay, 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;
const 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, 0, 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_line (ctx, buffer, commands->vector[i], ++level))
return false;
return true;
}
static bool
process_input_line_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_line (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_line_posthook (ctx, buffer, processed, alias_level);
free (processed);
return result;
}
static void
process_input (struct app_context *ctx, struct buffer *buffer,
const char *input)
{
struct strv lines = strv_make ();
cstr_split (input, "\r\n", false, &lines);
for (size_t i = 0; i < lines.len; i++)
(void) process_input_line (ctx, buffer, lines.vector[i], 0);
strv_free (&lines);
}
// --- 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_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 struct completion
completion_make (const char *line, size_t len)
{
struct completion 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;
}
return self;
}
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_from_cstr (items.vector[i]);
struct str serialized = str_make ();
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 buffer *buffer, 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
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 buffer *buffer, struct completion *data,
const char *word, struct strv *output)
{
size_t word_len = strlen (word);
if (buffer->type == BUFFER_SERVER)
{
struct user *self_user = buffer->server->irc_user;
if (self_user && !irc_server_strncmp (buffer->server,
word, self_user->nickname, word_len))
strv_append (output, self_user->nickname);
}
if (buffer->type != BUFFER_CHANNEL)
return;
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 struct strv
complete_word (struct app_context *ctx, struct buffer *buffer,
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 (buffer, data, word, &words);
complete_nicknames (buffer, data, word, &words);
}
else
complete_nicknames (buffer, 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 <= 2)
{
// When nothing matches, this copies the sentinel value
words.vector[0] = words.vector[1];
words.vector[1] = NULL;
words.len--;
}
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;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// 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
// TODO: Don't use this iconv handle directly at all, elsewhere in xC.
// And ideally use U+FFFD with EILSEQ.
(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 struct strv
make_completions (struct app_context *ctx, struct buffer *buffer,
const char *line_utf8, size_t start, size_t end)
{
struct completion comp = completion_make (line_utf8, strlen (line_utf8));
completion_locate (&comp, start);
char *word = xstrndup (line_utf8 + start, end - start);
struct strv completions = complete_word (ctx, buffer, &comp, word);
free (word);
completion_free (&comp);
return completions;
}
/// 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_input_completions
(struct app_context *ctx, const 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 strv completions =
make_completions (ctx, ctx->current_buffer, line_utf8, start, end);
free (line_utf8);
if (!completions.len)
{
strv_free (&completions);
return NULL;
}
for (size_t i = 0; i < completions.len; i++)
{
char *converted = iconv_xstrdup
(ctx->term_from_utf8, completions.vector[i], -1, NULL);
if (!soft_assert (converted))
converted = xstrdup ("?");
cstr_set (&completions.vector[i], converted);
}
return completions.vector;
}
// --- 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 the pager 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, NULL);
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 struct strv
build_editor_command (struct app_context *ctx, const char *filename)
{
struct strv argv = strv_make ();
const char *editor = get_config_string (ctx->config.root, "general.editor");
if (!editor)
{
const char *command;
if (!(command = getenv ("VISUAL"))
&& !(command = getenv ("EDITOR")))
command = "vi";
// Although most visual editors support a "+LINE" argument
// (every editor mentioned in the default value of general.editor,
// plus vi, mcedit, vis, ...), it isn't particularly useful by itself.
// We need to be able to specify the column number.
//
// Seeing as less popular software may try to process this as a filename
// and fail, do not bother with this "undocumented standard feature".
strv_append (&argv, command);
strv_append (&argv, filename);
return argv;
}
int cursor = 0;
char *input = CALL_ (ctx->input, get_line, &cursor);
hard_assert (cursor >= 0);
mbstate_t ps;
memset (&ps, 0, sizeof ps);
wchar_t wch;
size_t len, processed = 0, line_one_based = 1, column = 0;
while (processed < (size_t) cursor
&& (len = mbrtowc (&wch, input + processed, cursor - processed, &ps))
&& len != (size_t) -2 && len != (size_t) -1)
{
// Both VIM and Emacs use the caret notation with columns.
// Consciously leaving tabs broken, they're too difficult to handle.
int width = wcwidth (wch);
if (width < 0)
width = 2;
processed += len;
if (wch == '\n')
{
line_one_based++;
column = 0;
}
else
column += width;
}
free (input);
// Trivially split the command on spaces and substitute our values
struct str argument = str_make ();
for (; *editor; editor++)
{
if (*editor == ' ')
{
if (argument.len)
{
strv_append_owned (&argv, str_steal (&argument));
argument = str_make ();
}
continue;
}
if (*editor != '%' || !editor[1])
{
str_append_c (&argument, *editor);
continue;
}
// None of them are zero-length, thus words don't get lost
switch (*++editor)
{
case 'F':
str_append (&argument, filename);
continue;
case 'L':
str_append_printf (&argument, "%zu", line_one_based);
continue;
case 'C':
str_append_printf (&argument, "%zu", column + 1);
continue;
case 'B':
str_append_printf (&argument, "%d", cursor + 1);
continue;
case '%':
case ' ':
str_append_c (&argument, *editor);
continue;
}
const char *p = editor;
if (soft_assert (utf8_decode (&p, strlen (p)) > 0))
{
log_global_error (ctx, "Unknown substitution variable: %#&s",
xstrndup (editor, p - editor));
}
}
if (argument.len)
strv_append_owned (&argv, str_steal (&argument));
else
str_free (&argument);
return argv;
}
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;
struct strv argv = build_editor_command (ctx, filename);
if (!argv.len)
strv_append (&argv, "true");
hard_assert (!ctx->running_editor);
switch (spawn_helper_child (ctx))
{
case 0:
execvp (argv.vector[0], argv.vector);
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;
}
strv_free (&argv);
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 `#l': #l",
ctx->editor_filename, strerror (errno));
cstr_set (&ctx->editor_filename, NULL);
ctx->running_editor = false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
launch_pager (struct app_context *ctx,
int fd, const char *name, const char *path)
{
hard_assert (!ctx->running_pager);
switch (spawn_helper_child (ctx))
{
case 0:
dup2 (fd, STDIN_FILENO);
char *localized_name =
iconv_xstrdup (ctx->term_from_utf8, (char *) name, -1, NULL);
execl ("/bin/sh", "/bin/sh", "-c",
get_config_string (ctx->config.root, "general.pager"),
PROGRAM_NAME, localized_name, path, NULL);
print_error ("%s: %s", "Failed to launch pager", strerror (errno));
_exit (EXIT_FAILURE);
case -1:
log_global_error (ctx, "#s: #l",
"Failed to launch pager", strerror (errno));
break;
default:
ctx->running_pager = 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,
"general.pager_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_pager (ctx, fileno (backlog), buffer->name, NULL);
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 FILE *
open_log_path (struct app_context *ctx, struct buffer *buffer, const char *path)
{
FILE *fp = fopen (path, "rb");
if (!fp)
{
log_global_error (ctx,
"Failed to open `#l': #l", path, strerror (errno));
return NULL;
}
if (buffer->log_file)
// The regular flush will log any error eventually
(void) fflush (buffer->log_file);
set_cloexec (fileno (fp));
return fp;
}
static bool
on_display_full_log (int count, int key, void *user_data)
{
(void) count;
(void) key;
struct app_context *ctx = user_data;
struct buffer *buffer = ctx->current_buffer;
char *path = buffer_get_log_path (buffer);
FILE *full_log = open_log_path (ctx, buffer, path);
if (!full_log)
{
free (path);
return false;
}
launch_pager (ctx, fileno (full_log), buffer->name, path);
fclose (full_log);
free (path);
return true;
}
static bool
on_toggle_unimportant (int count, int key, void *user_data)
{
(void) count;
(void) key;
struct app_context *ctx = user_data;
buffer_toggle_unimportant (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_formatting_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", "IRC 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 if (isatty (STDIN_FILENO))
{
// Prevent Readline from showing the prompt twice for w/e reason
CALL (ctx->input, hide);
input_rl__restore (self);
CALL (ctx->input, ding);
}
else
request_quit (ctx, NULL);
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_input_completions (g_ctx, rl_line_buffer, start, end);
}
static void
app_readline_display_matches (char **matches, int len, int longest)
{
struct app_context *ctx = g_ctx;
CALL (ctx->input, hide);
rl_display_match_list (matches, len, longest);
CALL (ctx->input, show);
}
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_completion_display_matches_hook = app_readline_display_matches;
rl_variable_bind ("completion-ignore-case", "on");
rl_variable_bind ("menu-complete-display-prefix", "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;
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_input_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]);
// I'm not sure if Readline's menu-complete can at all be implemented
// with Editline--we have no way of detecting what the last executed handler
// was. Employ the formatter's wrapping feature to spew all options.
bool only_match = !completions[1];
if (!only_match)
{
CALL (ctx->input, hide);
redraw_screen (ctx);
struct formatter f = formatter_make (ctx, NULL);
for (char **p = completions; *++p; )
formatter_add (&f, " #l", *p);
formatter_add (&f, "\n");
formatter_flush (&f, stdout, 0);
formatter_free (&f);
CALL (ctx->input, show);
}
for (char **p = completions; *p; p++)
free (*p);
free (completions);
if (!only_match)
return CC_REFRESH_BEEP;
// 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;
wchar_t *line = xcalloc (len + 1, sizeof *info->buffer);
memcpy (line, info->buffer, sizeof *info->buffer * len);
if (*line)
{
HistEventW ev;
history_w (self->current->history, &ev, H_ENTER, line);
}
free (line);
// on_pending_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);
// We must invoke ch_reset(), which isn't done for us with EL_UNBUFFERED.
input_el__start_over (self);
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, "\r", "send-line");
CALL_ (input, bind, "\n", "send-line");
CALL_ (input, bind_control, 'i', "complete");
// Source the user's defaults file
el_source (self->editline, NULL);
// See input_el__redisplay(), functionally important
CALL_ (input, bind_control, 'q', "ed-redisplay");
// This is what buffered el_wgets() does, functionally important
CALL_ (input, bind_control, 'c', "ed-start-over");
}
#endif // HAVE_EDITLINE
// --- Configuration loading ---------------------------------------------------
static const char *g_first_time_help[] =
{
"",
"\x02Welcome to xC!",
"",
"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 general.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 IRCnet\x02",
" - \x02/set servers.IRCnet.addresses = \"open.ircnet.net\"\x02",
" - \x02/connect IRCnet\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 pager.
// 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
log_global_debug (ctx,
"A child has been stopped, killing its process group");
kill (-zombie, SIGKILL);
return true;
}
if (ctx->running_pager)
ctx->running_pager = 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_formatting_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;
// Letters mostly taken from their caret escapes + HTML element names.
// Additionally, 'm' stands for mono, 'x' for cross, 'r' for reset.
switch (buf->str[0])
{
case 'b' ^ 96:
case 'b': CALL_ (ctx->input, insert, "\x02"); break;
case 'c': CALL_ (ctx->input, insert, "\x03"); break;
case 'q':
case 'm': CALL_ (ctx->input, insert, "\x11"); break;
case 'v': CALL_ (ctx->input, insert, "\x16"); break;
case 'i' ^ 96:
case 'i':
case ']': CALL_ (ctx->input, insert, "\x1d"); break;
case 's' ^ 96:
case 's':
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 'r':
case 'o': CALL_ (ctx->input, insert, "\x0f"); break;
default:
goto error;
}
goto done;
error:
CALL (ctx->input, ding);
done:
str_reset (buf);
ctx->awaiting_formatting_escape = false;
}
#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted
static bool
insert_paste (struct app_context *ctx, char *paste, size_t len)
{
if (!get_config_boolean (ctx->config.root, "general.process_pasted_text"))
return CALL_ (ctx->input, insert, paste);
// Without ICRNL, which Editline keeps but Readline doesn't,
// the terminal sends newlines as carriage returns (seen on urxvt)
for (size_t i = 0; i < len; i++)
if (paste[i] == '\r')
paste[i] = '\n';
int position = 0;
char *input = CALL_ (ctx->input, get_line, &position);
bool quote_first_slash = !position || strchr ("\r\n", input[position - 1]);
free (input);
// Executing commands by accident is much more common than pasting them
// intentionally, although the latter may also have security consequences
struct str processed = str_make ();
str_reserve (&processed, len);
for (size_t i = 0; i < len; i++)
{
if (paste[i] == '/'
&& ((!i && quote_first_slash) || (i && paste[i - 1] == '\n')))
str_append_c (&processed, paste[i]);
str_append_c (&processed, paste[i]);
}
bool success = CALL_ (ctx->input, insert, processed.str);
str_free (&processed);
return success;
}
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 (insert_paste (ctx, buf->str, text_len))
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, "general.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, "general.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_formatting_escape)
process_formatting_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_SKIP_FILE, BUFFER_LINE_ERROR,
"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++)
{
char *input = iconv_xstrdup (ctx->term_to_utf8,
ctx->pending_input.vector[i], -1, NULL);
if (!input)
{
print_error ("character conversion failed for: %s", "user input");
continue;
}
relay_prepare_buffer_input (ctx, ctx->current_buffer, input);
relay_broadcast (ctx);
process_input (ctx, ctx->current_buffer, input);
free (input);
}
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;
}
// --- Relay processing --------------------------------------------------------
// XXX: This could be below completion code if reset_autoaway() was higher up.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
client_resync_buffer_input (struct client *c, struct buffer *buffer)
{
struct strv history =
CALL_ (c->ctx->input, buffer_get_history, buffer->input_data);
for (size_t i = 0; i < history.len; i++)
{
char *input = iconv_xstrdup (c->ctx->term_to_utf8,
history.vector[i], -1, NULL);
if (!input)
{
print_error ("character conversion failed for: %s",
"user input history");
continue;
}
relay_prepare_buffer_input (c->ctx, buffer, input);
relay_send (c);
free (input);
}
strv_free (&history);
}
static void
client_resync (struct client *c)
{
struct str_map_iter iter = str_map_iter_make (&c->ctx->servers);
struct server *s;
while ((s = str_map_iter_next (&iter)))
{
relay_prepare_server_update (c->ctx, s);
relay_send (c);
}
LIST_FOR_EACH (struct buffer, buffer, c->ctx->buffers)
{
relay_prepare_buffer_update (c->ctx, buffer);
relay_send (c);
relay_prepare_buffer_stats (c->ctx, buffer);
relay_send (c);
client_resync_buffer_input (c, buffer);
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
{
relay_prepare_buffer_line (c->ctx, buffer, line, false);
relay_send (c);
}
}
relay_prepare_buffer_activate (c->ctx, c->ctx->current_buffer);
relay_send (c);
}
static const char *
client_message_buffer_name (const struct relay_command_message *m)
{
switch (m->data.command)
{
case RELAY_COMMAND_BUFFER_COMPLETE:
return m->data.buffer_complete.buffer_name.str;
case RELAY_COMMAND_BUFFER_ACTIVATE:
return m->data.buffer_activate.buffer_name.str;
case RELAY_COMMAND_BUFFER_INPUT:
return m->data.buffer_input.buffer_name.str;
case RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT:
return m->data.buffer_toggle_unimportant.buffer_name.str;
case RELAY_COMMAND_BUFFER_LOG:
return m->data.buffer_log.buffer_name.str;
default:
return NULL;
}
}
static void
client_process_buffer_complete (struct client *c, uint32_t seq,
struct buffer *buffer, struct relay_command_data_buffer_complete *req)
{
struct str *line = &req->text;
uint32_t end = req->position;
if (line->len < end || line->len != strlen (line->str))
{
relay_prepare_error (c->ctx, seq, "Invalid arguments");
goto out;
}
uint32_t start = end;
while (start && !strchr (WORD_BREAKING_CHARS, line->str[start - 1]))
start--;
struct strv completions =
make_completions (c->ctx, buffer, line->str, start, end);
if (completions.len > UINT32_MAX)
{
relay_prepare_error (c->ctx, seq, "Internal error");
goto out_internal;
}
struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
e->data.command = RELAY_COMMAND_BUFFER_COMPLETE;
struct relay_response_data_buffer_complete *resp =
&e->data.buffer_complete;
resp->start = start;
resp->completions_len = completions.len;
resp->completions = xcalloc (completions.len, sizeof *resp->completions);
for (size_t i = 0; i < completions.len; i++)
resp->completions[i] = str_from_cstr (completions.vector[i]);
out_internal:
strv_free (&completions);
out:
relay_send (c);
}
static void
client_process_buffer_input
(struct client *c, struct buffer *buffer, const char *input)
{
char *mb = iconv_xstrdup (c->ctx->term_from_utf8, (char *) input, -1, NULL);
CALL_ (c->ctx->input, buffer_add_history, buffer->input_data, mb);
free (mb);
relay_prepare_buffer_input (c->ctx, buffer, input);
relay_broadcast_except (c->ctx, c);
process_input (c->ctx, buffer, input);
}
static void
client_process_buffer_log
(struct client *c, uint32_t seq, struct buffer *buffer)
{
// XXX: We log failures to the global buffer,
// so the client just receives nothing if there is no log file.
struct str log = str_make ();
char *path = buffer_get_log_path (buffer);
FILE *fp = open_log_path (c->ctx, buffer, path);
if (fp)
{
char buf[BUFSIZ];
size_t len;
while ((len = fread (buf, 1, sizeof buf, fp)))
str_append_data (&log, buf, len);
if (ferror (fp))
log_global_error (c->ctx, "Failed to read `#l': #l",
path, strerror (errno));
fclose (fp);
}
free (path);
struct relay_event_data_response *e = relay_prepare_response (c->ctx, seq);
e->data.command = RELAY_COMMAND_BUFFER_LOG;
// On overflow, it will later fail serialization (frame will be too long).
e->data.buffer_log.log_len = MIN (UINT32_MAX, log.len);
e->data.buffer_log.log = (uint8_t *) str_steal (&log);
relay_send (c);
}
static bool
client_process_message (struct client *c,
struct msg_unpacker *r, struct relay_command_message *m)
{
if (!relay_command_message_deserialize (m, r)
|| msg_unpacker_get_available (r))
{
log_global_error (c->ctx, "Deserialization failed, killing client");
return false;
}
const char *buffer_name = client_message_buffer_name (m);
struct buffer *buffer = NULL;
if (buffer_name && !(buffer = buffer_by_name (c->ctx, buffer_name)))
{
relay_prepare_error (c->ctx, m->command_seq, "Unknown buffer");
relay_send (c);
return true;
}
bool acknowledge = true;
switch (m->data.command)
{
case RELAY_COMMAND_HELLO:
c->initialized = true;
if (m->data.hello.version != RELAY_VERSION)
{
log_global_error (c->ctx,
"Protocol version mismatch, killing client");
relay_prepare_error (c->ctx,
m->command_seq, "Protocol version mismatch");
relay_send (c);
c->closing = true;
return true;
}
client_resync (c);
break;
case RELAY_COMMAND_PING:
break;
case RELAY_COMMAND_ACTIVE:
reset_autoaway (c->ctx);
break;
case RELAY_COMMAND_BUFFER_COMPLETE:
acknowledge = false;
client_process_buffer_complete (c, m->command_seq, buffer,
&m->data.buffer_complete);
break;
case RELAY_COMMAND_BUFFER_ACTIVATE:
buffer_activate (c->ctx, buffer);
break;
case RELAY_COMMAND_BUFFER_INPUT:
client_process_buffer_input (c, buffer, m->data.buffer_input.text.str);
break;
case RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT:
buffer_toggle_unimportant (c->ctx, buffer);
break;
case RELAY_COMMAND_BUFFER_LOG:
acknowledge = false;
client_process_buffer_log (c, m->command_seq, buffer);
break;
default:
acknowledge = false;
log_global_debug (c->ctx, "Unhandled client command");
relay_prepare_error (c->ctx, m->command_seq, "Unknown command");
relay_send (c);
}
if (acknowledge)
{
relay_prepare_response (c->ctx, m->command_seq)
->data.command = m->data.command;
relay_send (c);
}
return true;
}
static bool
client_process_buffer (struct client *c)
{
struct str *buf = &c->read_buffer;
size_t offset = 0;
while (true)
{
uint32_t frame_len = 0;
struct msg_unpacker r =
msg_unpacker_make (buf->str + offset, buf->len - offset);
if (!msg_unpacker_u32 (&r, &frame_len))
break;
r.len = MIN (r.len, sizeof frame_len + frame_len);
if (msg_unpacker_get_available (&r) < frame_len)
break;
struct relay_command_message m = {};
bool ok = c->closing || client_process_message (c, &r, &m);
relay_command_message_free (&m);
if (!ok)
return false;
offset += r.offset;
}
str_remove_slice (buf, 0, offset);
return true;
}
// --- Relay plumbing ----------------------------------------------------------
static bool
client_try_read (struct client *c)
{
struct str *buf = &c->read_buffer;
ssize_t n_read;
while ((n_read = read (c->socket_fd, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */)) > 0)
{
buf->len += n_read;
if (!client_process_buffer (c))
break;
str_reserve (buf, 512);
}
if (n_read < 0)
{
if (errno == EAGAIN || errno == EINTR)
return true;
log_global_debug (c->ctx,
"#s: #s: #l", __func__, "read", strerror (errno));
}
client_kill (c);
return false;
}
static bool
client_try_write (struct client *c)
{
struct str *buf = &c->write_buffer;
ssize_t n_written;
while (buf->len)
{
n_written = write (c->socket_fd, buf->str, buf->len);
if (n_written >= 0)
{
str_remove_slice (buf, 0, n_written);
continue;
}
if (errno == EAGAIN || errno == EINTR)
return true;
log_global_debug (c->ctx,
"#s: #s: #l", __func__, "write", strerror (errno));
client_kill (c);
return false;
}
return true;
}
static void
on_client_ready (const struct pollfd *pfd, void *user_data)
{
struct client *c = user_data;
if (client_try_read (c) && client_try_write (c))
{
client_update_poller (c, pfd);
if (c->closing && !c->write_buffer.len)
client_kill (c);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
relay_try_fetch_client (struct app_context *ctx, int listen_fd)
{
// XXX: `struct sockaddr_storage' is not the most portable thing
struct sockaddr_storage peer;
socklen_t peer_len = sizeof peer;
int fd = accept (listen_fd, (struct sockaddr *) &peer, &peer_len);
if (fd == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
return false;
if (errno == EINTR)
return true;
if (accept_error_is_transient (errno))
{
log_global_debug (ctx, "#s: #l", "accept", strerror (errno));
return true;
}
log_global_error (ctx, "#s: #l", "accept", strerror (errno));
app_context_relay_stop (ctx);
return false;
}
hard_assert (peer_len <= sizeof peer);
set_blocking (fd, false);
set_cloexec (fd);
// We already buffer our output, so reduce latencies.
int yes = 1;
soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
&yes, sizeof yes) != -1);
struct client *c = client_new ();
c->ctx = ctx;
c->socket_fd = fd;
LIST_PREPEND (ctx->clients, c);
c->socket_event = poller_fd_make (&c->ctx->poller, c->socket_fd);
c->socket_event.dispatcher = (poller_fd_fn) on_client_ready;
c->socket_event.user_data = c;
client_update_poller (c, NULL);
return true;
}
static void
on_relay_client_available (const struct pollfd *pfd, void *user_data)
{
struct app_context *ctx = user_data;
while (relay_try_fetch_client (ctx, pfd->fd))
;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
relay_listen (struct addrinfo *ai, struct error **e)
{
int fd = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd == -1)
{
error_set (e, "socket: %s", strerror (errno));
return -1;
}
set_cloexec (fd);
int yes = 1;
soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR,
&yes, sizeof yes) != -1);
if (bind (fd, ai->ai_addr, ai->ai_addrlen))
error_set (e, "bind: %s", strerror (errno));
else if (listen (fd, 16 /* arbitrary number */))
error_set (e, "listen: %s", strerror (errno));
else
return fd;
xclose (fd);
return -1;
}
static int
relay_listen_with_context (struct app_context *ctx, struct addrinfo *ai,
struct error **e)
{
char *address = gai_reconstruct_address (ai);
log_global_debug (ctx, "binding to `#l'", address);
struct error *error = NULL;
int fd = relay_listen (ai, &error);
if (fd == -1)
{
error_set (e, "binding to `%s' failed: %s", address, error->message);
error_free (error);
}
free (address);
return fd;
}
static bool
relay_start (struct app_context *ctx, char *address, struct error **e)
{
const char *port = NULL, *host = tokenize_host_port (address, &port);
if (!port || !*port)
return error_set (e, "missing port");
struct addrinfo hints = {}, *result = NULL;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
int err = getaddrinfo (*host ? host : NULL, port, &hints, &result);
if (err)
{
return error_set (e, "failed to resolve `%s', port `%s': %s: %s",
host, port, "getaddrinfo", gai_strerror (err));
}
// Just try the first one, disregarding IPv4/IPv6 ordering.
// Use 0.0.0.0 or [::] to request either one specifically.
int fd = relay_listen_with_context (ctx, result, e);
freeaddrinfo (result);
if (fd == -1)
return false;
set_blocking (fd, false);
struct poller_fd *event = &ctx->relay_event;
*event = poller_fd_make (&ctx->poller, fd);
event->dispatcher = (poller_fd_fn) on_relay_client_available;
event->user_data = ctx;
ctx->relay_fd = fd;
poller_fd_set (event, POLLIN);
return true;
}
static void
on_config_relay_bind_change (struct config_item *item)
{
struct app_context *ctx = item->user_data;
char *value = item->value.string.str;
app_context_relay_stop (ctx);
if (!value)
return;
// XXX: This should perhaps be reencoded as the locale encoding.
char *address = xstrdup (value);
struct error *e = NULL;
if (!relay_start (ctx, address, &e))
{
log_global_error (ctx, "#s: #l", item->schema->name, e->message);
error_free (e);
}
free (address);
}
// --- 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 const 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\002 a\0031 b";
// XXX: formatting continuation order is implementation-dependent here
// (irc_serialize_char_attrs() makes a choice in serialization)
static const char *split[] = { " foo", "bar", "foob", "ar",
"", "ób", "árb", "áz\x02", "\002a\0031", "\0031\002b" };
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[] =
{
"",
"\x02" PROGRAM_NAME "\x02 " 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)
{
// XXX: it might make sense to allow for redirection, using FLUSH_OPT_RAW
struct str s = str_make ();
int c = 0;
while ((c = fgetc (stdin)) != EOF)
{
if (c != '\n')
{
str_append_c (&s, c);
continue;
}
struct formatter f = formatter_make (ctx, NULL);
formatter_add (&f, "#m\n", s.str);
formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
formatter_free (&f);
str_reset (&s);
}
struct formatter f = formatter_make (ctx, NULL);
formatter_add (&f, "#m", s.str);
formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
formatter_free (&f);
str_free (&s);
exit (EXIT_SUCCESS);
}
int
main (int argc, char *argv[])
{
// We include a generated file from xD 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);
if (!cur_term)
exit_fatal ("terminal initialization failed");
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);
app_context_free (&ctx);
toggle_bracketed_paste (false);
free_terminal ();
return EXIT_SUCCESS;
}