Unreasonable IRC client, daemon and bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

14714 lines
406 KiB

/*
* xC.c: a terminal-based IRC client
*
* Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
// A table of all attributes we use for output
#define ATTR_TABLE(XX) \
XX( PROMPT, prompt, Terminal attrs for the prompt ) \
XX( RESET, reset, String to reset terminal attributes ) \
XX( DATE_CHANGE, date_change, Terminal attrs for date change ) \
XX( READ_MARKER, read_marker, Terminal attrs for the read marker ) \
XX( WARNING, warning, Terminal attrs for warnings ) \
XX( ERROR, error, Terminal attrs for errors ) \
XX( EXTERNAL, external, Terminal attrs for external lines ) \
XX( TIMESTAMP, timestamp, Terminal attrs for timestamps ) \
XX( HIGHLIGHT, highlight, Terminal attrs for highlights ) \
XX( ACTION, action, Terminal attrs for user actions ) \
XX( USERHOST, userhost, Terminal attrs for user@host ) \
XX( JOIN, join, Terminal attrs for joins ) \
XX( PART, part, Terminal attrs for parts )
enum
{
#define XX(x, y, z) ATTR_ ## x,
ATTR_TABLE (XX)
#undef XX
ATTR_COUNT
};
// User data for logger functions to enable formatted logging
#define print_fatal_data ((void *) ATTR_ERROR)
#define print_error_data ((void *) ATTR_ERROR)
#define print_warning_data ((void *) ATTR_WARNING)
#include "config.h"
#define PROGRAM_NAME "xC"
#include "common.c"
#include "xD-replies.c"
#include <math.h>
#include <langinfo.h>
#include <locale.h>
#include <pwd.h>
#include <sys/utsname.h>
#include <wchar.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <curses.h>
#include <term.h>
// Literally cancer
#undef lines
#undef columns
#include <ffi.h>
#ifdef HAVE_LUA
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#endif // HAVE_LUA
// --- Terminal information ----------------------------------------------------
static struct
{
bool initialized; ///< Terminal is available
bool stdout_is_tty; ///< `stdout' is a terminal
bool stderr_is_tty; ///< `stderr' is a terminal
struct termios termios; ///< Terminal attributes
char *color_set_fg[256]; ///< Codes to set the foreground colour
char *color_set_bg[256]; ///< Codes to set the background colour
int lines; ///< Number of lines
int columns; ///< Number of columns
}
g_terminal;
static void
update_screen_size (void)
{
#ifdef TIOCGWINSZ
if (!g_terminal.stdout_is_tty)
return;
struct winsize size;
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
{
char *row = getenv ("LINES");
char *col = getenv ("COLUMNS");
unsigned long tmp;
g_terminal.lines =
(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row;
g_terminal.columns =
(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col;
}
#endif // TIOCGWINSZ
}
static bool
init_terminal (void)
{
int tty_fd = -1;
if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
tty_fd = STDERR_FILENO;
if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
tty_fd = STDOUT_FILENO;
int err;
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
return false;
// Make sure all terminal features used by us are supported
if (!set_a_foreground || !set_a_background
|| !enter_bold_mode || !exit_attribute_mode
|| tcgetattr (tty_fd, &g_terminal.termios))
{
del_curterm (cur_term);
return false;
}
// Make sure newlines are output correctly
g_terminal.termios.c_oflag |= ONLCR;
(void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios);
g_terminal.lines = tigetnum ("lines");
g_terminal.columns = tigetnum ("cols");
update_screen_size ();
int max = MIN (256, max_colors);
for (int i = 0; i < max; i++)
{
g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
i, 0, 0, 0, 0, 0, 0, 0, 0));
g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
i, 0, 0, 0, 0, 0, 0, 0, 0));
}
return g_terminal.initialized = true;
}
static void
free_terminal (void)
{
if (!g_terminal.initialized)
return;
for (int i = 0; i < 256; i++)
{
free (g_terminal.color_set_fg[i]);
free (g_terminal.color_set_bg[i]);
}
del_curterm (cur_term);
}
// --- User interface ----------------------------------------------------------
// I'm not sure which one of these backends is worse: whether it's GNU Readline
// or BSD Editline. They both have their own annoying problems. We use lots
// of hacks to get the results we want and need.
//
// The abstraction is a necessary evil. It's still not 100%, though.
/// Some arbitrary limit for the history
#define HISTORY_LIMIT 10000
/// Characters that separate words
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
struct input
{
struct input_vtable *vtable; ///< Virtual methods
void (*add_functions) (void *); ///< Define functions for binding
void *user_data; ///< User data for callbacks
};
typedef void *input_buffer_t; ///< Pointer alias for input buffers
/// Named function that can be bound to a sequence of characters
typedef bool (*input_fn) (int count, int key, void *user_data);
// A little bit better than tons of forwarder functions in our case
#define CALL(self, name) ((self)->vtable->name ((self)))
#define CALL_(self, name, ...) ((self)->vtable->name ((self), __VA_ARGS__))
struct input_vtable
{
/// Start the interface under the given program name
void (*start) (void *input, const char *program_name);
/// Stop the interface
void (*stop) (void *input);
/// Prepare or unprepare terminal for our needs
void (*prepare) (void *input, bool enabled);
/// Destroy the object
void (*destroy) (void *input);
/// Hide the prompt if shown
void (*hide) (void *input);
/// Show the prompt if hidden
void (*show) (void *input);
/// Retrieve current prompt string
const char *(*get_prompt) (void *input);
/// Change the prompt string; takes ownership
void (*set_prompt) (void *input, char *prompt);
/// Ring the terminal bell
void (*ding) (void *input);
/// Create a new input buffer
input_buffer_t (*buffer_new) (void *input);
/// Destroy an input buffer
void (*buffer_destroy) (void *input, input_buffer_t buffer);
/// Switch to a different input buffer
void (*buffer_switch) (void *input, input_buffer_t buffer);
/// Register a function that can be bound to character sequences
void (*register_fn) (void *input,
const char *name, const char *help, input_fn fn, void *user_data);
/// Bind an arbitrary sequence of characters to the given named function
void (*bind) (void *input, const char *seq, const char *fn);
/// Bind Ctrl+key to the given named function
void (*bind_control) (void *input, char key, const char *fn);
/// Bind Alt+key to the given named function
void (*bind_meta) (void *input, char key, const char *fn);
/// Get the current line input and position within
char *(*get_line) (void *input, int *position);
/// Clear the current line input
void (*clear_line) (void *input);
/// Insert text at current position
bool (*insert) (void *input, const char *text);
/// Handle terminal resize
void (*on_tty_resized) (void *input);
/// Handle terminal input
void (*on_tty_readable) (void *input);
};
#define INPUT_VTABLE(XX) \
XX (start) XX (stop) XX (prepare) XX (destroy) \
XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \
XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \
XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta) \
XX (get_line) XX (clear_line) XX (insert) \
XX (on_tty_resized) XX (on_tty_readable)
// --- GNU Readline ------------------------------------------------------------
#ifdef HAVE_READLINE
#include <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;
hard_assert (self->prompt != NULL);
// The inputrc is read before any callbacks are called, so we need to
// register all functions that our user may want to map up front
self->super.add_functions (self->super.user_data);
rl_callback_handler_install (self->prompt, on_readline_input);
self->prompt_shown = 1;
self->active = true;
}
static void
input_rl_stop (void *input)
{
struct input_rl *self = input;
if (self->prompt_shown > 0)
input_rl__erase (self);
// This is okay as long as we're not called from within readline
rl_callback_handler_remove ();
self->active = false;
self->prompt_shown = false;
}
static void
input_rl_prepare (void *input, bool enabled)
{
(void) input;
if (enabled)
rl_prep_terminal (true);
else
rl_deprep_terminal ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The following part shows you why it's not a good idea to use
// GNU Readline for this kind of software. Or for anything else, really.
static void
input_rl__save_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
{
(void) self;
buffer->history = history_get_history_state ();
buffer->saved_line = rl_copy_text (0, rl_end);
buffer->saved_point = rl_point;
buffer->saved_mark = rl_mark;
rl_replace_line ("", true);
if (self->prompt_shown > 0)
rl_redisplay ();
}
static void
input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
{
if (buffer->history)
{
// history_get_history_state() just allocates a new HISTORY_STATE
// and fills it with its current internal data. We don't need that
// shell anymore after reviving it.
history_set_history_state (buffer->history);
free (buffer->history);
buffer->history = NULL;
}
else
{
// This should get us a clean history while keeping the flags.
// Note that we've either saved the previous history entries, or we've
// cleared them altogether, so there should be nothing to leak.
HISTORY_STATE *state = history_get_history_state ();
state->offset = state->length = state->size = 0;
state->entries = NULL;
history_set_history_state (state);
free (state);
}
if (buffer->saved_line)
{
rl_replace_line (buffer->saved_line, true);
rl_point = buffer->saved_point;
rl_mark = buffer->saved_mark;
cstr_set (&buffer->saved_line, NULL);
if (self->prompt_shown > 0)
rl_redisplay ();
}
}
static void
input_rl_buffer_switch (void *input, input_buffer_t input_buffer)
{
struct input_rl *self = input;
struct input_rl_buffer *buffer = input_buffer;
// There could possibly be occurences of the current undo list in some
// history entry. We either need to free the undo list, or move it
// somewhere else to load back later, as the buffer we're switching to
// has its own history state.
rl_free_undo_list ();
// Save this buffer's history so that it's independent for each buffer
if (self->current)
input_rl__save_buffer (self, self->current);
else
// Just throw it away; there should always be an active buffer however
#if RL_READLINE_VERSION >= 0x0603
rl_clear_history ();
#else // RL_READLINE_VERSION < 0x0603
// At least something... this may leak undo entries
clear_history ();
#endif // RL_READLINE_VERSION < 0x0603
input_rl__restore_buffer (self, buffer);
self->current = buffer;
}
static void
input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self)
{
free (self->history);
free (self->saved_line);
free (self);
}
static void
input_rl_buffer_destroy (void *input, input_buffer_t input_buffer)
{
(void) input;
struct input_rl_buffer *buffer = input_buffer;
// rl_clear_history, being the only way I know of to get rid of the complete
// history including attached data, is a pretty recent addition. *sigh*
#if RL_READLINE_VERSION >= 0x0603
if (buffer->history)
{
// See input_rl_buffer_switch() for why we need to do this BS
rl_free_undo_list ();
// This is probably the only way we can free the history fully
HISTORY_STATE *state = history_get_history_state ();
history_set_history_state (buffer->history);
rl_clear_history ();
// rl_clear_history just removes history entries,
// we have to reclaim memory for their actual container ourselves
free (buffer->history->entries);
free (buffer->history);
buffer->history = NULL;
history_set_history_state (state);
free (state);
}
#endif // RL_READLINE_VERSION
input_rl__buffer_destroy_wo_history (buffer);
}
static input_buffer_t
input_rl_buffer_new (void *input)
{
(void) input;
struct input_rl_buffer *self = xcalloc (1, sizeof *self);
return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Since {save,restore}_buffer() store history, we can't use them here like we
// do with libedit, because then buffer_destroy() can free memory that's still
// being used by readline. This situation is bound to happen on quit.
static void
input_rl__save (struct input_rl *self)
{
hard_assert (!self->saved_line);
self->saved_point = rl_point;
self->saved_mark = rl_mark;
self->saved_line = rl_copy_text (0, rl_end);
}
static void
input_rl__restore (struct input_rl *self)
{
hard_assert (self->saved_line);
rl_set_prompt (self->prompt);
rl_replace_line (self->saved_line, false);
rl_point = self->saved_point;
rl_mark = self->saved_mark;
cstr_set (&self->saved_line, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_hide (void *input)
{
struct input_rl *self = input;
if (!self->active || self->prompt_shown-- < 1)
return;
input_rl__save (self);
input_rl__erase (self);
}
static void
input_rl_show (void *input)
{
struct input_rl *self = input;
if (!self->active || ++self->prompt_shown < 1)
return;
input_rl__restore (self);
rl_redisplay ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_on_tty_resized (void *input)
{
(void) input;
// This fucks up big time on terminals with automatic wrapping such as
// rxvt-unicode or newer VTE when the current line overflows, however we
// can't do much about that
rl_resize_terminal ();
}
static void
input_rl_on_tty_readable (void *input)
{
(void) input;
rl_callback_read_char ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_destroy (void *input)
{
struct input_rl *self = input;
free (self->saved_line);
LIST_FOR_EACH (struct input_rl_fn, iter, self->fns)
ffi_closure_free (iter);
free (self->prompt);
free (self);
}
#define XX(a) .a = input_rl_ ## a,
static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
#undef XX
static struct input *
input_rl_new (void)
{
struct input_rl *self = xcalloc (1, sizeof *self);
self->super.vtable = &input_rl_vtable;
return &self->super;
}
#define input_new input_rl_new
#endif // HAVE_READLINE
// --- BSD Editline ------------------------------------------------------------
#ifdef HAVE_EDITLINE
#include <histedit.h>
#define INPUT_START_IGNORE '\x01'
#define INPUT_END_IGNORE '\x01'
struct input_el_fn
{
ffi_closure closure; ///< Closure
LIST_HEADER (struct input_el_fn)
input_fn callback; ///< Real callback
void *user_data; ///< Real callback user data
wchar_t *name; ///< Function name
wchar_t *help; ///< Function help
};
struct input_el_buffer
{
HistoryW *history; ///< The history object
wchar_t *saved_line; ///< Saved line content
int saved_len; ///< Length of the saved line
int saved_point; ///< Saved cursor position
};
struct input_el
{
struct input super; ///< Parent class
EditLine *editline; ///< The EditLine object
bool active; ///< Are we a thing?
char *prompt; ///< The prompt we use
int prompt_shown; ///< Whether the prompt is shown now
struct input_el_fn *fns; ///< Named functions
struct input_el_buffer *current; ///< Current input buffer
};
static void app_editline_init (struct input_el *self);
static void
input_el__redisplay (void *input)
{
// See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT
// so we bind redisplay somewhere else in app_editline_init()
struct input_el *self = input;
char x[] = { 'q' & 31, 0 };
el_push (self->editline, x);
// We have to do this or it gets stuck and nothing is done
int count = 0;
(void) el_wgets (self->editline, &count);
}
static char *
input_el__make_prompt (EditLine *editline)
{
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
if (!self->prompt)
return "";
return self->prompt;
}
static char *
input_el__make_empty_prompt (EditLine *editline)
{
(void) editline;
return "";
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_ding (void *input)
{
// XXX: this isn't probably very portable;
// we could use "bell" from terminfo but that creates a dependency
(void) input;
write (STDOUT_FILENO, "\a", 1);
}
static const char *
input_el_get_prompt (void *input)
{
struct input_el *self = input;
return self->prompt;
}
static void
input_el_set_prompt (void *input, char *prompt)
{
struct input_el *self = input;
cstr_set (&self->prompt, prompt);
if (self->prompt_shown > 0)
input_el__redisplay (self);
}
static void
input_el_clear_line (void *input)
{
struct input_el *self = input;
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
input_el__redisplay (self);
}
static void
input_el__erase (struct input_el *self)
{
el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
input_el_clear_line (self);
}
static bool
input_el_insert (void *input, const char *s)
{
struct input_el *self = input;
bool success = !*s || !el_insertstr (self->editline, s);
if (self->prompt_shown > 0)
input_el__redisplay (self);
return success;
}
static char *
input_el_get_line (void *input, int *position)
{
struct input_el *self = input;
const LineInfo *info = el_line (self->editline);
int point = info->cursor - info->buffer;
if (position) *position = point;
return xstrndup (info->buffer, info->lastchar - info->buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_bind (void *input, const char *seq, const char *function_name)
{
struct input_el *self = input;
el_set (self->editline, EL_BIND, seq, function_name, NULL);
}
static void
input_el_bind_meta (void *input, char key, const char *function_name)
{
char keyseq[] = { 'M', '-', key, 0 };
input_el_bind (input, keyseq, function_name);
}
static void
input_el_bind_control (void *input, char key, const char *function_name)
{
char keyseq[] = { '^', key, 0 };
input_el_bind (input, keyseq, function_name);
}
static void
input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
{
(void) cif;
struct input_el_fn *data = user_data;
*(unsigned char *) ret = data->callback
(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
}
static wchar_t *
ascii_to_wide (const char *ascii)
{
size_t len = strlen (ascii) + 1;
wchar_t *wide = xcalloc (sizeof *wide, len);
while (len--)
hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
return wide;
}
static void
input_el_register_fn (void *input,
const char *name, const char *help, input_fn callback, void *user_data)
{
void *bound_fn = NULL;
struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
hard_assert (data);
static ffi_cif cif;
static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
hard_assert (ffi_prep_cif
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
data->user_data = user_data;
data->callback = callback;
data->name = ascii_to_wide (name);
data->help = ascii_to_wide (help);
hard_assert (ffi_prep_closure_loc (&data->closure,
&cif, input_el__forward, data, bound_fn) == FFI_OK);
struct input_el *self = input;
el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
LIST_PREPEND (self->fns, data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_start (void *input, const char *program_name)
{
struct input_el *self = input;
self->editline = el_init (program_name, stdin, stdout, stderr);
el_set (self->editline, EL_CLIENTDATA, self);
el_set (self->editline, EL_PROMPT_ESC,
input_el__make_prompt, INPUT_START_IGNORE);
el_set (self->editline, EL_SIGNAL, false);
el_set (self->editline, EL_UNBUFFERED, true);
el_set (self->editline, EL_EDITOR, "emacs");
app_editline_init (self);
self->prompt_shown = 1;
self->active = true;
}
static void
input_el_stop (void *input)
{
struct input_el *self = input;
if (self->prompt_shown > 0)
input_el__erase (self);
el_end (self->editline);
self->editline = NULL;
self->active = false;
self->prompt_shown = false;
}
static void
input_el_prepare (void *input, bool enabled)
{
struct input_el *self = input;
el_set (self->editline, EL_PREP_TERM, enabled);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
{
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
wchar_t *line = calloc (sizeof *info->buffer, len + 1);
memcpy (line, info->buffer, sizeof *info->buffer * len);
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
buffer->saved_line = line;
buffer->saved_point = point;
buffer->saved_len = len;
}
static void
input_el__save (struct input_el *self)
{
if (self->current)
input_el__save_buffer (self, self->current);
}
static void
input_el__restore_buffer (struct input_el *self, struct input_el_buffer *buffer)
{
if (buffer->saved_line)
{
el_winsertstr (self->editline, buffer->saved_line);
el_cursor (self->editline,
-(buffer->saved_len - buffer->saved_point));
free (buffer->saved_line);
buffer->saved_line = NULL;
}
}
static void
input_el__restore (struct input_el *self)
{
if (self->current)
input_el__restore_buffer (self, self->current);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_buffer_switch (void *input, input_buffer_t input_buffer)
{
struct input_el *self = input;
struct input_el_buffer *buffer = input_buffer;
if (self->current)
input_el__save_buffer (self, self->current);
input_el__restore_buffer (self, buffer);
el_wset (self->editline, EL_HIST, history, buffer->history);
self->current = buffer;
}
static void
input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
{
(void) input;
struct input_el_buffer *buffer = input_buffer;
history_wend (buffer->history);
free (buffer->saved_line);
free (buffer);
}
static input_buffer_t
input_el_buffer_new (void *input)
{
(void) input;
struct input_el_buffer *self = xcalloc (1, sizeof *self);
self->history = history_winit ();
HistEventW ev;
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_hide (void *input)
{
struct input_el *self = input;
if (!self->active || self->prompt_shown-- < 1)
return;
input_el__save (self);
input_el__erase (self);
}
static void
input_el_show (void *input)
{
struct input_el *self = input;
if (!self->active || ++self->prompt_shown < 1)
return;
input_el__restore (self);
el_set (self->editline,
EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE);
input_el__redisplay (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_on_tty_resized (void *input)
{
struct input_el *self = input;
el_resize (self->editline);
}
static void
input_el_on_tty_readable (void *input)
{
// We bind the return key to process it how we need to
struct input_el *self = input;
// el_gets() with EL_UNBUFFERED doesn't work with UTF-8,
// we must use the wide-character interface
int count = 0;
const wchar_t *buf = el_wgets (self->editline, &count);
if (!buf || count-- <= 0)
return;
if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */)
{
el_deletestr (self->editline, 1);
input_el__redisplay (self);
input_el_ding (self);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_destroy (void *input)
{
struct input_el *self = input;
LIST_FOR_EACH (struct input_el_fn, iter, self->fns)
{
free (iter->name);
free (iter->help);
ffi_closure_free (iter);
}
free (self->prompt);
free (self);
}
#define XX(a) .a = input_el_ ## a,
static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) };
#undef XX
static struct input *
input_el_new (void)
{
struct input_el *self = xcalloc (1, sizeof *self);
self->super.vtable = &input_el_vtable;
return &self->super;
}
#define input_new input_el_new
#endif // HAVE_EDITLINE
// --- Application data --------------------------------------------------------
// All text stored in our data structures is encoded in UTF-8. Or at least
// should be--our only ways of retrieving strings are: via the command line
// (converted from locale, no room for errors), via the configuration file
// (restrictive ASCII grammar for bare words and an internal check for strings),
// and via plugins (meticulously validated).
//
// The only exception is IRC identifiers.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// We need a few reference countable objects with support for both strong
// and weak references (mainly used for scripted plugins).
//
// Beware that if you don't own the object, you will most probably want
// to keep the weak reference link so that you can get rid of it later.
// Also note that you have to make sure the user_data don't leak resources.
//
// Having a callback is more versatile than just nulling out a pointer.
/// Callback just before a reference counted object is destroyed
typedef void (*destroy_cb_fn) (void *object, void *user_data);
struct weak_ref_link
{
LIST_HEADER (struct weak_ref_link)
destroy_cb_fn on_destroy; ///< Called when object is destroyed
void *user_data; ///< User data
};
static struct weak_ref_link *
weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data)
{
struct weak_ref_link *link = xcalloc (1, sizeof *link);
link->on_destroy = cb;
link->user_data = user_data;
LIST_PREPEND (*list, link);
return link;
}
static void
weak_unref (struct weak_ref_link **list, struct weak_ref_link **link)
{
if (*link)
LIST_UNLINK (*list, *link);
free (*link);
*link = NULL;
}
#define REF_COUNTABLE_HEADER \
size_t ref_count; /**< Reference count */ \
struct weak_ref_link *weak_refs; /**< To remove any weak references */
#define REF_COUNTABLE_METHODS(name) \
static struct name * \
name ## _ref (struct name *self) \
{ \
self->ref_count++; \
return self; \
} \
\
static void \
name ## _unref (struct name *self) \
{ \
if (--self->ref_count) \
return; \
LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \
{ \
iter->on_destroy (self, iter->user_data); \
free (iter); \
} \
name ## _destroy (self); \
} \
\
static struct weak_ref_link * \
name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \
{ return weak_ref (&self->weak_refs, cb, user_data); } \
\
static void \
name ## _weak_unref (struct name *self, struct weak_ref_link **link) \
{ weak_unref (&self->weak_refs, link); }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Simple introspection framework to simplify exporting stuff to Lua, since
// there is a lot of it. While not fully automated, at least we have control
// over which fields are exported.
enum ispect_type
{
ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING,
ISPECT_STR, ///< "struct str"
ISPECT_STR_MAP, ///< "struct str_map"
ISPECT_REF ///< Weakly referenced object
};
struct ispect_field
{
const char *name; ///< Name of the field
size_t offset; ///< Offset in the structure
enum ispect_type type; ///< Type of the field
enum ispect_type subtype; ///< STR_MAP subtype
struct ispect_field *fields; ///< REF target fields
bool is_list; ///< REF target is a list
};
#define ISPECT(object, field, type) \
{ #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false },
#define ISPECT_REF(object, field, is_list, ref_type) \
{ #field, offsetof (struct object, field), ISPECT_REF, 0, \
g_##ref_type##_ispect, is_list },
#define ISPECT_MAP(object, field, subtype) \
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \
ISPECT_##subtype, NULL, false },
#define ISPECT_MAP_REF(object, field, is_list, ref_type) \
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \
ISPECT_REF, g_##ref_type##_ispect, is_list },
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct user_channel
{
LIST_HEADER (struct user_channel)
struct channel *channel; ///< Reference to channel
};
static struct user_channel *
user_channel_new (struct channel *channel)
{
struct user_channel *self = xcalloc (1, sizeof *self);
self->channel = channel;
return self;
}
static void
user_channel_destroy (struct user_channel *self)
{
// The "channel" reference is weak and this object should get
// destroyed whenever the user stops being in the channel.
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// We keep references to user information in channels and buffers,
// and weak references in the name lookup table.
struct user
{
REF_COUNTABLE_HEADER
char *nickname; ///< Literal nickname
bool away; ///< User is away
struct user_channel *channels; ///< Channels the user is on (with us)
};
static struct ispect_field g_user_ispect[] =
{
ISPECT( user, nickname, STRING )
ISPECT( user, away, BOOL )
{}
};
static struct user *
user_new (char *nickname)
{
struct user *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
self->nickname = nickname;
return self;
}
static void
user_destroy (struct user *self)
{
free (self->nickname);
LIST_FOR_EACH (struct user_channel, iter, self->channels)
user_channel_destroy (iter);
free (self);
}
REF_COUNTABLE_METHODS (user)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct channel_user
{
LIST_HEADER (struct channel_user)
struct user *user; ///< Reference to user
char *prefixes; ///< Ordered @+... characters
};
static struct channel_user *
channel_user_new (struct user *user, const char *prefixes)
{
struct channel_user *self = xcalloc (1, sizeof *self);
self->user = user;
self->prefixes = xstrdup (prefixes);
return self;
}
static void
channel_user_destroy (struct channel_user *self)
{
user_unref (self->user);
free (self->prefixes);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// We keep references to channels in their buffers,
// and weak references in their users and the name lookup table.
struct channel
{
REF_COUNTABLE_HEADER
struct server *s; ///< Server
char *name; ///< Channel name
char *topic; ///< Channel topic
// XXX: write something like an ordered set of characters object?
struct str no_param_modes; ///< No parameter channel modes
struct str_map param_modes; ///< Parametrized channel modes
struct channel_user *users; ///< Channel users
struct strv names_buf; ///< Buffer for RPL_NAMREPLY
size_t users_len; ///< User count
bool left_manually; ///< Don't rejoin on reconnect
bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES
};
static struct ispect_field g_channel_ispect[] =
{
ISPECT( channel, name, STRING )
ISPECT( channel, topic, STRING )
ISPECT( channel, no_param_modes, STR )
ISPECT_MAP( channel, param_modes, STRING )
ISPECT( channel, users_len, SIZE )
ISPECT( channel, left_manually, BOOL )
{}
};
static struct channel *
channel_new (struct server *s, char *name)
{
struct channel *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
self->s = s;
self->name = name;
self->no_param_modes = str_make ();
self->param_modes = str_map_make (free);
self->names_buf = strv_make ();
return self;
}
static void
channel_destroy (struct channel *self)
{
free (self->name);
free (self->topic);
str_free (&self->no_param_modes);
str_map_free (&self->param_modes);
// Owner has to make sure we have no users by now
hard_assert (!self->users && !self->users_len);
strv_free (&self->names_buf);
free (self);
}
REF_COUNTABLE_METHODS (channel)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum formatter_item_type
{
FORMATTER_ITEM_END, ///< Sentinel value for arrays
FORMATTER_ITEM_TEXT, ///< Text
FORMATTER_ITEM_ATTR, ///< Formatting attributes
FORMATTER_ITEM_FG_COLOR, ///< Foreground colour
FORMATTER_ITEM_BG_COLOR, ///< Background colour
FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
};
struct formatter_item
{
enum formatter_item_type type : 16; ///< Type of this item
int attribute : 16; ///< Attribute ID
int color; ///< Colour
char *text; ///< String
};
static void
formatter_item_free (struct formatter_item *self)
{
free (self->text);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct formatter
{
struct app_context *ctx; ///< Application context
struct server *s; ///< Server
struct formatter_item *items; ///< Items
size_t items_len; ///< Items used
size_t items_alloc; ///< Items allocated
};
static struct formatter
formatter_make (struct app_context *ctx, struct server *s)
{
struct formatter self = { .ctx = ctx, .s = s };
self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16));
return self;
}
static void
formatter_free (struct formatter *self)
{
for (size_t i = 0; i < self->items_len; i++)
formatter_item_free (&self->items[i]);
free (self->items);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum buffer_line_flags
{
BUFFER_LINE_STATUS = 1 << 0, ///< Status message
BUFFER_LINE_ERROR = 1 << 1, ///< Error message
BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this
BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file
BUFFER_LINE_INDENT = 1 << 4, ///< Just indent the line
BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam
};
struct buffer_line
{
LIST_HEADER (struct buffer_line)
int flags; ///< Flags
time_t when; ///< Time of the event
struct formatter_item items[]; ///< Line data
};
/// Create a new buffer line stealing all data from the provided formatter
struct buffer_line *
buffer_line_new (struct formatter *f)
{
// We make space for one more item that gets initialized to all zeros,
// meaning FORMATTER_ITEM_END (because it's the first value in the enum)
size_t items_size = f->items_len * sizeof *f->items;
struct buffer_line *self =
xcalloc (1, sizeof *self + items_size + sizeof *self->items);
memcpy (self->items, f->items, items_size);
// We've stolen pointers from the formatter, let's destroy it altogether
free (f->items);
memset (f, 0, sizeof *f);
return self;
}
static void
buffer_line_destroy (struct buffer_line *self)
{
for (struct formatter_item *iter = self->items; iter->type; iter++)
formatter_item_free (iter);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum buffer_type
{
BUFFER_GLOBAL, ///< Global information
BUFFER_SERVER, ///< Server-related messages
BUFFER_CHANNEL, ///< Channels
BUFFER_PM ///< Private messages (query)
};
struct buffer
{
LIST_HEADER (struct buffer)
REF_COUNTABLE_HEADER
enum buffer_type type; ///< Type of the buffer
char *name; ///< The name of the buffer
struct input *input; ///< API for "input_data"
input_buffer_t input_data; ///< User interface data
// Buffer contents:
struct buffer_line *lines; ///< All lines in this buffer
struct buffer_line *lines_tail; ///< The tail of buffer lines
unsigned lines_count; ///< How many lines we have
unsigned new_messages_count; ///< # messages since last left
unsigned new_unimportant_count; ///< How much of that is unimportant
bool highlighted; ///< We've been highlighted
bool hide_unimportant; ///< Hide unimportant messages
FILE *log_file; ///< Log file
// Origin information:
struct server *server; ///< Reference to server
struct channel *channel; ///< Reference to channel
struct user *user; ///< Reference to user
};
static struct ispect_field g_server_ispect[];
static struct ispect_field g_buffer_ispect[] =
{
ISPECT( buffer, name, STRING )
ISPECT( buffer, new_messages_count, UINT )
ISPECT( buffer, new_unimportant_count, UINT )
ISPECT( buffer, highlighted, BOOL )
ISPECT( buffer, hide_unimportant, BOOL )
ISPECT_REF( buffer, server, false, server )
ISPECT_REF( buffer, channel, false, channel )
ISPECT_REF( buffer, user, false, user )
{}
};
static struct buffer *
buffer_new (struct input *input, enum buffer_type type, char *name)
{
struct buffer *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
self->input = input;
self->input_data = CALL (input, buffer_new);
self->type = type;
self->name = name;
return self;
}
static void
buffer_destroy (struct buffer *self)
{
free (self->name);
if (self->input_data)
{
#ifdef HAVE_READLINE
// FIXME: can't really free "history" contents from here, as we cannot
// be sure that the user interface pointer is valid and usable
input_rl__buffer_destroy_wo_history (self->input_data);
#else // ! HAVE_READLINE
CALL_ (self->input, buffer_destroy, self->input_data);
#endif // ! HAVE_READLINE
}
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
buffer_line_destroy (iter);
if (self->log_file)
(void) fclose (self->log_file);
if (self->user)
user_unref (self->user);
if (self->channel)
channel_unref (self->channel);
free (self);
}
REF_COUNTABLE_METHODS (buffer)
#define buffer_ref do_not_use_dangerous
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The only real purpose of this is to abstract away TLS
struct transport
{
/// Initialize the transport
bool (*init) (struct server *s, const char *hostname, struct error **e);
/// Destroy the user data pointer
void (*cleanup) (struct server *s);
/// The underlying socket may have become readable, update `read_buffer'
enum socket_io_result (*try_read) (struct server *s);
/// The underlying socket may have become writeable, flush `write_buffer'
enum socket_io_result (*try_write) (struct server *s);
/// Return event mask to use in the poller
int (*get_poll_events) (struct server *s);
/// Called just before closing the connection from our side
void (*in_before_shutdown) (struct server *s);
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum server_state
{
IRC_DISCONNECTED, ///< Not connected
IRC_CONNECTING, ///< Connecting to the server
IRC_CONNECTED, ///< Trying to register
IRC_REGISTERED, ///< We can chat now
IRC_CLOSING, ///< Flushing output before shutdown
IRC_HALF_CLOSED ///< Connection shutdown from our side
};
/// Convert an IRC identifier character to lower-case
typedef int (*irc_tolower_fn) (int);
/// Key conversion function for hashmap lookups
typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t);
struct server
{
REF_COUNTABLE_HEADER
struct app_context *ctx; ///< Application context
char *name; ///< Server identifier
struct buffer *buffer; ///< The buffer for this server
struct config_item *config; ///< Configuration root
// Connection:
enum server_state state; ///< Connection state
struct connector *connector; ///< Connection establisher
struct socks_connector *socks_conn; ///< SOCKS connection establisher
unsigned reconnect_attempt; ///< Number of reconnect attempt
bool manual_disconnect; ///< Don't reconnect after disconnect
int socket; ///< Socket FD of the server
struct str read_buffer; ///< Input yet to be processed
struct str write_buffer; ///< Outut yet to be be sent out
struct poller_fd socket_event; ///< We can read from the socket
struct transport *transport; ///< Transport method
void *transport_data; ///< Transport data
// Events:
struct poller_timer ping_tmr; ///< We should send a ping
struct poller_timer timeout_tmr; ///< Connection seems to be dead
struct poller_timer reconnect_tmr; ///< We should reconnect now
struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate
// IRC:
// TODO: an output queue to prevent excess floods (this will be needed
// especially for away status polling)
bool rehashing; ///< Rehashing IRC identifiers
struct str_map irc_users; ///< IRC user data
struct str_map irc_channels; ///< IRC channel data
struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers
struct user *irc_user; ///< Our own user
int nick_counter; ///< Iterates "nicks" when registering
struct str irc_user_mode; ///< Our current user modes
char *irc_user_host; ///< Our current user@host
bool autoaway_active; ///< Autoaway is currently active
struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS
bool cap_echo_message; ///< Whether the server echoes messages
bool cap_away_notify; ///< Whether we get AWAY notifications
bool cap_sasl; ///< Whether SASL is available
// Server-specific information (from RPL_ISUPPORT):
irc_tolower_fn irc_tolower; ///< Server tolower()
irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm()
char *irc_chantypes; ///< Channel types (name prefixes)
char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
char *irc_statusmsg; ///< Prefixes for channel targets
char irc_extban_prefix; ///< EXTBAN prefix or \0
char *irc_extban_types; ///< EXTBAN types
char *irc_chanmodes_list; ///< Channel modes for lists
char *irc_chanmodes_param_always; ///< Channel modes with mandatory param
char *irc_chanmodes_param_when_set; ///< Channel modes with param when set
char *irc_chanmodes_param_never; ///< Channel modes without param
char *irc_chanuser_prefixes; ///< Channel user prefixes
char *irc_chanuser_modes; ///< Channel user modes
unsigned irc_max_modes; ///< Max parametrized modes per command
};
static struct ispect_field g_server_ispect[] =
{
ISPECT( server, name, STRING )
ISPECT( server, state, INT )
ISPECT( server, reconnect_attempt, UINT )
ISPECT( server, manual_disconnect, BOOL )
ISPECT( server, irc_user_host, STRING )
ISPECT( server, autoaway_active, BOOL )
ISPECT( server, cap_echo_message, BOOL )
ISPECT_REF( server, buffer, false, buffer )
// TODO: either rename the underlying field or fix the plugins
{ "user", offsetof (struct server, irc_user),
ISPECT_REF, 0, g_user_ispect, false },
{ "user_mode", offsetof (struct server, irc_user_mode),
ISPECT_STR, 0, NULL, false },
{}
};
static void on_irc_timeout (void *user_data);
static void on_irc_ping_timeout (void *user_data);
static void on_irc_autojoin_timeout (void *user_data);
static void irc_initiate_connect (struct server *s);
static void
server_init_specifics (struct server *self)
{
// Defaults as per the RPL_ISUPPORT drafts, or RFC 1459
self->irc_tolower = irc_tolower;
self->irc_strxfrm = irc_strxfrm;
self->irc_chantypes = xstrdup ("#&");
self->irc_idchan_prefixes = xstrdup ("");
self->irc_statusmsg = xstrdup ("");
self->irc_extban_prefix = 0;
self->irc_extban_types = xstrdup ("");
self->irc_chanmodes_list = xstrdup ("b");
self->irc_chanmodes_param_always = xstrdup ("k");
self->irc_chanmodes_param_when_set = xstrdup ("l");
self->irc_chanmodes_param_never = xstrdup ("imnpst");
self->irc_chanuser_prefixes = xstrdup ("@+");
self->irc_chanuser_modes = xstrdup ("ov");
self->irc_max_modes = 3;
}
static void
server_free_specifics (struct server *self)
{
free (self->irc_chantypes);
free (self->irc_idchan_prefixes);
free (self->irc_statusmsg);
free (self->irc_extban_types);
free (self->irc_chanmodes_list);
free (self->irc_chanmodes_param_always);
free (self->irc_chanmodes_param_when_set);
free (self->irc_chanmodes_param_never);
free (self->irc_chanuser_prefixes);
free (self->irc_chanuser_modes);
}
static struct server *
server_new (struct poller *poller)
{
struct server *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
self->socket = -1;
self->read_buffer = str_make ();
self->write_buffer = str_make ();
self->state = IRC_DISCONNECTED;
self->timeout_tmr = poller_timer_make (poller);
self->timeout_tmr.dispatcher = on_irc_timeout;
self->timeout_tmr.user_data = self;
self->ping_tmr = poller_timer_make (poller);
self->ping_tmr.dispatcher = on_irc_ping_timeout;
self->ping_tmr.user_data = self;
self->reconnect_tmr = poller_timer_make (poller);
self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
self->reconnect_tmr.user_data = self;
self->autojoin_tmr = poller_timer_make (poller);
self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
self->autojoin_tmr.user_data = self;
self->irc_users = str_map_make (NULL);
self->irc_users.key_xfrm = irc_strxfrm;
self->irc_channels = str_map_make (NULL);
self->irc_channels.key_xfrm = irc_strxfrm;
self->irc_buffer_map = str_map_make (NULL);
self->irc_buffer_map.key_xfrm = irc_strxfrm;
self->irc_user_mode = str_make ();
self->cap_ls_buf = strv_make ();
server_init_specifics (self);
return self;
}
static void
server_destroy (struct server *self)
{
free (self->name);
if (self->connector)
{
connector_free (self->connector);
free (self->connector);
}
if (self->socks_conn)
{
socks_connector_free (self->socks_conn);
free (self->socks_conn);
}
if (self->transport
&& self->transport->cleanup)
self->transport->cleanup (self);
if (self->socket != -1)
{
poller_fd_reset (&self->socket_event);
xclose (self->socket);
}
str_free (&self->read_buffer);
str_free (&self->write_buffer);
poller_timer_reset (&self->ping_tmr);
poller_timer_reset (&self->timeout_tmr);
poller_timer_reset (&self->reconnect_tmr);
poller_timer_reset (&self->autojoin_tmr);
str_map_free (&self->irc_users);
str_map_free (&self->irc_channels);
str_map_free (&self->irc_buffer_map);
if (self->irc_user)
user_unref (self->irc_user);
str_free (&self->irc_user_mode);
free (self->irc_user_host);
strv_free (&self->cap_ls_buf);
server_free_specifics (self);
free (self);
}
REF_COUNTABLE_METHODS (server)
#define server_ref do_not_use_dangerous
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct plugin
{
LIST_HEADER (struct plugin)
char *name; ///< Name of the plugin
struct plugin_vtable *vtable; ///< Methods
};
struct plugin_vtable
{
/// Collect garbage
void (*gc) (struct plugin *self);
/// Unregister and free the plugin including all relevant resources
void (*free) (struct plugin *self);
};
static void
plugin_destroy (struct plugin *self)
{
self->vtable->free (self);
free (self->name);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This is a bit ugly since insertion is O(n) and the need to get rid of the
// specific type because of list macros, however I don't currently posses any
// strictly better, ordered data structure
struct hook
{
LIST_HEADER (struct hook)
int priority; ///< The lesser the sooner
};
static struct hook *
hook_insert (struct hook *list, struct hook *item)
{
// Corner cases: list is empty or we precede everything
if (!list || item->priority < list->priority)
{
LIST_PREPEND (list, item);
return list;
}
// Otherwise fast-forward to the last entry that precedes us
struct hook *before = list;
while (before->next && before->next->priority < item->priority)
before = before->next;
// And link ourselves in between it and its successor
if ((item->next = before->next))
item->next->prev = item;
before->next = item;
item->prev = before;
return list;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct input_hook
{
struct hook super; ///< Common hook fields
/// Takes over the ownership of "input", returns either NULL if input
/// was thrown away, or a possibly modified version of it
char *(*filter) (struct input_hook *self,
struct buffer *buffer, char *input);
};
struct irc_hook
{
struct hook super; ///< Common hook fields
/// Takes over the ownership of "message", returns either NULL if message
/// was thrown away, or a possibly modified version of it
char *(*filter) (struct irc_hook *self,
struct server *server, char *message);
};
struct prompt_hook
{
struct hook super; ///< Common hook fields
/// Returns what the prompt should look like right now based on other state
char *(*make) (struct prompt_hook *self);
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct completion_word
{
size_t start; ///< Offset to start of word
size_t end; ///< Offset to end of word
};
struct completion
{
char *line; ///< The line which is being completed
struct completion_word *words; ///< Word locations
size_t words_len; ///< Number of words
size_t words_alloc; ///< Number of words allocated
size_t location; ///< Which word is being completed
};
struct completion_hook
{
struct hook super; ///< Common hook fields
/// Tries to add possible completions of "word" to "output"
void (*complete) (struct completion_hook *self,
struct completion *data, const char *word, struct strv *output);
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct app_context
{
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
// Configuration:
struct config config; ///< Program configuration
char *attrs[ATTR_COUNT]; ///< Terminal attributes
bool isolate_buffers; ///< Isolate global/server buffers
bool beep_on_highlight; ///< Beep on highlight
bool logging; ///< Logging to file enabled
bool show_all_prefixes; ///< Show all prefixes before nicks
bool word_wrapping; ///< Enable simple word wrapping
struct str_map servers; ///< Our servers
// Events:
struct poller_fd tty_event; ///< Terminal input event
struct poller_fd signal_event; ///< Signal FD event
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
struct poller_timer date_chg_tmr; ///< Print a date change
struct poller_timer autoaway_tmr; ///< Autoaway timer
struct poller poller; ///< Manages polled descriptors
bool quitting; ///< User requested quitting
bool polling; ///< The event loop is running
// Buffers:
struct buffer *buffers; ///< All our buffers in order
struct buffer *buffers_tail; ///< The tail of our buffers
struct buffer *global_buffer; ///< The global buffer
struct buffer *current_buffer; ///< The current buffer
struct buffer *last_buffer; ///< Last used buffer
struct str_map buffers_by_name; ///< Buffers by name
unsigned backlog_limit; ///< Limit for buffer lines
time_t last_displayed_msg_time; ///< Time of last displayed message
// Terminal:
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
struct input *input; ///< User interface
struct poller_idle prompt_event; ///< Deferred prompt refresh
struct poller_idle input_event; ///< Pending input event
struct strv pending_input; ///< Pending input lines
int *nick_palette; ///< A 256-colour palette for nicknames
size_t nick_palette_len; ///< Number of entries in nick_palette
bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape
bool in_bracketed_paste; ///< User is pasting some content
struct str input_buffer; ///< Buffered pasted content
bool running_backlog_helper; ///< Running a backlog helper
bool running_editor; ///< Running editor for the input
char *editor_filename; ///< The file being edited by user
int terminal_suspended; ///< Terminal suspension level
struct plugin *plugins; ///< Loaded plugins
struct hook *input_hooks; ///< Input hooks
struct hook *irc_hooks; ///< IRC hooks
struct hook *prompt_hooks; ///< Prompt hooks
struct hook *completion_hooks; ///< Autocomplete hooks
}
*g_ctx;
static struct ispect_field g_ctx_ispect[] =
{
ISPECT_MAP_REF( app_context, servers, false, server )
ISPECT_REF( app_context, buffers, true, buffer )
ISPECT_REF( app_context, global_buffer, false, buffer )
ISPECT_REF( app_context, current_buffer, false, buffer )
{}
};
static int *
filter_color_cube_for_acceptable_nick_colors (size_t *len)
{
// This is a pure function and we don't use threads, static storage is fine
static int table[6 * 6 * 6];
size_t len_counter = 0;
for (int x = 0; x < (int) N_ELEMENTS (table); x++)
{
int r = x / 36;
int g = (x / 6) % 6;
int b = (x % 6);
// The first step is 95/255, the rest are 40/255,
// as an approximation we can double the first step
double linear_R = pow ((r + !!r) / 6., 2.2);
double linear_G = pow ((g + !!g) / 6., 2.2);
double linear_B = pow ((b + !!b) / 6., 2.2);
// Use the relative luminance of colours within the cube to filter
// colours that look okay-ish on terminals with both black and white
// backgrounds (use the test-nick-colors script to calibrate)
double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
if (Y >= .25 && Y <= .4)
table[len_counter++] = 16 + x;
}
*len = len_counter;
return table;
}
static bool
app_iconv_open (iconv_t *target, const char *to, const char *from)
{
if (ICONV_ACCEPTS_TRANSLIT)
{
char *to_real = xstrdup_printf ("%s//TRANSLIT", to);
*target = iconv_open (to_real, from);
free (to_real);
}
else
*target = iconv_open (to, from);
return *target != (iconv_t) -1;
}
static void
app_context_init (struct app_context *self)
{
memset (self, 0, sizeof *self);
self->config = config_make ();
poller_init (&self->poller);
self->servers = str_map_make ((str_map_free_fn) server_unref);
self->servers.key_xfrm = tolower_ascii_strxfrm;
self->buffers_by_name = str_map_make (NULL);
self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
// So that we don't lose the logo shortly after startup
self->backlog_limit = 1000;
self->last_displayed_msg_time = time (NULL);
char *native = nl_langinfo (CODESET);
if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8")
|| !app_iconv_open (&self->term_to_utf8, "UTF-8", native))
exit_fatal ("creating the UTF-8 conversion object failed: %s",
strerror (errno));
self->input = input_new ();
self->input->user_data = self;
self->pending_input = strv_make ();
self->input_buffer = str_make ();
self->nick_palette =
filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
}
static void
app_context_free (struct app_context *self)
{
// Plugins can try to use of the other fields when destroyed
LIST_FOR_EACH (struct plugin, iter, self->plugins)
plugin_destroy (iter);
config_free (&self->config);
for (size_t i = 0; i < ATTR_COUNT; i++)
{
free (self->attrs_defaults[i]);
free (self->attrs[i]);
}
LIST_FOR_EACH (struct buffer, iter, self->buffers)
{
#ifdef HAVE_READLINE
// We can use the user interface here; see buffer_destroy()
CALL_ (self->input, buffer_destroy, iter->input_data);
iter->input_data = NULL;
#endif // HAVE_READLINE
buffer_unref (iter);
}
str_map_free (&self->buffers_by_name);
str_map_free (&self->servers);
poller_free (&self->poller);
iconv_close (self->term_from_utf8);
iconv_close (self->term_to_utf8);
CALL (self->input, destroy);
strv_free (&self->pending_input);
str_free (&self->input_buffer);
free (self->editor_filename);
}
static void
refresh_prompt (struct app_context *ctx)
{
// XXX: the need for this conditional could probably be resolved
// by some clever reordering
if (ctx->prompt_event.poller)
poller_idle_set (&ctx->prompt_event);
}
// --- Configuration -----------------------------------------------------------
static void
on_config_debug_mode_change (struct config_item *item)
{
g_debug_mode = item->value.boolean;
}
static void
on_config_show_all_prefixes_change (struct config_item *item)
{
struct app_context *ctx = item->user_data;
ctx->show_all_prefixes = item->value.boolean;
refresh_prompt (ctx);
}
static void on_config_backlog_limit_change (struct config_item *item);
static void on_config_attribute_change (struct config_item *item);
static void on_config_logging_change (struct config_item *item);
#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \
static void \
on_config_ ## name ## _change (struct config_item *item) \
{ \
struct app_context *ctx = item->user_data; \
ctx->name = item->value.boolean; \
}
TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers)
TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight)
TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
config_validate_nonjunk_string
(const struct config_item *item, struct error **e)
{
if (item->type == CONFIG_ITEM_NULL)
return true;
hard_assert (config_item_type_is_string (item->type));
for (size_t i = 0; i < item->value.string.len; i++)
{
// Not even a tabulator
unsigned char c = item->value.string.str[i];
if (iscntrl_ascii (c))
{
error_set (e, "control characters are not allowed");
return false;
}
}
return true;
}
static bool
config_validate_addresses
(const struct config_item *item, struct error **e)
{
if (item->type == CONFIG_ITEM_NULL)
return true;
if (!config_validate_nonjunk_string (item, e))
return false;
// Comma-separated list of "host[:port]" pairs
regex_t re;
int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?"
"(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB);
hard_assert (!err);
bool result = !regexec (&re, item->value.string.str, 0, NULL, 0);
if (!result)
error_set (e, "invalid address list string");
regfree (&re);
return result;
}
static bool
config_validate_nonnegative
(const struct config_item *item, struct error **e)
{
if (item->type == CONFIG_ITEM_NULL)
return true;
hard_assert (item->type == CONFIG_ITEM_INTEGER);
if (item->value.integer >= 0)
return true;
error_set (e, "must be non-negative");
return false;
}
static struct config_schema g_config_server[] =
{
{ .name = "nicks",
.comment = "IRC nickname",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_nonjunk_string },
{ .name = "username",
.comment = "IRC user name",
.type = CONFIG_ITEM_STRING,
.validate = config_validate_nonjunk_string },
{ .name = "realname",
.comment = "IRC real name/e-mail",
.type = CONFIG_ITEM_STRING,
.validate = config_validate_nonjunk_string },
{ .name = "addresses",
.comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_addresses },
{ .name = "password",
.comment = "Password to connect to the server, if any",
.type = CONFIG_ITEM_STRING,
.validate = config_validate_nonjunk_string },
// XXX: if we add support for new capabilities, the value stays unchanged
{ .name = "capabilities",
.comment = "Capabilities to use if supported by server",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_nonjunk_string,
.default_ = "\"multi-prefix,invite-notify,server-time,echo-message,"
"message-tags,away-notify,cap-notify,chghost\"" },
{ .name = "tls",
.comment = "Whether to use TLS",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off" },
{ .name = "tls_cert",
.comment = "Client TLS certificate (PEM)",
.type = CONFIG_ITEM_STRING },
{ .name = "tls_verify",
.comment = "Whether to verify certificates",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "tls_ca_file",
.comment = "OpenSSL CA bundle file",
.type = CONFIG_ITEM_STRING },
{ .name = "tls_ca_path",
.comment = "OpenSSL CA bundle path",
.type = CONFIG_ITEM_STRING },
{ .name = "tls_ciphers",
.comment = "OpenSSL cipher preference list",
.type = CONFIG_ITEM_STRING,
.default_ = "\"DEFAULT:!MEDIUM:!LOW\"" },
{ .name = "autoconnect",
.comment = "Connect automatically on startup",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "autojoin",
.comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_nonjunk_string },
{ .name = "command",
.comment = "Command to execute after a successful connect",
.type = CONFIG_ITEM_STRING },
{ .name = "command_delay",
.comment = "Delay between executing \"command\" and joining channels",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "0" },
{ .name = "reconnect",
.comment = "Whether to reconnect on error",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "reconnect_delay",
.comment = "Time between reconnecting",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "5" },
{ .name = "socks_host",
.comment = "Address of a SOCKS 4a/5 proxy",
.type = CONFIG_ITEM_STRING,
.validate = config_validate_nonjunk_string },
{ .name = "socks_port",
.comment = "SOCKS port number",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "1080" },
{ .name = "socks_username",
.comment = "SOCKS auth. username",
.type = CONFIG_ITEM_STRING },
{ .name = "socks_password",
.comment = "SOCKS auth. password",
.type = CONFIG_ITEM_STRING },
{}
};
static struct config_schema g_config_behaviour[] =
{
{ .name = "isolate_buffers",
.comment = "Don't leak messages from the server and global buffers",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_isolate_buffers_change },
{ .name = "beep_on_highlight",
.comment = "Beep when highlighted or on a new invisible PM",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on",
.on_change = on_config_beep_on_highlight_change },
{ .name = "show_all_prefixes",
.comment = "Show all prefixes in front of nicknames",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_show_all_prefixes_change },
{ .name = "word_wrapping",
.comment = "Enable simple word wrapping in buffers",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on",
.on_change = on_config_word_wrapping_change },
{ .name = "editor_command",
.comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\"",
.type = CONFIG_ITEM_STRING },
{ .name = "process_pasted_text",
.comment = "Normalize newlines and quote the command prefix in pastes",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "date_change_line",
.comment = "Input to strftime(3) for the date change line",
.type = CONFIG_ITEM_STRING,
.default_ = "\"%F\"" },
{ .name = "read_marker_char",
.comment = "The character to use for the read marker line",
.type = CONFIG_ITEM_STRING,
.default_ = "\"-\"",
.validate = config_validate_nonjunk_string },
{ .name = "logging",
.comment = "Log buffer contents to file",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_logging_change },
{ .name = "save_on_quit",
.comment = "Save configuration before quitting",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "debug_mode",
.comment = "Produce some debugging output",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_debug_mode_change },
{ .name = "backlog_limit",
.comment = "Maximum number of lines stored in the backlog",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "1000",
.on_change = on_config_backlog_limit_change },
{ .name = "backlog_helper",
.comment = "Shell command to display a buffer's history",
.type = CONFIG_ITEM_STRING,
.default_ = "\"LESSSECURE=1 less -M -R +Gb\"" },
{ .name = "backlog_helper_strip_formatting",
.comment = "Strip formatting from backlog helper input",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off" },
{ .name = "reconnect_delay_growing",
.comment = "Growing factor for reconnect delay",
.type = CONFIG_ITEM_INTEGER,
.validate = config_validate_nonnegative,
.default_ = "2" },
{ .name = "reconnect_delay_max",
.comment = "Maximum reconnect delay in seconds",
.type =