13461 lines
		
	
	
		
			364 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			13461 lines
		
	
	
		
			364 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * degesch.c: the experimental IRC client
 | |
|  *
 | |
|  * Copyright (c) 2015 - 2016, Přemysl Janouch <p.janouch@gmail.com>
 | |
|  *
 | |
|  * Permission to use, copy, modify, and/or distribute this software for any
 | |
|  * purpose with or without fee is hereby granted, provided that the above
 | |
|  * copyright notice and this permission notice appear in all copies.
 | |
|  *
 | |
|  * 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
 | |
| // FIXME: awful naming, collides with ATTRIBUTE_*
 | |
| #define ATTR_TABLE(XX)                                                         \
 | |
| 	XX( PROMPT,      "prompt",      "Terminal attrs for the prompt"       )    \
 | |
| 	XX( RESET,       "reset",       "String to reset terminal attributes" )    \
 | |
| 	XX( DATE_CHANGE, "date_change", "Terminal attrs for date change"      )    \
 | |
| 	XX( READ_MARKER, "read_marker", "Terminal attrs for the read marker"  )    \
 | |
| 	XX( WARNING,     "warning",     "Terminal attrs for warnings"         )    \
 | |
| 	XX( ERROR,       "error",       "Terminal attrs for errors"           )    \
 | |
| 	XX( EXTERNAL,    "external",    "Terminal attrs for external lines"   )    \
 | |
| 	XX( TIMESTAMP,   "timestamp",   "Terminal attrs for timestamps"       )    \
 | |
| 	XX( HIGHLIGHT,   "highlight",   "Terminal attrs for highlights"       )    \
 | |
| 	XX( ACTION,      "action",      "Terminal attrs for user actions"     )    \
 | |
| 	XX( USERHOST,    "userhost",    "Terminal attrs for user@host"        )    \
 | |
| 	XX( JOIN,        "join",        "Terminal attrs for joins"            )    \
 | |
| 	XX( PART,        "part",        "Terminal attrs for parts"            )
 | |
| 
 | |
| enum
 | |
| {
 | |
| #define XX(x, y, z) ATTR_ ## x,
 | |
| 	ATTR_TABLE (XX)
 | |
| #undef XX
 | |
| 	ATTR_COUNT
 | |
| };
 | |
| 
 | |
| // User data for logger functions to enable formatted logging
 | |
| #define print_fatal_data    ((void *) ATTR_ERROR)
 | |
| #define print_error_data    ((void *) ATTR_ERROR)
 | |
| #define print_warning_data  ((void *) ATTR_WARNING)
 | |
| 
 | |
| #include "config.h"
 | |
| #define PROGRAM_NAME "degesch"
 | |
| 
 | |
| #include "common.c"
 | |
| #include "kike-replies.c"
 | |
| 
 | |
| #include <langinfo.h>
 | |
| #include <locale.h>
 | |
| #include <pwd.h>
 | |
| #include <sys/utsname.h>
 | |
| #include <wchar.h>
 | |
| 
 | |
| #include <termios.h>
 | |
| #include <sys/ioctl.h>
 | |
| 
 | |
| #include <curses.h>
 | |
| #include <term.h>
 | |
| 
 | |
| // Literally cancer
 | |
| #undef lines
 | |
| #undef columns
 | |
| 
 | |
| #include <ffi.h>
 | |
| 
 | |
| #ifdef HAVE_LUA
 | |
| #include <lua.h>
 | |
| #include <lualib.h>
 | |
| #include <lauxlib.h>
 | |
| #endif // HAVE_LUA
 | |
| 
 | |
| // --- Terminal information ----------------------------------------------------
 | |
| 
 | |
| static struct
 | |
| {
 | |
| 	bool initialized;                   ///< Terminal is available
 | |
| 	bool stdout_is_tty;                 ///< `stdout' is a terminal
 | |
| 	bool stderr_is_tty;                 ///< `stderr' is a terminal
 | |
| 
 | |
| 	struct termios termios;             ///< Terminal attributes
 | |
| 	char *color_set_fg[256];            ///< Codes to set the foreground colour
 | |
| 	char *color_set_bg[256];            ///< Codes to set the background colour
 | |
| 
 | |
| 	int lines;                          ///< Number of lines
 | |
| 	int columns;                        ///< Number of columns
 | |
| }
 | |
| g_terminal;
 | |
| 
 | |
| static void
 | |
| update_screen_size (void)
 | |
| {
 | |
| #ifdef TIOCGWINSZ
 | |
| 	if (!g_terminal.stdout_is_tty)
 | |
| 		return;
 | |
| 
 | |
| 	struct winsize size;
 | |
| 	if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
 | |
| 	{
 | |
| 		char *row = getenv ("LINES");
 | |
| 		char *col = getenv ("COLUMNS");
 | |
| 		unsigned long tmp;
 | |
| 		g_terminal.lines =
 | |
| 			(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row;
 | |
| 		g_terminal.columns =
 | |
| 			(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col;
 | |
| 	}
 | |
| #endif // TIOCGWINSZ
 | |
| }
 | |
| 
 | |
| static bool
 | |
| init_terminal (void)
 | |
| {
 | |
| 	int tty_fd = -1;
 | |
| 	if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
 | |
| 		tty_fd = STDERR_FILENO;
 | |
| 	if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
 | |
| 		tty_fd = STDOUT_FILENO;
 | |
| 
 | |
| 	int err;
 | |
| 	if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
 | |
| 		return false;
 | |
| 
 | |
| 	// Make sure all terminal features used by us are supported
 | |
| 	if (!set_a_foreground || !set_a_background
 | |
| 	 || !enter_bold_mode || !exit_attribute_mode
 | |
| 	 || tcgetattr (tty_fd, &g_terminal.termios))
 | |
| 	{
 | |
| 		del_curterm (cur_term);
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	// Make sure newlines are output correctly
 | |
| 	g_terminal.termios.c_oflag |= ONLCR;
 | |
| 	(void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios);
 | |
| 
 | |
| 	g_terminal.lines   = tigetnum ("lines");
 | |
| 	g_terminal.columns = tigetnum ("cols");
 | |
| 	update_screen_size ();
 | |
| 
 | |
| 	int max = MIN (256, max_colors);
 | |
| 	for (int i = 0; i < max; i++)
 | |
| 	{
 | |
| 		g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
 | |
| 			i, 0, 0, 0, 0, 0, 0, 0, 0));
 | |
| 		g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
 | |
| 			i, 0, 0, 0, 0, 0, 0, 0, 0));
 | |
| 	}
 | |
| 	return g_terminal.initialized = true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| free_terminal (void)
 | |
| {
 | |
| 	if (!g_terminal.initialized)
 | |
| 		return;
 | |
| 
 | |
| 	for (int i = 0; i < 256; i++)
 | |
| 	{
 | |
| 		free (g_terminal.color_set_fg[i]);
 | |
| 		free (g_terminal.color_set_bg[i]);
 | |
| 	}
 | |
| 	del_curterm (cur_term);
 | |
| }
 | |
| 
 | |
| // --- User interface ----------------------------------------------------------
 | |
| 
 | |
| // I'm not sure which one of these backends is worse: whether it's GNU Readline
 | |
| // or BSD Editline.  They both have their own annoying problems.  We use lots
 | |
| // of hacks to get the results we want and need.
 | |
| //
 | |
| // The abstraction is a necessary evil.  It's still not 100%, though.
 | |
| 
 | |
| /// Some arbitrary limit for the history
 | |
| #define HISTORY_LIMIT 10000
 | |
| 
 | |
| /// Characters that separate words
 | |
| #define WORD_BREAKING_CHARS " \f\n\r\t\v"
 | |
| 
 | |
| struct input
 | |
| {
 | |
| 	struct input_vtable *vtable;        ///< Virtual methods
 | |
| 	void (*add_functions) (void *);     ///< Define functions for binding
 | |
| 	void *user_data;                    ///< User data for callbacks
 | |
| };
 | |
| 
 | |
| typedef void *input_buffer_t;           ///< Pointer alias for input buffers
 | |
| 
 | |
| /// Named function that can be bound to a sequence of characters
 | |
| typedef bool (*input_fn) (int count, int key, void *user_data);
 | |
| 
 | |
| // A little bit better than tons of forwarder functions in our case
 | |
| #define CALL(self, name)        ((self)->vtable->name ((self)))
 | |
| #define CALL_(self, name, ...)  ((self)->vtable->name ((self), __VA_ARGS__))
 | |
| 
 | |
| struct input_vtable
 | |
| {
 | |
| 	/// Start the interface under the given program name
 | |
| 	void (*start) (void *input, const char *program_name);
 | |
| 	/// Stop the interface
 | |
| 	void (*stop) (void *input);
 | |
| 	/// Prepare or unprepare terminal for our needs
 | |
| 	void (*prepare) (void *input, bool enabled);
 | |
| 	/// Destroy the object
 | |
| 	void (*destroy) (void *input);
 | |
| 
 | |
| 	/// Hide the prompt if shown
 | |
| 	void (*hide) (void *input);
 | |
| 	/// Show the prompt if hidden
 | |
| 	void (*show) (void *input);
 | |
| 	/// Retrieve current prompt string
 | |
| 	const char *(*get_prompt) (void *input);
 | |
| 	/// Change the prompt string; takes ownership
 | |
| 	void (*set_prompt) (void *input, char *prompt);
 | |
| 	/// Ring the terminal bell
 | |
| 	void (*ding) (void *input);
 | |
| 
 | |
| 	/// Create a new input buffer
 | |
| 	input_buffer_t (*buffer_new) (void *input);
 | |
| 	/// Destroy an input buffer
 | |
| 	void (*buffer_destroy) (void *input, input_buffer_t buffer);
 | |
| 	/// Switch to a different input buffer
 | |
| 	void (*buffer_switch) (void *input, input_buffer_t buffer);
 | |
| 
 | |
| 	/// Register a function that can be bound to character sequences
 | |
| 	void (*register_fn) (void *input,
 | |
| 		const char *name, const char *help, input_fn fn, void *user_data);
 | |
| 	/// Bind an arbitrary sequence of characters to the given named function
 | |
| 	void (*bind) (void *input, const char *seq, const char *fn);
 | |
| 	/// Bind Ctrl+key to the given named function
 | |
| 	void (*bind_control) (void *input, char key, const char *fn);
 | |
| 	/// Bind Alt+key to the given named function
 | |
| 	void (*bind_meta) (void *input, char key, const char *fn);
 | |
| 
 | |
| 	/// Get the current line input
 | |
| 	char *(*get_line) (void *input);
 | |
| 	/// Clear the current line input
 | |
| 	void (*clear_line) (void *input);
 | |
| 	/// Insert text at current position
 | |
| 	bool (*insert) (void *input, const char *text);
 | |
| 
 | |
| 	/// Handle terminal resize
 | |
| 	void (*on_tty_resized) (void *input);
 | |
| 	/// Handle terminal input
 | |
| 	void (*on_tty_readable) (void *input);
 | |
| };
 | |
| 
 | |
| #define INPUT_VTABLE(XX)                                                       \
 | |
| 	XX (start) XX (stop) XX (prepare) XX (destroy)                             \
 | |
| 	XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding)              \
 | |
| 	XX (buffer_new) XX (buffer_destroy) XX (buffer_switch)                     \
 | |
| 	XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta)                \
 | |
| 	XX (get_line) XX (clear_line) XX (insert)                                  \
 | |
| 	XX (on_tty_resized) XX (on_tty_readable)
 | |
| 
 | |
| // --- GNU Readline ------------------------------------------------------------
 | |
| 
 | |
| #ifdef HAVE_READLINE
 | |
| 
 | |
| #include <readline/readline.h>
 | |
| #include <readline/history.h>
 | |
| 
 | |
| #define INPUT_START_IGNORE  RL_PROMPT_START_IGNORE
 | |
| #define INPUT_END_IGNORE    RL_PROMPT_END_IGNORE
 | |
| 
 | |
| struct input_rl_fn
 | |
| {
 | |
| 	ffi_closure closure;                ///< Closure
 | |
| 
 | |
| 	LIST_HEADER (struct input_rl_fn)
 | |
| 	input_fn callback;                  ///< Real callback
 | |
| 	void *user_data;                    ///< Real callback user data
 | |
| };
 | |
| 
 | |
| struct input_rl_buffer
 | |
| {
 | |
| 	HISTORY_STATE *history;             ///< Saved history state
 | |
| 	char *saved_line;                   ///< Saved line content
 | |
| 	int saved_point;                    ///< Saved cursor position
 | |
| 	int saved_mark;                     ///< Saved mark
 | |
| };
 | |
| 
 | |
| struct input_rl
 | |
| {
 | |
| 	struct input super;                 ///< Parent class
 | |
| 
 | |
| 	bool active;                        ///< Interface has been started
 | |
| 	char *prompt;                       ///< The prompt we use
 | |
| 	int prompt_shown;                   ///< Whether the prompt is shown now
 | |
| 
 | |
| 	char *saved_line;                   ///< Saved line content
 | |
| 	int saved_point;                    ///< Saved cursor position
 | |
| 	int saved_mark;                     ///< Saved mark
 | |
| 
 | |
| 	struct input_rl_fn *fns;            ///< Named functions
 | |
| 	struct input_rl_buffer *current;    ///< Current input buffer
 | |
| };
 | |
| 
 | |
| static void
 | |
| input_rl_ding (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	rl_ding ();
 | |
| }
 | |
| 
 | |
| static const char *
 | |
| input_rl_get_prompt (void *input)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	return self->prompt;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_set_prompt (void *input, char *prompt)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	free (self->prompt);
 | |
| 	self->prompt = prompt;
 | |
| 
 | |
| 	if (!self->active)
 | |
| 		return;
 | |
| 
 | |
| 	// First reset the prompt to work around a bug in readline
 | |
| 	rl_set_prompt ("");
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		rl_redisplay ();
 | |
| 
 | |
| 	rl_set_prompt (self->prompt);
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		rl_redisplay ();
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_clear_line (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	rl_replace_line ("", false);
 | |
| 	rl_redisplay ();
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl__erase (struct input_rl *self)
 | |
| {
 | |
| 	rl_set_prompt ("");
 | |
| 	input_rl_clear_line (self);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| input_rl_insert (void *input, const char *s)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	rl_insert_text (s);
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		rl_redisplay ();
 | |
| 
 | |
| 	// GNU Readline, contrary to Editline, doesn't care about validity
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static char *
 | |
| input_rl_get_line (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	return rl_copy_text (0, rl_end);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_rl_bind (void *input, const char *seq, const char *function_name)
 | |
| {
 | |
| 	(void) input;
 | |
| 	rl_bind_keyseq (seq, rl_named_function (function_name));
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_bind_meta (void *input, char key, const char *function_name)
 | |
| {
 | |
| 	// This one seems to actually work
 | |
| 	char keyseq[] = { '\\', 'e', key, 0 };
 | |
| 	input_rl_bind (input, keyseq, function_name);
 | |
| #if 0
 | |
| 	// While this one only fucks up UTF-8
 | |
| 	// Tested with urxvt and xterm, on Debian Jessie/Arch, default settings
 | |
| 	// \M-<key> behaves exactly the same
 | |
| 	rl_bind_key (META (key), rl_named_function (function_name));
 | |
| #endif
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_bind_control (void *input, char key, const char *function_name)
 | |
| {
 | |
| 	char keyseq[] = { '\\', 'C', '-', key, 0 };
 | |
| 	input_rl_bind (input, keyseq, function_name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
 | |
| {
 | |
| 	(void) cif;
 | |
| 
 | |
| 	struct input_rl_fn *data = user_data;
 | |
| 	if (!data->callback
 | |
| 		(*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data))
 | |
| 		rl_ding ();
 | |
| 	*(int *) ret = 0;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_register_fn (void *input,
 | |
| 	const char *name, const char *help, input_fn callback, void *user_data)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	(void) help;
 | |
| 
 | |
| 	void *bound_fn = NULL;
 | |
| 	struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
 | |
| 	hard_assert (data);
 | |
| 
 | |
| 	static ffi_cif cif;
 | |
| 	static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint };
 | |
| 	hard_assert (ffi_prep_cif
 | |
| 		(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK);
 | |
| 
 | |
| 	data->prev = data->next = NULL;
 | |
| 	data->callback = callback;
 | |
| 	data->user_data = user_data;
 | |
| 	hard_assert (ffi_prep_closure_loc (&data->closure,
 | |
| 		&cif, input_rl__forward, data, bound_fn) == FFI_OK);
 | |
| 
 | |
| 	rl_add_defun (name, (rl_command_func_t *) bound_fn, -1);
 | |
| 	LIST_PREPEND (self->fns, data);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static int app_readline_init (void);
 | |
| static void on_readline_input (char *line);
 | |
| static char **app_readline_completion (const char *text, int start, int end);
 | |
| 
 | |
| static void
 | |
| input_rl_start (void *input, const char *program_name)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	using_history ();
 | |
| 	// This can cause memory leaks, or maybe even a segfault.  Funny, eh?
 | |
| 	stifle_history (HISTORY_LIMIT);
 | |
| 
 | |
| 	const char *slash = strrchr (program_name, '/');
 | |
| 	rl_readline_name = slash ? ++slash : program_name;
 | |
| 	rl_startup_hook = app_readline_init;
 | |
| 	rl_catch_sigwinch = false;
 | |
| 
 | |
| 	rl_basic_word_break_characters = WORD_BREAKING_CHARS;
 | |
| 	rl_completer_word_break_characters = NULL;
 | |
| 	rl_attempted_completion_function = app_readline_completion;
 | |
| 
 | |
| 	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;
 | |
| 		free (buffer->saved_line);
 | |
| 		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;
 | |
| 	free (self->saved_line);
 | |
| 	self->saved_line = NULL;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_rl_hide (void *input)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	if (!self->active || self->prompt_shown-- < 1)
 | |
| 		return;
 | |
| 
 | |
| 	input_rl__save (self);
 | |
| 	input_rl__erase (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_show (void *input)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	if (!self->active || ++self->prompt_shown < 1)
 | |
| 		return;
 | |
| 
 | |
| 	input_rl__restore (self);
 | |
| 	rl_redisplay ();
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_rl_on_tty_resized (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	// This fucks up big time on terminals with automatic wrapping such as
 | |
| 	// rxvt-unicode or newer VTE when the current line overflows, however we
 | |
| 	// can't do much about that
 | |
| 	rl_resize_terminal ();
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_rl_on_tty_readable (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	rl_callback_read_char ();
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_rl_destroy (void *input)
 | |
| {
 | |
| 	struct input_rl *self = input;
 | |
| 	free (self->saved_line);
 | |
| 	LIST_FOR_EACH (struct input_rl_fn, iter, self->fns)
 | |
| 		ffi_closure_free (iter);
 | |
| 	free (self->prompt);
 | |
| 	free (self);
 | |
| }
 | |
| 
 | |
| #define XX(a) .a = input_rl_ ## a,
 | |
| static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
 | |
| #undef XX
 | |
| 
 | |
| static struct input *
 | |
| input_rl_new (void)
 | |
| {
 | |
| 	struct input_rl *self = xcalloc (1, sizeof *self);
 | |
| 	self->super.vtable = &input_rl_vtable;
 | |
| 	return &self->super;
 | |
| }
 | |
| 
 | |
| #define input_new input_rl_new
 | |
| #endif // HAVE_READLINE
 | |
| 
 | |
| // --- BSD Editline ------------------------------------------------------------
 | |
| 
 | |
| #ifdef HAVE_EDITLINE
 | |
| 
 | |
| #include <histedit.h>
 | |
| 
 | |
| #define INPUT_START_IGNORE  '\x01'
 | |
| #define INPUT_END_IGNORE    '\x01'
 | |
| 
 | |
| struct input_el_fn
 | |
| {
 | |
| 	ffi_closure closure;                ///< Closure
 | |
| 
 | |
| 	LIST_HEADER (struct input_el_fn)
 | |
| 	input_fn callback;                  ///< Real callback
 | |
| 	void *user_data;                    ///< Real callback user data
 | |
| 
 | |
| 	wchar_t *name;                      ///< Function name
 | |
| 	wchar_t *help;                      ///< Function help
 | |
| };
 | |
| 
 | |
| struct input_el_buffer
 | |
| {
 | |
| 	HistoryW *history;                  ///< The history object
 | |
| 	wchar_t *saved_line;                ///< Saved line content
 | |
| 	int saved_len;                      ///< Length of the saved line
 | |
| 	int saved_point;                    ///< Saved cursor position
 | |
| };
 | |
| 
 | |
| struct input_el
 | |
| {
 | |
| 	struct input super;                 ///< Parent class
 | |
| 	EditLine *editline;                 ///< The EditLine object
 | |
| 
 | |
| 	bool active;                        ///< Are we a thing?
 | |
| 	char *prompt;                       ///< The prompt we use
 | |
| 	int prompt_shown;                   ///< Whether the prompt is shown now
 | |
| 
 | |
| 	struct input_el_fn *fns;            ///< Named functions
 | |
| 	struct input_el_buffer *current;    ///< Current input buffer
 | |
| };
 | |
| 
 | |
| static void app_editline_init (struct input_el *self);
 | |
| 
 | |
| static int
 | |
| input_el__get_termios (int character, int fallback)
 | |
| {
 | |
| 	if (!g_terminal.initialized)
 | |
| 		return fallback;
 | |
| 
 | |
| 	cc_t value = g_terminal.termios.c_cc[character];
 | |
| 	if (value == _POSIX_VDISABLE)
 | |
| 		return fallback;
 | |
| 	return value;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__redisplay (void *input)
 | |
| {
 | |
| 	// See rl_redisplay()
 | |
| 	struct input_el *self = input;
 | |
| 	char x[] = { input_el__get_termios (VREPRINT, 'R' - 0x40), 0 };
 | |
| 	el_push (self->editline, x);
 | |
| 
 | |
| 	// We have to do this or it gets stuck and nothing is done
 | |
| 	(void) el_gets (self->editline, NULL);
 | |
| }
 | |
| 
 | |
| 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;
 | |
| 	free (self->prompt);
 | |
| 	self->prompt = prompt;
 | |
| 
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		input_el__redisplay (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_clear_line (void *input)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	const LineInfoW *info = el_wline (self->editline);
 | |
| 	int len = info->lastchar - info->buffer;
 | |
| 	int point = info->cursor - info->buffer;
 | |
| 	el_cursor (self->editline, len - point);
 | |
| 	el_wdeletestr (self->editline, len);
 | |
| 	input_el__redisplay (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__erase (struct input_el *self)
 | |
| {
 | |
| 	el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
 | |
| 	input_el_clear_line (self);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| input_el_insert (void *input, const char *s)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	bool success = !*s || !el_insertstr (self->editline, s);
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		input_el__redisplay (self);
 | |
| 	return success;
 | |
| }
 | |
| 
 | |
| static char *
 | |
| input_el_get_line (void *input)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	const LineInfo *info = el_line (self->editline);
 | |
| 	return xstrndup (info->buffer, info->lastchar - info->buffer);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_el_bind (void *input, const char *seq, const char *function_name)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	el_set (self->editline, EL_BIND, seq, function_name, NULL);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_bind_meta (void *input, char key, const char *function_name)
 | |
| {
 | |
| 	char keyseq[] = { 'M', '-', key, 0 };
 | |
| 	input_el_bind (input, keyseq, function_name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_bind_control (void *input, char key, const char *function_name)
 | |
| {
 | |
| 	char keyseq[] = { '^', key, 0 };
 | |
| 	input_el_bind (input, keyseq, function_name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
 | |
| {
 | |
| 	(void) cif;
 | |
| 
 | |
| 	struct input_el_fn *data = user_data;
 | |
| 	*(unsigned char *) ret = data->callback
 | |
| 		(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
 | |
| }
 | |
| 
 | |
| static wchar_t *
 | |
| ascii_to_wide (const char *ascii)
 | |
| {
 | |
| 	size_t len = strlen (ascii) + 1;
 | |
| 	wchar_t *wide = xcalloc (sizeof *wide, len);
 | |
| 	while (len--)
 | |
| 		hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
 | |
| 	return wide;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_register_fn (void *input,
 | |
| 	const char *name, const char *help, input_fn callback, void *user_data)
 | |
| {
 | |
| 	void *bound_fn = NULL;
 | |
| 	struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
 | |
| 	hard_assert (data);
 | |
| 
 | |
| 	static ffi_cif cif;
 | |
| 	static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
 | |
| 	hard_assert (ffi_prep_cif
 | |
| 		(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
 | |
| 
 | |
| 	data->user_data = user_data;
 | |
| 	data->callback = callback;
 | |
| 	data->name = ascii_to_wide (name);
 | |
| 	data->help = ascii_to_wide (help);
 | |
| 	hard_assert (ffi_prep_closure_loc (&data->closure,
 | |
| 		&cif, input_el__forward, data, bound_fn) == FFI_OK);
 | |
| 
 | |
| 	struct input_el *self = input;
 | |
| 	el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
 | |
| 	LIST_PREPEND (self->fns, data);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_el_start (void *input, const char *program_name)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	self->editline = el_init (program_name, stdin, stdout, stderr);
 | |
| 	el_set (self->editline, EL_CLIENTDATA, self);
 | |
| 	el_set (self->editline, EL_PROMPT_ESC,
 | |
| 		input_el__make_prompt, INPUT_START_IGNORE);
 | |
| 	el_set (self->editline, EL_SIGNAL, false);
 | |
| 	el_set (self->editline, EL_UNBUFFERED, true);
 | |
| 	el_set (self->editline, EL_EDITOR, "emacs");
 | |
| 
 | |
| 	app_editline_init (self);
 | |
| 
 | |
| 	self->prompt_shown = 1;
 | |
| 	self->active = true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_stop (void *input)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	if (self->prompt_shown > 0)
 | |
| 		input_el__erase (self);
 | |
| 
 | |
| 	el_end (self->editline);
 | |
| 	self->editline = NULL;
 | |
| 	self->active = false;
 | |
| 	self->prompt_shown = false;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_prepare (void *input, bool enabled)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	el_set (self->editline, EL_PREP_TERM, enabled);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
 | |
| {
 | |
| 	const LineInfoW *info = el_wline (self->editline);
 | |
| 	int len = info->lastchar - info->buffer;
 | |
| 	int point = info->cursor - info->buffer;
 | |
| 
 | |
| 	wchar_t *line = calloc (sizeof *info->buffer, len + 1);
 | |
| 	memcpy (line, info->buffer, sizeof *info->buffer * len);
 | |
| 	el_cursor (self->editline, len - point);
 | |
| 	el_wdeletestr (self->editline, len);
 | |
| 
 | |
| 	buffer->saved_line = line;
 | |
| 	buffer->saved_point = point;
 | |
| 	buffer->saved_len = len;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__save (struct input_el *self)
 | |
| {
 | |
| 	if (self->current)
 | |
| 		input_el__save_buffer (self, self->current);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__restore_buffer (struct input_el *self, struct input_el_buffer *buffer)
 | |
| {
 | |
| 	if (buffer->saved_line)
 | |
| 	{
 | |
| 		el_winsertstr (self->editline, buffer->saved_line);
 | |
| 		el_cursor (self->editline,
 | |
| 			-(buffer->saved_len - buffer->saved_point));
 | |
| 		free (buffer->saved_line);
 | |
| 		buffer->saved_line = NULL;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el__restore (struct input_el *self)
 | |
| {
 | |
| 	if (self->current)
 | |
| 		input_el__restore_buffer (self, self->current);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_el_buffer_switch (void *input, input_buffer_t input_buffer)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	struct input_el_buffer *buffer = input_buffer;
 | |
| 
 | |
| 	if (self->current)
 | |
| 		input_el__save_buffer (self, self->current);
 | |
| 
 | |
| 	input_el__restore_buffer (self, buffer);
 | |
| 	el_wset (self->editline, EL_HIST, history, buffer->history);
 | |
| 	self->current = buffer;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
 | |
| {
 | |
| 	(void) input;
 | |
| 	struct input_el_buffer *buffer = input_buffer;
 | |
| 
 | |
| 	history_wend (buffer->history);
 | |
| 	free (buffer->saved_line);
 | |
| 	free (buffer);
 | |
| }
 | |
| 
 | |
| static input_buffer_t
 | |
| input_el_buffer_new (void *input)
 | |
| {
 | |
| 	(void) input;
 | |
| 	struct input_el_buffer *self = xcalloc (1, sizeof *self);
 | |
| 	self->history = history_winit ();
 | |
| 
 | |
| 	HistEventW ev;
 | |
| 	history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
 | |
| 	return self;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| input_el_hide (void *input)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	if (!self->active || self->prompt_shown-- < 1)
 | |
| 		return;
 | |
| 
 | |
| 	input_el__save (self);
 | |
| 	input_el__erase (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_el_show (void *input)
 | |
| {
 | |
| 	struct input_el *self = input;
 | |
| 	if (!self->active || ++self->prompt_shown < 1)
 | |
| 		return;
 | |
| 
 | |
| 	input_el__restore (self);
 | |
| 	// XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539
 | |
| 	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.  The 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); }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| struct user_channel
 | |
| {
 | |
| 	LIST_HEADER (struct user_channel)
 | |
| 
 | |
| 	struct channel *channel;            ///< Reference to channel
 | |
| };
 | |
| 
 | |
| static struct user_channel *
 | |
| user_channel_new (void)
 | |
| {
 | |
| 	struct user_channel *self = xcalloc (1, sizeof *self);
 | |
| 	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
 | |
| 	// TODO: write code to poll for the away status
 | |
| 	bool away;                          ///< User is away
 | |
| 
 | |
| 	struct user_channel *channels;      ///< Channels the user is on
 | |
| };
 | |
| 
 | |
| static struct user *
 | |
| user_new (void)
 | |
| {
 | |
| 	struct user *self = xcalloc (1, sizeof *self);
 | |
| 	self->ref_count = 1;
 | |
| 	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
 | |
| 	struct str prefixes;                ///< Ordered @+... characters
 | |
| };
 | |
| 
 | |
| static struct channel_user *
 | |
| channel_user_new (void)
 | |
| {
 | |
| 	struct channel_user *self = xcalloc (1, sizeof *self);
 | |
| 	str_init (&self->prefixes);
 | |
| 	return self;
 | |
| }
 | |
| 
 | |
| static void
 | |
| channel_user_destroy (struct channel_user *self)
 | |
| {
 | |
| 	user_unref (self->user);
 | |
| 	str_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
 | |
| 
 | |
| 	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 str_vector names_buf;        ///< Buffer for RPL_NAMREPLY
 | |
| 
 | |
| 	bool left_manually;                 ///< Don't rejoin on reconnect
 | |
| };
 | |
| 
 | |
| static struct channel *
 | |
| channel_new (void)
 | |
| {
 | |
| 	struct channel *self = xcalloc (1, sizeof *self);
 | |
| 	self->ref_count = 1;
 | |
| 	str_init (&self->no_param_modes);
 | |
| 	str_map_init (&self->param_modes);
 | |
| 	self->param_modes.free = free;
 | |
| 	str_vector_init (&self->names_buf);
 | |
| 	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);
 | |
| 	str_vector_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 color
 | |
| 	FORMATTER_ITEM_BG_COLOR,            ///< Background color
 | |
| 	FORMATTER_ITEM_SIMPLE,              ///< Toggle mIRC formatting
 | |
| 	FORMATTER_ITEM_IGNORE_ATTR          ///< Un/set attribute ignoration
 | |
| };
 | |
| 
 | |
| struct formatter_item
 | |
| {
 | |
| 	enum formatter_item_type type : 16; ///< Type of this item
 | |
| 	int attribute                 : 16; ///< Attribute ID
 | |
| 	int color;                          ///< Color
 | |
| 	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 void
 | |
| formatter_init (struct formatter *self,
 | |
| 	struct app_context *ctx, struct server *s)
 | |
| {
 | |
| 	memset (self, 0, sizeof *self);
 | |
| 	self->ctx = ctx;
 | |
| 	self->s = s;
 | |
| 	self->items = xcalloc (sizeof *self->items, (self->items_alloc = 16));
 | |
| 	self->items_len = 0;
 | |
| }
 | |
| 
 | |
| 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
 | |
| 
 | |
| 	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 buffer *
 | |
| buffer_new (struct input *input)
 | |
| {
 | |
| 	struct buffer *self = xcalloc (1, sizeof *self);
 | |
| 	self->ref_count = 1;
 | |
| 	self->input = input;
 | |
| 	self->input_data = CALL (input, buffer_new);
 | |
| 	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
 | |
| 
 | |
| 	bool cap_echo_message;              ///< Whether the server echos messages
 | |
| 
 | |
| 	// 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_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 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_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_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;
 | |
| 	str_init (&self->read_buffer);
 | |
| 	str_init (&self->write_buffer);
 | |
| 	self->state = IRC_DISCONNECTED;
 | |
| 
 | |
| 	poller_timer_init (&self->timeout_tmr, poller);
 | |
| 	self->timeout_tmr.dispatcher = on_irc_timeout;
 | |
| 	self->timeout_tmr.user_data = self;
 | |
| 
 | |
| 	poller_timer_init (&self->ping_tmr, poller);
 | |
| 	self->ping_tmr.dispatcher = on_irc_ping_timeout;
 | |
| 	self->ping_tmr.user_data = self;
 | |
| 
 | |
| 	poller_timer_init (&self->reconnect_tmr, poller);
 | |
| 	self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
 | |
| 	self->reconnect_tmr.user_data = self;
 | |
| 
 | |
| 	poller_timer_init (&self->autojoin_tmr, poller);
 | |
| 	self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
 | |
| 	self->autojoin_tmr.user_data = self;
 | |
| 
 | |
| 	str_map_init (&self->irc_users);
 | |
| 	self->irc_users.key_xfrm = irc_strxfrm;
 | |
| 	str_map_init (&self->irc_channels);
 | |
| 	self->irc_channels.key_xfrm = irc_strxfrm;
 | |
| 	str_map_init (&self->irc_buffer_map);
 | |
| 	self->irc_buffer_map.key_xfrm = irc_strxfrm;
 | |
| 
 | |
| 	str_init (&self->irc_user_mode);
 | |
| 
 | |
| 	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)
 | |
| 	{
 | |
| 		xclose (self->socket);
 | |
| 		self->socket_event.closed = true;
 | |
| 		poller_fd_reset (&self->socket_event);
 | |
| 	}
 | |
| 	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);
 | |
| 
 | |
| 	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
 | |
| {
 | |
| 	/// 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
 | |
| 	struct input_hook_vtable *vtable;   ///< Methods
 | |
| };
 | |
| 
 | |
| struct input_hook_vtable
 | |
| {
 | |
| 	/// 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
 | |
| 	struct irc_hook_vtable *vtable;     ///< Methods
 | |
| };
 | |
| 
 | |
| struct irc_hook_vtable
 | |
| {
 | |
| 	/// 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 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
 | |
| 	struct completion_hook_vtable *vtable;
 | |
| };
 | |
| 
 | |
| struct completion_hook_vtable
 | |
| {
 | |
| 	/// Tries to add possible completions of "word" to "output"
 | |
| 	void (*complete) (struct completion_hook *self,
 | |
| 		struct completion *data, const char *word, struct str_vector *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
 | |
| 
 | |
| 	// TODO: make buffer names fully unique like weechat does
 | |
| 	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 input_event;     ///< Pending input event
 | |
| 	struct str_vector pending_input;    ///< Pending input lines
 | |
| 
 | |
| 	int *nick_palette;                  ///< A 256-color palette for nicknames
 | |
| 	size_t nick_palette_len;            ///< Number of entries in nick_palette
 | |
| 
 | |
| 	bool awaiting_mirc_escape;          ///< Awaiting a mIRC attribute escape
 | |
| 	bool in_bracketed_paste;            ///< User is pasting some content
 | |
| 	struct str input_buffer;            ///< Buffered pasted content
 | |
| 
 | |
| 	bool running_backlog_helper;        ///< Running a backlog helper
 | |
| 	bool running_editor;                ///< Running editor for the input
 | |
| 	char *editor_filename;              ///< The file being edited by user
 | |
| 	int terminal_suspended;             ///< Terminal suspension level
 | |
| 
 | |
| 	struct plugin *plugins;             ///< Loaded plugins
 | |
| 	struct hook *input_hooks;           ///< Input hooks
 | |
| 	struct hook *irc_hooks;             ///< IRC hooks
 | |
| 	struct hook *completion_hooks;      ///< Autocomplete hooks
 | |
| }
 | |
| *g_ctx;
 | |
| 
 | |
| 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 < 6 * 6 * 6; x++)
 | |
| 	{
 | |
| 		int r =  x / 36;
 | |
| 		int g = (x / 6) % 6;
 | |
| 		int b = (x % 6);
 | |
| 
 | |
| 		// Use the luma value of colours within the cube to filter colours that
 | |
| 		// look okay-ish on terminals with both black and white backgrounds
 | |
| 		double luma = 0.2126 * r / 6. + 0.7152 * g / 6. + 0.0722 * b / 6.;
 | |
| 		if (luma >= .3 && luma <= .5)
 | |
| 			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);
 | |
| 
 | |
| 	config_init (&self->config);
 | |
| 	poller_init (&self->poller);
 | |
| 
 | |
| 	str_map_init (&self->servers);
 | |
| 	self->servers.free = (str_map_free_fn) server_unref;
 | |
| 	self->servers.key_xfrm = tolower_ascii_strxfrm;
 | |
| 
 | |
| 	str_map_init (&self->buffers_by_name);
 | |
| 	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;
 | |
| 	str_vector_init (&self->pending_input);
 | |
| 	str_init (&self->input_buffer);
 | |
| 
 | |
| 	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);
 | |
| 	str_vector_free (&self->pending_input);
 | |
| 	str_free (&self->input_buffer);
 | |
| 
 | |
| 	free (self->editor_filename);
 | |
| }
 | |
| 
 | |
| static void refresh_prompt (struct app_context *ctx);
 | |
| 
 | |
| // --- 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 (c < 32)
 | |
| 		{
 | |
| 			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\"" },
 | |
| 
 | |
| 	{ .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",
 | |
| 	  .type      = CONFIG_ITEM_STRING_ARRAY,
 | |
| 	  .validate  = config_validate_nonjunk_string },
 | |
| 	{ .name      = "command",
 | |
| 	  .comment   = "Command to execute after a successful connect",
 | |
| 	  .type      = CONFIG_ITEM_STRING },
 | |
| 	{ .name      = "command_delay",
 | |
| 	  .comment   = "Delay between executing \"command\" and joining channels",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "0" },
 | |
| 	{ .name      = "reconnect",
 | |
| 	  .comment   = "Whether to reconnect on error",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "on" },
 | |
| 	{ .name      = "reconnect_delay",
 | |
| 	  .comment   = "Time between reconnecting",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "5" },
 | |
| 
 | |
| 	{ .name      = "socks_host",
 | |
| 	  .comment   = "Address of a SOCKS 4a/5 proxy",
 | |
| 	  .type      = CONFIG_ITEM_STRING,
 | |
| 	  .validate  = config_validate_nonjunk_string },
 | |
| 	{ .name      = "socks_port",
 | |
| 	  .comment   = "SOCKS port number",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "1080" },
 | |
| 	{ .name      = "socks_username",
 | |
| 	  .comment   = "SOCKS auth. username",
 | |
| 	  .type      = CONFIG_ITEM_STRING },
 | |
| 	{ .name      = "socks_password",
 | |
| 	  .comment   = "SOCKS auth. password",
 | |
| 	  .type      = CONFIG_ITEM_STRING },
 | |
| 	{}
 | |
| };
 | |
| 
 | |
| static struct config_schema g_config_behaviour[] =
 | |
| {
 | |
| 	{ .name      = "isolate_buffers",
 | |
| 	  .comment   = "Don't leak messages from the server and global buffers",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "off",
 | |
| 	  .on_change = on_config_isolate_buffers_change },
 | |
| 	{ .name      = "beep_on_highlight",
 | |
| 	  .comment   = "Beep when highlighted or on a new invisible PM",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "on",
 | |
| 	  .on_change = on_config_beep_on_highlight_change },
 | |
| 	{ .name      = "show_all_prefixes",
 | |
| 	  .comment   = "Show all prefixes in front of nicknames",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "off",
 | |
| 	  .on_change = on_config_show_all_prefixes_change },
 | |
| 	{ .name      = "word_wrapping",
 | |
| 	  .comment   = "Enable simple word wrapping in buffers",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "on",
 | |
| 	  .on_change = on_config_word_wrapping_change },
 | |
| 	{ .name      = "date_change_line",
 | |
| 	  .comment   = "Input to strftime(3) for the date change line",
 | |
| 	  .type      = CONFIG_ITEM_STRING,
 | |
| 	  .default_  = "\"%F\"" },
 | |
| 	{ .name      = "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 },
 | |
| 
 | |
| 	// GNU screen has an ^O in its formatting attributes reset string,
 | |
| 	// therefore we can't just pipe raw formatting to `less -R`.
 | |
| 	// You can use the -r switch, however that makes `less` very confused
 | |
| 	// about line wrapping, and the result is suboptimal.
 | |
| 
 | |
| 	{ .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 +G\"" },
 | |
| 	{ .name      = "backlog_helper_strip_formatting",
 | |
| 	  .comment   = "Strip formatting from backlog helper input",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "on" },
 | |
| 
 | |
| 	{ .name      = "reconnect_delay_growing",
 | |
| 	  .comment   = "Growing factor for reconnect delay",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "2" },
 | |
| 	{ .name      = "reconnect_delay_max",
 | |
| 	  .comment   = "Maximum reconnect delay in seconds",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "600" },
 | |
| 
 | |
| 	{ .name      = "autoaway_message",
 | |
| 	  .comment   = "Automated away message",
 | |
| 	  .type      = CONFIG_ITEM_STRING,
 | |
| 	  .default_  = "\"I'm not here right now\"" },
 | |
| 	{ .name      = "autoaway_delay",
 | |
| 	  .comment   = "Delay from the last keypress in seconds",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "1800" },
 | |
| 
 | |
| 	{ .name      = "plugin_autoload",
 | |
| 	  .comment   = "Plugins to automatically load on start",
 | |
| 	  .type      = CONFIG_ITEM_STRING_ARRAY,
 | |
| 	  .validate  = config_validate_nonjunk_string },
 | |
| 	{}
 | |
| };
 | |
| 
 | |
| static struct config_schema g_config_attributes[] =
 | |
| {
 | |
| #define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING, \
 | |
| 	.on_change = on_config_attribute_change },
 | |
| 	ATTR_TABLE (XX)
 | |
| #undef XX
 | |
| 	{}
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| load_config_behaviour (struct config_item *subtree, void *user_data)
 | |
| {
 | |
| 	config_schema_apply_to_object (g_config_behaviour,  subtree, user_data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| load_config_attributes (struct config_item *subtree, void *user_data)
 | |
| {
 | |
| 	config_schema_apply_to_object (g_config_attributes, subtree, user_data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| register_config_modules (struct app_context *ctx)
 | |
| {
 | |
| 	struct config *config = &ctx->config;
 | |
| 	// The servers are loaded later when we can create buffers for them
 | |
| 	config_register_module (config, "servers",    NULL, NULL);
 | |
| 	config_register_module (config, "aliases",    NULL, NULL);
 | |
| 	config_register_module (config, "plugins",    NULL, NULL);
 | |
| 	config_register_module (config, "behaviour",  load_config_behaviour,  ctx);
 | |
| 	config_register_module (config, "attributes", load_config_attributes, ctx);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static const char *
 | |
| get_config_string (struct config_item *root, const char *key)
 | |
| {
 | |
| 	struct config_item *item = config_item_get (root, key, NULL);
 | |
| 	hard_assert (item);
 | |
| 	if (item->type == CONFIG_ITEM_NULL)
 | |
| 		return NULL;
 | |
| 	hard_assert (config_item_type_is_string (item->type));
 | |
| 	return item->value.string.str;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| set_config_string
 | |
| 	(struct config_item *root, const char *key, const char *value)
 | |
| {
 | |
| 	struct config_item *item = config_item_get (root, key, NULL);
 | |
| 	hard_assert (item);
 | |
| 
 | |
| 	struct config_item *new_ = config_item_string_from_cstr (value);
 | |
| 	struct error *e = NULL;
 | |
| 	if (config_item_set_from (item, new_, &e))
 | |
| 		return true;
 | |
| 
 | |
| 	config_item_destroy (new_);
 | |
| 	print_error ("couldn't set `%s' in configuration: %s", key, e->message);
 | |
| 	error_free (e);
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| static int64_t
 | |
| get_config_integer (struct config_item *root, const char *key)
 | |
| {
 | |
| 	struct config_item *item = config_item_get (root, key, NULL);
 | |
| 	hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
 | |
| 	return item->value.integer;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| get_config_boolean (struct config_item *root, const char *key)
 | |
| {
 | |
| 	struct config_item *item = config_item_get (root, key, NULL);
 | |
| 	hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
 | |
| 	return item->value.boolean;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static struct str_map *
 | |
| get_servers_config (struct app_context *ctx)
 | |
| {
 | |
| 	return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
 | |
| }
 | |
| 
 | |
| static struct str_map *
 | |
| get_aliases_config (struct app_context *ctx)
 | |
| {
 | |
| 	return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
 | |
| }
 | |
| 
 | |
| static struct str_map *
 | |
| get_plugins_config (struct app_context *ctx)
 | |
| {
 | |
| 	return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| serialize_configuration (struct config_item *root, struct str *output)
 | |
| {
 | |
| 	str_append (output,
 | |
| 		"# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n"
 | |
| 		"#\n"
 | |
| 		"# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n"
 | |
| 		"# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n"
 | |
| 		"#\n"
 | |
| 		"# Everything is in UTF-8.  Any custom comments will be overwritten.\n"
 | |
| 		"\n");
 | |
| 
 | |
| 	config_item_write (root, true, output);
 | |
| }
 | |
| 
 | |
| // --- Terminal output ---------------------------------------------------------
 | |
| 
 | |
| /// Default color pair
 | |
| #define COLOR_DEFAULT -1
 | |
| 
 | |
| /// Bright versions of the basic color set
 | |
| #define COLOR_BRIGHT(x) (COLOR_ ## x + 8)
 | |
| 
 | |
| /// Builds a color pair for 256-color terminals with a 16-color backup value
 | |
| #define COLOR_256(name, c256) \
 | |
| 	(((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16))
 | |
| 
 | |
| typedef int (*terminal_printer_fn) (int);
 | |
| 
 | |
| static int
 | |
| putchar_stderr (int c)
 | |
| {
 | |
| 	return fputc (c, stderr);
 | |
| }
 | |
| 
 | |
| static terminal_printer_fn
 | |
| get_attribute_printer (FILE *stream)
 | |
| {
 | |
| 	if (stream == stdout && g_terminal.stdout_is_tty)
 | |
| 		return putchar;
 | |
| 	if (stream == stderr && g_terminal.stderr_is_tty)
 | |
| 		return putchar_stderr;
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static void
 | |
| vprint_attributed (struct app_context *ctx,
 | |
| 	FILE *stream, intptr_t attribute, const char *fmt, va_list ap)
 | |
| {
 | |
| 	terminal_printer_fn printer = get_attribute_printer (stream);
 | |
| 	if (!attribute)
 | |
| 		printer = NULL;
 | |
| 
 | |
| 	if (printer)
 | |
| 		tputs (ctx->attrs[attribute], 1, printer);
 | |
| 
 | |
| 	vfprintf (stream, fmt, ap);
 | |
| 
 | |
| 	if (printer)
 | |
| 		tputs (ctx->attrs[ATTR_RESET], 1, printer);
 | |
| }
 | |
| 
 | |
| static void
 | |
| print_attributed (struct app_context *ctx,
 | |
| 	FILE *stream, intptr_t attribute, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 	va_start (ap, fmt);
 | |
| 	vprint_attributed (ctx, stream, attribute, fmt, ap);
 | |
| 	va_end (ap);
 | |
| }
 | |
| 
 | |
| static void
 | |
| log_message_attributed (void *user_data, const char *quote, const char *fmt,
 | |
| 	va_list ap)
 | |
| {
 | |
| 	FILE *stream = stderr;
 | |
| 	struct app_context *ctx = g_ctx;
 | |
| 
 | |
| 	CALL (ctx->input, hide);
 | |
| 
 | |
| 	print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
 | |
| 	vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
 | |
| 	fputs ("\n", stream);
 | |
| 
 | |
| 	CALL (ctx->input, show);
 | |
| }
 | |
| 
 | |
| static ssize_t
 | |
| attr_by_name (const char *name)
 | |
| {
 | |
| 	static const char *table[ATTR_COUNT] =
 | |
| 	{
 | |
| #define XX(x, y, z) [ATTR_ ## x] = y,
 | |
| 		ATTR_TABLE (XX)
 | |
| #undef XX
 | |
| 	};
 | |
| 
 | |
| 	for (size_t i = 0; i < N_ELEMENTS (table); i++)
 | |
| 		if (!strcmp (name, table[i]))
 | |
| 			return i;
 | |
| 	return -1;
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_config_attribute_change (struct config_item *item)
 | |
| {
 | |
| 	struct app_context *ctx = item->user_data;
 | |
| 	ssize_t id = attr_by_name (item->schema->name);
 | |
| 	if (id != -1)
 | |
| 	{
 | |
| 		free (ctx->attrs[id]);
 | |
| 		ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
 | |
| 			? ctx->attrs_defaults[id]
 | |
| 			: item->value.string.str);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| init_colors (struct app_context *ctx)
 | |
| {
 | |
| 	bool have_ti = init_terminal ();
 | |
| 	char **defaults = ctx->attrs_defaults;
 | |
| 
 | |
| #define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "")
 | |
| 
 | |
| 	INIT_ATTR (PROMPT,      enter_bold_mode);
 | |
| 	INIT_ATTR (RESET,       exit_attribute_mode);
 | |
| 	INIT_ATTR (DATE_CHANGE, enter_bold_mode);
 | |
| 	INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]);
 | |
| 	INIT_ATTR (WARNING,     g_terminal.color_set_fg[COLOR_YELLOW]);
 | |
| 	INIT_ATTR (ERROR,       g_terminal.color_set_fg[COLOR_RED]);
 | |
| 
 | |
| 	INIT_ATTR (EXTERNAL,    g_terminal.color_set_fg[COLOR_WHITE]);
 | |
| 	INIT_ATTR (TIMESTAMP,   g_terminal.color_set_fg[COLOR_WHITE]);
 | |
| 	INIT_ATTR (ACTION,      g_terminal.color_set_fg[COLOR_RED]);
 | |
| 	INIT_ATTR (USERHOST,    g_terminal.color_set_fg[COLOR_CYAN]);
 | |
| 	INIT_ATTR (JOIN,        g_terminal.color_set_fg[COLOR_GREEN]);
 | |
| 	INIT_ATTR (PART,        g_terminal.color_set_fg[COLOR_RED]);
 | |
| 
 | |
| 	char *highlight = have_ti ? xstrdup_printf ("%s%s%s",
 | |
| 		g_terminal.color_set_fg[COLOR_YELLOW],
 | |
| 		g_terminal.color_set_bg[COLOR_MAGENTA],
 | |
| 		enter_bold_mode) : NULL;
 | |
| 	INIT_ATTR (HIGHLIGHT, highlight);
 | |
| 	free (highlight);
 | |
| 
 | |
| #undef INIT_ATTR
 | |
| 
 | |
| 	// This prevents formatters from obtaining an attribute printer function
 | |
| 	if (!have_ti)
 | |
| 	{
 | |
| 		g_terminal.stdout_is_tty = false;
 | |
| 		g_terminal.stderr_is_tty = false;
 | |
| 	}
 | |
| 
 | |
| 	g_log_message_real = log_message_attributed;
 | |
| 
 | |
| 	// Apply the default values so that we start with any formatting at all
 | |
| 	config_schema_call_changed
 | |
| 		(config_item_get (ctx->config.root, "attributes", NULL));
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // A little tool that tries to make the most of the terminal's capabilities
 | |
| // to set up text attributes.  It mostly targets just terminal emulators as that
 | |
| // is what people are using these days.  At least no stupid ncurses limits us
 | |
| // with color pairs.
 | |
| 
 | |
| enum
 | |
| {
 | |
| 	ATTRIBUTE_BOLD      = 1 << 0,
 | |
| 	ATTRIBUTE_ITALIC    = 1 << 1,
 | |
| 	ATTRIBUTE_UNDERLINE = 1 << 2,
 | |
| 	ATTRIBUTE_INVERSE   = 1 << 3,
 | |
| 	ATTRIBUTE_BLINK     = 1 << 4
 | |
| };
 | |
| 
 | |
| struct attribute_printer
 | |
| {
 | |
| 	struct app_context *ctx;            ///< Application context
 | |
| 	FILE *stream;                       ///< Output stream
 | |
| 	bool dirty;                         ///< Attributes are set
 | |
| 
 | |
| 	int want;                           ///< Desired attributes
 | |
| 	int want_foreground;                ///< Desired foreground color
 | |
| 	int want_background;                ///< Desired background color
 | |
| };
 | |
| 
 | |
| static void
 | |
| attribute_printer_tputs (struct attribute_printer *self, const char *attr)
 | |
| {
 | |
| 	terminal_printer_fn printer = get_attribute_printer (self->stream);
 | |
| 	if (printer)
 | |
| 		tputs (attr, 1, printer);
 | |
| 	else
 | |
| 		// We shouldn't really do this but we need it to
 | |
| 		// output formatting to the backlog
 | |
| 		fputs (attr, self->stream);
 | |
| }
 | |
| 
 | |
| static void
 | |
| attribute_printer_reset (struct attribute_printer *self)
 | |
| {
 | |
| 	if (self->dirty)
 | |
| 		attribute_printer_tputs (self, self->ctx->attrs[ATTR_RESET]);
 | |
| 
 | |
| 	self->dirty = false;
 | |
| }
 | |
| 
 | |
| static void
 | |
| attribute_printer_init (struct attribute_printer *self,
 | |
| 	struct app_context *ctx, FILE *stream)
 | |
| {
 | |
| 	self->ctx = ctx;
 | |
| 	self->stream = stream;
 | |
| 	self->dirty = true;
 | |
| 
 | |
| 	self->want = 0;
 | |
| 	self->want_foreground = -1;
 | |
| 	self->want_background = -1;
 | |
| }
 | |
| 
 | |
| static void
 | |
| attribute_printer_apply (struct attribute_printer *self, int attribute)
 | |
| {
 | |
| 	attribute_printer_reset (self);
 | |
| 	if (attribute != ATTR_RESET)
 | |
| 	{
 | |
| 		attribute_printer_tputs (self, self->ctx->attrs[attribute]);
 | |
| 		self->dirty = true;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NOTE: commonly terminals have:
 | |
| //   8 colors (worst, bright fg with BOLD, bg sometimes with BLINK)
 | |
| //   16 colors (okayish, we have the full basic range guaranteed)
 | |
| //   88 colors (the same plus a 4^3 RGB cube and a few shades of gray)
 | |
| //   256 colors (best, like above but with a larger cube and more gray)
 | |
| 
 | |
| /// Interpolate from the 256-color palette to the 88-color one
 | |
| static int
 | |
| attribute_printer_256_to_88 (int color)
 | |
| {
 | |
| 	// These colours are the same everywhere
 | |
| 	if (color < 16)
 | |
| 		return color;
 | |
| 
 | |
| 	// 24 -> 8 extra shades of gray
 | |
| 	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
 | |
| attribute_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;
 | |
| 		}
 | |
| 	case 16:
 | |
| 		return c16;
 | |
| 
 | |
| 	case 88:
 | |
| 		return c256 <= 0 ? c16 : attribute_printer_256_to_88 (c256);
 | |
| 	case 256:
 | |
| 		return c256 <= 0 ? c16 : c256;
 | |
| 
 | |
| 	default:
 | |
| 		// Unsupported palette
 | |
| 		return -1;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| attribute_printer_update (struct attribute_printer *self)
 | |
| {
 | |
| 	bool fg_is_bright;
 | |
| 	int fg = attribute_printer_decode_color
 | |
| 		(self->want_foreground, &fg_is_bright);
 | |
| 	bool bg_is_bright;
 | |
| 	int bg = attribute_printer_decode_color
 | |
| 		(self->want_background, &bg_is_bright);
 | |
| 
 | |
| 	int attributes = self->want;
 | |
| 	bool have_inverse = !!(attributes & ATTRIBUTE_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)
 | |
| 		attributes |= ATTRIBUTE_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;
 | |
| 		attributes |= ATTRIBUTE_BOLD | ATTRIBUTE_INVERSE;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		// This is what works on normal, decent terminals
 | |
| 		if (fg_is_bright) attributes |= ATTRIBUTE_BOLD;
 | |
| 		if (bg_is_bright) attributes |= ATTRIBUTE_BLINK;
 | |
| 	}
 | |
| 
 | |
| 	attribute_printer_reset (self);
 | |
| 
 | |
| 	if (attributes)
 | |
| 		attribute_printer_tputs (self, tparm (set_attributes,
 | |
| 			0,   // standout
 | |
| 			attributes & ATTRIBUTE_UNDERLINE,
 | |
| 			attributes & ATTRIBUTE_INVERSE,
 | |
| 			attributes & ATTRIBUTE_BLINK,
 | |
| 			0,   // dim
 | |
| 			attributes & ATTRIBUTE_BOLD,
 | |
| 			0,   // blank
 | |
| 			0,   // protect
 | |
| 			0)); // acs
 | |
| 	if (enter_italics_mode && (attributes & ATTRIBUTE_ITALIC))
 | |
| 		attribute_printer_tputs (self, enter_italics_mode);
 | |
| 
 | |
| 	if (fg >= 0)
 | |
| 		attribute_printer_tputs (self, g_terminal.color_set_fg[fg]);
 | |
| 	if (bg >= 0)
 | |
| 		attribute_printer_tputs (self, g_terminal.color_set_bg[bg]);
 | |
| 
 | |
| 	self->dirty = true;
 | |
| }
 | |
| 
 | |
| // --- Helpers -----------------------------------------------------------------
 | |
| 
 | |
| static int
 | |
| irc_server_strcmp (struct server *s, const char *a, const char *b)
 | |
| {
 | |
| 	int x;
 | |
| 	while (*a || *b)
 | |
| 		if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
 | |
| 			return x;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n)
 | |
| {
 | |
| 	int x;
 | |
| 	while (n-- && (*a || *b))
 | |
| 		if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
 | |
| 			return x;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static char *
 | |
| irc_cut_nickname (const char *prefix)
 | |
| {
 | |
| 	return cstr_cut_until (prefix, "!@");
 | |
| }
 | |
| 
 | |
| static const char *
 | |
| irc_find_userhost (const char *prefix)
 | |
| {
 | |
| 	const char *p = strchr (prefix, '!');
 | |
| 	return p ? p + 1 : NULL;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| irc_is_this_us (struct server *s, const char *prefix)
 | |
| {
 | |
| 	// This shouldn't be called before successfully registering.
 | |
| 	// Better safe than sorry, though.
 | |
| 	if (!s->irc_user)
 | |
| 		return false;
 | |
| 
 | |
| 	char *nick = irc_cut_nickname (prefix);
 | |
| 	bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname);
 | |
| 	free (nick);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| irc_is_channel (struct server *s, const char *ident)
 | |
| {
 | |
| 	return *ident
 | |
| 		&& (!!strchr (s->irc_chantypes,       *ident) ||
 | |
| 			!!strchr (s->irc_idchan_prefixes, *ident));
 | |
| }
 | |
| 
 | |
| // Message targets can be prefixed by a character filtering their targets
 | |
| static const char *
 | |
| irc_skip_statusmsg (struct server *s, const char *target)
 | |
| {
 | |
| 	return target + (*target && strchr (s->irc_statusmsg, *target));
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // As of 2015, 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;
 | |
| 	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] =
 | |
| 	{
 | |
| 		"\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_init (&s);
 | |
| 	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);
 | |
| }
 | |
| 
 | |
| // This function is used to output debugging IRC traffic to the terminal.
 | |
| // It's far from ideal, as any non-UTF-8 text degrades the entire line to
 | |
| // ISO Latin 1.  But it should work good enough most of the time.
 | |
| static char *
 | |
| irc_to_term (struct app_context *ctx, const char *text)
 | |
| {
 | |
| 	char *utf8 = irc_to_utf8 (text);
 | |
| 	char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
 | |
| 	free (utf8);
 | |
| 	return term;
 | |
| }
 | |
| 
 | |
| // --- Output formatter --------------------------------------------------------
 | |
| 
 | |
| // This complicated piece of code makes attributed text formatting simple.
 | |
| // We use a printf-inspired syntax to push attributes and text to the object,
 | |
| // then flush it either to a terminal, or a log file with formatting stripped.
 | |
| //
 | |
| // Format strings use a #-quoted notation, to differentiate from printf:
 | |
| //   #s inserts a string (expected to be in UTF-8)
 | |
| //   #d inserts a signed integer
 | |
| //   #l inserts a locale-encoded string
 | |
| //
 | |
| //   #S inserts a string from the server with unknown encoding
 | |
| //   #m inserts a mIRC-formatted string (auto-resets at boundaries)
 | |
| //   #n cuts the nickname from a string and automatically colours it
 | |
| //   #N is like #n but also appends userhost, if present
 | |
| //
 | |
| //   #a inserts named attributes (auto-resets)
 | |
| //   #r resets terminal attributes
 | |
| //   #c sets foreground color
 | |
| //   #C sets background color
 | |
| //
 | |
| // Modifiers:
 | |
| //    & free() the string argument after using it
 | |
| 
 | |
| static void
 | |
| formatter_add_item (struct formatter *self, struct formatter_item template_)
 | |
| {
 | |
| 	if (template_.text)
 | |
| 		template_.text = xstrdup (template_.text);
 | |
| 
 | |
| 	if (self->items_len == self->items_alloc)
 | |
| 		self->items = xreallocarray
 | |
| 			(self->items, sizeof *self->items, (self->items_alloc <<= 1));
 | |
| 	self->items[self->items_len++] = template_;
 | |
| }
 | |
| 
 | |
| #define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
 | |
| 	(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
 | |
| 
 | |
| #define FORMATTER_ADD_RESET(self) \
 | |
| 	FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
 | |
| #define FORMATTER_ADD_TEXT(self, text_) \
 | |
| 	FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
 | |
| #define FORMATTER_ADD_SIMPLE(self, attribute_) \
 | |
| 	FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = ATTRIBUTE_ ## attribute_)
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| enum
 | |
| {
 | |
| 	MIRC_WHITE,  MIRC_BLACK,    MIRC_BLUE,   MIRC_GREEN,
 | |
| 	MIRC_L_RED,  MIRC_RED,      MIRC_PURPLE, MIRC_ORANGE,
 | |
| 	MIRC_YELLOW, MIRC_L_GREEN,  MIRC_CYAN,   MIRC_L_CYAN,
 | |
| 	MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY,   MIRC_L_GRAY,
 | |
| };
 | |
| 
 | |
| // We use estimates from the 16 color terminal palette, or the 256 color 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),
 | |
| };
 | |
| 
 | |
| static const char *
 | |
| formatter_parse_mirc_color (struct formatter *self, const char *s)
 | |
| {
 | |
| 	if (!isdigit_ascii (*s))
 | |
| 	{
 | |
| 		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
 | |
| 		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = -1);
 | |
| 		return s;
 | |
| 	}
 | |
| 
 | |
| 	int fg = *s++ - '0';
 | |
| 	if (isdigit_ascii (*s))
 | |
| 		fg = fg * 10 + (*s++ - '0');
 | |
| 	if (fg >= 0 && fg < 16)
 | |
| 		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
 | |
| 
 | |
| 	if (*s != ',' || !isdigit_ascii (s[1]))
 | |
| 		return s;
 | |
| 	s++;
 | |
| 
 | |
| 	int bg = *s++ - '0';
 | |
| 	if (isdigit_ascii (*s))
 | |
| 		bg = bg * 10 + (*s++ - '0');
 | |
| 	if (bg >= 0 && bg < 16)
 | |
| 		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
 | |
| 
 | |
| 	return s;
 | |
| }
 | |
| 
 | |
| static void
 | |
| formatter_parse_mirc (struct formatter *self, const char *s)
 | |
| {
 | |
| 	struct str buf;
 | |
| 	str_init (&buf);
 | |
| 
 | |
| 	FORMATTER_ADD_RESET (self);
 | |
| 
 | |
| 	unsigned char c;
 | |
| 	while ((c = *s++))
 | |
| 	{
 | |
| 		if (buf.len && c < 0x20)
 | |
| 		{
 | |
| 			FORMATTER_ADD_TEXT (self, buf.str);
 | |
| 			str_reset (&buf);
 | |
| 		}
 | |
| 
 | |
| 		switch (c)
 | |
| 		{
 | |
| 		case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD);      break;
 | |
| 		case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC);    break;
 | |
| 		case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break;
 | |
| 		case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE);   break;
 | |
| 
 | |
| 		case '\x03':
 | |
| 			s = formatter_parse_mirc_color (self, s);
 | |
| 			break;
 | |
| 		case '\x0f':
 | |
| 			FORMATTER_ADD_RESET (self);
 | |
| 			break;
 | |
| 		default:
 | |
| 			str_append_c (&buf, c);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (buf.len)
 | |
| 		FORMATTER_ADD_TEXT (self, buf.str);
 | |
| 
 | |
| 	str_free (&buf);
 | |
| 	FORMATTER_ADD_RESET (self);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| formatter_parse_nick (struct formatter *self, 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 color from the 256-color cube if available
 | |
| 	color |= self->ctx->nick_palette[siphash_wrapper (nick,
 | |
| 		strlen (nick)) % self->ctx->nick_palette_len] << 16;
 | |
| 
 | |
| 	// We always use the default color 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 color afterwards
 | |
| 	FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
 | |
| }
 | |
| 
 | |
| static void
 | |
| formatter_parse_nick_full (struct formatter *self, char *s)
 | |
| {
 | |
| 	formatter_parse_nick (self, s);
 | |
| 
 | |
| 	const char *userhost;
 | |
| 	if (!(userhost = irc_find_userhost (s)))
 | |
| 		return;
 | |
| 
 | |
| 	FORMATTER_ADD_TEXT (self, " (");
 | |
| 	FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);
 | |
| 
 | |
| 	char *x = irc_to_utf8 (userhost);
 | |
| 	FORMATTER_ADD_TEXT (self, x);
 | |
| 	free (x);
 | |
| 
 | |
| 	FORMATTER_ADD_RESET (self);
 | |
| 	FORMATTER_ADD_TEXT (self, ")");
 | |
| }
 | |
| 
 | |
| static const char *
 | |
| formatter_parse_field (struct formatter *self,
 | |
| 	const char *field, struct str *buf, va_list *ap)
 | |
| {
 | |
| 	bool free_string = false;
 | |
| 	char *s = NULL;
 | |
| 	char *tmp = NULL;
 | |
| 	int c;
 | |
| 
 | |
| restart:
 | |
| 	switch ((c = *field++))
 | |
| 	{
 | |
| 		// We can push boring text content to the caller's buffer
 | |
| 		// and let it flush the buffer only when it's actually needed
 | |
| 	case 'd':
 | |
| 		tmp = xstrdup_printf ("%d", va_arg (*ap, int));
 | |
| 		str_append (buf, tmp);
 | |
| 		free (tmp);
 | |
| 		break;
 | |
| 	case 's':
 | |
| 		str_append (buf, (s = va_arg (*ap, char *)));
 | |
| 		break;
 | |
| 	case 'l':
 | |
| 		if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
 | |
| 			(s = va_arg (*ap, char *)), -1, NULL)))
 | |
| 			print_error ("character conversion failed for: %s", "output");
 | |
| 		else
 | |
| 			str_append (buf, tmp);
 | |
| 		free (tmp);
 | |
| 		break;
 | |
| 
 | |
| 	case 'S':
 | |
| 		tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
 | |
| 		str_append (buf, tmp);
 | |
| 		free (tmp);
 | |
| 		break;
 | |
| 	case 'm':
 | |
| 		tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
 | |
| 		formatter_parse_mirc (self, tmp);
 | |
| 		free (tmp);
 | |
| 		break;
 | |
| 	case 'n':
 | |
| 		formatter_parse_nick (self, (s = va_arg (*ap, char *)));
 | |
| 		break;
 | |
| 	case 'N':
 | |
| 		formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
 | |
| 		break;
 | |
| 
 | |
| 	case 'a':
 | |
| 		FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
 | |
| 		break;
 | |
| 	case 'c':
 | |
| 		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
 | |
| 		break;
 | |
| 	case 'C':
 | |
| 		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
 | |
| 		break;
 | |
| 	case 'r':
 | |
| 		FORMATTER_ADD_RESET (self);
 | |
| 		break;
 | |
| 
 | |
| 	default:
 | |
| 		if (c == '&' && !free_string)
 | |
| 			free_string = true;
 | |
| 		else if (c)
 | |
| 			hard_assert (!"unexpected format specifier");
 | |
| 		else
 | |
| 			hard_assert (!"unexpected end of format string");
 | |
| 		goto restart;
 | |
| 	}
 | |
| 
 | |
| 	if (free_string)
 | |
| 		free (s);
 | |
| 	return field;
 | |
| }
 | |
| 
 | |
| // I was unable to take a pointer of a bare "va_list" when it was passed in
 | |
| // as a function argument, so it has to be a pointer from the beginning
 | |
| static void
 | |
| formatter_addv (struct formatter *self, const char *format, va_list *ap)
 | |
| {
 | |
| 	struct str buf;
 | |
| 	str_init (&buf);
 | |
| 
 | |
| 	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
 | |
| {
 | |
| 	int named;                          ///< Named attribute or -1
 | |
| 	int text;                           ///< Text attributes
 | |
| 	int fg;                             ///< Foreground color (-1 for default)
 | |
| 	int bg;                             ///< Background color (-1 for default)
 | |
| };
 | |
| 
 | |
| struct line_char
 | |
| {
 | |
| 	LIST_HEADER (struct line_char)
 | |
| 
 | |
| 	char bytes[MB_LEN_MAX];             ///< The character
 | |
| 	size_t len;                         ///< Length of the character in bytes
 | |
| 	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 (const char *mb, size_t mb_len, wchar_t wc)
 | |
| {
 | |
| 	struct line_char *self = xcalloc (1, sizeof *self);
 | |
| 	memcpy (self->bytes, mb, (self->len = MIN (mb_len, sizeof self->bytes)));
 | |
| 	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 ("\n", 1, 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 ("\n", 1, 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 (" ", 1, 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)
 | |
| {
 | |
| 	size_t term_len = 0;
 | |
| 	char *term = iconv_xstrdup (self->ctx->term_from_utf8,
 | |
| 		(char *) text, -1, &term_len);
 | |
| 
 | |
| 	mbstate_t ps;
 | |
| 	memset (&ps, 0, sizeof ps);
 | |
| 
 | |
| 	wchar_t wch;
 | |
| 	size_t len, processed = 0;
 | |
| 	while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
 | |
| 	{
 | |
| 		hard_assert (len != (size_t) -2 && len != (size_t) -1);
 | |
| 		processed += len;
 | |
| 
 | |
| 		// Throw away any potentially harmful control characters
 | |
| 		// XXX: this is likely to break shift state encodings
 | |
| 		if (wcschr (L"\a\b\x1b", wch))
 | |
| 			continue;
 | |
| 
 | |
| 		struct line_char *c = line_char_new (term + processed - len, len, 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
 | |
| };
 | |
| 
 | |
| 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)
 | |
| 		{
 | |
| 			fwrite (c->bytes, c->len, 1, stream);
 | |
| 			free (c);
 | |
| 		}
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP))
 | |
| 		line = line_wrap (line, g_terminal.columns);
 | |
| 
 | |
| 	// TODO: rewrite the sloppily hacked mess around attribute_printer;
 | |
| 	//   so far I just didn't want to break everything at once
 | |
| 	struct attribute_printer state;
 | |
| 	attribute_printer_init (&state, self->ctx, stream);
 | |
| 	attribute_printer_reset (&state);
 | |
| 
 | |
| 	struct line_char_attrs attrs =
 | |
| 		{ .fg = -1, .bg = -1, .named = ATTR_RESET, .text = 0 };
 | |
| 	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)
 | |
| 		{
 | |
| 			if (c->attrs.named != -1)
 | |
| 				attribute_printer_apply (&state, c->attrs.named);
 | |
| 			else
 | |
| 			{
 | |
| 				state.want            = c->attrs.text;
 | |
| 				state.want_foreground = c->attrs.fg;
 | |
| 				state.want_background = c->attrs.bg;
 | |
| 				attribute_printer_reset (&state);
 | |
| 				attribute_printer_update (&state);
 | |
| 			}
 | |
| 			attrs = c->attrs;
 | |
| 		}
 | |
| 
 | |
| 		fwrite (c->bytes, c->len, 1, stream);
 | |
| 		free (c);
 | |
| 	}
 | |
| 	attribute_printer_reset (&state);
 | |
| }
 | |
| 
 | |
| // --- Buffers -----------------------------------------------------------------
 | |
| 
 | |
| static void
 | |
| buffer_pop_excess_lines (struct app_context *ctx, struct buffer *self)
 | |
| {
 | |
| 	int to_delete = (int) self->lines_count - (int) ctx->backlog_limit;
 | |
| 	while (to_delete-- > 0 && self->lines)
 | |
| 	{
 | |
| 		struct buffer_line *excess = self->lines;
 | |
| 		LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, excess);
 | |
| 		buffer_line_destroy (excess);
 | |
| 		self->lines_count--;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_config_backlog_limit_change (struct config_item *item)
 | |
| {
 | |
| 	struct app_context *ctx = item->user_data;
 | |
| 	ctx->backlog_limit = MIN (item->value.integer, INT_MAX);
 | |
| 
 | |
| 	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
 | |
| 		buffer_pop_excess_lines (ctx, iter);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_update_time (struct app_context *ctx, time_t now, FILE *stream,
 | |
| 	int flush_opts)
 | |
| {
 | |
| 	struct tm last, current;
 | |
| 	if (!localtime_r (&ctx->last_displayed_msg_time, &last)
 | |
| 	 || !localtime_r (&now, ¤t))
 | |
| 	{
 | |
| 		// Strange but nonfatal
 | |
| 		print_error ("%s: %s", "localtime_r", strerror (errno));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	ctx->last_displayed_msg_time = now;
 | |
| 	if (last.tm_year == current.tm_year
 | |
| 	 && last.tm_mon  == current.tm_mon
 | |
| 	 && last.tm_mday == current.tm_mday)
 | |
| 		return;
 | |
| 
 | |
| 	char buf[64] = "";
 | |
| 	const char *format =
 | |
| 		get_config_string (ctx->config.root, "behaviour.date_change_line");
 | |
| 	if (!strftime (buf, sizeof buf, format, ¤t))
 | |
| 	{
 | |
| 		print_error ("%s: %s", "strftime", strerror (errno));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	struct formatter f;
 | |
| 	formatter_init (&f, ctx, NULL);
 | |
| 	formatter_add (&f, "#a#s\n", ATTR_DATE_CHANGE, buf);
 | |
| 	formatter_flush (&f, stream, flush_opts);
 | |
| 	// Flush the trailing formatting reset item
 | |
| 	fflush (stream);
 | |
| 	formatter_free (&f);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output,
 | |
| 	int flush_opts)
 | |
| {
 | |
| 	int flags = line->flags;
 | |
| 	if (flags & BUFFER_LINE_INDENT)  formatter_add (f, "    ");
 | |
| 	if (flags & BUFFER_LINE_STATUS)  formatter_add (f, " -  ");
 | |
| 	if (flags & BUFFER_LINE_ERROR)   formatter_add (f, "#a=!=#r ", ATTR_ERROR);
 | |
| 
 | |
| 	for (struct formatter_item *iter = line->items; iter->type; iter++)
 | |
| 		formatter_add_item (f, *iter);
 | |
| 
 | |
| 	formatter_add (f, "\n");
 | |
| 	formatter_flush (f, output, flush_opts);
 | |
| 	formatter_free (f);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_line_write_time (struct formatter *f, struct buffer_line *line,
 | |
| 	FILE *stream, int flush_opts)
 | |
| {
 | |
| 	// Normal timestamps don't include the date, this way the user won't be
 | |
| 	// confused as to when an event has happened
 | |
| 	buffer_update_time (f->ctx, line->when, stream, flush_opts);
 | |
| 
 | |
| 	struct tm current;
 | |
| 	char buf[9];
 | |
| 	if (!localtime_r (&line->when, ¤t))
 | |
| 		print_error ("%s: %s", "localtime_r", strerror (errno));
 | |
| 	else if (!strftime (buf, sizeof buf, "%T", ¤t))
 | |
| 		print_error ("%s: %s", "strftime", "buffer too small");
 | |
| 	else
 | |
| 		formatter_add (f, "#a#s#r ", ATTR_TIMESTAMP, buf);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_line_display (struct app_context *ctx,
 | |
| 	struct buffer_line *line, bool is_external)
 | |
| {
 | |
| 	CALL (ctx->input, hide);
 | |
| 
 | |
| 	struct formatter f;
 | |
| 	formatter_init (&f, 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_init (&f, 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_init (&f, ctx, NULL);
 | |
| 
 | |
| 	struct tm current;
 | |
| 	char buf[20];
 | |
| 	if (!gmtime_r (&line->when, ¤t))
 | |
| 		print_error ("%s: %s", "gmtime_r", strerror (errno));
 | |
| 	else if (!strftime (buf, sizeof buf, "%F %T", ¤t))
 | |
| 		print_error ("%s: %s", "strftime", "buffer too small");
 | |
| 	else
 | |
| 		formatter_add (&f, "#s ", buf);
 | |
| 
 | |
| 	// The target is not a terminal, thus it won't wrap in spite of the 0
 | |
| 	buffer_line_flush (line, &f, log_file, 0);
 | |
| }
 | |
| 
 | |
| static void
 | |
| log_formatter (struct app_context *ctx,
 | |
| 	struct buffer *buffer, int flags, struct formatter *f)
 | |
| {
 | |
| 	if (!buffer)
 | |
| 		buffer = ctx->global_buffer;
 | |
| 
 | |
| 	struct buffer_line *line = buffer_line_new (f);
 | |
| 	line->flags = flags;
 | |
| 	// TODO: allow providing custom time (IRCv3.2 server-time)
 | |
| 	line->when = time (NULL);
 | |
| 
 | |
| 	buffer_pop_excess_lines (ctx, buffer);
 | |
| 	LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
 | |
| 	buffer->lines_count++;
 | |
| 
 | |
| 	if (buffer->log_file)
 | |
| 		buffer_line_write_to_log (ctx, line, buffer->log_file);
 | |
| 
 | |
| 	bool unseen_pm = buffer->type == BUFFER_PM
 | |
| 		&& buffer != ctx->current_buffer
 | |
| 		&& !(flags & BUFFER_LINE_UNIMPORTANT);
 | |
| 	bool important = (flags & BUFFER_LINE_HIGHLIGHT) || unseen_pm;
 | |
| 	if (ctx->beep_on_highlight && important)
 | |
| 		CALL (ctx->input, ding);
 | |
| 
 | |
| 	bool can_leak = false;
 | |
| 	if ((buffer == ctx->global_buffer)
 | |
| 	 || (ctx->current_buffer->type == BUFFER_GLOBAL
 | |
| 		&& buffer->type == BUFFER_SERVER)
 | |
| 	 || (ctx->current_buffer->type != BUFFER_GLOBAL
 | |
| 		&& buffer == ctx->current_buffer->server->buffer))
 | |
| 		can_leak = true;
 | |
| 
 | |
| 	bool displayed = true;
 | |
| 	if (ctx->terminal_suspended > 0)
 | |
| 		// Another process is using the terminal
 | |
| 		displayed = false;
 | |
| 	else if (buffer == ctx->current_buffer)
 | |
| 		buffer_line_display (ctx, line, false);
 | |
| 	else if (!ctx->isolate_buffers && can_leak)
 | |
| 		buffer_line_display (ctx, line, true);
 | |
| 	else
 | |
| 		displayed = false;
 | |
| 
 | |
| 	// Advance the unread marker in active buffers but don't create a new one
 | |
| 	if (!displayed
 | |
| 	 || (buffer == ctx->current_buffer && buffer->new_messages_count))
 | |
| 	{
 | |
| 		buffer->new_messages_count++;
 | |
| 		if (flags & BUFFER_LINE_UNIMPORTANT)
 | |
| 			buffer->new_unimportant_count++;
 | |
| 		buffer->highlighted |= important;
 | |
| 	}
 | |
| 	if (!displayed)
 | |
| 		refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
 | |
| 	int flags, const char *format, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 	va_start (ap, format);
 | |
| 
 | |
| 	struct formatter f;
 | |
| 	formatter_init (&f, ctx, s);
 | |
| 	formatter_addv (&f, format, &ap);
 | |
| 	log_formatter (ctx, buffer, flags, &f);
 | |
| 
 | |
| 	va_end (ap);
 | |
| }
 | |
| 
 | |
| #define log_global(ctx, flags, ...)                                            \
 | |
| 	log_full ((ctx), NULL, (ctx)->global_buffer, flags, __VA_ARGS__)
 | |
| #define log_server(s, buffer, flags, ...)                                      \
 | |
| 	log_full ((s)->ctx, s, (buffer), flags, __VA_ARGS__)
 | |
| 
 | |
| #define log_global_status(ctx, ...)                                            \
 | |
| 	log_global ((ctx), BUFFER_LINE_STATUS, __VA_ARGS__)
 | |
| #define log_global_error(ctx, ...)                                             \
 | |
| 	log_global ((ctx), BUFFER_LINE_ERROR,  __VA_ARGS__)
 | |
| #define log_global_indent(ctx, ...)                                            \
 | |
| 	log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__)
 | |
| 
 | |
| #define log_server_status(s, buffer, ...)                                      \
 | |
| 	log_server ((s), (buffer), BUFFER_LINE_STATUS, __VA_ARGS__)
 | |
| #define log_server_error(s, buffer, ...)                                       \
 | |
| 	log_server ((s), (buffer), BUFFER_LINE_ERROR,  __VA_ARGS__)
 | |
| 
 | |
| #define log_global_debug(ctx, ...)                                             \
 | |
| 	BLOCK_START                                                                \
 | |
| 		if (g_debug_mode)                                                      \
 | |
| 			log_global ((ctx), 0, "(*) " __VA_ARGS__);                         \
 | |
| 	BLOCK_END
 | |
| 
 | |
| #define log_server_debug(s, ...)                                               \
 | |
| 	BLOCK_START                                                                \
 | |
| 		if (g_debug_mode)                                                      \
 | |
| 			log_server ((s), (s)->buffer, 0, "(*) " __VA_ARGS__);              \
 | |
| 	BLOCK_END
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // Lines that are used in more than one place
 | |
| 
 | |
| #define log_nick_self(s, buffer, new_)                                         \
 | |
| 	log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT,   \
 | |
| 		"You are now known as #n", (new_))
 | |
| #define log_nick(s, buffer, old, new_)                                         \
 | |
| 	log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT,   \
 | |
| 		"#n is now known as #n", (old), (new_))
 | |
| 
 | |
| #define log_outcoming_notice(s, buffer, who, text)                             \
 | |
| 	log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text))
 | |
| #define log_outcoming_privmsg(s, buffer, prefixes, who, text)                  \
 | |
| 	log_server ((s), (buffer), 0, "<#s#n> #m", (prefixes), (who), (text))
 | |
| #define log_outcoming_action(s, buffer, who, text)                             \
 | |
| 	log_server ((s), (buffer), 0, " #a*#r  #n #m", ATTR_ACTION, (who), (text))
 | |
| 
 | |
| #define log_outcoming_orphan_notice(s, target, text)                           \
 | |
| 	log_server_status ((s), (s)->buffer, "Notice -> #n: #m", (target), (text))
 | |
| #define log_outcoming_orphan_privmsg(s, target, text)                          \
 | |
| 	log_server_status ((s), (s)->buffer, "MSG(#n): #m", (target), (text))
 | |
| 
 | |
| #define log_ctcp_query(s, target, tag)                                         \
 | |
| 	log_server_status ((s), (s)->buffer, "CTCP query to #S: #S", target, tag)
 | |
| #define log_ctcp_reply(s, target, reply /* freed! */)                          \
 | |
| 	log_server_status ((s), (s)->buffer, "CTCP reply to #S: #&S", target, reply)
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| make_log_filename (const char *filename, struct str *output)
 | |
| {
 | |
| 	for (const char *p = filename; *p; p++)
 | |
| 		// XXX: anything more to replace?
 | |
| 		if (strchr ("/\\ ", *p))
 | |
| 			str_append_c (output, '_');
 | |
| 		else
 | |
| 			str_append_c (output, tolower_ascii (*p));
 | |
| }
 | |
| 
 | |
| static char *
 | |
| buffer_get_log_path (struct buffer *buffer)
 | |
| {
 | |
| 	struct str path;
 | |
| 	str_init (&path);
 | |
| 	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);
 | |
| 
 | |
| 	// TODO: make sure global and server buffers don't collide with filenames
 | |
| 	str_append_c (&path, '/');
 | |
| 	make_log_filename (buffer->name, &path);
 | |
| 	str_append (&path, ".log");
 | |
| 	return str_steal (&path);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	if (!ctx->logging || buffer->log_file)
 | |
| 		return;
 | |
| 
 | |
| 	char *path = buffer_get_log_path (buffer);
 | |
| 	if (!(buffer->log_file = fopen (path, "ab")))
 | |
| 		log_global_error (ctx, "Couldn't open log file `#s': #l",
 | |
| 			path, strerror (errno));
 | |
| 	else
 | |
| 		set_cloexec (fileno (buffer->log_file));
 | |
| 	free (path);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_close_log_file (struct buffer *buffer)
 | |
| {
 | |
| 	if (buffer->log_file)
 | |
| 		(void) fclose (buffer->log_file);
 | |
| 	buffer->log_file = NULL;
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_config_logging_change (struct config_item *item)
 | |
| {
 | |
| 	struct app_context *ctx = item->user_data;
 | |
| 	ctx->logging = item->value.boolean;
 | |
| 
 | |
| 	for (struct buffer *buffer = ctx->buffers; buffer; buffer = buffer->next)
 | |
| 		if (ctx->logging)
 | |
| 			buffer_open_log_file (ctx, buffer);
 | |
| 		else
 | |
| 			buffer_close_log_file (buffer);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static struct buffer *
 | |
| buffer_by_name (struct app_context *ctx, const char *name)
 | |
| {
 | |
| 	return str_map_find (&ctx->buffers_by_name, name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_add (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	hard_assert (!buffer_by_name (ctx, buffer->name));
 | |
| 
 | |
| 	str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
 | |
| 	LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
 | |
| 
 | |
| 	buffer_open_log_file (ctx, buffer);
 | |
| 
 | |
| 	// In theory this can't cause changes in the prompt
 | |
| 	refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_remove (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	hard_assert (buffer != ctx->current_buffer);
 | |
| 	hard_assert (buffer != ctx->global_buffer);
 | |
| 
 | |
| 	CALL_ (ctx->input, buffer_destroy, buffer->input_data);
 | |
| 	buffer->input_data = NULL;
 | |
| 
 | |
| 	// And make sure to unlink the buffer from "irc_buffer_map"
 | |
| 	struct server *s = buffer->server;
 | |
| 	if (buffer->channel)
 | |
| 		str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL);
 | |
| 	if (buffer->user)
 | |
| 		str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);
 | |
| 
 | |
| 	if (buffer == ctx->last_buffer)
 | |
| 		ctx->last_buffer = NULL;
 | |
| 	if (buffer->type == BUFFER_SERVER)
 | |
| 		buffer->server->buffer = NULL;
 | |
| 
 | |
| 	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
 | |
| 	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
 | |
| 	buffer_unref (buffer);
 | |
| 
 | |
| 	refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_print_read_marker (struct app_context *ctx, FILE *stream, int flush_opts)
 | |
| {
 | |
| 	struct formatter f;
 | |
| 	formatter_init (&f, ctx, NULL);
 | |
| 	formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
 | |
| 	formatter_flush (&f, stream, flush_opts);
 | |
| 	// Flush the trailing formatting reset item
 | |
| 	fflush (stream);
 | |
| 	formatter_free (&f);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	// The prompt can take considerable time to redraw
 | |
| 	CALL (ctx->input, hide);
 | |
| 
 | |
| 	// That is, minus the readline prompt
 | |
| 	int display_limit = MAX (10, g_terminal.lines - 1);
 | |
| 
 | |
| 	// 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);
 | |
| 	}
 | |
| 
 | |
| 	struct buffer_line *line = buffer->lines_tail;
 | |
| 	int to_display = line != NULL;
 | |
| 	for (; line && line->prev && --display_limit > 0; line = line->prev)
 | |
| 		to_display++;
 | |
| 
 | |
| 	// 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, 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;
 | |
| 	if (ctx->current_buffer)
 | |
| 	{
 | |
| 		ctx->current_buffer->new_messages_count = 0;
 | |
| 		ctx->current_buffer->new_unimportant_count = 0;
 | |
| 		ctx->current_buffer->highlighted = false;
 | |
| 	}
 | |
| 
 | |
| 	buffer_print_backlog (ctx, buffer);
 | |
| 	CALL_ (ctx->input, buffer_switch, buffer->input_data);
 | |
| 
 | |
| 	// Now at last we can switch the pointers
 | |
| 	ctx->last_buffer = ctx->current_buffer;
 | |
| 	ctx->current_buffer = buffer;
 | |
| 
 | |
| 	refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_merge (struct app_context *ctx,
 | |
| 	struct buffer *buffer, struct buffer *merged)
 | |
| {
 | |
| 	// XXX: anything better to do?  This situation is arguably rare and I'm
 | |
| 	//   not entirely sure what action to take.
 | |
| 	log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS,
 | |
| 		"Buffer #s was merged into this buffer", merged->name);
 | |
| 
 | |
| 	// Find all lines from "merged" newer than the newest line in "buffer"
 | |
| 	struct buffer_line *start = merged->lines;
 | |
| 	if (buffer->lines_tail)
 | |
| 		while (start && start->when < buffer->lines_tail->when)
 | |
| 			start = start->next;
 | |
| 	if (!start)
 | |
| 		return;
 | |
| 
 | |
| 	// Count how many of them we have
 | |
| 	size_t n = 0;
 | |
| 	for (struct buffer_line *iter = start; iter; iter = iter->next)
 | |
| 		n++;
 | |
| 	struct buffer_line *tail = merged->lines_tail;
 | |
| 
 | |
| 	// Cut them from the original buffer
 | |
| 	if (start == merged->lines)
 | |
| 		merged->lines = NULL;
 | |
| 	else if (start->prev)
 | |
| 		start->prev->next = NULL;
 | |
| 	if (start == merged->lines_tail)
 | |
| 		merged->lines_tail = start->prev;
 | |
| 	merged->lines_count -= n;
 | |
| 
 | |
| 	// And append them to current lines in the buffer
 | |
| 	buffer->lines_tail->next = start;
 | |
| 	start->prev = buffer->lines_tail;
 | |
| 	buffer->lines_tail = tail;
 | |
| 	buffer->lines_count += n;
 | |
| 
 | |
| 	log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE,
 | |
| 		"End of merged content");
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_rename (struct app_context *ctx,
 | |
| 	struct buffer *buffer, const char *new_name)
 | |
| {
 | |
| 	struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name);
 | |
| 	if (collision == buffer)
 | |
| 		return;
 | |
| 
 | |
| 	hard_assert (!collision);
 | |
| 
 | |
| 	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
 | |
| 	str_map_set (&ctx->buffers_by_name, new_name, buffer);
 | |
| 
 | |
| 	buffer_close_log_file (buffer);
 | |
| 	buffer_open_log_file (ctx, buffer);
 | |
| 
 | |
| 	free (buffer->name);
 | |
| 	buffer->name = xstrdup (new_name);
 | |
| 
 | |
| 	// We might have renamed the current buffer
 | |
| 	refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_clear (struct buffer *buffer)
 | |
| {
 | |
| 	LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
 | |
| 		buffer_line_destroy (iter);
 | |
| 
 | |
| 	buffer->lines = buffer->lines_tail = NULL;
 | |
| 	buffer->lines_count = 0;
 | |
| }
 | |
| 
 | |
| static struct buffer *
 | |
| buffer_at_index (struct app_context *ctx, int n)
 | |
| {
 | |
| 	int i = 0;
 | |
| 	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
 | |
| 		if (++i == n)
 | |
| 			return iter;
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static struct buffer *
 | |
| buffer_next (struct app_context *ctx, int count)
 | |
| {
 | |
| 	struct buffer *new_buffer = ctx->current_buffer;
 | |
| 	while (count-- > 0)
 | |
| 		if (!(new_buffer = new_buffer->next))
 | |
| 			new_buffer = ctx->buffers;
 | |
| 	return new_buffer;
 | |
| }
 | |
| 
 | |
| static struct buffer *
 | |
| buffer_previous (struct app_context *ctx, int count)
 | |
| {
 | |
| 	struct buffer *new_buffer = ctx->current_buffer;
 | |
| 	while (count-- > 0)
 | |
| 		if (!(new_buffer = new_buffer->prev))
 | |
| 			new_buffer = ctx->buffers_tail;
 | |
| 	return new_buffer;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| buffer_goto (struct app_context *ctx, int n)
 | |
| {
 | |
| 	struct buffer *buffer = buffer_at_index (ctx, n);
 | |
| 	if (!buffer)
 | |
| 		return false;
 | |
| 
 | |
| 	buffer_activate (ctx, buffer);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static int
 | |
| buffer_count (struct app_context *ctx)
 | |
| {
 | |
| 	int total = 0;
 | |
| 	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
 | |
| 		total++;
 | |
| 	return total;
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_move (struct app_context *ctx, struct buffer *buffer, int n)
 | |
| {
 | |
| 	hard_assert (n >= 1 && n <= buffer_count (ctx));
 | |
| 	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
 | |
| 
 | |
| 	struct buffer *following = ctx->buffers;
 | |
| 	while (--n && following)
 | |
| 		following = following->next;
 | |
| 
 | |
| 	LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
 | |
| 	refresh_prompt (ctx);
 | |
| }
 | |
| 
 | |
| static int
 | |
| buffer_get_index (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	int index = 1;
 | |
| 	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
 | |
| 	{
 | |
| 		if (iter == buffer)
 | |
| 			return index;
 | |
| 		index++;
 | |
| 	}
 | |
| 	return -1;
 | |
| }
 | |
| 
 | |
| static void
 | |
| buffer_remove_safe (struct app_context *ctx, struct buffer *buffer)
 | |
| {
 | |
| 	if (buffer == ctx->current_buffer)
 | |
| 		buffer_activate (ctx, ctx->last_buffer
 | |
| 			? ctx->last_buffer
 | |
| 			: buffer_next (ctx, 1));
 | |
| 	buffer_remove (ctx, buffer);
 | |
| }
 | |
| 
 | |
| static void
 | |
| init_global_buffer (struct app_context *ctx)
 | |
| {
 | |
| 	struct buffer *global = ctx->global_buffer = buffer_new (ctx->input);
 | |
| 	global->type = BUFFER_GLOBAL;
 | |
| 	global->name = xstrdup (PROGRAM_NAME);
 | |
| 
 | |
| 	buffer_add (ctx, global);
 | |
| 	buffer_activate (ctx, global);
 | |
| }
 | |
| 
 | |
| // --- Users, channels ---------------------------------------------------------
 | |
| 
 | |
| 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 ();
 | |
| 	(void) user_weak_ref (user, irc_user_on_destroy, s);
 | |
| 	user->nickname = nickname;
 | |
| 	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->type = BUFFER_PM;
 | |
| 	buffer->name = xstrdup_printf ("%s.%s", s->name, 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.str);
 | |
| 	else if (channel_user->prefixes.len)
 | |
| 		str_append_c (output, channel_user->prefixes.str[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;
 | |
| }
 | |
| 
 | |
| // 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 ();
 | |
| 	user_channel->channel = channel;
 | |
| 	LIST_PREPEND (user->channels, user_channel);
 | |
| 
 | |
| 	struct channel_user *channel_user = channel_user_new ();
 | |
| 	channel_user->user = user;
 | |
| 	str_append (&channel_user->prefixes, prefixes);
 | |
| 	LIST_PREPEND (channel->users, channel_user);
 | |
| }
 | |
| 
 | |
| 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);
 | |
| 		}
 | |
| 
 | |
| 	// Then just unlink the user from the channel
 | |
| 	LIST_UNLINK (channel->users, channel_user);
 | |
| 	channel_user_destroy (channel_user);
 | |
| }
 | |
| 
 | |
| 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 ();
 | |
| 	(void) channel_weak_ref (channel, irc_channel_on_destroy, s);
 | |
| 	channel->name = name;
 | |
| 	channel->topic = NULL;
 | |
| 	str_map_set (&s->irc_channels, channel->name, channel);
 | |
| 	return channel;
 | |
| }
 | |
| 
 | |
| static struct channel_user *
 | |
| irc_channel_get_user (struct channel *channel, struct user *user)
 | |
| {
 | |
| 	LIST_FOR_EACH (struct channel_user, iter, channel->users)
 | |
| 		if (iter->user == user)
 | |
| 			return iter;
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_remove_user_from_channel (struct user *user, struct channel *channel)
 | |
| {
 | |
| 	struct channel_user *channel_user = irc_channel_get_user (channel, user);
 | |
| 	if (channel_user)
 | |
| 		irc_channel_unlink_user (channel, channel_user);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_left_channel (struct channel *channel)
 | |
| {
 | |
| 	LIST_FOR_EACH (struct channel_user, iter, channel->users)
 | |
| 		irc_channel_unlink_user (channel, iter);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| remove_conflicting_buffer (struct server *s, struct buffer *buffer)
 | |
| {
 | |
| 	log_server_status (s, s->buffer,
 | |
| 		"Removed buffer #s because of casemapping conflict", buffer->name);
 | |
| 	if (s->ctx->current_buffer == buffer)
 | |
| 		buffer_activate (s->ctx, s->buffer);
 | |
| 	buffer_remove (s->ctx, buffer);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_try_readd_user (struct server *s,
 | |
| 	struct user *user, struct buffer *buffer)
 | |
| {
 | |
| 	if (str_map_find (&s->irc_users, user->nickname))
 | |
| 	{
 | |
| 		// Remove user from all channels and destroy any PM buffer
 | |
| 		user_ref (user);
 | |
| 		LIST_FOR_EACH (struct user_channel, iter, user->channels)
 | |
| 			irc_remove_user_from_channel (user, iter->channel);
 | |
| 		if (buffer)
 | |
| 			remove_conflicting_buffer (s, buffer);
 | |
| 		user_unref (user);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		str_map_set (&s->irc_users, user->nickname, user);
 | |
| 		str_map_set (&s->irc_buffer_map, user->nickname, buffer);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_try_readd_channel (struct server *s,
 | |
| 	struct channel *channel, struct buffer *buffer)
 | |
| {
 | |
| 	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;
 | |
| 
 | |
| 	str_map_init (&s->irc_users);
 | |
| 	str_map_init (&s->irc_channels);
 | |
| 	str_map_init (&s->irc_buffer_map);
 | |
| 
 | |
| 	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;
 | |
| 
 | |
| 	str_map_iter_init (&iter, &old_users);
 | |
| 	while ((user = str_map_iter_next (&iter)))
 | |
| 		irc_try_readd_user (s, user,
 | |
| 			str_map_find (&old_buffer_map, user->nickname));
 | |
| 
 | |
| 	str_map_iter_init (&iter, &old_channels);
 | |
| 	while ((channel = str_map_iter_next (&iter)))
 | |
| 		irc_try_readd_channel (s, channel,
 | |
| 			str_map_find (&old_buffer_map, channel->name));
 | |
| 
 | |
| 	// Hopefully we've either moved or destroyed all the old content
 | |
| 	s->rehashing = false;
 | |
| 
 | |
| 	str_map_free (&old_users);
 | |
| 	str_map_free (&old_channels);
 | |
| 	str_map_free (&old_buffer_map);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_set_casemapping (struct server *s,
 | |
| 	irc_tolower_fn tolower, irc_strxfrm_fn strxfrm)
 | |
| {
 | |
| 	if (tolower == s->irc_tolower
 | |
| 	 && strxfrm == s->irc_strxfrm)
 | |
| 		return;
 | |
| 
 | |
| 	s->irc_tolower = tolower;
 | |
| 	s->irc_strxfrm = strxfrm;
 | |
| 
 | |
| 	// Ideally we would never have to do this but I can't think of a workaround
 | |
| 	irc_rehash_and_fix_conflicts (s);
 | |
| }
 | |
| 
 | |
| // --- Core functionality ------------------------------------------------------
 | |
| 
 | |
| static bool
 | |
| irc_is_connected (struct server *s)
 | |
| {
 | |
| 	return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_update_poller (struct server *s, const struct pollfd *pfd)
 | |
| {
 | |
| 	int new_events = s->transport->get_poll_events (s);
 | |
| 	hard_assert (new_events != 0);
 | |
| 
 | |
| 	if (!pfd || pfd->events != new_events)
 | |
| 		poller_fd_set (&s->socket_event, new_events);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_cancel_timers (struct server *s)
 | |
| {
 | |
| 	poller_timer_reset (&s->timeout_tmr);
 | |
| 	poller_timer_reset (&s->ping_tmr);
 | |
| 	poller_timer_reset (&s->reconnect_tmr);
 | |
| 	poller_timer_reset (&s->autojoin_tmr);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_reset_connection_timeouts (struct server *s)
 | |
| {
 | |
| 	poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
 | |
| 	poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
 | |
| 	poller_timer_reset (&s->reconnect_tmr);
 | |
| }
 | |
| 
 | |
| static int64_t
 | |
| irc_get_reconnect_delay (struct server *s)
 | |
| {
 | |
| 	int64_t delay = get_config_integer (s->config, "reconnect_delay");
 | |
| 	int64_t delay_factor = get_config_integer (s->ctx->config.root,
 | |
| 		"behaviour.reconnect_delay_growing");
 | |
| 	for (unsigned i = 0; i < s->reconnect_attempt; i++)
 | |
| 	{
 | |
| 		if (delay_factor && delay > INT64_MAX / delay_factor)
 | |
| 			break;
 | |
| 		delay *= delay_factor;
 | |
| 	}
 | |
| 
 | |
| 	int64_t delay_max = get_config_integer (s->ctx->config.root,
 | |
| 		"behaviour.reconnect_delay_max");
 | |
| 	return MIN (delay, delay_max);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_queue_reconnect (struct server *s)
 | |
| {
 | |
| 	// As long as the user wants us to, that is
 | |
| 	if (!get_config_boolean (s->config, "reconnect"))
 | |
| 		return;
 | |
| 
 | |
| 	// XXX: maybe add a state for when a connect is queued?
 | |
| 	hard_assert (s->state == IRC_DISCONNECTED);
 | |
| 
 | |
| 	int64_t delay = irc_get_reconnect_delay (s);
 | |
| 	s->reconnect_attempt++;
 | |
| 
 | |
| 	log_server_status (s, s->buffer,
 | |
| 		"Trying to reconnect in #&s seconds...",
 | |
| 		xstrdup_printf ("%" PRId64, delay));
 | |
| 	poller_timer_set (&s->reconnect_tmr, delay * 1000);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void irc_process_sent_message
 | |
| 	(const struct irc_message *msg, struct server *s);
 | |
| static void irc_send (struct server *s,
 | |
| 	const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
 | |
| 
 | |
| static void
 | |
| irc_send (struct server *s, const char *format, ...)
 | |
| {
 | |
| 	if (!soft_assert (irc_is_connected (s)))
 | |
| 	{
 | |
| 		log_server_debug (s, "sending a message to a dead server connection");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (s->state == IRC_CLOSING
 | |
| 	 || s->state == IRC_HALF_CLOSED)
 | |
| 		return;
 | |
| 
 | |
| 	va_list ap;
 | |
| 	va_start (ap, format);
 | |
| 	struct str str;
 | |
| 	str_init (&str);
 | |
| 	str_append_vprintf (&str, format, ap);
 | |
| 	va_end (ap);
 | |
| 
 | |
| 	log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);
 | |
| 
 | |
| 	struct irc_message msg;
 | |
| 	irc_parse_message (&msg, str.str);
 | |
| 	irc_process_sent_message (&msg, s);
 | |
| 	irc_free_message (&msg);
 | |
| 
 | |
| 	str_append_str (&s->write_buffer, &str);
 | |
| 	str_free (&str);
 | |
| 	str_append (&s->write_buffer, "\r\n");
 | |
| 	irc_update_poller (s, NULL);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| irc_real_shutdown (struct server *s)
 | |
| {
 | |
| 	hard_assert (irc_is_connected (s) && s->state != IRC_HALF_CLOSED);
 | |
| 
 | |
| 	if (s->transport
 | |
| 	 && s->transport->in_before_shutdown)
 | |
| 		s->transport->in_before_shutdown (s);
 | |
| 
 | |
| 	while (shutdown (s->socket, SHUT_WR) == -1)
 | |
| 		if (!soft_assert (errno == EINTR))
 | |
| 			break;
 | |
| 
 | |
| 	s->state = IRC_HALF_CLOSED;
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_shutdown (struct server *s)
 | |
| {
 | |
| 	if (s->state == IRC_CLOSING
 | |
| 	 || s->state == IRC_HALF_CLOSED)
 | |
| 		return;
 | |
| 
 | |
| 	// TODO: set a timer to cut the connection if we don't receive an EOF
 | |
| 	s->state = IRC_CLOSING;
 | |
| 
 | |
| 	// Either there's still some data in the write buffer and we wait
 | |
| 	// until they're sent, or we send an EOF to the server right away
 | |
| 	if (!s->write_buffer.len)
 | |
| 		irc_real_shutdown (s);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_destroy_connector (struct server *s)
 | |
| {
 | |
| 	if (s->connector)
 | |
| 		connector_free (s->connector);
 | |
| 	free (s->connector);
 | |
| 	s->connector = NULL;
 | |
| 
 | |
| 	if (s->socks_conn)
 | |
| 		socks_connector_free (s->socks_conn);
 | |
| 	free (s->socks_conn);
 | |
| 	s->socks_conn = NULL;
 | |
| 
 | |
| 	// Not connecting anymore
 | |
| 	s->state = IRC_DISCONNECTED;
 | |
| }
 | |
| 
 | |
| static void
 | |
| try_finish_quit (struct app_context *ctx)
 | |
| {
 | |
| 	if (!ctx->quitting)
 | |
| 		return;
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &ctx->servers);
 | |
| 
 | |
| 	bool disconnected_all = true;
 | |
| 	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
 | |
| initiate_quit (struct app_context *ctx)
 | |
| {
 | |
| 	log_global_status (ctx, "Shutting down");
 | |
| 
 | |
| 	// Hide the user interface
 | |
| 	CALL (ctx->input, hide);
 | |
| 
 | |
| 	// Initiate a connection close
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &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_shutdown (s);
 | |
| 			s->manual_disconnect = true;
 | |
| 		}
 | |
| 		else if (s->state == IRC_CONNECTING)
 | |
| 			irc_destroy_connector (s);
 | |
| 	}
 | |
| 
 | |
| 	ctx->quitting = true;
 | |
| 	try_finish_quit (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_destroy_transport (struct server *s)
 | |
| {
 | |
| 	if (s->transport
 | |
| 	 && s->transport->cleanup)
 | |
| 		s->transport->cleanup (s);
 | |
| 	s->transport = NULL;
 | |
| 
 | |
| 	xclose (s->socket);
 | |
| 	s->socket = -1;
 | |
| 	s->state = IRC_DISCONNECTED;
 | |
| 
 | |
| 	s->socket_event.closed = true;
 | |
| 	poller_fd_reset (&s->socket_event);
 | |
| 
 | |
| 	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_init (&iter, &s->irc_channels);
 | |
| 	struct channel *channel;
 | |
| 	while ((channel = str_map_iter_next (&iter)))
 | |
| 		irc_left_channel (channel);
 | |
| 
 | |
| 	if (s->irc_user)
 | |
| 	{
 | |
| 		user_unref (s->irc_user);
 | |
| 		s->irc_user = NULL;
 | |
| 	}
 | |
| 
 | |
| 	str_reset (&s->irc_user_mode);
 | |
| 	free (s->irc_user_host);
 | |
| 	s->irc_user_host = NULL;
 | |
| 
 | |
| 	s->cap_echo_message = 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_init (&iter, &s->irc_buffer_map);
 | |
| 	struct buffer *buffer;
 | |
| 	while ((buffer = str_map_iter_next (&iter)))
 | |
| 		log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT,
 | |
| 			"Disconnected from server");
 | |
| 
 | |
| 	irc_cancel_timers (s);
 | |
| 	irc_destroy_transport (s);
 | |
| 	irc_destroy_state (s);
 | |
| 
 | |
| 	// Take any relevant actions
 | |
| 	if (s->ctx->quitting)
 | |
| 		try_finish_quit (s->ctx);
 | |
| 	else if (s->manual_disconnect)
 | |
| 		s->manual_disconnect = false;
 | |
| 	else
 | |
| 	{
 | |
| 		s->reconnect_attempt = 0;
 | |
| 		irc_queue_reconnect (s);
 | |
| 	}
 | |
| 
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_initiate_disconnect (struct server *s, const char *reason)
 | |
| {
 | |
| 	hard_assert (irc_is_connected (s));
 | |
| 
 | |
| 	s->manual_disconnect = true;
 | |
| 	if (reason)
 | |
| 		irc_send (s, "QUIT :%s", reason);
 | |
| 	else
 | |
| 		irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| on_irc_ping_timeout (void *user_data)
 | |
| {
 | |
| 	struct server *s = user_data;
 | |
| 	log_server_error (s, s->buffer, "Connection 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_init (&joins_sent);
 | |
| 
 | |
| 	// 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 str_vector v;
 | |
| 		str_vector_init (&v);
 | |
| 		cstr_split (autojoin, ",", &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);
 | |
| 		}
 | |
| 		str_vector_free (&v);
 | |
| 	}
 | |
| 
 | |
| 	// Then also rejoin any channels from the last disconnect
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &s->irc_channels);
 | |
| 	struct channel *channel;
 | |
| 	while ((channel = str_map_iter_next (&iter)))
 | |
| 		if (!channel->left_manually
 | |
| 		 && !str_map_find (&joins_sent, channel->name))
 | |
| 			irc_send (s, "JOIN :%s", channel->name);
 | |
| 
 | |
| 	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->vtable->filter (hook, s, input)))
 | |
| 		{
 | |
| 			log_server_debug (s, "#a>= #s#r", ATTR_JOIN, "thrown away by hook");
 | |
| 			return NULL;
 | |
| 		}
 | |
| 
 | |
| 		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
 | |
| 		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)
 | |
| {
 | |
| 	struct error *e = NULL;
 | |
| 	enum socket_io_result result =
 | |
| 		socket_io_try_read (s->socket, &s->read_buffer, &e);
 | |
| 	if (e)
 | |
| 	{
 | |
| 		print_debug ("%s: %s", __func__, e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static enum socket_io_result
 | |
| transport_plain_try_write (struct server *s)
 | |
| {
 | |
| 	struct error *e = NULL;
 | |
| 	enum socket_io_result result =
 | |
| 		socket_io_try_write (s->socket, &s->write_buffer, &e);
 | |
| 	if (e)
 | |
| 	{
 | |
| 		print_debug ("%s: %s", __func__, e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| 	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;
 | |
| 
 | |
| 		FAIL ("%s: %s", "Failed to set locations for the CA certificate bundle",
 | |
| 			ERR_reason_error_string (ERR_get_error ()));
 | |
| 	}
 | |
| 
 | |
| 	if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
 | |
| 		FAIL ("%s: %s", "Couldn't load the default CA certificate bundle",
 | |
| 			ERR_reason_error_string (ERR_get_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
 | |
| 
 | |
| 	struct error *error = NULL;
 | |
| 	if (!transport_tls_init_ca (s, ssl_ctx, &error))
 | |
| 	{
 | |
| 		if (verify)
 | |
| 		{
 | |
| 			error_propagate (e, error);
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		// Only 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",
 | |
| 			ERR_reason_error_string (ERR_get_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",
 | |
| 			ERR_reason_error_string (ERR_get_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_ensure_space (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
 | |
| 	struct passwd *pwd = getpwuid (geteuid ());
 | |
| 	if (!pwd)
 | |
| 		FAIL ("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 str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (get_config_string (s->config, "nicks"), ',', &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;
 | |
| 
 | |
| 	str_vector_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.1 capability negotiation;
 | |
| 	// at worst the server will ignore this or send a harmless error message
 | |
| 	irc_send (s, "CAP LS");
 | |
| 
 | |
| 	const char *password = get_config_string (s->config, "password");
 | |
| 	if (password)
 | |
| 		irc_send (s, "PASS :%s", password);
 | |
| 
 | |
| 	s->nick_counter = 0;
 | |
| 
 | |
| 	char *nickname = irc_fetch_next_nickname (s);
 | |
| 	if (nickname)
 | |
| 		irc_send (s, "NICK :%s", nickname);
 | |
| 	else
 | |
| 		log_server_error (s, s->buffer, "No nicks present in configuration");
 | |
| 	free (nickname);
 | |
| 
 | |
| 	// IRC servers may ignore the last argument if it's empty
 | |
| 	irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " ");
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_finish_connection (struct server *s, int socket, const char *hostname)
 | |
| {
 | |
| 	struct app_context *ctx = s->ctx;
 | |
| 
 | |
| 	// Most of our output comes from the user one full command at a time and we
 | |
| 	// use output buffering, so it makes a lot of sense to avoid these delays
 | |
| 	int yes = 1;
 | |
| 	soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY,
 | |
| 		&yes, sizeof yes) != -1);
 | |
| 
 | |
| 	set_blocking (socket, false);
 | |
| 	s->socket = socket;
 | |
| 	s->transport = get_config_boolean (s->config, "tls")
 | |
| 		? &g_transport_tls
 | |
| 		: &g_transport_plain;
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	if (s->transport->init && !s->transport->init (s, hostname, &e))
 | |
| 	{
 | |
| 		log_server_error (s, s->buffer, "Connection failed: #s", e->message);
 | |
| 		error_free (e);
 | |
| 
 | |
| 		xclose (s->socket);
 | |
| 		s->socket = -1;
 | |
| 
 | |
| 		s->transport = NULL;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	log_server_status (s, s->buffer, "Connection established");
 | |
| 	s->state = IRC_CONNECTED;
 | |
| 
 | |
| 	poller_fd_init (&s->socket_event, &ctx->poller, s->socket);
 | |
| 	s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready;
 | |
| 	s->socket_event.user_data = s;
 | |
| 
 | |
| 	irc_update_poller (s, NULL);
 | |
| 	irc_reset_connection_timeouts (s);
 | |
| 	irc_register (s);
 | |
| 
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_split_host_port (char *s, char **host, char **port)
 | |
| {
 | |
| 	char *colon = strrchr (s, ':');
 | |
| 	if (colon)
 | |
| 	{
 | |
| 		*colon = '\0';
 | |
| 		*port = ++colon;
 | |
| 	}
 | |
| 	else
 | |
| 		*port = "6667";
 | |
| 
 | |
| 	// Unwrap IPv6 addresses in format_host_port_pair() format
 | |
| 	size_t host_end = strlen (s) - 1;
 | |
| 	if (*s == '[' && s[host_end] == ']')
 | |
| 		s++[host_end] = '\0';
 | |
| 
 | |
| 	*host = 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 str_vector *addresses)
 | |
| {
 | |
| 	struct connector *connector = xmalloc (sizeof *connector);
 | |
| 	connector_init (connector, &s->ctx->poller);
 | |
| 	s->connector = connector;
 | |
| 
 | |
| 	connector->user_data     = s;
 | |
| 	connector->on_connecting = irc_on_connector_connecting;
 | |
| 	connector->on_error      = irc_on_connector_error;
 | |
| 	connector->on_connected  = irc_on_connector_connected;
 | |
| 	connector->on_failure    = irc_on_connector_failure;
 | |
| 
 | |
| 	for (size_t i = 0; i < addresses->len; i++)
 | |
| 	{
 | |
| 		char *host, *port;
 | |
| 		irc_split_host_port (addresses->vector[i], &host, &port);
 | |
| 		connector_add_target (connector, host, port);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // TODO: see if we can further merge code for the two connectors, for example
 | |
| //   by making SOCKS 4A and 5 mere plugins for the connector, or by using
 | |
| //   a virtual interface common to them both (seems more likely)
 | |
| 
 | |
| static void
 | |
| irc_on_socks_connecting (void *user_data,
 | |
| 	const char *address, const char *via, const char *version)
 | |
| {
 | |
| 	struct server *s = user_data;
 | |
| 	log_server_status (s, s->buffer,
 | |
| 		"Connecting to #s via #s (#s)...", address, via, version);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| irc_setup_connector_socks (struct server *s,
 | |
| 	const struct str_vector *addresses, struct error **e)
 | |
| {
 | |
| 	const char *socks_host = get_config_string (s->config, "socks_host");
 | |
| 	int64_t socks_port_int = get_config_integer (s->config, "socks_port");
 | |
| 
 | |
| 	if (!socks_host)
 | |
| 		return false;
 | |
| 
 | |
| 	struct socks_connector *connector = xmalloc (sizeof *connector);
 | |
| 	socks_connector_init (connector, &s->ctx->poller);
 | |
| 	s->socks_conn = connector;
 | |
| 
 | |
| 	connector->user_data     = s;
 | |
| 	connector->on_connecting = irc_on_socks_connecting;
 | |
| 	connector->on_error      = irc_on_connector_error;
 | |
| 	connector->on_connected  = irc_on_connector_connected;
 | |
| 	connector->on_failure    = irc_on_connector_failure;
 | |
| 
 | |
| 	for (size_t i = 0; i < addresses->len; i++)
 | |
| 	{
 | |
| 		char *host, *port;
 | |
| 		irc_split_host_port (addresses->vector[i], &host, &port);
 | |
| 
 | |
| 		if (!socks_connector_add_target (connector, host, port, e))
 | |
| 			return false;
 | |
| 	}
 | |
| 
 | |
| 	char *service = xstrdup_printf ("%" PRIi64, socks_port_int);
 | |
| 	socks_connector_run (connector, socks_host, service,
 | |
| 		get_config_string (s->config, "socks_username"),
 | |
| 		get_config_string (s->config, "socks_password"));
 | |
| 	free (service);
 | |
| 
 | |
| 	// The SOCKS connector can have already failed; we mustn't return true then
 | |
| 	if (!s->socks_conn)
 | |
| 		FAIL ("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 str_vector servers;
 | |
| 	str_vector_init (&servers);
 | |
| 	cstr_split_ignore_empty (addresses, ',', &servers);
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	if (!irc_setup_connector_socks (s, &servers, &e) && !e)
 | |
| 		irc_setup_connector (s, &servers);
 | |
| 
 | |
| 	str_vector_free (&servers);
 | |
| 
 | |
| 	if (e)
 | |
| 	{
 | |
| 		irc_destroy_connector (s);
 | |
| 
 | |
| 		log_server_error (s, s->buffer, "#s", e->message);
 | |
| 		error_free (e);
 | |
| 		irc_queue_reconnect (s);
 | |
| 	}
 | |
| 	else if (s->state != IRC_CONNECTED)
 | |
| 		s->state = IRC_CONNECTING;
 | |
| }
 | |
| 
 | |
| // --- Input prompt ------------------------------------------------------------
 | |
| 
 | |
| static void
 | |
| make_unseen_prefix (struct app_context *ctx, struct str *active_buffers)
 | |
| {
 | |
| 	size_t buffer_no = 0;
 | |
| 	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
 | |
| 	{
 | |
| 		buffer_no++;
 | |
| 		if (!(iter->new_messages_count - iter->new_unimportant_count)
 | |
| 		 || iter == ctx->current_buffer)
 | |
| 			continue;
 | |
| 
 | |
| 		if (active_buffers->len)
 | |
| 			str_append_c (active_buffers, ',');
 | |
| 		if (iter->highlighted)
 | |
| 			str_append_c (active_buffers, '!');
 | |
| 		str_append_printf (active_buffers, "%zu", buffer_no);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| make_chanmode_postfix (struct channel *channel, struct str *modes)
 | |
| {
 | |
| 	if (channel->no_param_modes.len)
 | |
| 		str_append (modes, channel->no_param_modes.str);
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &channel->param_modes);
 | |
| 
 | |
| 	char *param;
 | |
| 	while ((param = str_map_iter_next (&iter)))
 | |
| 		str_append_c (modes, iter.link->key[0]);
 | |
| }
 | |
| 
 | |
| static void
 | |
| make_server_postfix_registered (struct buffer *buffer, struct str *output)
 | |
| {
 | |
| 	struct server *s = buffer->server;
 | |
| 	if (buffer->type == BUFFER_CHANNEL)
 | |
| 	{
 | |
| 		struct server *s = buffer->server;
 | |
| 		struct channel_user *channel_user =
 | |
| 			irc_channel_get_user (buffer->channel, s->irc_user);
 | |
| 		if (channel_user)
 | |
| 			irc_get_channel_user_prefix (s, channel_user, output);
 | |
| 	}
 | |
| 	str_append (output, s->irc_user->nickname);
 | |
| 	if (s->irc_user_mode.len)
 | |
| 		str_append_printf (output, "(%s)", s->irc_user_mode.str);
 | |
| }
 | |
| 
 | |
| static void
 | |
| make_server_postfix (struct buffer *buffer, struct str *output)
 | |
| {
 | |
| 	struct server *s = buffer->server;
 | |
| 	str_append_c (output, ' ');
 | |
| 	if (!irc_is_connected (s))
 | |
| 		str_append (output, "(disconnected)");
 | |
| 	else if (s->state != IRC_REGISTERED)
 | |
| 		str_append (output, "(unregistered)");
 | |
| 	else
 | |
| 		make_server_postfix_registered (buffer, output);
 | |
| }
 | |
| static void
 | |
| make_prompt (struct app_context *ctx, struct str *output)
 | |
| {
 | |
| 	struct buffer *buffer = ctx->current_buffer;
 | |
| 	if (!buffer)
 | |
| 		return;
 | |
| 
 | |
| 	str_append_c (output, '[');
 | |
| 
 | |
| 	struct str active_buffers;
 | |
| 	str_init (&active_buffers);
 | |
| 	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);
 | |
| 	if (buffer->type == BUFFER_CHANNEL)
 | |
| 	{
 | |
| 		struct str modes;
 | |
| 		str_init (&modes);
 | |
| 		make_chanmode_postfix (buffer->channel, &modes);
 | |
| 		if (modes.len)
 | |
| 			str_append_printf (output, "(+%s)", modes.str);
 | |
| 		str_free (&modes);
 | |
| 	}
 | |
| 
 | |
| 	if (buffer != ctx->global_buffer)
 | |
| 		make_server_postfix (buffer, output);
 | |
| 
 | |
| 	str_append_c (output, ']');
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_maybe_set_prompt (struct input *self, char *new_prompt)
 | |
| {
 | |
| 	// 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
 | |
| refresh_prompt (struct app_context *ctx)
 | |
| {
 | |
| 	bool have_attributes = !!get_attribute_printer (stdout);
 | |
| 
 | |
| 	struct str prompt;
 | |
| 	str_init (&prompt);
 | |
| 	make_prompt (ctx, &prompt);
 | |
| 	str_append_c (&prompt, ' ');
 | |
| 	char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
 | |
| 	str_free (&prompt);
 | |
| 
 | |
| 	if (have_attributes)
 | |
| 	{
 | |
| 		// XXX: to be completely correct, we should use tputs, but we cannot
 | |
| 		input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c",
 | |
| 			INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
 | |
| 			INPUT_END_IGNORE,
 | |
| 			localized,
 | |
| 			INPUT_START_IGNORE, ctx->attrs[ATTR_RESET],
 | |
| 			INPUT_END_IGNORE));
 | |
| 		free (localized);
 | |
| 	}
 | |
| 	else
 | |
| 		input_maybe_set_prompt (ctx->input, localized);
 | |
| }
 | |
| 
 | |
| // --- Helpers -----------------------------------------------------------------
 | |
| 
 | |
| static struct buffer *
 | |
| irc_get_buffer_for_message (struct server *s,
 | |
| 	const struct irc_message *msg, const char *target)
 | |
| {
 | |
| 	// TODO: display such messages differently
 | |
| 	target = irc_skip_statusmsg (s, target);
 | |
| 
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
 | |
| 	if (irc_is_channel (s, target))
 | |
| 	{
 | |
| 		struct channel *channel = str_map_find (&s->irc_channels, target);
 | |
| 		hard_assert ((channel && buffer) ||
 | |
| 			(channel && !buffer) || (!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;
 | |
| 
 | |
| 	// Well, this is rather crude but it should make most users happy.
 | |
| 	// Ideally we could do this at least in proper Unicode.
 | |
| 	char *copy = xstrdup (message);
 | |
| 	cstr_transform (copy, s->irc_tolower);
 | |
| 
 | |
| 	char *nick = xstrdup (s->irc_user->nickname);
 | |
| 	cstr_transform (nick, s->irc_tolower);
 | |
| 
 | |
| 	// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
 | |
| 	// Also excluded from the ASCII: common user channel prefixes: +%@&~
 | |
| 	const char *delimiters = ",.;:!?()<>/=#$* \t\r\n\v\f\"'";
 | |
| 
 | |
| 	bool result = false;
 | |
| 	char *save = NULL;
 | |
| 	for (char *token = strtok_r (copy, delimiters, &save);
 | |
| 		token; token = strtok_r (NULL, delimiters, &save))
 | |
| 		if (!strcmp (token, nick))
 | |
| 		{
 | |
| 			result = true;
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 	free (copy);
 | |
| 	free (nick);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static char *
 | |
| irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
 | |
| {
 | |
| 	struct str prefix;
 | |
| 	str_init (&prefix);
 | |
| 
 | |
| 	target = irc_skip_statusmsg (s, target);
 | |
| 	if (user && irc_is_channel (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
 | |
| };
 | |
| 
 | |
| /// 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];
 | |
| 
 | |
| 	struct str *prefixes = &channel_user->prefixes;
 | |
| 	const char *pos = strchr (prefixes->str, prefix);
 | |
| 	if (self->adding == !!pos)
 | |
| 		return;
 | |
| 
 | |
| 	if (self->adding)
 | |
| 	{
 | |
| 		// Add the new mode prefix while retaining the right order
 | |
| 		char *old_prefixes = str_steal (prefixes);
 | |
| 		str_init (prefixes);
 | |
| 		for (const char *p = all_prefixes; *p; p++)
 | |
| 			if (*p == prefix || strchr (old_prefixes, *p))
 | |
| 				str_append_c (prefixes, *p);
 | |
| 		free (old_prefixes);
 | |
| 	}
 | |
| 	else
 | |
| 		str_remove_slice (prefixes, pos - prefixes->str, 1);
 | |
| }
 | |
| 
 | |
| 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)
 | |
| {
 | |
| 	if      (strchr (self->s->irc_chanuser_modes,           self->mode_char))
 | |
| 		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;
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_mode_channel
 | |
| 	(struct server *s, struct channel *channel, char **params)
 | |
| {
 | |
| 	struct mode_processor p = { .s = s, .channel = channel };
 | |
| 	mode_processor_run (&p, params, mode_processor_apply_channel);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static bool
 | |
| mode_processor_apply_user (struct mode_processor *self)
 | |
| {
 | |
| 	mode_processor_toggle (self, &self->s->irc_user_mode);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_mode_user (struct server *s, char **params)
 | |
| {
 | |
| 	struct mode_processor p = { .s = s };
 | |
| 	mode_processor_run (&p, params, mode_processor_apply_user);
 | |
| }
 | |
| 
 | |
| // --- Output processing -------------------------------------------------------
 | |
| 
 | |
| // Both user and plugins can send whatever the heck they want to,
 | |
| // we need to parse it back so that it's evident what's happening
 | |
| 
 | |
| static void
 | |
| irc_handle_sent_cap (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	const char *subcommand = msg->params.vector[1];
 | |
| 	const char *args = (msg->params.len > 2) ? msg->params.vector[2] : "";
 | |
| 	if (!strcasecmp_ascii (subcommand, "REQ"))
 | |
| 		log_server_status (s, s->buffer,
 | |
| 			"#s: #S", "Capabilities requested", args);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_sent_notice_text (struct server *s,
 | |
| 	const struct irc_message *msg, struct str *text)
 | |
| {
 | |
| 	const char *target = msg->params.vector[0];
 | |
| 	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
 | |
| 	if (buffer && soft_assert (s->irc_user))
 | |
| 		log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str);
 | |
| 	else
 | |
| 		log_outcoming_orphan_notice (s, target, text->str);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_sent_notice (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2 || s->cap_echo_message)
 | |
| 		return;
 | |
| 
 | |
| 	// This ignores empty messages which we should not normally send
 | |
| 	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
 | |
| 	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
 | |
| 	{
 | |
| 		if (iter->is_extended)
 | |
| 			log_ctcp_reply (s, msg->params.vector[0],
 | |
| 				xstrdup_printf ("%s %s", iter->tag.str, iter->text.str));
 | |
| 		else
 | |
| 			irc_handle_sent_notice_text (s, msg, &iter->text);
 | |
| 	}
 | |
| 	ctcp_destroy (chunks);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_sent_privmsg_text (struct server *s,
 | |
| 	const struct irc_message *msg, struct str *text, bool is_action)
 | |
| {
 | |
| 	const char *target = msg->params.vector[0];
 | |
| 	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
 | |
| 	if (buffer && soft_assert (s->irc_user))
 | |
| 	{
 | |
| 		char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target);
 | |
| 		if (is_action)
 | |
| 			log_outcoming_action (s, buffer, s->irc_user->nickname, text->str);
 | |
| 		else
 | |
| 			log_outcoming_privmsg (s, buffer,
 | |
| 				prefixes, s->irc_user->nickname, text->str);
 | |
| 		free (prefixes);
 | |
| 	}
 | |
| 	else
 | |
| 		// TODO: also handle actions here
 | |
| 		log_outcoming_orphan_privmsg (s, target, text->str);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2 || s->cap_echo_message)
 | |
| 		return;
 | |
| 
 | |
| 	// This ignores empty messages which we should not normally send
 | |
| 	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
 | |
| 	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
 | |
| 	{
 | |
| 		if (!iter->is_extended)
 | |
| 			irc_handle_sent_privmsg_text (s, msg, &iter->text, false);
 | |
| 		else if (!strcmp (iter->tag.str, "ACTION"))
 | |
| 			irc_handle_sent_privmsg_text (s, msg, &iter->text, true);
 | |
| 		else
 | |
| 			log_ctcp_query (s, msg->params.vector[0], iter->tag.str);
 | |
| 	}
 | |
| 	ctcp_destroy (chunks);
 | |
| }
 | |
| 
 | |
| static struct irc_handler
 | |
| {
 | |
| 	const char *name;
 | |
| 	void (*handler) (struct server *s, const struct irc_message *msg);
 | |
| }
 | |
| g_irc_sent_handlers[] =
 | |
| {
 | |
| 	// This list needs to stay sorted
 | |
| 	{ "CAP",     irc_handle_sent_cap     },
 | |
| 	{ "NOTICE",  irc_handle_sent_notice  },
 | |
| 	{ "PRIVMSG", irc_handle_sent_privmsg },
 | |
| };
 | |
| 
 | |
| static int
 | |
| irc_handler_cmp_by_name (const void *a, const void *b)
 | |
| {
 | |
| 	const struct irc_handler *first  = a;
 | |
| 	const struct irc_handler *second = b;
 | |
| 	return strcasecmp_ascii (first->name, second->name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_process_sent_message (const struct irc_message *msg, struct server *s)
 | |
| {
 | |
| 	// The server is free to reject even a matching prefix
 | |
| 	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_cap (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 
 | |
| 	const char *args = "";
 | |
| 	if (msg->params.len > 2)
 | |
| 		cstr_split_ignore_empty ((args = msg->params.vector[2]), ' ', &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++;
 | |
| 			}
 | |
| 			if (!strcasecmp_ascii (cap, "echo-message"))
 | |
| 				s->cap_echo_message = active;
 | |
| 		}
 | |
| 		irc_send (s, "CAP END");
 | |
| 	}
 | |
| 	else if (!strcasecmp_ascii (subcommand, "NAK"))
 | |
| 	{
 | |
| 		log_server_error (s, s->buffer,
 | |
| 			"#s: #S", "Capabilities not acknowledged", args);
 | |
| 		irc_send (s, "CAP END");
 | |
| 	}
 | |
| 	else if (!strcasecmp_ascii (subcommand, "LS"))
 | |
| 	{
 | |
| 		log_server_status (s, s->buffer,
 | |
| 			"#s: #S", "Capabilities supported", args);
 | |
| 
 | |
| 		struct str_vector chosen; str_vector_init (&chosen);
 | |
| 		struct str_vector use;    str_vector_init (&use);
 | |
| 
 | |
| 		cstr_split_ignore_empty
 | |
| 			(get_config_string (s->config, "capabilities"), ',', &use);
 | |
| 
 | |
| 		// Filter server capabilities for ones we can make use of
 | |
| 		for (size_t i = 0; i < v.len; i++)
 | |
| 		{
 | |
| 			const char *cap = v.vector[i];
 | |
| 			for (size_t k = 0; k < use.len; k++)
 | |
| 				if (!strcasecmp_ascii (use.vector[k], cap))
 | |
| 					str_vector_add (&chosen, cap);
 | |
| 		}
 | |
| 
 | |
| 		char *chosen_str = join_str_vector (&chosen, ' ');
 | |
| 		str_vector_free (&chosen);
 | |
| 		str_vector_free (&use);
 | |
| 
 | |
| 		irc_send (s, "CAP REQ :%s", chosen_str);
 | |
| 		free (chosen_str);
 | |
| 	}
 | |
| 
 | |
| 	str_vector_free (&v);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_error (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 1)
 | |
| 		return;
 | |
| 
 | |
| 	log_server_error (s, s->buffer, "#m", msg->params.vector[0]);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_invite (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	const char *target       = msg->params.vector[0];
 | |
| 	const char *channel_name = msg->params.vector[1];
 | |
| 
 | |
| 	struct buffer *buffer;
 | |
| 	if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
 | |
| 		buffer = s->buffer;
 | |
| 
 | |
| 	// IRCv3.2 invite-notify extension allows the target to be someone else
 | |
| 	if (irc_is_this_us (s, target))
 | |
| 		log_server_status (s, buffer,
 | |
| 			"#n has invited you to #S", msg->prefix, channel_name);
 | |
| 	else
 | |
| 		log_server_status (s, buffer,
 | |
| 			"#n has invited #n to #S", msg->prefix, target, channel_name);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_join (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 1)
 | |
| 		return;
 | |
| 
 | |
| 	const char *channel_name = msg->params.vector[0];
 | |
| 	if (!irc_is_channel (s, channel_name))
 | |
| 		return;
 | |
| 
 | |
| 	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
 | |
| 	hard_assert ((channel && buffer) ||
 | |
| 		(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 	// We've joined a new channel
 | |
| 	if (!channel && irc_is_this_us (s, msg->prefix))
 | |
| 	{
 | |
| 		buffer = buffer_new (s->ctx->input);
 | |
| 		buffer->type = BUFFER_CHANNEL;
 | |
| 		buffer->name = xstrdup_printf ("%s.%s", s->name, 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);
 | |
| 		buffer_activate (s->ctx, buffer);
 | |
| 
 | |
| 		// Request the channel mode as we don't get it automatically
 | |
| 		irc_send (s, "MODE %s", channel_name);
 | |
| 	}
 | |
| 
 | |
| 	// This is weird, ignoring
 | |
| 	if (!channel)
 | |
| 		return;
 | |
| 
 | |
| 	// Reset the field so that we rejoin the channel after reconnecting
 | |
| 	channel->left_manually = false;
 | |
| 
 | |
| 	// Add the user to the channel
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 	irc_channel_link_user (channel, irc_get_or_make_user (s, nickname), "");
 | |
| 	free (nickname);
 | |
| 
 | |
| 	// Finally log the message
 | |
| 	if (buffer)
 | |
| 	{
 | |
| 		log_server (s, buffer, BUFFER_LINE_UNIMPORTANT, "#a-->#r #N #a#s#r #S",
 | |
| 			ATTR_JOIN, msg->prefix, ATTR_JOIN, "has joined", channel_name);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_kick (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	const char *channel_name = msg->params.vector[0];
 | |
| 	const char *target = msg->params.vector[1];
 | |
| 	if (!irc_is_channel (s, channel_name)
 | |
| 	 || irc_is_channel (s, target))
 | |
| 		return;
 | |
| 
 | |
| 	const char *message = NULL;
 | |
| 	if (msg->params.len > 2)
 | |
| 		message = msg->params.vector[2];
 | |
| 
 | |
| 	struct user *user = str_map_find (&s->irc_users, target);
 | |
| 	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
 | |
| 	hard_assert ((channel && buffer) ||
 | |
| 		(channel && !buffer) || (!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_init (&f, s->ctx, s);
 | |
| 		formatter_add (&f, "#a<--#r #N #a#s#r #n",
 | |
| 			ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target);
 | |
| 		if (message)
 | |
| 			formatter_add (&f, " (#m)", message);
 | |
| 		log_formatter (s->ctx, buffer, 0, &f);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_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 str_vector copy;
 | |
| 	str_vector_init (©);
 | |
| 	str_vector_add_vector (©, msg->params.vector + 1);
 | |
| 	char *modes = join_str_vector (©, ' ');
 | |
| 	str_vector_free (©);
 | |
| 
 | |
| 	if (irc_is_channel (s, context))
 | |
| 	{
 | |
| 		struct channel *channel = str_map_find (&s->irc_channels, context);
 | |
| 		struct buffer *buffer = str_map_find (&s->irc_buffer_map, context);
 | |
| 		hard_assert ((channel && buffer) ||
 | |
| 			(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 		if (channel)
 | |
| 			irc_handle_mode_channel (s, channel, msg->params.vector + 1);
 | |
| 
 | |
| 		if (buffer)
 | |
| 		{
 | |
| 			log_server_status (s, buffer,
 | |
| 				"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);
 | |
| 
 | |
| 	// Our own modes might have changed
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_nick (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 1)
 | |
| 		return;
 | |
| 
 | |
| 	const char *new_nickname = msg->params.vector[0];
 | |
| 
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 	struct user *user = str_map_find (&s->irc_users, nickname);
 | |
| 	free (nickname);
 | |
| 	if (!user)
 | |
| 		return;
 | |
| 
 | |
| 	bool lexicographically_different =
 | |
| 		!!irc_server_strcmp (s, user->nickname, new_nickname);
 | |
| 
 | |
| 	// What the fuck, someone renamed themselves to ourselves
 | |
| 	// TODO: probably log a message and force a reconnect
 | |
| 	if (lexicographically_different
 | |
| 	 && !irc_server_strcmp (s, new_nickname, s->irc_user->nickname))
 | |
| 		return;
 | |
| 
 | |
| 	// Log a message in any PM buffer (we may even have one for ourselves)
 | |
| 	struct buffer *pm_buffer =
 | |
| 		str_map_find (&s->irc_buffer_map, user->nickname);
 | |
| 	if (pm_buffer)
 | |
| 	{
 | |
| 		if (irc_is_this_us (s, msg->prefix))
 | |
| 			log_nick_self (s, pm_buffer, new_nickname);
 | |
| 		else
 | |
| 			log_nick (s, pm_buffer, msg->prefix, new_nickname);
 | |
| 	}
 | |
| 
 | |
| 	// The new nickname may collide with a user referenced by a PM buffer,
 | |
| 	// or in case of data inconsistency with the server, channels.
 | |
| 	// In the latter case we need the colliding user to leave all of them.
 | |
| 	struct user *user_collision = NULL;
 | |
| 	if (lexicographically_different
 | |
| 	 && (user_collision = str_map_find (&s->irc_users, new_nickname)))
 | |
| 		LIST_FOR_EACH (struct user_channel, iter, user_collision->channels)
 | |
| 			irc_remove_user_from_channel (user_collision, iter->channel);
 | |
| 
 | |
| 	struct buffer *buffer_collision = NULL;
 | |
| 	if (lexicographically_different
 | |
| 	 && (buffer_collision = str_map_find (&s->irc_buffer_map, new_nickname)))
 | |
| 	{
 | |
| 		hard_assert (buffer_collision->type == BUFFER_PM);
 | |
| 		hard_assert (buffer_collision->user == user_collision);
 | |
| 
 | |
| 		user_unref (buffer_collision->user);
 | |
| 		buffer_collision->user = user_ref (user);
 | |
| 	}
 | |
| 
 | |
| 	if (pm_buffer && buffer_collision)
 | |
| 	{
 | |
| 		// There's not much else we can do other than somehow try to merge
 | |
| 		// one buffer into the other.  In our case, the original buffer wins.
 | |
| 		buffer_merge (s->ctx, buffer_collision, pm_buffer);
 | |
| 		if (s->ctx->current_buffer == pm_buffer)
 | |
| 			buffer_activate (s->ctx, buffer_collision);
 | |
| 		buffer_remove (s->ctx, pm_buffer);
 | |
| 		pm_buffer = buffer_collision;
 | |
| 	}
 | |
| 
 | |
| 	// The colliding user should be completely gone by now
 | |
| 	if (lexicographically_different)
 | |
| 		hard_assert (!str_map_find (&s->irc_users, new_nickname));
 | |
| 
 | |
| 	// Now we can rename the PM buffer to reflect the new nickname
 | |
| 	if (pm_buffer)
 | |
| 	{
 | |
| 		str_map_set (&s->irc_buffer_map, user->nickname, NULL);
 | |
| 		str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);
 | |
| 
 | |
| 		char *x = xstrdup_printf ("%s.%s", s->name, new_nickname);
 | |
| 		buffer_rename (s->ctx, pm_buffer, x);
 | |
| 		free (x);
 | |
| 	}
 | |
| 
 | |
| 	if (irc_is_this_us (s, msg->prefix))
 | |
| 	{
 | |
| 		log_nick_self (s, s->buffer, new_nickname);
 | |
| 
 | |
| 		// Log a message in all open buffers on this server
 | |
| 		struct str_map_iter iter;
 | |
| 		str_map_iter_init (&iter, &s->irc_buffer_map);
 | |
| 		struct buffer *buffer;
 | |
| 		while ((buffer = str_map_iter_next (&iter)))
 | |
| 		{
 | |
| 			// We've already done that
 | |
| 			if (buffer != pm_buffer)
 | |
| 				log_nick_self (s, buffer, new_nickname);
 | |
| 		}
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		// Log a message in all channels the user is in
 | |
| 		LIST_FOR_EACH (struct user_channel, iter, user->channels)
 | |
| 		{
 | |
| 			struct buffer *buffer =
 | |
| 				str_map_find (&s->irc_buffer_map, iter->channel->name);
 | |
| 			hard_assert (buffer != NULL);
 | |
| 			log_nick (s, buffer, msg->prefix, new_nickname);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Finally rename the user as it should be safe now
 | |
| 	str_map_set (&s->irc_users, user->nickname, NULL);
 | |
| 	str_map_set (&s->irc_users, new_nickname, user);
 | |
| 
 | |
| 	free (user->nickname);
 | |
| 	user->nickname = xstrdup (new_nickname);
 | |
| 
 | |
| 	// We might have renamed ourselves
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_ctcp_reply (struct server *s,
 | |
| 	const struct irc_message *msg, struct ctcp_chunk *chunk)
 | |
| {
 | |
| 	const char *target = msg->params.vector[0];
 | |
| 	if (irc_is_this_us (s, msg->prefix))
 | |
| 		log_ctcp_reply (s, target,
 | |
| 			xstrdup_printf ("%s %s", chunk->tag.str, chunk->text.str));
 | |
| 	else
 | |
| 		log_server_status (s, s->buffer, "CTCP reply from #n: #S #S",
 | |
| 			msg->prefix, chunk->tag.str, chunk->text.str);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_notice_text (struct server *s,
 | |
| 	const struct irc_message *msg, struct str *text)
 | |
| {
 | |
| 	const char *target = msg->params.vector[0];
 | |
| 	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
 | |
| 	if (!buffer)
 | |
| 	{
 | |
| 		if (irc_is_this_us (s, msg->prefix))
 | |
| 			log_outcoming_orphan_notice (s, target, text->str);
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	char *nick = irc_cut_nickname (msg->prefix);
 | |
| 	// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
 | |
| 	if (!irc_is_this_us (s, msg->prefix) && irc_is_highlight (s, text->str))
 | |
| 		log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_HIGHLIGHT,
 | |
| 			"#a#s(#S)#r: #m", ATTR_HIGHLIGHT, "Notice", nick, text->str);
 | |
| 	else
 | |
| 		log_outcoming_notice (s, buffer, msg->prefix, text->str);
 | |
| 	free (nick);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_notice (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	// This ignores empty messages which we should never receive anyway
 | |
| 	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
 | |
| 	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
 | |
| 		if (!iter->is_extended)
 | |
| 			irc_handle_notice_text (s, msg, &iter->text);
 | |
| 		else
 | |
| 			irc_handle_ctcp_reply (s, msg, iter);
 | |
| 	ctcp_destroy (chunks);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_part (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 1)
 | |
| 		return;
 | |
| 
 | |
| 	const char *channel_name = msg->params.vector[0];
 | |
| 	if (!irc_is_channel (s, channel_name))
 | |
| 		return;
 | |
| 
 | |
| 	const char *message = NULL;
 | |
| 	if (msg->params.len > 1)
 | |
| 		message = msg->params.vector[1];
 | |
| 
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 	struct user *user = str_map_find (&s->irc_users, nickname);
 | |
| 	free (nickname);
 | |
| 
 | |
| 	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
 | |
| 	hard_assert ((channel && buffer) ||
 | |
| 		(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 	// It would be is 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_init (&f, s->ctx, s);
 | |
| 		formatter_add (&f, "#a<--#r #N #a#s#r #S",
 | |
| 			ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name);
 | |
| 		if (message)
 | |
| 			formatter_add (&f, " (#m)", message);
 | |
| 		log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &f);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_ping (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len)
 | |
| 		irc_send (s, "PONG :%s", msg->params.vector[0]);
 | |
| 	else
 | |
| 		irc_send (s, "PONG");
 | |
| }
 | |
| 
 | |
| static char *
 | |
| ctime_now (char buf[26])
 | |
| {
 | |
| 	struct tm tm_;
 | |
| 	time_t now = time (NULL);
 | |
| 	if (!asctime_r (localtime_r (&now, &tm_), buf))
 | |
| 		return NULL;
 | |
| 
 | |
| 	// Annoying thing
 | |
| 	*strchr (buf, '\n') = '\0';
 | |
| 	return buf;
 | |
| }
 | |
| 
 | |
| static void irc_send_ctcp_reply (struct server *s, const char *recipient,
 | |
| 	const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
 | |
| 
 | |
| static void
 | |
| irc_send_ctcp_reply (struct server *s,
 | |
| 	const char *recipient, const char *format, ...)
 | |
| {
 | |
| 	struct str m;
 | |
| 	str_init (&m);
 | |
| 
 | |
| 	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_init (&f, s->ctx, s);
 | |
| 	formatter_add (&f, "CTCP requested by #n", msg->prefix);
 | |
| 	if (irc_is_channel (s, irc_skip_statusmsg (s, target)))
 | |
| 		formatter_add (&f, " (to #S)", target);
 | |
| 	formatter_add (&f, ": #S", chunk->tag.str);
 | |
| 	log_formatter (s->ctx, s->buffer, BUFFER_LINE_STATUS, &f);
 | |
| 
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 
 | |
| 	if (!strcmp (chunk->tag.str, "CLIENTINFO"))
 | |
| 		irc_send_ctcp_reply (s, nickname, "CLIENTINFO %s %s %s %s",
 | |
| 			"PING", "VERSION", "TIME", "CLIENTINFO");
 | |
| 	else if (!strcmp (chunk->tag.str, "PING"))
 | |
| 		irc_send_ctcp_reply (s, nickname, "PING %s", chunk->text.str);
 | |
| 	else if (!strcmp (chunk->tag.str, "VERSION"))
 | |
| 	{
 | |
| 		struct utsname info;
 | |
| 		if (uname (&info))
 | |
| 			LOG_LIBC_FAILURE ("uname");
 | |
| 		else
 | |
| 			irc_send_ctcp_reply (s, nickname, "VERSION %s %s on %s %s",
 | |
| 				PROGRAM_NAME, PROGRAM_VERSION, info.sysname, info.machine);
 | |
| 	}
 | |
| 	else if (!strcmp (chunk->tag.str, "TIME"))
 | |
| 	{
 | |
| 		char buf[26];
 | |
| 		if (!ctime_now (buf))
 | |
| 			LOG_LIBC_FAILURE ("asctime_r");
 | |
| 		else
 | |
| 			irc_send_ctcp_reply (s, nickname, "TIME %s", buf);
 | |
| 	}
 | |
| 
 | |
| 	free (nickname);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_privmsg_text (struct server *s,
 | |
| 	const struct irc_message *msg, struct str *text, bool is_action)
 | |
| {
 | |
| 	const char *target = msg->params.vector[0];
 | |
| 	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
 | |
| 	if (!buffer)
 | |
| 	{
 | |
| 		if (irc_is_this_us (s, msg->prefix))
 | |
| 			log_outcoming_orphan_privmsg (s, target, text->str);
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 	char *prefixes = irc_get_privmsg_prefix
 | |
| 		(s, str_map_find (&s->irc_users, nickname), target);
 | |
| 
 | |
| 	// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
 | |
| 	if (irc_is_this_us (s, msg->prefix) || !irc_is_highlight (s, text->str))
 | |
| 	{
 | |
| 		if (is_action)
 | |
| 			log_outcoming_action (s, buffer, nickname, text->str);
 | |
| 		else
 | |
| 			log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str);
 | |
| 	}
 | |
| 	else if (is_action)
 | |
| 		log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
 | |
| 			" #a*#r  #n #m", ATTR_HIGHLIGHT, msg->prefix, text->str);
 | |
| 	else
 | |
| 		log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
 | |
| 			"#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);
 | |
| 
 | |
| 	free (nickname);
 | |
| 	free (prefixes);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_privmsg (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix || msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	// This ignores empty messages which we should never receive anyway
 | |
| 	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
 | |
| 	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
 | |
| 		if (!iter->is_extended)
 | |
| 			irc_handle_privmsg_text (s, msg, &iter->text, false);
 | |
| 		else if (!strcmp (iter->tag.str, "ACTION"))
 | |
| 			irc_handle_privmsg_text (s, msg, &iter->text, true);
 | |
| 		else
 | |
| 			irc_handle_ctcp_request (s, msg, iter);
 | |
| 	ctcp_destroy (chunks);
 | |
| }
 | |
| 
 | |
| static void
 | |
| log_quit (struct server *s,
 | |
| 	struct buffer *buffer, const char *prefix, const char *reason)
 | |
| {
 | |
| 	struct formatter f;
 | |
| 	formatter_init (&f, s->ctx, s);
 | |
| 	formatter_add (&f, "#a<--#r #N #a#s#r",
 | |
| 		ATTR_PART, prefix, ATTR_PART, "has quit");
 | |
| 	if (reason)
 | |
| 		formatter_add (&f, " (#m)", reason);
 | |
| 	log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &f);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_quit (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (!msg->prefix)
 | |
| 		return;
 | |
| 
 | |
| 	// What the fuck, the server never sends this back
 | |
| 	if (irc_is_this_us (s, msg->prefix))
 | |
| 		return;
 | |
| 
 | |
| 	char *nickname = irc_cut_nickname (msg->prefix);
 | |
| 	struct user *user = str_map_find (&s->irc_users, nickname);
 | |
| 	free (nickname);
 | |
| 	if (!user)
 | |
| 		return;
 | |
| 
 | |
| 	const char *message = NULL;
 | |
| 	if (msg->params.len > 0)
 | |
| 		message = msg->params.vector[0];
 | |
| 
 | |
| 	// Log a message in any PM buffer
 | |
| 	struct buffer *buffer =
 | |
| 		str_map_find (&s->irc_buffer_map, user->nickname);
 | |
| 	if (buffer)
 | |
| 	{
 | |
| 		log_quit (s, buffer, msg->prefix, message);
 | |
| 
 | |
| 		// TODO: set some kind of a flag in the buffer and when the user
 | |
| 		//   reappears on a channel (JOIN), log a "is back online" message.
 | |
| 		//   Also set this flag when we receive a "no such nick" numeric
 | |
| 		//   and reset it when we send something to the buffer.
 | |
| 	}
 | |
| 
 | |
| 	// Log a message in all channels the user is in
 | |
| 	LIST_FOR_EACH (struct user_channel, iter, user->channels)
 | |
| 	{
 | |
| 		if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name)))
 | |
| 			log_quit (s, buffer, msg->prefix, message);
 | |
| 
 | |
| 		// This destroys "iter" which doesn't matter to us
 | |
| 		irc_remove_user_from_channel (user, iter->channel);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_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) ||
 | |
| 		(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 	// It would be is weird for this to be false
 | |
| 	if (channel)
 | |
| 	{
 | |
| 		free (channel->topic);
 | |
| 		channel->topic = xstrdup (topic);
 | |
| 	}
 | |
| 
 | |
| 	if (buffer)
 | |
| 	{
 | |
| 		log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"",
 | |
| 			msg->prefix, "has changed the topic to", topic);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static struct irc_handler g_irc_handlers[] =
 | |
| {
 | |
| 	// This list needs to stay sorted
 | |
| 	{ "CAP",     irc_handle_cap     },
 | |
| 	{ "ERROR",   irc_handle_error   },
 | |
| 	{ "INVITE",  irc_handle_invite  },
 | |
| 	{ "JOIN",    irc_handle_join    },
 | |
| 	{ "KICK",    irc_handle_kick    },
 | |
| 	{ "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    },
 | |
| 	{ "TOPIC",   irc_handle_topic   },
 | |
| };
 | |
| 
 | |
| static bool
 | |
| irc_try_parse_word_for_userhost (struct server *s, const char *word)
 | |
| {
 | |
| 	regex_t re;
 | |
| 	int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
 | |
| 	if (!soft_assert (!err))
 | |
| 		return false;
 | |
| 
 | |
| 	regmatch_t matches[2];
 | |
| 	bool result = false;
 | |
| 	if (!regexec (&re, word, 2, matches, 0))
 | |
| 	{
 | |
| 		free (s->irc_user_host);
 | |
| 		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 str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (m, ' ', &v);
 | |
| 	for (size_t i = 0; i < v.len; i++)
 | |
| 		if (irc_try_parse_word_for_userhost (s, v.vector[i]))
 | |
| 			break;
 | |
| 	str_vector_free (&v);
 | |
| }
 | |
| 
 | |
| static bool process_input_utf8
 | |
| 	(struct app_context *, struct buffer *, const char *, int);
 | |
| static void on_autoaway_timer (struct app_context *ctx);
 | |
| 
 | |
| static void
 | |
| irc_on_registered (struct server *s, const char *nickname)
 | |
| {
 | |
| 	s->irc_user = irc_get_or_make_user (s, nickname);
 | |
| 	str_reset (&s->irc_user_mode);
 | |
| 	s->irc_user_host = NULL;
 | |
| 
 | |
| 	s->state = IRC_REGISTERED;
 | |
| 	refresh_prompt (s->ctx);
 | |
| 
 | |
| 	// XXX: we can also use WHOIS if it's not supported (optional by RFC 2812)
 | |
| 	irc_send (s, "USERHOST %s", s->irc_user->nickname);
 | |
| 
 | |
| 	// A little hack that reinstates auto-away status when we get disconnected
 | |
| 	if (s->autoaway_active)
 | |
| 		on_autoaway_timer (s->ctx);
 | |
| 
 | |
| 	const char *command = get_config_string (s->config, "command");
 | |
| 	if (command)
 | |
| 	{
 | |
| 		log_server_debug (s, "Executing \"#s\"", command);
 | |
| 		process_input_utf8 (s->ctx, s->buffer, command, 0);
 | |
| 	}
 | |
| 
 | |
| 	int64_t command_delay = get_config_integer (s->config, "command_delay");
 | |
| 	log_server_debug (s, "Autojoining channels in #&s seconds...",
 | |
| 		xstrdup_printf ("%" PRId64, command_delay));
 | |
| 	poller_timer_set (&s->autojoin_tmr, command_delay * 1000);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_rpl_userhost (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	const char *response = msg->params.vector[1];
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (response, ' ', &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))
 | |
| 		{
 | |
| 			free (s->irc_user_host);
 | |
| 			s->irc_user_host = xstrdup (userhost);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	str_vector_free (&v);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_rpl_umodeis (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	str_reset (&s->irc_user_mode);
 | |
| 	irc_handle_mode_user (s, msg->params.vector + 1);
 | |
| 
 | |
| 	// XXX: do we want to log a message?
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| 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_ignore_empty (nicks, ' ', &channel->names_buf);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| struct channel_user_sort_entry
 | |
| {
 | |
| 	struct server *s;                   ///< Server
 | |
| 	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.str);
 | |
| 	const char *prio_b = strchr (s->irc_chanuser_prefixes,
 | |
| 		*b->channel_user->prefixes.str);
 | |
| 
 | |
| 	// 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 char *
 | |
| make_channel_users_list (struct server *s, struct channel *channel)
 | |
| {
 | |
| 	size_t n_users = 0;
 | |
| 	LIST_FOR_EACH (struct channel_user, iter, channel->users)
 | |
| 		n_users++;
 | |
| 
 | |
| 	struct channel_user_sort_entry entries[n_users];
 | |
| 	size_t i = 0;
 | |
| 	LIST_FOR_EACH (struct channel_user, iter, channel->users)
 | |
| 	{
 | |
| 		entries[i].s = s;
 | |
| 		entries[i].channel_user = iter;
 | |
| 		i++;
 | |
| 	}
 | |
| 
 | |
| 	qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
 | |
| 
 | |
| 	struct str list;
 | |
| 	str_init (&list);
 | |
| 	for (i = 0; i < n_users; i++)
 | |
| 	{
 | |
| 		irc_get_channel_user_prefix (s, entries[i].channel_user, &list);
 | |
| 		str_append (&list, entries[i].channel_user->user->nickname);
 | |
| 		str_append_c (&list, ' ');
 | |
| 	}
 | |
| 	if (list.len)
 | |
| 		list.str[--list.len] = '\0';
 | |
| 	return str_steal (&list);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_sync_channel_user (struct server *s, struct channel *channel,
 | |
| 	const char *nickname, const char *prefixes)
 | |
| {
 | |
| 	struct user *user = irc_get_or_make_user (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.str[0] != prefixes[0])
 | |
| 	{
 | |
| 		str_reset (&channel_user->prefixes);
 | |
| 		str_append (&channel_user->prefixes, prefixes);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_process_names (struct server *s, struct channel *channel)
 | |
| {
 | |
| 	struct str_map present;
 | |
| 	str_map_init (&present);
 | |
| 	present.key_xfrm = s->irc_strxfrm;
 | |
| 
 | |
| 	struct str_vector *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, 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 (s, 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);
 | |
| 	str_vector_reset (&channel->names_buf);
 | |
| 
 | |
| 	char *all_users = make_channel_users_list (s, channel);
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
 | |
| 	if (buffer)
 | |
| 	{
 | |
| 		log_server_status (s, buffer, "Users on #S: #S",
 | |
| 			channel->name, all_users);
 | |
| 	}
 | |
| 
 | |
| 	free (all_users);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| 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_init (&iter, &s->irc_channels);
 | |
| 		struct channel *channel;
 | |
| 		while ((channel = str_map_iter_next (&iter)))
 | |
| 			irc_process_names (s, channel);
 | |
| 	}
 | |
| 	else if (channel)
 | |
| 		irc_process_names (s, channel);
 | |
| }
 | |
| 
 | |
| 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) ||
 | |
| 		(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 	if (channel)
 | |
| 	{
 | |
| 		free (channel->topic);
 | |
| 		channel->topic = xstrdup (topic);
 | |
| 	}
 | |
| 
 | |
| 	if (buffer)
 | |
| 		log_server_status (s, buffer, "The topic is: #m", topic);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_rpl_channelmodeis (struct server *s, const struct irc_message *msg)
 | |
| {
 | |
| 	if (msg->params.len < 2)
 | |
| 		return;
 | |
| 
 | |
| 	const char *channel_name = msg->params.vector[1];
 | |
| 
 | |
| 	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
 | |
| 	hard_assert ((channel && buffer) ||
 | |
| 		(channel && !buffer) || (!channel && !buffer));
 | |
| 
 | |
| 	if (channel)
 | |
| 	{
 | |
| 		str_reset (&channel->no_param_modes);
 | |
| 		str_map_clear (&channel->param_modes);
 | |
| 
 | |
| 		irc_handle_mode_channel (s, channel, msg->params.vector + 1);
 | |
| 	}
 | |
| 
 | |
| 	// XXX: do we want to log a message?
 | |
| 	refresh_prompt (s->ctx);
 | |
| }
 | |
| 
 | |
| 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) ||
 | |
| 		(channel && !buffer) || (!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) ||
 | |
| 		(channel && !buffer) || (!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;
 | |
| 
 | |
| 	free (s->irc_chanuser_modes);
 | |
| 	free (s->irc_chanuser_prefixes);
 | |
| 
 | |
| 	s->irc_chanuser_modes    = xstrndup (modes,    n_prefixes);
 | |
| 	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)
 | |
| {
 | |
| 	free (s->irc_chantypes);
 | |
| 	s->irc_chantypes = xstrdup (value);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_isupport_idchan (struct server *s, char *value)
 | |
| {
 | |
| 	struct str prefixes;
 | |
| 	str_init (&prefixes);
 | |
| 
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (value, ',', &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);
 | |
| 	}
 | |
| 	str_vector_free (&v);
 | |
| 
 | |
| 	free (s->irc_idchan_prefixes);
 | |
| 	s->irc_idchan_prefixes = str_steal (&prefixes);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_isupport_statusmsg (struct server *s, char *value)
 | |
| {
 | |
| 	free (s->irc_statusmsg);
 | |
| 	s->irc_statusmsg = xstrdup (value);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_handle_isupport_chanmodes (struct server *s, char *value)
 | |
| {
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (value, ',', &v);
 | |
| 	if (v.len >= 4)
 | |
| 	{
 | |
| 		free (s->irc_chanmodes_list);
 | |
| 		s->irc_chanmodes_list           = xstrdup (v.vector[0]);
 | |
| 		free (s->irc_chanmodes_param_always);
 | |
| 		s->irc_chanmodes_param_always   = xstrdup (v.vector[1]);
 | |
| 		free (s->irc_chanmodes_param_when_set);
 | |
| 		s->irc_chanmodes_param_when_set = xstrdup (v.vector[2]);
 | |
| 		free (s->irc_chanmodes_param_never);
 | |
| 		s->irc_chanmodes_param_never    = xstrdup (v.vector[3]);
 | |
| 	}
 | |
| 	str_vector_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 ("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_init (&value_unescaped);
 | |
| 		unescape_isupport_value (value, &value_unescaped);
 | |
| 		dispatch_isupport (s, param, value_unescaped.str);
 | |
| 		str_free (&value_unescaped);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| irc_process_numeric (struct server *s,
 | |
| 	const struct irc_message *msg, unsigned long numeric)
 | |
| {
 | |
| 	// Numerics typically have human-readable information
 | |
| 
 | |
| 	// Get rid of the first parameter, if there's any at all,
 | |
| 	// as it contains our nickname and is of no practical use to the user
 | |
| 	struct str_vector copy;
 | |
| 	str_vector_init (©);
 | |
| 	str_vector_add_vector (©, msg->params.vector + !!msg->params.len);
 | |
| 
 | |
| 	struct buffer *buffer = s->buffer;
 | |
| 	int flags = BUFFER_LINE_STATUS;
 | |
| 	switch (numeric)
 | |
| 	{
 | |
| 	case IRC_RPL_WELCOME:
 | |
| 		irc_on_registered (s, msg->params.vector[0]);
 | |
| 
 | |
| 		// We still issue a USERHOST anyway as this is in general unreliable
 | |
| 		if (msg->params.len == 2)
 | |
| 			irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]);
 | |
| 		break;
 | |
| 
 | |
| 	case IRC_RPL_ISUPPORT:
 | |
| 		irc_handle_rpl_isupport      (s, msg);                break;
 | |
| 	case IRC_RPL_USERHOST:
 | |
| 		irc_handle_rpl_userhost      (s, msg);                break;
 | |
| 	case IRC_RPL_UMODEIS:
 | |
| 		irc_handle_rpl_umodeis       (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_NAMREPLY:
 | |
| 		irc_handle_rpl_namreply      (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_ENDOFNAMES:
 | |
| 		irc_handle_rpl_endofnames    (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_TOPIC:
 | |
| 		irc_handle_rpl_topic         (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_CHANNELMODEIS:
 | |
| 		irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_CREATIONTIME:
 | |
| 		irc_handle_rpl_creationtime  (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_TOPICWHOTIME:
 | |
| 		irc_handle_rpl_topicwhotime  (s, msg); buffer = NULL; break;
 | |
| 	case IRC_RPL_INVITING:
 | |
| 		irc_handle_rpl_inviting      (s, msg); buffer = NULL; break;
 | |
| 
 | |
| 	case IRC_ERR_NICKNAMEINUSE:
 | |
| 		irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break;
 | |
| 
 | |
| 		// Auto-away spams server buffers with activity
 | |
| 	case IRC_RPL_NOWAWAY:
 | |
| 		flags |= BUFFER_LINE_UNIMPORTANT;
 | |
| 		if (s->irc_user) s->irc_user->away = true;            break;
 | |
| 	case IRC_RPL_UNAWAY:
 | |
| 		flags |= BUFFER_LINE_UNIMPORTANT;
 | |
| 		if (s->irc_user) s->irc_user->away = false;           break;
 | |
| 
 | |
| 	case IRC_RPL_LIST:
 | |
| 	case IRC_RPL_WHOREPLY:
 | |
| 	case IRC_RPL_ENDOFWHO:
 | |
| 
 | |
| 	case IRC_ERR_UNKNOWNCOMMAND:
 | |
| 	case IRC_ERR_NEEDMOREPARAMS:
 | |
| 		// Just preventing these commands from getting printed in a more
 | |
| 		// specific buffer as that would be unwanted
 | |
| 		break;
 | |
| 
 | |
| 	default:
 | |
| 		// If the second parameter is something we have a buffer for
 | |
| 		// (a channel, a PM buffer), log it in that buffer.  This is very basic.
 | |
| 		// TODO: whitelist/blacklist a lot more replies in here.
 | |
| 		// TODO: we should either strip the first parameter from the resulting
 | |
| 		//   buffer line, or at least put it in brackets
 | |
| 		if (msg->params.len > 1)
 | |
| 		{
 | |
| 			struct buffer *x;
 | |
| 			if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
 | |
| 				buffer = x;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (buffer)
 | |
| 	{
 | |
| 		// Join the parameter vector back and send it to the server buffer
 | |
| 		log_server (s, buffer, flags, "#&m", join_str_vector (©, ' '));
 | |
| 	}
 | |
| 
 | |
| 	str_vector_free (©);
 | |
| }
 | |
| 
 | |
| static void
 | |
| irc_process_message (const struct irc_message *msg, struct server *s)
 | |
| {
 | |
| 	// TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec())
 | |
| 	//   -> change all calls to log_{server,nick,outcoming,ctcp}*() to take
 | |
| 	//   an extra 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);
 | |
| }
 | |
| 
 | |
| // --- Message autosplitting magic ---------------------------------------------
 | |
| 
 | |
| // This is the most basic acceptable algorithm; something like ICU with proper
 | |
| // locale specification would be needed to make it work better.
 | |
| 
 | |
| static size_t
 | |
| wrap_text_for_single_line (const char *text, size_t text_len,
 | |
| 	size_t line_len, struct str *output)
 | |
| {
 | |
| 	size_t eaten = 0;
 | |
| 
 | |
| 	// First try going word by word
 | |
| 	const char *word_start;
 | |
| 	const char *word_end = text + strcspn (text, " ");
 | |
| 	size_t word_len = word_end - text;
 | |
| 	while (line_len && word_len <= line_len)
 | |
| 	{
 | |
| 		if (word_len)
 | |
| 		{
 | |
| 			str_append_data (output, text, word_len);
 | |
| 
 | |
| 			text += word_len;
 | |
| 			eaten += word_len;
 | |
| 			line_len -= word_len;
 | |
| 		}
 | |
| 
 | |
| 		// Find the next word's end
 | |
| 		word_start = text + strspn (text, " ");
 | |
| 		word_end = word_start + strcspn (word_start, " ");
 | |
| 		word_len = word_end - text;
 | |
| 	}
 | |
| 
 | |
| 	if (eaten)
 | |
| 		// Discard whitespace between words if split
 | |
| 		return eaten + (word_start - text);
 | |
| 
 | |
| 	// And if that doesn't help, cut the longest valid block of characters
 | |
| 	for (const char *p = text; (size_t) (p - text) <= line_len; )
 | |
| 	{
 | |
| 		eaten = p - text;
 | |
| 		hard_assert ((p = utf8_next (p, text_len - eaten, NULL)));
 | |
| 	}
 | |
| 	str_append_data (output, text, eaten);
 | |
| 	return eaten;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static bool
 | |
| wrap_message (const char *message,
 | |
| 	int line_max, struct str_vector *output, struct error **e)
 | |
| {
 | |
| 	if (line_max <= 0)
 | |
| 		goto error;
 | |
| 
 | |
| 	int message_left = strlen (message);
 | |
| 	while (message_left > line_max)
 | |
| 	{
 | |
| 		struct str m;
 | |
| 		str_init (&m);
 | |
| 
 | |
| 		size_t eaten = wrap_text_for_single_line
 | |
| 			(message, message_left, line_max, &m);
 | |
| 		if (!eaten)
 | |
| 		{
 | |
| 			str_free (&m);
 | |
| 			goto error;
 | |
| 		}
 | |
| 
 | |
| 		str_vector_add_owned (output, str_steal (&m));
 | |
| 		message += eaten;
 | |
| 		message_left -= eaten;
 | |
| 	}
 | |
| 
 | |
| 	if (message_left)
 | |
| 		str_vector_add (output, message);
 | |
| 
 | |
| 	return true;
 | |
| 
 | |
| error:
 | |
| 	// Well, that's just weird
 | |
| 	error_set (e,
 | |
| 		"Message splitting was unsuccessful as there was "
 | |
| 		"too little room for UTF-8 characters");
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| /// Automatically splits messages that arrive at other clients with our prefix
 | |
| /// so that they don't arrive cut off by the server
 | |
| static bool
 | |
| irc_autosplit_message (struct server *s, const char *message,
 | |
| 	int fixed_part, struct str_vector *output, struct error **e)
 | |
| {
 | |
| 	// :<nick>!<user>@<host> <fixed-part><message>
 | |
| 	int space_in_one_message = 0;
 | |
| 	if (s->irc_user && s->irc_user_host)
 | |
| 		space_in_one_message = 510
 | |
| 			- 1 - (int) strlen (s->irc_user->nickname)
 | |
| 			- 1 - (int) strlen (s->irc_user_host)
 | |
| 			- 1 - fixed_part;
 | |
| 
 | |
| 	// However we don't always have the full info for message splitting
 | |
| 	if (!space_in_one_message)
 | |
| 		str_vector_add (output, message);
 | |
| 	else if (!wrap_message (message, space_in_one_message, output, e))
 | |
| 		return false;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| send_autosplit_message (struct server *s,
 | |
| 	const char *command, const char *target, const char *message,
 | |
| 	const char *prefix, const char *suffix)
 | |
| {
 | |
| 	struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
 | |
| 	int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
 | |
| 		+ strlen (prefix) + strlen (suffix);
 | |
| 
 | |
| 	// We might also want to preserve attributes across splits but
 | |
| 	// that would make this code a lot more complicated
 | |
| 
 | |
| 	struct str_vector lines;
 | |
| 	str_vector_init (&lines);
 | |
| 	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);
 | |
| 	}
 | |
| 	str_vector_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_level
 | |
| {
 | |
| 	struct config_dump_level *next;     ///< Next print level
 | |
| 	const char *name;                   ///< Name of the object
 | |
| };
 | |
| 
 | |
| struct config_dump_data
 | |
| {
 | |
| 	struct config_dump_level *head;     ///< The first level
 | |
| 	struct config_dump_level **tail;    ///< Where to place further levels
 | |
| 	struct str_vector *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 config_dump_level level;
 | |
| 	level.next = NULL;
 | |
| 
 | |
| 	struct config_dump_level **prev_tail = data->tail;
 | |
| 	*data->tail = &level;
 | |
| 	data->tail = &level.next;
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &object->value.object);
 | |
| 	struct config_item *child;
 | |
| 	while ((child = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		level.name = iter.link->key;
 | |
| 		config_dump_item (child, data);
 | |
| 	}
 | |
| 
 | |
| 	data->tail = prev_tail;
 | |
| 	*data->tail = NULL;
 | |
| }
 | |
| 
 | |
| static void
 | |
| config_dump_item (struct config_item *item, struct config_dump_data *data)
 | |
| {
 | |
| 	// Empty objects will show as such
 | |
| 	if (item->type == CONFIG_ITEM_OBJECT
 | |
| 	 && item->value.object.len)
 | |
| 	{
 | |
| 		config_dump_children (item, data);
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	// Currently there's no reason for us to dump unknown items
 | |
| 	struct config_schema *schema = item->schema;
 | |
| 	if (!schema)
 | |
| 		return;
 | |
| 
 | |
| 	struct str line;
 | |
| 	str_init (&line);
 | |
| 
 | |
| 	struct config_dump_level *iter = data->head;
 | |
| 	if (iter)
 | |
| 	{
 | |
| 		str_append (&line, iter->name);
 | |
| 		iter = iter->next;
 | |
| 	}
 | |
| 	for (; iter; iter = iter->next)
 | |
| 		str_append_printf (&line, ".%s", iter->name);
 | |
| 
 | |
| 	struct str value;
 | |
| 	str_init (&value);
 | |
| 	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);
 | |
| 	str_vector_add_owned (data->output, str_steal (&line));
 | |
| }
 | |
| 
 | |
| static void
 | |
| config_dump (struct config_item *root, struct str_vector *output)
 | |
| {
 | |
| 	struct config_dump_data data;
 | |
| 	data.head = NULL;
 | |
| 	data.tail = &data.head;
 | |
| 	data.output = output;
 | |
| 
 | |
| 	config_dump_item (root, &data);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static int
 | |
| str_vector_sort_cb (const void *a, const void *b)
 | |
| {
 | |
| 	return strcmp (*(const char **) a, *(const char **) b);
 | |
| }
 | |
| 
 | |
| static void
 | |
| str_vector_sort (struct str_vector *self)
 | |
| {
 | |
| 	qsort (self->vector, self->len, sizeof *self->vector, str_vector_sort_cb);
 | |
| }
 | |
| 
 | |
| static void
 | |
| dump_matching_options
 | |
| 	(struct config_item *root, const char *mask, struct str_vector *output)
 | |
| {
 | |
| 	config_dump (root, output);
 | |
| 	str_vector_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))
 | |
| 			str_vector_remove (output, i--);
 | |
| 		free (key);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| save_configuration (struct app_context *ctx)
 | |
| {
 | |
| 	struct str data;
 | |
| 	str_init (&data);
 | |
| 	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 (*p < 32 || *p == '.')
 | |
| 			return false;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static const char *
 | |
| check_server_name_for_addition (struct app_context *ctx, const char *name)
 | |
| {
 | |
| 	if (!strcasecmp_ascii (name, ctx->global_buffer->name))
 | |
| 		return "name collides with the global buffer";
 | |
| 	if (str_map_find (&ctx->servers, name))
 | |
| 		return "server already exists";
 | |
| 	if (!validate_server_name (name))
 | |
| 		return "invalid server name";
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static struct server *
 | |
| server_add (struct app_context *ctx,
 | |
| 	const char *name, struct config_item *subtree)
 | |
| {
 | |
| 	hard_assert (!str_map_find (&ctx->servers, name));
 | |
| 
 | |
| 	struct server *s = server_new (&ctx->poller);
 | |
| 	s->ctx = ctx;
 | |
| 	s->name = xstrdup (name);
 | |
| 	str_map_set (&ctx->servers, s->name, s);
 | |
| 	s->config = subtree;
 | |
| 
 | |
| 	// Add a buffer and activate it
 | |
| 	struct buffer *buffer = s->buffer = buffer_new (ctx->input);
 | |
| 	buffer->type = BUFFER_SERVER;
 | |
| 	buffer->name = xstrdup (s->name);
 | |
| 	buffer->server = s;
 | |
| 
 | |
| 	buffer_add (ctx, buffer);
 | |
| 	buffer_activate (ctx, buffer);
 | |
| 
 | |
| 	config_schema_apply_to_object (g_config_server, subtree, s);
 | |
| 	config_schema_call_changed (subtree);
 | |
| 
 | |
| 	if (get_config_boolean (s->config, "autoconnect"))
 | |
| 		// Connect to the server ASAP
 | |
| 		poller_timer_set (&s->reconnect_tmr, 0);
 | |
| 	return s;
 | |
| }
 | |
| 
 | |
| static void
 | |
| server_add_new (struct app_context *ctx, const char *name)
 | |
| {
 | |
| 	// Note that there may already be something in the configuration under
 | |
| 	// that key that we've ignored earlier, and there may also be
 | |
| 	// a case-insensitive conflict.  Those things may only happen as a result
 | |
| 	// of manual edits to the configuration, though, and they're not really
 | |
| 	// going to break anything.  They only cause surprises when loading.
 | |
| 	struct str_map *servers = get_servers_config (ctx);
 | |
| 	struct config_item *subtree = config_item_object ();
 | |
| 	str_map_set (servers, name, subtree);
 | |
| 
 | |
| 	struct server *s = server_add (ctx, name, subtree);
 | |
| 	struct error *e = NULL;
 | |
| 	if (!irc_autofill_user_info (s, &e))
 | |
| 	{
 | |
| 		log_server_error (s, s->buffer,
 | |
| 			"#s: #s", "Failed to fill in user details", e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| server_remove (struct app_context *ctx, struct server *s)
 | |
| {
 | |
| 	hard_assert (!irc_is_connected (s));
 | |
| 
 | |
| 	if (s->buffer)
 | |
| 		buffer_remove_safe (ctx, s->buffer);
 | |
| 
 | |
| 	struct str_map_unset_iter iter;
 | |
| 	str_map_unset_iter_init (&iter, &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)
 | |
| {
 | |
| 	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));
 | |
| 
 | |
| 	free (s->name);
 | |
| 	s->name = xstrdup (new_name);
 | |
| 
 | |
| 	buffer_rename (ctx, s->buffer, new_name);
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &s->irc_buffer_map);
 | |
| 	struct buffer *buffer;
 | |
| 	while ((buffer = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		// FIXME: creation of buffer names should be centralized
 | |
| 		char *x = NULL;
 | |
| 		switch (buffer->type)
 | |
| 		{
 | |
| 		case BUFFER_PM:
 | |
| 			x = xstrdup_printf ("%s.%s", s->name, buffer->user->nickname);
 | |
| 			break;
 | |
| 		case BUFFER_CHANNEL:
 | |
| 			x = xstrdup_printf ("%s.%s", s->name, buffer->channel->name);
 | |
| 			break;
 | |
| 		default:
 | |
| 			hard_assert (!"unexpected type of server-related buffer");
 | |
| 		}
 | |
| 		buffer_rename (ctx, buffer, x);
 | |
| 		free (x);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // --- Plugins -----------------------------------------------------------------
 | |
| 
 | |
| /// Returns the basename of the plugin's name without any extensions,
 | |
| /// or NULL if the name isn't suitable (starts with a dot)
 | |
| static char *
 | |
| plugin_config_name (struct plugin *self)
 | |
| {
 | |
| 	const char *begin = self->name;
 | |
| 	for (const char *p = begin; *p; )
 | |
| 		if (*p++ == '/')
 | |
| 			begin = p;
 | |
| 
 | |
| 	size_t len = strcspn (begin, ".");
 | |
| 	if (!len)
 | |
| 		return NULL;
 | |
| 
 | |
| 	// XXX: we might also allow arbitrary strings as object keys (except dots)
 | |
| 	char *copy = xstrndup (begin, len);
 | |
| 	for (char *p = copy; *p; p++)
 | |
| 		if (!config_tokenizer_is_word_char (*p))
 | |
| 			*p = '_';
 | |
| 	return copy;
 | |
| }
 | |
| 
 | |
| // --- Lua ---------------------------------------------------------------------
 | |
| 
 | |
| // Each plugin has its own Lua state object, so that a/ they don't disturb each
 | |
| // other and b/ unloading a plugin releases all resources.
 | |
| //
 | |
| // References to internal objects (buffers, servers) are all weak.
 | |
| 
 | |
| #ifdef HAVE_LUA
 | |
| 
 | |
| struct lua_plugin
 | |
| {
 | |
| 	struct plugin super;                ///< The structure we're deriving
 | |
| 	struct app_context *ctx;            ///< Application context
 | |
| 	lua_State *L;                       ///< Lua state
 | |
| 
 | |
| 	struct lua_schema_item *schemas;    ///< Registered schema items
 | |
| };
 | |
| 
 | |
| 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 =
 | |
| {
 | |
| 	.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 str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (message, '\n', &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]);
 | |
| 	}
 | |
| 
 | |
| 	str_vector_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)
 | |
| {
 | |
| 	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))
 | |
| 	{
 | |
| 		free (*original);
 | |
| 		*original = NULL;
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (!lua_isstring (L, -1))
 | |
| 		FAIL ("must return either a string or nil");
 | |
| 
 | |
| 	size_t len;
 | |
| 	const char *processed = lua_tolstring (L, -1, &len);
 | |
| 	if (utf8 && !utf8_validate (processed, len))
 | |
| 		FAIL ("must return valid UTF-8");
 | |
| 
 | |
| 	// Only replace the string if it's different
 | |
| 	if (strcmp (processed, *original))
 | |
| 	{
 | |
| 		free (*original);
 | |
| 		*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), 1, "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);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| 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_init (&iter, &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
 | |
| 	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
 | |
| 	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 (struct lua_plugin *plugin, void *object,
 | |
| 	struct lua_weak_info *info)
 | |
| {
 | |
| 	lua_State *L = plugin->L;
 | |
| 	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->object = object;
 | |
| 	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);
 | |
| 		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,                                                 \
 | |
| 		.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)
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static int
 | |
| lua_user_gc (lua_State *L)
 | |
| {
 | |
| 	return lua_weak_gc (L, &lua_user_info);
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_user_get_nickname (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_user_info);
 | |
| 	struct user *user = wrapper->object;
 | |
| 	lua_pushstring (L, user->nickname);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| 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 (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_nickname", lua_user_get_nickname },
 | |
| 	{ "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_name (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_channel_info);
 | |
| 	struct channel *channel = wrapper->object;
 | |
| 	lua_pushstring (L, channel->name);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| 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 (wrapper->plugin, iter->user, &lua_user_info);
 | |
| 		lua_setfield (L, -2, "user");
 | |
| 		lua_plugin_kv (L, "prefixes", iter->prefixes.str);
 | |
| 
 | |
| 		lua_rawseti (L, -2, i++);
 | |
| 	}
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static luaL_Reg lua_channel_table[] =
 | |
| {
 | |
| 	{ "__gc",      lua_channel_gc        },
 | |
| 	{ "get_name",  lua_channel_get_name  },
 | |
| 	{ "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_get_user (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
 | |
| 	struct buffer *buffer = wrapper->object;
 | |
| 	lua_weak_push (wrapper->plugin, buffer->user, &lua_user_info);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_buffer_get_channel (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
 | |
| 	struct buffer *buffer = wrapper->object;
 | |
| 	lua_weak_push (wrapper->plugin, buffer->channel, &lua_channel_info);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_buffer_get_server (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
 | |
| 	struct buffer *buffer = wrapper->object;
 | |
| 	lua_weak_push (wrapper->plugin, buffer->server, &lua_server_info);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_buffer_log (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
 | |
| 	struct buffer *buffer = wrapper->object;
 | |
| 	const char *message = lua_plugin_check_utf8 (L, 2);
 | |
| 	log_full (wrapper->plugin->ctx, buffer->server, buffer,
 | |
| 		BUFFER_LINE_STATUS, "#s", message);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_buffer_execute (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
 | |
| 	struct buffer *buffer = wrapper->object;
 | |
| 	const char *line = lua_plugin_check_utf8 (L, 2);
 | |
| 	process_input_utf8 (wrapper->plugin->ctx, buffer, line, 0);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static luaL_Reg lua_buffer_table[] =
 | |
| {
 | |
| 	// TODO: some useful methods or values
 | |
| 	{ "__gc",        lua_buffer_gc          },
 | |
| 	{ "get_user",    lua_buffer_get_user    },
 | |
| 	{ "get_channel", lua_buffer_get_channel },
 | |
| 	{ "get_server",  lua_buffer_get_server  },
 | |
| 	{ "log",         lua_buffer_log         },
 | |
| 	{ "execute",     lua_buffer_execute     },
 | |
| 	{ NULL,          NULL                   }
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static int
 | |
| lua_server_gc (lua_State *L)
 | |
| {
 | |
| 	return lua_weak_gc (L, &lua_server_info);
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_server_get_user (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
 | |
| 	struct server *server = wrapper->object;
 | |
| 	lua_weak_push (wrapper->plugin, server->irc_user, &lua_user_info);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_server_get_buffer (lua_State *L)
 | |
| {
 | |
| 	struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
 | |
| 	struct server *server = wrapper->object;
 | |
| 	lua_weak_push (wrapper->plugin, server->buffer, &lua_buffer_info);
 | |
| 	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[] =
 | |
| {
 | |
| 	// TODO: some useful methods or values
 | |
| 	{ "__gc",       lua_server_gc         },
 | |
| 	{ "get_user",   lua_server_get_user   },
 | |
| 	{ "get_buffer", lua_server_get_buffer },
 | |
| 	{ "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_COMPLETION,               ///< Autocomplete
 | |
| 	XLUA_HOOK_TIMER,                    ///< One-shot timer
 | |
| };
 | |
| 
 | |
| 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 completion_hook c_hook;  ///< Autocomplete hook
 | |
| 
 | |
| 		struct poller_timer timer;      ///< Timer
 | |
| 	}
 | |
| 	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_COMPLETION:
 | |
| 		LIST_UNLINK (hook->plugin->ctx->completion_hooks, &hook->data.hook);
 | |
| 		break;
 | |
| 	case XLUA_HOOK_TIMER:
 | |
| 		poller_timer_reset (&hook->data.timer);
 | |
| 		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 (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;
 | |
| }
 | |
| 
 | |
| struct input_hook_vtable lua_input_hook_vtable =
 | |
| {
 | |
| 	.filter = lua_input_hook_filter,
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| 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 (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;
 | |
| }
 | |
| 
 | |
| struct irc_hook_vtable lua_irc_hook_vtable =
 | |
| {
 | |
| 	.filter = lua_irc_hook_filter,
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| 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 str_vector *output,
 | |
| 	struct error **e)
 | |
| {
 | |
| 	if (lua_type (L, -1) != LUA_TSTRING)
 | |
| 		FAIL ("%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))
 | |
| 		FAIL ("must be valid UTF-8");
 | |
| 
 | |
| 	str_vector_add (output, value);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| lua_completion_hook_process (lua_State *L, struct str_vector *output,
 | |
| 	struct error **e)
 | |
| {
 | |
| 	if (lua_isnil (L, -1))
 | |
| 		return true;
 | |
| 	if (!lua_istable (L, -1))
 | |
| 		FAIL ("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 str_vector *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 (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);
 | |
| }
 | |
| 
 | |
| struct completion_hook_vtable lua_completion_hook_vtable =
 | |
| {
 | |
| 	.complete = lua_completion_hook_complete,
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| lua_timer_hook_dispatch (void *user_data)
 | |
| {
 | |
| 	struct lua_hook *hook = user_data;
 | |
| 	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;
 | |
| 	if (!lua_plugin_call (plugin, 1, 0, &e))
 | |
| 		lua_plugin_log_error (plugin, "timer hook", e);
 | |
| 
 | |
| 	// There's no need to keep the hook around once the timer is dispatched
 | |
| 	lua_cache_invalidate (L, hook);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static struct lua_hook *
 | |
| lua_plugin_push_hook (struct lua_plugin *plugin, int callback_index,
 | |
| 	enum lua_hook_type type, int priority)
 | |
| {
 | |
| 	lua_State *L = plugin->L;
 | |
| 	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
 | |
| 		(plugin, 1, XLUA_HOOK_INPUT, luaL_optinteger (L, 2, 0));
 | |
| 	hook->data.input_hook.vtable = &lua_input_hook_vtable;
 | |
| 	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
 | |
| 		(plugin, 1, XLUA_HOOK_IRC, luaL_optinteger (L, 2, 0));
 | |
| 	hook->data.irc_hook.vtable = &lua_irc_hook_vtable;
 | |
| 	plugin->ctx->irc_hooks =
 | |
| 		hook_insert (plugin->ctx->irc_hooks, &hook->data.hook);
 | |
| 	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
 | |
| 		(plugin, 1, XLUA_HOOK_COMPLETION, luaL_optinteger (L, 2, 0));
 | |
| 	hook->data.c_hook.vtable = &lua_completion_hook_vtable;
 | |
| 	plugin->ctx->completion_hooks =
 | |
| 		hook_insert (plugin->ctx->completion_hooks, &hook->data.hook);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_plugin_hook_timer (lua_State *L)
 | |
| {
 | |
| 	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
 | |
| 	lua_Integer timeout = luaL_checkinteger (L, 2);
 | |
| 
 | |
| 	if (timeout < 0)
 | |
| 		luaL_argerror (L, 2, "timeout mustn't be negative");
 | |
| 
 | |
| 	// This doesn't really hook anything but we can reuse the code
 | |
| 	struct lua_hook *hook = lua_plugin_push_hook
 | |
| 		(plugin, 1, XLUA_HOOK_TIMER, 0 /* priority doesn't apply */);
 | |
| 
 | |
| 	struct poller_timer *timer = &hook->data.timer;
 | |
| 	poller_timer_init (timer, &plugin->ctx->poller);
 | |
| 	timer->dispatcher = lua_timer_hook_dispatch;
 | |
| 	timer->user_data = hook;
 | |
| 	poller_timer_set (timer, timeout);
 | |
| 	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_init (&iter, &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 (struct lua_plugin *plugin,
 | |
| 	struct config_item *subtree, const char *name)
 | |
| {
 | |
| 	struct config_item *item = str_map_find (&subtree->value.object, name);
 | |
| 	lua_State *L = plugin->L;
 | |
| 
 | |
| 	// 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 (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;
 | |
| 		(void) shutdown (self->socket_fd, SHUT_RD);
 | |
| 
 | |
| 		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,
 | |
| 	struct error *error, struct error **e)
 | |
| {
 | |
| 	if (!self->closing
 | |
| 	 && lua_connection_cb_lookup (self, "on_error", e)
 | |
| 	 && !lua_connection_eat_nil (self))
 | |
| 	{
 | |
| 		lua_pushstring (self->plugin->L, error->message);
 | |
| 		lua_plugin_call (self->plugin, 1, 0, e);
 | |
| 	}
 | |
| 	error_free (error);
 | |
| 	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;
 | |
| 
 | |
| 	struct error *error = NULL;
 | |
| 	enum socket_io_result read_result =
 | |
| 		socket_io_try_read (self->socket_fd, &self->read_buffer, &error);
 | |
| 
 | |
| 	// 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))
 | |
| 		{
 | |
| 			if (error)
 | |
| 				error_free (error);
 | |
| 			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)
 | |
| {
 | |
| 	struct error *error = NULL;
 | |
| 	enum socket_io_result write_result =
 | |
| 		socket_io_try_write (self->socket_fd, &self->write_buffer, &error);
 | |
| 
 | |
| 	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);
 | |
| 	poller_fd_init (&self->socket_event, &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);
 | |
| 
 | |
| 	str_init (&self->read_buffer);
 | |
| 	str_init (&self->write_buffer);
 | |
| 
 | |
| 	// Make sure the connection doesn't get garbage collected and return it
 | |
| 	lua_cache_store (L, self, -1);
 | |
| 	return self;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| /// Identifier for the Lua metatable
 | |
| #define XLUA_CONNECTOR_METATABLE "connector"
 | |
| 
 | |
| struct lua_connector
 | |
| {
 | |
| 	struct lua_plugin *plugin;          ///< The plugin we belong to
 | |
| 	struct connector connector;         ///< Connector object
 | |
| 	bool active;                        ///< Whether the connector is alive
 | |
| 
 | |
| 	int ref_on_success;                 ///< Reference to "on_success" callback
 | |
| 	int ref_on_error;                   ///< Reference to "on_error" callback
 | |
| 
 | |
| 	char *last_error;                   ///< Connecting error, if any
 | |
| };
 | |
| 
 | |
| static void
 | |
| lua_connector_discard (struct lua_connector *self)
 | |
| {
 | |
| 	if (self->active)
 | |
| 	{
 | |
| 		connector_free (&self->connector);
 | |
| 		self->active = false;
 | |
| 
 | |
| 		luaL_unref (self->plugin->L, LUA_REGISTRYINDEX, self->ref_on_success);
 | |
| 		luaL_unref (self->plugin->L, LUA_REGISTRYINDEX, self->ref_on_error);
 | |
| 		self->ref_on_success = LUA_REFNIL;
 | |
| 		self->ref_on_error   = LUA_REFNIL;
 | |
| 	}
 | |
| 
 | |
| 	free (self->last_error);
 | |
| 	self->last_error = NULL;
 | |
| 
 | |
| 	lua_cache_invalidate (self->plugin->L, self);
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_connector_abort (lua_State *L)
 | |
| {
 | |
| 	lua_connector_discard (luaL_checkudata (L, 1, XLUA_CONNECTOR_METATABLE));
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static luaL_Reg lua_connector_table[] =
 | |
| {
 | |
| 	{ "abort", lua_connector_abort },
 | |
| 	{ "__gc",  lua_connector_abort },
 | |
| 	{ NULL,    NULL                }
 | |
| };
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| lua_connector_on_connected (void *user_data, int socket, const char *hostname)
 | |
| {
 | |
| 	struct lua_connector *self = user_data;
 | |
| 
 | |
| 	// TODO: use the hostname for SNI once TLS is implemented
 | |
| 
 | |
| 	if (self->ref_on_success != LUA_REFNIL)
 | |
| 	{
 | |
| 		lua_State *L = self->plugin->L;
 | |
| 		lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_success);
 | |
| 		struct lua_connection *connection =
 | |
| 			lua_plugin_push_connection (self->plugin, socket);  // 1: connection
 | |
| 		lua_pushstring (L, hostname);                           // 2: hostname
 | |
| 
 | |
| 		struct error *e = NULL;
 | |
| 		if (!lua_plugin_call (self->plugin, 2, 0, &e))
 | |
| 		{
 | |
| 			lua_plugin_log_error (self->plugin, "connector on_success", e);
 | |
| 			// The connection has placed itself in the cache
 | |
| 			lua_connection_discard (connection);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	lua_connector_discard (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| lua_connector_on_failure (void *user_data)
 | |
| {
 | |
| 	struct lua_connector *self = user_data;
 | |
| 	if (self->ref_on_error != LUA_REFNIL)
 | |
| 	{
 | |
| 		lua_State *L = self->plugin->L;
 | |
| 		lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_error);
 | |
| 		lua_pushstring (L, self->last_error);  // 1: error string
 | |
| 
 | |
| 		struct error *e = NULL;
 | |
| 		if (!lua_plugin_call (self->plugin, 1, 0, &e))
 | |
| 			lua_plugin_log_error (self->plugin, "connector on_error", e);
 | |
| 	}
 | |
| 
 | |
| 	lua_connector_discard (self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| lua_connector_on_error (void *user_data, const char *error)
 | |
| {
 | |
| 	struct lua_connector *self = user_data;
 | |
| 	free (self->last_error);
 | |
| 	self->last_error = xstrdup (error);
 | |
| }
 | |
| 
 | |
| static int
 | |
| lua_plugin_connect (lua_State *L)
 | |
| {
 | |
| 	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
 | |
| 	const char *host = luaL_checkstring (L, 1);
 | |
| 	const char *service = luaL_checkstring (L, 2);
 | |
| 	luaL_checktype (L, 3, LUA_TTABLE);
 | |
| 
 | |
| 	struct lua_connector *self = lua_newuserdata (L, sizeof *self);
 | |
| 	luaL_setmetatable (L, XLUA_CONNECTOR_METATABLE);
 | |
| 	memset (self, 0, sizeof *self);
 | |
| 
 | |
| 	self->plugin         = plugin;
 | |
| 	self->ref_on_success = LUA_REFNIL;
 | |
| 	self->ref_on_error   = LUA_REFNIL;
 | |
| 
 | |
| 	(void) lua_plugin_check_field (L, 3, "on_success", LUA_TFUNCTION, true);
 | |
| 	self->ref_on_success = luaL_ref (L, LUA_REGISTRYINDEX);
 | |
| 	(void) lua_plugin_check_field (L, 3, "on_error",   LUA_TFUNCTION, true);
 | |
| 	self->ref_on_error   = luaL_ref (L, LUA_REGISTRYINDEX);
 | |
| 
 | |
| 	struct app_context *ctx = plugin->ctx;
 | |
| 	struct connector *connector = &self->connector;
 | |
| 	connector_init (connector, &ctx->poller);
 | |
| 	connector_add_target (connector, host, service);
 | |
| 
 | |
| 	connector->on_connected  = lua_connector_on_connected;
 | |
| 	connector->on_connecting = NULL;
 | |
| 	connector->on_error      = lua_connector_on_error;
 | |
| 	connector->on_failure    = lua_connector_on_failure;
 | |
| 	connector->user_data     = self;
 | |
| 
 | |
| 	self->active = true;
 | |
| 	lua_cache_store (L, self, -1);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| static luaL_Reg lua_plugin_library[] =
 | |
| {
 | |
| 	{ "parse",           lua_plugin_parse           },
 | |
| 	{ "hook_input",      lua_plugin_hook_input      },
 | |
| 	{ "hook_irc",        lua_plugin_hook_irc        },
 | |
| 	{ "hook_completion", lua_plugin_hook_completion },
 | |
| 	{ "hook_timer",      lua_plugin_hook_timer      },
 | |
| 	{ "setup_config",    lua_plugin_setup_config    },
 | |
| 	{ "connect",         lua_plugin_connect         },
 | |
| 	{ 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 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;
 | |
| 	}
 | |
| 
 | |
| 	// 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_create_meta (lua_State *L, const char *name, luaL_Reg *fns)
 | |
| {
 | |
| 	luaL_newmetatable (L, name);
 | |
| 	luaL_setfuncs (L, fns, 0);
 | |
| 
 | |
| 	// Emulate properties for convenience
 | |
| 	lua_pushcfunction (L, lua_plugin_property_get);
 | |
| 	lua_setfield (L, -2, "__index");
 | |
| 	lua_pushcfunction (L, lua_plugin_property_set);
 | |
| 	lua_setfield (L, -2, "__newindex");
 | |
| 
 | |
| 	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;
 | |
| 
 | |
| 	// Register the degesch library with "plugin" as an upvalue
 | |
| 	luaL_checkversion (L);
 | |
| 	luaL_newlibtable (L, lua_plugin_library);
 | |
| 	lua_pushlightuserdata (L, plugin);
 | |
| 	luaL_setfuncs (L, lua_plugin_library, 1);
 | |
| 	lua_setglobal (L, PROGRAM_NAME);
 | |
| 
 | |
| 	// Create metatables for our objects
 | |
| 	lua_plugin_create_meta (L, XLUA_HOOK_METATABLE,       lua_hook_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_USER_METATABLE,       lua_user_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_CHANNEL_METATABLE,    lua_channel_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_BUFFER_METATABLE,     lua_buffer_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_SERVER_METATABLE,     lua_server_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_SCHEMA_METATABLE,     lua_schema_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
 | |
| 	lua_plugin_create_meta (L, XLUA_CONNECTOR_METATABLE,  lua_connector_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)
 | |
| 		FAIL ("no plugin handler for \"%s\"", filename);
 | |
| 	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 str_vector paths;
 | |
| 	str_vector_init (&paths);
 | |
| 	get_xdg_data_dirs (&paths);
 | |
| 	char *result = resolve_relative_filename_generic
 | |
| 		(&paths, PROGRAM_NAME "/plugins/", filename);
 | |
| 	str_vector_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)
 | |
| 		FAIL ("plugin already loaded");
 | |
| 
 | |
| 	// 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)
 | |
| 		FAIL ("file not found");
 | |
| 
 | |
| 	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
 | |
| 		free (plugin->name);
 | |
| 		plugin->name = xstrdup (name);
 | |
| 
 | |
| 		log_global_status (ctx, "Plugin \"#s\" loaded", name);
 | |
| 		LIST_PREPEND (ctx->plugins, plugin);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		log_global_error (ctx, "Can't load plugin \"#s\": #s",
 | |
| 			name, e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| plugin_unload (struct app_context *ctx, const char *name)
 | |
| {
 | |
| 	struct plugin *plugin = plugin_find (ctx, name);
 | |
| 	if (!plugin)
 | |
| 		log_global_error (ctx, "Can't unload plugin \"#s\": #s",
 | |
| 			name, "plugin not loaded");
 | |
| 	else
 | |
| 	{
 | |
| 		log_global_status (ctx, "Plugin \"#s\" unloaded", name);
 | |
| 		LIST_UNLINK (ctx->plugins, plugin);
 | |
| 		plugin_destroy (plugin);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| load_plugins (struct app_context *ctx)
 | |
| {
 | |
| 	const char *plugins = get_config_string
 | |
| 		(ctx->config.root, "behaviour.plugin_autoload");
 | |
| 	if (plugins)
 | |
| 	{
 | |
| 		struct str_vector v;
 | |
| 		str_vector_init (&v);
 | |
| 		cstr_split_ignore_empty (plugins, ',', &v);
 | |
| 		for (size_t i = 0; i < v.len; i++)
 | |
| 			plugin_load (ctx, v.vector[i]);
 | |
| 		str_vector_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
 | |
| 	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_init (&s);
 | |
| 
 | |
| 		int new = iter->new_messages_count - iter->new_unimportant_count;
 | |
| 		if (new && iter != ctx->current_buffer)
 | |
| 			str_append_printf (&s, " (%d%s)", new, &"!"[!iter->highlighted]);
 | |
| 		log_global_indent (ctx,
 | |
| 			"  [#d] #s#&s", i++, iter->name, str_steal (&s));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| part_channel (struct server *s, const char *channel_name, const char *reason)
 | |
| {
 | |
| 	if (*reason)
 | |
| 		irc_send (s, "PART %s :%s", channel_name, reason);
 | |
| 	else
 | |
| 		irc_send (s, "PART %s", channel_name);
 | |
| 
 | |
| 	struct channel *channel;
 | |
| 	if ((channel = str_map_find (&s->irc_channels, channel_name)))
 | |
| 		channel->left_manually = true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_buffer_goto (struct app_context *ctx, struct handler_args *a)
 | |
| {
 | |
| 	if (!*a->arguments)
 | |
| 		return false;
 | |
| 
 | |
| 	const char *which = cut_word (&a->arguments);
 | |
| 	struct buffer *buffer = try_decode_buffer (ctx, which);
 | |
| 	if (buffer)
 | |
| 		buffer_activate (ctx, buffer);
 | |
| 	else
 | |
| 		log_global_error (ctx, "#s: #s", "No such buffer", which);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| handle_buffer_close (struct app_context *ctx, struct handler_args *a)
 | |
| {
 | |
| 	struct buffer *buffer = NULL;
 | |
| 	const char *which = NULL;
 | |
| 	if (!*a->arguments)
 | |
| 		buffer = a->buffer;
 | |
| 	else
 | |
| 		buffer = try_decode_buffer (ctx, (which = cut_word (&a->arguments)));
 | |
| 
 | |
| 	if (!buffer)
 | |
| 		log_global_error (ctx, "#s: #s", "No such buffer", which);
 | |
| 	else if (buffer == ctx->global_buffer)
 | |
| 		log_global_error (ctx, "Can't close the global buffer");
 | |
| 	else if (buffer->type == BUFFER_SERVER)
 | |
| 		log_global_error (ctx, "Can't close a server buffer");
 | |
| 	else
 | |
| 	{
 | |
| 		// The user would be unable to recreate the buffer otherwise
 | |
| 		if (buffer->type == BUFFER_CHANNEL
 | |
| 		 && irc_channel_is_joined (buffer->channel))
 | |
| 			part_channel (buffer->server, buffer->channel->name, "");
 | |
| 		buffer_remove_safe (ctx, buffer);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_buffer_move (struct app_context *ctx, struct handler_args *a)
 | |
| {
 | |
| 	unsigned long request;
 | |
| 	if (!xstrtoul (&request, a->arguments, 10))
 | |
| 		return false;
 | |
| 
 | |
| 	if (request == 0 || request > (unsigned long) buffer_count (ctx))
 | |
| 	{
 | |
| 		log_global_error (ctx, "#s: #s",
 | |
| 			"Can't move buffer", "requested position is out of range");
 | |
| 		return true;
 | |
| 	}
 | |
| 	buffer_move (ctx, a->buffer, request);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_buffer (struct handler_args *a)
 | |
| {
 | |
| 	struct app_context *ctx = a->ctx;
 | |
| 	char *action = cut_word (&a->arguments);
 | |
| 	if (try_handle_buffer_goto (ctx, action))
 | |
| 		return true;
 | |
| 
 | |
| 	bool result = true;
 | |
| 	if (!*action || !strcasecmp_ascii (action, "list"))
 | |
| 		show_buffers_list (ctx);
 | |
| 	else if (!strcasecmp_ascii (action, "clear"))
 | |
| 	{
 | |
| 		buffer_clear (a->buffer);
 | |
| 		buffer_print_backlog (ctx, a->buffer);
 | |
| 	}
 | |
| 	else if (!strcasecmp_ascii (action, "move"))
 | |
| 		result = handle_buffer_move (ctx, a);
 | |
| 	else if (!strcasecmp_ascii (action, "goto"))
 | |
| 		result = handle_buffer_goto (ctx, a);
 | |
| 	else if (!strcasecmp_ascii (action, "close"))
 | |
| 		handle_buffer_close (ctx, a);
 | |
| 	else
 | |
| 		result = false;
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| replace_string_array
 | |
| 	(struct config_item *item, struct str_vector *array, struct error **e)
 | |
| {
 | |
| 	char *changed = join_str_vector (array, ',');
 | |
| 	struct str tmp = { .str = changed, .len = strlen (changed) };
 | |
| 	bool result = config_item_set_from (item,
 | |
| 		config_item_string_array (&tmp), e);
 | |
| 	free (changed);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_set_add
 | |
| 	(struct config_item *item, const char *value, struct error **e)
 | |
| {
 | |
| 	struct str_vector items;
 | |
| 	str_vector_init (&items);
 | |
| 	if (item->type != CONFIG_ITEM_NULL)
 | |
| 		cstr_split (item->value.string.str, ",", &items);
 | |
| 	if (items.len == 1 && !*items.vector[0])
 | |
| 		str_vector_reset (&items);
 | |
| 
 | |
| 	// FIXME: handle multiple items properly
 | |
| 	bool result = false;
 | |
| 	if (str_vector_find (&items, value) != -1)
 | |
| 		error_set (e, "already present in the array: %s", value);
 | |
| 	else
 | |
| 	{
 | |
| 		str_vector_add (&items, value);
 | |
| 		result = replace_string_array (item, &items, e);
 | |
| 	}
 | |
| 
 | |
| 	str_vector_free (&items);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_set_remove
 | |
| 	(struct config_item *item, const char *value, struct error **e)
 | |
| {
 | |
| 	struct str_vector items;
 | |
| 	str_vector_init (&items);
 | |
| 	if (item->type != CONFIG_ITEM_NULL)
 | |
| 		cstr_split (item->value.string.str, ",", &items);
 | |
| 	if (items.len == 1 && !*items.vector[0])
 | |
| 		str_vector_reset (&items);
 | |
| 
 | |
| 	// FIXME: handle multiple items properly
 | |
| 	bool result = false;
 | |
| 	ssize_t i = str_vector_find (&items, value);
 | |
| 	if (i == -1)
 | |
| 		error_set (e, "not present in the array: %s", value);
 | |
| 	else
 | |
| 	{
 | |
| 		str_vector_remove (&items, i);
 | |
| 		result = replace_string_array (item, &items, e);
 | |
| 	}
 | |
| 
 | |
| 	str_vector_free (&items);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static void
 | |
| handle_command_set_assign_item (struct app_context *ctx,
 | |
| 	char *key, struct config_item *new_, bool add, bool remove)
 | |
| {
 | |
| 	struct config_item *item =
 | |
| 		config_item_get (ctx->config.root, key, NULL);
 | |
| 	hard_assert (item);
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	if (!item->schema)
 | |
| 		error_set (&e, "option not recognized");
 | |
| 	else if (!add && !remove)
 | |
| 		config_item_set_from (item, config_item_clone (new_), &e);
 | |
| 	else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY)
 | |
| 		error_set (&e, "not a string array");
 | |
| 	else if (add)
 | |
| 		handle_command_set_add (item, new_->value.string.str, &e);
 | |
| 	else if (remove)
 | |
| 		handle_command_set_remove (item, new_->value.string.str, &e);
 | |
| 
 | |
| 	if (e)
 | |
| 	{
 | |
| 		log_global_error (ctx,
 | |
| 			"Failed to set option \"#s\": #s", key, e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		struct str_vector tmp;
 | |
| 		str_vector_init (&tmp);
 | |
| 		dump_matching_options (ctx->config.root, key, &tmp);
 | |
| 		log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
 | |
| 		str_vector_free (&tmp);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_set_assign
 | |
| 	(struct app_context *ctx, struct str_vector *all, char *arguments)
 | |
| {
 | |
| 	char *op = cut_word (&arguments);
 | |
| 	bool add = false;
 | |
| 	bool remove = false;
 | |
| 
 | |
| 	if      (!strcmp (op, "+="))  add = true;
 | |
| 	else if (!strcmp (op, "-="))  remove = true;
 | |
| 	else if  (strcmp (op, "="))   return false;
 | |
| 
 | |
| 	if (!*arguments)
 | |
| 		return false;
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	struct config_item *new_ =
 | |
| 		config_item_parse (arguments, strlen (arguments), true, &e);
 | |
| 	if (e)
 | |
| 	{
 | |
| 		log_global_error (ctx, "Invalid value: #s", e->message);
 | |
| 		error_free (e);
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	if ((add | remove) && !config_item_type_is_string (new_->type))
 | |
| 	{
 | |
| 		log_global_error (ctx, "+= / -= operators need a string argument");
 | |
| 		config_item_destroy (new_);
 | |
| 		return true;
 | |
| 	}
 | |
| 	for (size_t i = 0; i < all->len; i++)
 | |
| 	{
 | |
| 		char *key = cstr_cut_until (all->vector[i], " ");
 | |
| 		handle_command_set_assign_item (ctx, key, new_, add, remove);
 | |
| 		free (key);
 | |
| 	}
 | |
| 	config_item_destroy (new_);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_set (struct handler_args *a)
 | |
| {
 | |
| 	struct app_context *ctx = a->ctx;
 | |
| 	char *option = "*";
 | |
| 	if (*a->arguments)
 | |
| 		option = cut_word (&a->arguments);
 | |
| 
 | |
| 	struct str_vector all;
 | |
| 	str_vector_init (&all);
 | |
| 	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);
 | |
| 
 | |
| 	str_vector_free (&all);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_save (struct handler_args *a)
 | |
| {
 | |
| 	if (*a->arguments)
 | |
| 		return false;
 | |
| 
 | |
| 	save_configuration (a->ctx);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| show_plugin_list (struct app_context *ctx)
 | |
| {
 | |
| 	log_global_indent (ctx, "");
 | |
| 	log_global_indent (ctx, "Plugins:");
 | |
| 	LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
 | |
| 		log_global_indent (ctx, "  #s", iter->name);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_plugin (struct handler_args *a)
 | |
| {
 | |
| 	char *action = cut_word (&a->arguments);
 | |
| 	if (!*action || !strcasecmp_ascii (action, "list"))
 | |
| 		show_plugin_list (a->ctx);
 | |
| 	else if (!strcasecmp_ascii (action, "load"))
 | |
| 	{
 | |
| 		if (!*a->arguments)
 | |
| 			return false;
 | |
| 
 | |
| 		plugin_load (a->ctx, cut_word (&a->arguments));
 | |
| 	}
 | |
| 	else if (!strcasecmp_ascii (action, "unload"))
 | |
| 	{
 | |
| 		if (!*a->arguments)
 | |
| 			return false;
 | |
| 
 | |
| 		plugin_unload (a->ctx, cut_word (&a->arguments));
 | |
| 	}
 | |
| 	else
 | |
| 		return false;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| show_aliases_list (struct app_context *ctx)
 | |
| {
 | |
| 	log_global_indent (ctx, "");
 | |
| 	log_global_indent (ctx, "Aliases:");
 | |
| 
 | |
| 	struct str_map *aliases = get_aliases_config (ctx);
 | |
| 	if (!aliases->len)
 | |
| 	{
 | |
| 		log_global_indent (ctx, "  (none)");
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, aliases);
 | |
| 	struct config_item *alias;
 | |
| 	while ((alias = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		struct str definition;
 | |
| 		str_init (&definition);
 | |
| 		if (config_item_type_is_string (alias->type))
 | |
| 			config_item_write_string (&definition, &alias->value.string);
 | |
| 		else
 | |
| 			str_append (&definition, "alias definition is not a string");
 | |
| 		log_global_indent (ctx, "  /#s: #s", iter.link->key, definition.str);
 | |
| 		str_free (&definition);
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| handle_command_alias (struct handler_args *a)
 | |
| {
 | |
| 	if (!*a->arguments)
 | |
| 		return show_aliases_list (a->ctx);
 | |
| 
 | |
| 	// TODO: validate the name; maybe also while loading configuration
 | |
| 	char *name = cut_word (&a->arguments);
 | |
| 	if (!*a->arguments)
 | |
| 		return false;
 | |
| 
 | |
| 	struct config_item *alias = config_item_string_from_cstr (a->arguments);
 | |
| 
 | |
| 	struct str definition;
 | |
| 	str_init (&definition);
 | |
| 	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)
 | |
| 		log_server_error (a->s, a->s->buffer, "No text to send");
 | |
| 	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_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)
 | |
| {
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &a->ctx->servers);
 | |
| 
 | |
| 	struct server *s;
 | |
| 	while ((s = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		if (irc_is_connected (s))
 | |
| 			irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
 | |
| 	}
 | |
| 
 | |
| 	initiate_quit (a->ctx);
 | |
| 	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 str_vector v;
 | |
| 		str_vector_init (&v);
 | |
| 		cstr_split_ignore_empty (cut_word (&a->arguments), ' ', &v);
 | |
| 		for (size_t i = 0; i < v.len; i++)
 | |
| 			part_channel (a->s, v.vector[i], a->arguments);
 | |
| 		str_vector_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 str_vector v;
 | |
| 		str_vector_init (&v);
 | |
| 		cstr_split_ignore_empty (cut_word (&a->arguments), ' ', &v);
 | |
| 		for (size_t i = 0; i < v.len; i++)
 | |
| 			cycle_channel (a->s, v.vector[i], a->arguments);
 | |
| 		str_vector_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 unset the topic
 | |
| 		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 str_vector *v)
 | |
| {
 | |
| 	size_t n;
 | |
| 	for (size_t i = 0; i < v->len; i += n)
 | |
| 	{
 | |
| 		struct str modes;   str_init (&modes);
 | |
| 		struct str params;  str_init (¶ms);
 | |
| 
 | |
| 		n = MIN (v->len - i, s->irc_max_modes);
 | |
| 		str_append_printf (&modes, "MODE %s %c", channel_name, "-+"[adding]);
 | |
| 		for (size_t k = 0; k < n; k++)
 | |
| 		{
 | |
| 			str_append_c (&modes, mode_char);
 | |
| 			str_append_printf (¶ms, " %s", v->vector[i + k]);
 | |
| 		}
 | |
| 
 | |
| 		irc_send (s, "%s%s", modes.str, params.str);
 | |
| 
 | |
| 		str_free (&modes);
 | |
| 		str_free (¶ms);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| mass_channel_mode_mask_list
 | |
| 	(struct handler_args *a, bool adding, char mode_char)
 | |
| {
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (a->arguments, ' ', &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, "!@*?"))
 | |
| 			continue;
 | |
| 
 | |
| 		v.vector[i] = xstrdup_printf ("%s!*@*", target);
 | |
| 		free (target);
 | |
| 	}
 | |
| 
 | |
| 	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
 | |
| 	str_vector_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 str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (a->arguments, ' ', &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);
 | |
| 
 | |
| 	str_vector_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_init (&iter, &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_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)
 | |
| {
 | |
| 	if (!*a->arguments)
 | |
| 		return false;
 | |
| 
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	cstr_split_ignore_empty (a->arguments, ' ', &v);
 | |
| 	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
 | |
| 	str_vector_free (&v);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| #define CHANMODE_HANDLER(name, adding, mode_char)                              \
 | |
| 	static bool                                                                \
 | |
| 	handle_command_ ## name (struct handler_args *a)                           \
 | |
| 	{                                                                          \
 | |
| 		return handle_command_channel_mode (a, (adding), (mode_char));         \
 | |
| 	}
 | |
| 
 | |
| CHANMODE_HANDLER (op,      true,  'o')  CHANMODE_HANDLER (deop,    false, 'o')
 | |
| CHANMODE_HANDLER (voice,   true,  'v')  CHANMODE_HANDLER (devoice, false, 'v')
 | |
| 
 | |
| #define TRIVIAL_HANDLER(name, command)                                         \
 | |
| 	static bool                                                                \
 | |
| 	handle_command_ ## name (struct handler_args *a)                           \
 | |
| 	{                                                                          \
 | |
| 		if (*a->arguments)                                                     \
 | |
| 			irc_send (a->s, command " %s", a->arguments);                      \
 | |
| 		else                                                                   \
 | |
| 			irc_send (a->s, command);                                          \
 | |
| 		return true;                                                           \
 | |
| 	}
 | |
| 
 | |
| TRIVIAL_HANDLER (list,  "LIST")
 | |
| TRIVIAL_HANDLER (who,   "WHO")
 | |
| TRIVIAL_HANDLER (motd,  "MOTD")
 | |
| TRIVIAL_HANDLER (oper,  "OPER")
 | |
| TRIVIAL_HANDLER (stats, "STATS")
 | |
| TRIVIAL_HANDLER (away,  "AWAY")
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static bool handle_command_help (struct handler_args *);
 | |
| 
 | |
| static struct command_handler
 | |
| {
 | |
| 	const char *name;
 | |
| 	const char *description;
 | |
| 	const char *usage;
 | |
| 	bool (*handler) (struct handler_args *a);
 | |
| 	enum handler_flags flags;
 | |
| }
 | |
| g_command_handlers[] =
 | |
| {
 | |
| 	{ "help",       "Show help",
 | |
| 	  "[<command> | <option>]",
 | |
| 	  handle_command_help,       0 },
 | |
| 	{ "quit",       "Quit the program",
 | |
| 	  "[<message>]",
 | |
| 	  handle_command_quit,       0 },
 | |
| 	{ "buffer",     "Manage buffers",
 | |
| 	  "<N> | list | clear | move <N> | goto <N or name> | close [<N or name>]",
 | |
| 	  handle_command_buffer,     0 },
 | |
| 	{ "set",        "Manage configuration",
 | |
| 	  "[<option>]",
 | |
| 	  handle_command_set,        0 },
 | |
| 	{ "save",       "Save configuration",
 | |
| 	  NULL,
 | |
| 	  handle_command_save,       0 },
 | |
| 	{ "plugin",     "Manage plugins",
 | |
| 	  "list | load <name> | unload <name>",
 | |
| 	  handle_command_plugin,     0 },
 | |
| 
 | |
| 	{ "alias",      "List or set aliases",
 | |
| 	  "[<name> <definition>]",
 | |
| 	  handle_command_alias,      0 },
 | |
| 	{ "unalias",    "Unset aliases",
 | |
| 	  "<name>...",
 | |
| 	  handle_command_unalias,    0 },
 | |
| 
 | |
| 	{ "msg",        "Send message to a nick or channel",
 | |
| 	  "<target> <message>",
 | |
| 	  handle_command_msg,        HANDLER_SERVER | HANDLER_NEEDS_REG },
 | |
| 	{ "query",      "Send a private message to a nick",
 | |
| 	  "<nick> <message>",
 | |
| 	  handle_command_query,      HANDLER_SERVER | HANDLER_NEEDS_REG },
 | |
| 	{ "notice",     "Send notice to a nick or channel",
 | |
| 	  "<target> <message>",
 | |
| 	  handle_command_notice,     HANDLER_SERVER | HANDLER_NEEDS_REG },
 | |
| 	{ "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 },
 | |
| 	{ "stats",      "Query server statistics",
 | |
| 	  "[<query> [<target>]]",
 | |
| 	  handle_command_stats,      HANDLER_SERVER },
 | |
| 	{ "away",       "Set away status",
 | |
| 	  "[<text>]",
 | |
| 	  handle_command_away,       HANDLER_SERVER },
 | |
| 	{ "nick",       "Change current nick",
 | |
| 	  "<nickname>",
 | |
| 	  handle_command_nick,       HANDLER_SERVER },
 | |
| 	{ "quote",      "Send a raw command to the server",
 | |
| 	  "<command>",
 | |
| 	  handle_command_quote,      HANDLER_SERVER },
 | |
| };
 | |
| 
 | |
| static bool
 | |
| try_handle_command_help_option (struct app_context *ctx, const char *name)
 | |
| {
 | |
| 	struct config_item *item =
 | |
| 		config_item_get (ctx->config.root, name, NULL);
 | |
| 	if (!item)
 | |
| 		return false;
 | |
| 
 | |
| 	struct config_schema *schema = item->schema;
 | |
| 	if (!schema)
 | |
| 	{
 | |
| 		log_global_error (ctx, "#s: #s", "Option not recognized", name);
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	log_global_indent (ctx, "");
 | |
| 	log_global_indent (ctx, "Option \"#s\":", name);
 | |
| 	log_global_indent (ctx, "  Description: #s",
 | |
| 		schema->comment ? schema->comment : "(none)");
 | |
| 	log_global_indent (ctx, "  Type: #s", config_item_type_name (schema->type));
 | |
| 	log_global_indent (ctx, "  Default: #s",
 | |
| 		schema->default_ ? schema->default_ : "null");
 | |
| 
 | |
| 	struct str tmp;
 | |
| 	str_init (&tmp);
 | |
| 	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);
 | |
| 
 | |
| 	char *command = cut_word (&a->arguments);
 | |
| 	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, command))
 | |
| 		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", command);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| init_user_command_map (struct str_map *map)
 | |
| {
 | |
| 	str_map_init (map);
 | |
| 	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 str_vector words;
 | |
| 	str_vector_init (&words);
 | |
| 	cstr_split_ignore_empty (arguments, ' ', &words);
 | |
| 
 | |
| 	// TODO: eventually also add support for argument ranges
 | |
| 	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);
 | |
| 
 | |
| 	str_vector_free (&words);
 | |
| 	return ++p;
 | |
| }
 | |
| 
 | |
| static void
 | |
| expand_alias_definition (const char *definition, const char *arguments,
 | |
| 	struct str_vector *commands)
 | |
| {
 | |
| 	struct str expanded;
 | |
| 	str_init (&expanded);
 | |
| 
 | |
| 	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 == ';')
 | |
| 		{
 | |
| 			str_vector_add_owned (commands, str_steal (&expanded));
 | |
| 			str_init (&expanded);
 | |
| 		}
 | |
| 		else if (*p == '$' && p[1])
 | |
| 			escape = true;
 | |
| 		else
 | |
| 			str_append_c (&expanded, *p);
 | |
| 	}
 | |
| 	str_vector_add_owned (commands, str_steal (&expanded));
 | |
| }
 | |
| 
 | |
| static bool
 | |
| expand_alias (struct app_context *ctx,
 | |
| 	const char *alias_name, char *input, struct str_vector *commands)
 | |
| {
 | |
| 	struct config_item *entry =
 | |
| 		str_map_find (get_aliases_config (ctx), alias_name);
 | |
| 	if (!entry)
 | |
| 		return false;
 | |
| 
 | |
| 	if (!config_item_type_is_string (entry->type))
 | |
| 	{
 | |
| 		log_global_error (ctx, "Error executing `/%s': %s",
 | |
| 			alias_name, "alias definition is not a string");
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	expand_alias_definition (entry->value.string.str, input, commands);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| send_message_to_target (struct server *s,
 | |
| 	const char *target, char *message, struct buffer *buffer)
 | |
| {
 | |
| 	if (!irc_is_connected (s))
 | |
| 		log_server_error (s, buffer, "Not connected");
 | |
| 	else
 | |
| 		SEND_AUTOSPLIT_PRIVMSG (s, target, message);
 | |
| }
 | |
| 
 | |
| static void
 | |
| send_message_to_buffer (struct app_context *ctx, struct buffer *buffer,
 | |
| 	char *message)
 | |
| {
 | |
| 	hard_assert (buffer != NULL);
 | |
| 
 | |
| 	switch (buffer->type)
 | |
| 	{
 | |
| 	case BUFFER_CHANNEL:
 | |
| 		send_message_to_target (buffer->server,
 | |
| 			buffer->channel->name, message, buffer);
 | |
| 		break;
 | |
| 	case BUFFER_PM:
 | |
| 		send_message_to_target (buffer->server,
 | |
| 			buffer->user->nickname, message, buffer);
 | |
| 		break;
 | |
| 	default:
 | |
| 		log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR,
 | |
| 			"This buffer is not a channel");
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static bool
 | |
| process_alias (struct app_context *ctx, struct buffer *buffer,
 | |
| 	struct str_vector *commands, int level)
 | |
| {
 | |
| 	for (size_t i = 0; i < commands->len; i++)
 | |
| 		log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
 | |
| 			(int) i, commands->vector[i]);
 | |
| 	for (size_t i = 0; i < commands->len; i++)
 | |
| 		if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level))
 | |
| 			return false;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer,
 | |
| 	char *input, int alias_level)
 | |
| {
 | |
| 	if (*input != '/' || *++input == '/')
 | |
| 	{
 | |
| 		send_message_to_buffer (ctx, buffer, input);
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	char *name = cut_word (&input);
 | |
| 	if (process_user_command (ctx, buffer, name, input))
 | |
| 		return true;
 | |
| 
 | |
| 	struct str_vector commands;
 | |
| 	str_vector_init (&commands);
 | |
| 
 | |
| 	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);
 | |
| 
 | |
| 	str_vector_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->vtable->filter (hook, buffer, input)))
 | |
| 		{
 | |
| 			log_global_debug (ctx, "Input thrown away by hook");
 | |
| 			return NULL;
 | |
| 		}
 | |
| 
 | |
| 		uint64_t new_hash = siphash_wrapper (input, strlen (input));
 | |
| 		if (new_hash != hash)
 | |
| 			log_global_debug (ctx, "Input transformed to \"#s\"#r", input);
 | |
| 		hash = new_hash;
 | |
| 	}
 | |
| 	return input;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| process_input_utf8 (struct app_context *ctx, struct buffer *buffer,
 | |
| 	const char *input, int alias_level)
 | |
| {
 | |
| 	// Note that this also gets called on expanded aliases,
 | |
| 	// which might or might not be desirable (we can forward "alias_level")
 | |
| 	char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
 | |
| 	bool result = !processed
 | |
| 		|| process_input_utf8_posthook (ctx, buffer, processed, alias_level);
 | |
| 	free (processed);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| static void
 | |
| process_input (struct app_context *ctx, char *user_input)
 | |
| {
 | |
| 	char *input;
 | |
| 	if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
 | |
| 		print_error ("character conversion failed for: %s", "user input");
 | |
| 	else
 | |
| 	{
 | |
| 		struct str_vector lines;
 | |
| 		str_vector_init (&lines);
 | |
| 
 | |
| 		// XXX: this interprets commands in pasted text
 | |
| 		cstr_split (input, "\r\n", &lines);
 | |
| 		for (size_t i = 0; i < lines.len; i++)
 | |
| 			(void) process_input_utf8 (ctx,
 | |
| 				ctx->current_buffer, lines.vector[i], 0);
 | |
| 
 | |
| 		str_vector_free (&lines);
 | |
| 	}
 | |
| 	free (input);
 | |
| }
 | |
| 
 | |
| // --- Word completion ---------------------------------------------------------
 | |
| 
 | |
| // The amount of crap that goes into this is truly insane.
 | |
| // It's mostly because of Editline's total ignorance of this task.
 | |
| 
 | |
| static void
 | |
| completion_init (struct completion *self)
 | |
| {
 | |
| 	memset (self, 0, sizeof *self);
 | |
| }
 | |
| 
 | |
| static void
 | |
| completion_free (struct completion *self)
 | |
| {
 | |
| 	free (self->line);
 | |
| 	free (self->words);
 | |
| }
 | |
| 
 | |
| static void
 | |
| completion_add_word (struct completion *self, size_t start, size_t end)
 | |
| {
 | |
| 	if (!self->words)
 | |
| 		self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
 | |
| 	if (self->words_len == self->words_alloc)
 | |
| 		self->words = xreallocarray (self->words,
 | |
| 			(self->words_alloc <<= 1), sizeof *self->words);
 | |
| 	self->words[self->words_len++] = (struct completion_word) { start, end };
 | |
| }
 | |
| 
 | |
| static void
 | |
| completion_parse (struct completion *self, const char *line, size_t len)
 | |
| {
 | |
| 	self->line = xstrndup (line, len);
 | |
| 
 | |
| 	// The first and the last word may be empty
 | |
| 	const char *s = self->line;
 | |
| 	while (true)
 | |
| 	{
 | |
| 		const char *start = s;
 | |
| 		size_t word_len = strcspn (s, WORD_BREAKING_CHARS);
 | |
| 		const char *end = start + word_len;
 | |
| 		s = end + strspn (end, WORD_BREAKING_CHARS);
 | |
| 
 | |
| 		completion_add_word (self, start - self->line, end - self->line);
 | |
| 		if (s == end)
 | |
| 			break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| completion_locate (struct completion *self, size_t offset)
 | |
| {
 | |
| 	size_t i = 0;
 | |
| 	for (; i < self->words_len; i++)
 | |
| 		if (self->words[i].start > offset)
 | |
| 			break;
 | |
| 	self->location = i - 1;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| completion_matches (struct completion *self, int word, const char *pattern)
 | |
| {
 | |
| 	hard_assert (word >= 0 && word < (int) self->words_len);
 | |
| 	char *text = xstrndup (self->line + self->words[word].start,
 | |
| 		self->words[word].end - self->words[word].start);
 | |
| 	bool result = !fnmatch (pattern, text, 0);
 | |
| 	free (text);
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // 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++)
 | |
| 		utf8_iter_init (&a[i], vector[i]);
 | |
| 
 | |
| 	size_t ch_len;
 | |
| 	int32_t ch;
 | |
| 	while ((ch = utf8_iter_next (&a[0], &ch_len)) != -1)
 | |
| 	{
 | |
| 		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 str_vector *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))
 | |
| 			str_vector_add_owned (output,
 | |
| 				xstrdup_printf ("%s%s", prefix, handler->name));
 | |
| 	}
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, get_aliases_config (ctx));
 | |
| 	struct config_item *alias;
 | |
| 	while ((alias = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		if (!strncasecmp_ascii (word, iter.link->key, word_len))
 | |
| 			str_vector_add_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 str_vector *output)
 | |
| {
 | |
| 	(void) data;
 | |
| 
 | |
| 	struct str_vector options;
 | |
| 	str_vector_init (&options);
 | |
| 
 | |
| 	config_dump (ctx->config.root, &options);
 | |
| 	str_vector_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))
 | |
| 			str_vector_add_owned (output, key);
 | |
| 		else
 | |
| 			free (key);
 | |
| 	}
 | |
| 	free (mask);
 | |
| 	str_vector_free (&options);
 | |
| }
 | |
| 
 | |
| static void
 | |
| complete_topic (struct app_context *ctx, struct completion *data,
 | |
| 	const char *word, struct str_vector *output)
 | |
| {
 | |
| 	(void) data;
 | |
| 
 | |
| 	// TODO: make it work in other server-related buffers, too, i.e. when we're
 | |
| 	//   completing the third word and the second word is a known channel name
 | |
| 	struct buffer *buffer = ctx->current_buffer;
 | |
| 	if (buffer->type != BUFFER_CHANNEL)
 | |
| 		return;
 | |
| 
 | |
| 	const char *topic = buffer->channel->topic;
 | |
| 	if (topic && !strncasecmp_ascii (word, topic, strlen (word)))
 | |
| 	{
 | |
| 		// We must prepend the channel name if the topic itself starts
 | |
| 		// with something that could be regarded as a channel name
 | |
| 		str_vector_add_owned (output, irc_is_channel (buffer->server, topic)
 | |
| 			? xstrdup_printf ("%s %s", buffer->channel->name, topic)
 | |
| 			: xstrdup (topic));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| complete_nicknames (struct app_context *ctx, struct completion *data,
 | |
| 	const char *word, struct str_vector *output)
 | |
| {
 | |
| 	struct buffer *buffer = ctx->current_buffer;
 | |
| 	if (buffer->type == BUFFER_SERVER)
 | |
| 	{
 | |
| 		struct user *self_user = buffer->server->irc_user;
 | |
| 		if (self_user)
 | |
| 			str_vector_add (output, self_user->nickname);
 | |
| 	}
 | |
| 	if (buffer->type != BUFFER_CHANNEL)
 | |
| 		return;
 | |
| 
 | |
| 	size_t word_len = strlen (word);
 | |
| 	LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
 | |
| 	{
 | |
| 		const char *nickname = iter->user->nickname;
 | |
| 		if (irc_server_strncmp (buffer->server, word, nickname, word_len))
 | |
| 			continue;
 | |
| 		str_vector_add_owned (output, data->location == 0
 | |
| 			? xstrdup_printf ("%s:", nickname)
 | |
| 			: xstrdup (nickname));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static char **
 | |
| complete_word (struct app_context *ctx, struct completion *data,
 | |
| 	const char *word)
 | |
| {
 | |
| 	// First figure out what exactly we need to complete
 | |
| 	bool try_commands = false;
 | |
| 	bool try_options = false;
 | |
| 	bool try_topic = false;
 | |
| 	bool try_nicknames = false;
 | |
| 
 | |
| 	if (data->location == 0 && completion_matches (data, 0, "/*"))
 | |
| 		try_commands = true;
 | |
| 	else if (data->location == 1 && completion_matches (data, 0, "/set"))
 | |
| 		try_options = true;
 | |
| 	else if (data->location == 1 && completion_matches (data, 0, "/help"))
 | |
| 		try_commands = try_options = true;
 | |
| 	else if (data->location == 1 && completion_matches (data, 0, "/topic"))
 | |
| 		try_topic = try_nicknames = true;
 | |
| 	else
 | |
| 		try_nicknames = true;
 | |
| 
 | |
| 	struct str_vector words;
 | |
| 	str_vector_init (&words);
 | |
| 
 | |
| 	// Add placeholder
 | |
| 	str_vector_add_owned (&words, NULL);
 | |
| 
 | |
| 	if (try_commands)   complete_command (ctx, data, word, &words);
 | |
| 	if (try_options)    complete_option (ctx, data, word, &words);
 | |
| 	if (try_topic)      complete_topic (ctx, data, word, &words);
 | |
| 	if (try_nicknames)  complete_nicknames (ctx, data, word, &words);
 | |
| 
 | |
| 	LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
 | |
| 	{
 | |
| 		struct completion_hook *hook = (struct completion_hook *) iter;
 | |
| 		hook->vtable->complete (hook, data, word, &words);
 | |
| 	}
 | |
| 
 | |
| 	if (words.len == 1)
 | |
| 	{
 | |
| 		// Nothing matched
 | |
| 		str_vector_free (&words);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	if (words.len == 2)
 | |
| 	{
 | |
| 		words.vector[0] = words.vector[1];
 | |
| 		words.vector[1] = NULL;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		size_t prefix = utf8_common_prefix
 | |
| 			((const char **) words.vector + 1, words.len - 1);
 | |
| 		if (!prefix)
 | |
| 			words.vector[0] = xstrdup (word);
 | |
| 		else
 | |
| 			words.vector[0] = xstrndup (words.vector[1], prefix);
 | |
| 	}
 | |
| 	return words.vector;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| /// A special wrapper for iconv_xstrdup() that also fixes indexes into the
 | |
| /// original string to point to the right location in the output.
 | |
| /// Thanks, Readline!  Without you I would have never needed to deal with this.
 | |
| static char *
 | |
| locale_to_utf8 (struct app_context *ctx, const char *locale,
 | |
| 	int *indexes[], size_t n_indexes)
 | |
| {
 | |
| 	struct str utf8;  str_init (&utf8);
 | |
| 	mbstate_t state;  memset (&state, 0, sizeof state);
 | |
| 
 | |
| 	size_t remaining = strlen (locale) + 1;
 | |
| 	const char *p = locale;
 | |
| 
 | |
| 	// Reset the shift state, FWIW
 | |
| 	(void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL);
 | |
| 
 | |
| 	bool fixed[n_indexes];
 | |
| 	memset (fixed, 0, sizeof fixed);
 | |
| 
 | |
| 	while (true)
 | |
| 	{
 | |
| 		size_t len = mbrlen (p, remaining, &state);
 | |
| 
 | |
| 		// Incomplete multibyte character or illegal sequence (probably)
 | |
| 		if (len == (size_t) -2
 | |
| 		 || len == (size_t) -1)
 | |
| 		{
 | |
| 			str_free (&utf8);
 | |
| 			return NULL;
 | |
| 		}
 | |
| 
 | |
| 		// Convert indexes into the multibyte string to UTF-8
 | |
| 		for (size_t i = 0; i < n_indexes; i++)
 | |
| 			if (!fixed[i] && *indexes[i] <= p - locale)
 | |
| 			{
 | |
| 				*indexes[i] = utf8.len;
 | |
| 				fixed[i] = true;
 | |
| 			}
 | |
| 
 | |
| 		// End of string
 | |
| 		if (!len)
 | |
| 			break;
 | |
| 
 | |
| 		// EINVAL (incomplete sequence) should never happen and
 | |
| 		// EILSEQ neither because we've already checked for that with mbrlen().
 | |
| 		// E2BIG is what iconv_xstrdup solves.  This must succeed.
 | |
| 		size_t ch_len;
 | |
| 		char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len);
 | |
| 		hard_assert (ch != NULL);
 | |
| 		str_append_data (&utf8, ch, ch_len);
 | |
| 		free (ch);
 | |
| 
 | |
| 		p += len;
 | |
| 		remaining -= len;
 | |
| 	}
 | |
| 	return str_steal (&utf8);
 | |
| }
 | |
| 
 | |
| static void
 | |
| utf8_vector_to_locale (struct app_context *ctx, char **vector)
 | |
| {
 | |
| 	for (; *vector; vector++)
 | |
| 	{
 | |
| 		char *converted = iconv_xstrdup
 | |
| 			(ctx->term_from_utf8, *vector, -1, NULL);
 | |
| 		if (!soft_assert (converted))
 | |
| 			converted = xstrdup ("");
 | |
| 
 | |
| 		free (*vector);
 | |
| 		*vector = converted;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| /// Takes a line in locale-specific encoding and position of a word to complete,
 | |
| /// returns a vector of matches in locale-specific encoding.
 | |
| static char **
 | |
| make_completions (struct app_context *ctx, char *line, int start, int end)
 | |
| {
 | |
| 	int *fixes[] = { &start, &end };
 | |
| 	char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
 | |
| 	if (!line_utf8)
 | |
| 		return NULL;
 | |
| 
 | |
| 	hard_assert (start >= 0 && end >= 0 && start <= end);
 | |
| 
 | |
| 	struct completion c;
 | |
| 	completion_init (&c);
 | |
| 	completion_parse (&c, line, strlen (line));
 | |
| 	completion_locate (&c, start);
 | |
| 	char *word = xstrndup (line + start, end - start);
 | |
| 	char **completions = complete_word (ctx, &c, word);
 | |
| 	free (word);
 | |
| 	completion_free (&c);
 | |
| 
 | |
| 	if (completions)
 | |
| 		utf8_vector_to_locale (ctx, completions);
 | |
| 
 | |
| 	free (line_utf8);
 | |
| 	return completions;
 | |
| }
 | |
| 
 | |
| // --- Common code for user actions --------------------------------------------
 | |
| 
 | |
| static void
 | |
| toggle_bracketed_paste (bool enable)
 | |
| {
 | |
| 	fprintf (stdout, "\x1b[?2004%c", "lh"[enable]);
 | |
| 	fflush (stdout);
 | |
| }
 | |
| 
 | |
| static void
 | |
| suspend_terminal (struct app_context *ctx)
 | |
| {
 | |
| 	// Terminal can get suspended by both backlog helper and SIGTSTP handling
 | |
| 	if (ctx->terminal_suspended++ > 0)
 | |
| 		return;
 | |
| 
 | |
| 	toggle_bracketed_paste (false);
 | |
| 	CALL (ctx->input, hide);
 | |
| 	poller_fd_reset (&ctx->tty_event);
 | |
| 
 | |
| 	CALL_ (ctx->input, prepare, false);
 | |
| }
 | |
| 
 | |
| static void
 | |
| resume_terminal (struct app_context *ctx)
 | |
| {
 | |
| 	if (--ctx->terminal_suspended > 0)
 | |
| 		return;
 | |
| 
 | |
| 	update_screen_size ();
 | |
| 	CALL_ (ctx->input, prepare, true);
 | |
| 
 | |
| 	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)
 | |
| 		FAIL ("%s", strerror (errno));
 | |
| 
 | |
| 	char *input = CALL (ctx->input, get_line);
 | |
| 	bool success = xwrite (fd, input, strlen (input), e);
 | |
| 	free (input);
 | |
| 
 | |
| 	if (!success)
 | |
| 		(void) unlink (template);
 | |
| 
 | |
| 	xclose (fd);
 | |
| 	return success;
 | |
| }
 | |
| 
 | |
| static char *
 | |
| try_dump_input_to_file (struct app_context *ctx)
 | |
| {
 | |
| 	char *template = resolve_filename
 | |
| 		("input.XXXXXX", resolve_relative_runtime_unique_filename);
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	if (dump_input_to_file (ctx, template, &e))
 | |
| 		return template;
 | |
| 
 | |
| 	log_global_error (ctx, "#s: #s",
 | |
| 		"Failed to create a temporary file for editing", e->message);
 | |
| 	error_free (e);
 | |
| 	free (template);
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_edit_input (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 	struct app_context *ctx = user_data;
 | |
| 
 | |
| 	char *filename;
 | |
| 	if (!(filename = try_dump_input_to_file (ctx)))
 | |
| 		return false;
 | |
| 
 | |
| 	const char *command;
 | |
| 	if (!(command = getenv ("VISUAL"))
 | |
| 	 && !(command = getenv ("EDITOR")))
 | |
| 		command = "vi";
 | |
| 
 | |
| 	hard_assert (!ctx->running_editor);
 | |
| 	switch (spawn_helper_child (ctx))
 | |
| 	{
 | |
| 	case 0:
 | |
| 		execlp (command, command, filename, NULL);
 | |
| 		print_error ("%s: %s",
 | |
| 			"Failed to launch editor", strerror (errno));
 | |
| 		_exit (EXIT_FAILURE);
 | |
| 	case -1:
 | |
| 		log_global_error (ctx, "#s: #l",
 | |
| 			"Failed to launch editor", strerror (errno));
 | |
| 		free (filename);
 | |
| 		break;
 | |
| 	default:
 | |
| 		ctx->running_editor = true;
 | |
| 		ctx->editor_filename = filename;
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_editor_process (struct app_context *ctx)
 | |
| {
 | |
| 	struct str input;
 | |
| 	str_init (&input);
 | |
| 
 | |
| 	struct error *e = NULL;
 | |
| 	if (!read_file (ctx->editor_filename, &input, &e))
 | |
| 	{
 | |
| 		log_global_error (ctx, "#s: #s", "Input editing failed", e->message);
 | |
| 		error_free (e);
 | |
| 	}
 | |
| 	else
 | |
| 		CALL (ctx->input, clear_line);
 | |
| 
 | |
| 	if (!CALL_ (ctx->input, insert, input.str))
 | |
| 		log_global_error (ctx, "#s: #s", "Input editing failed",
 | |
| 			"could not re-insert the modified text");
 | |
| 
 | |
| 	str_free (&input);
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_editor_cleanup (struct app_context *ctx)
 | |
| {
 | |
| 	if (unlink (ctx->editor_filename))
 | |
| 		log_global_error (ctx, "Could not unlink `#s': #l",
 | |
| 			ctx->editor_filename, strerror (errno));
 | |
| 
 | |
| 	free (ctx->editor_filename);
 | |
| 	ctx->editor_filename = NULL;
 | |
| 
 | |
| 	ctx->running_editor = false;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| launch_backlog_helper (struct app_context *ctx, int backlog_fd)
 | |
| {
 | |
| 	hard_assert (!ctx->running_backlog_helper);
 | |
| 	switch (spawn_helper_child (ctx))
 | |
| 	{
 | |
| 	case 0:
 | |
| 		dup2 (backlog_fd, STDIN_FILENO);
 | |
| 		execl ("/bin/sh", "/bin/sh", "-c", get_config_string
 | |
| 			(ctx->config.root, "behaviour.backlog_helper"), NULL);
 | |
| 		print_error ("%s: %s",
 | |
| 			"Failed to launch backlog helper", strerror (errno));
 | |
| 		_exit (EXIT_FAILURE);
 | |
| 	case -1:
 | |
| 		log_global_error (ctx, "#s: #l",
 | |
| 			"Failed to launch backlog helper", strerror (errno));
 | |
| 		break;
 | |
| 	default:
 | |
| 		ctx->running_backlog_helper = true;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static bool
 | |
| display_backlog (struct app_context *ctx, int flush_opts)
 | |
| {
 | |
| 	FILE *backlog = tmpfile ();
 | |
| 	if (!backlog)
 | |
| 	{
 | |
| 		log_global_error (ctx, "#s: #l",
 | |
| 			"Failed to create a temporary file", strerror (errno));
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	if (!get_config_boolean (ctx->config.root,
 | |
| 		"behaviour.backlog_helper_strip_formatting"))
 | |
| 		flush_opts |= FLUSH_OPT_RAW;
 | |
| 
 | |
| 	struct buffer *buffer = ctx->current_buffer;
 | |
| 	int until_marker =
 | |
| 		(int) buffer->lines_count - (int) buffer->new_messages_count;
 | |
| 	for (struct buffer_line *line = buffer->lines; line; line = line->next)
 | |
| 	{
 | |
| 		if (until_marker-- == 0
 | |
| 		 && buffer->new_messages_count != buffer->lines_count)
 | |
| 			buffer_print_read_marker (ctx, backlog, flush_opts);
 | |
| 		buffer_line_write_to_backlog (ctx, line, backlog, flush_opts);
 | |
| 	}
 | |
| 
 | |
| 	// So that it is obvious if the last line in the buffer is not from today
 | |
| 	buffer_update_time (ctx, time (NULL), backlog, flush_opts);
 | |
| 
 | |
| 	rewind (backlog);
 | |
| 	set_cloexec (fileno (backlog));
 | |
| 	launch_backlog_helper (ctx, fileno (backlog));
 | |
| 	fclose (backlog);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_display_backlog (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 	return display_backlog (user_data, 0);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_display_backlog_nowrap (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 	return display_backlog (user_data, FLUSH_OPT_NOWRAP);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_display_full_log (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 	struct app_context *ctx = user_data;
 | |
| 
 | |
| 	char *path = buffer_get_log_path (ctx->current_buffer);
 | |
| 	FILE *full_log = fopen (path, "rb");
 | |
| 	free (path);
 | |
| 
 | |
| 	if (!full_log)
 | |
| 	{
 | |
| 		log_global_error (ctx, "Failed to open log file for #s: #l",
 | |
| 			ctx->current_buffer->name, strerror (errno));
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	if (ctx->current_buffer->log_file)
 | |
| 		fflush (ctx->current_buffer->log_file);
 | |
| 
 | |
| 	set_cloexec (fileno (full_log));
 | |
| 	launch_backlog_helper (ctx, fileno (full_log));
 | |
| 	fclose (full_log);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static bool
 | |
| on_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;
 | |
| 	if (n <= 0)  n += total * (1 + -n / total);
 | |
| 	buffer_move (ctx, ctx->current_buffer, (n - 1) % total + 1);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_move_buffer_right (int count, int key, void *user_data)
 | |
| {
 | |
| 	return on_move_buffer_left (-count, key, user_data);
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_redraw_screen (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 
 | |
| 	redraw_screen (user_data);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_insert_attribute (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 
 | |
| 	struct app_context *ctx = user_data;
 | |
| 	ctx->awaiting_mirc_escape = true;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static bool
 | |
| on_start_paste_mode (int count, int key, void *user_data)
 | |
| {
 | |
| 	(void) count;
 | |
| 	(void) key;
 | |
| 
 | |
| 	struct app_context *ctx = user_data;
 | |
| 	ctx->in_bracketed_paste = true;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| input_add_functions (void *user_data)
 | |
| {
 | |
| 	struct app_context *ctx = user_data;
 | |
| #define XX(...) CALL_ (ctx->input, register_fn, __VA_ARGS__, ctx);
 | |
| 	XX ("previous-buffer",    "Previous buffer",   on_previous_buffer)
 | |
| 	XX ("next-buffer",        "Next buffer",       on_next_buffer)
 | |
| 	XX ("goto-buffer",        "Go to buffer",      on_goto_buffer)
 | |
| 	XX ("switch-buffer",      "Switch buffer",     on_switch_buffer)
 | |
| 	XX ("goto-highlight",     "Go to highlight",   on_goto_highlight)
 | |
| 	XX ("goto-activity",      "Go to activity",    on_goto_activity)
 | |
| 	XX ("move-buffer-left",   "Move buffer left",  on_move_buffer_left)
 | |
| 	XX ("move-buffer-right",  "Move buffer right", on_move_buffer_right)
 | |
| 	XX ("display-backlog",    "Show backlog",      on_display_backlog)
 | |
| 	XX ("display-backlog-nw", "Non-wrapped log",   on_display_backlog_nowrap)
 | |
| 	XX ("display-full-log",   "Show full log",     on_display_full_log)
 | |
| 	XX ("edit-input",         "Edit input",        on_edit_input)
 | |
| 	XX ("redraw-screen",      "Redraw screen",     on_redraw_screen)
 | |
| 	XX ("insert-attribute",   "mIRC formatting",   on_insert_attribute)
 | |
| 	XX ("start-paste-mode",   "Bracketed paste",   on_start_paste_mode)
 | |
| #undef XX
 | |
| }
 | |
| 
 | |
| static void
 | |
| bind_common_keys (struct app_context *ctx)
 | |
| {
 | |
| 	struct input *self = ctx->input;
 | |
| 	CALL_ (self, bind_control, 'p', "previous-buffer");
 | |
| 	CALL_ (self, bind_control, 'n', "next-buffer");
 | |
| 
 | |
| 	// Redefine M-0 through M-9 to switch buffers
 | |
| 	for (int i = 0; i <= 9; i++)
 | |
| 		CALL_ (self, bind_meta, '0' + i, "goto-buffer");
 | |
| 
 | |
| 	CALL_ (self, bind_meta, '\t', "switch-buffer");
 | |
| 	CALL_ (self, bind_meta, '!', "goto-highlight");
 | |
| 	CALL_ (self, bind_meta, 'a', "goto-activity");
 | |
| 	CALL_ (self, bind_meta, 'm', "insert-attribute");
 | |
| 	CALL_ (self, bind_meta, 'h', "display-full-log");
 | |
| 	CALL_ (self, bind_meta, '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-nw");
 | |
| 
 | |
| 	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 in order to allow correct buffer switching
 | |
| 		str_vector_add_owned (&ctx->pending_input, line);
 | |
| 		poller_idle_set (&ctx->input_event);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		// Prevent readline from showing the prompt twice for w/e reason
 | |
| 		CALL (ctx->input, hide);
 | |
| 		input_rl__restore (self);
 | |
| 
 | |
| 		CALL (ctx->input, ding);
 | |
| 	}
 | |
| 
 | |
| 	if (self->active)
 | |
| 		// Readline automatically redisplays it
 | |
| 		self->prompt_shown = 1;
 | |
| }
 | |
| 
 | |
| static char **
 | |
| app_readline_completion (const char *text, int start, int end)
 | |
| {
 | |
| 	// We will reconstruct that ourselves
 | |
| 	(void) text;
 | |
| 
 | |
| 	// Don't iterate over filenames and stuff
 | |
| 	rl_attempted_completion_over = true;
 | |
| 
 | |
| 	return make_completions (g_ctx, rl_line_buffer, start, end);
 | |
| }
 | |
| 
 | |
| static int
 | |
| app_readline_init (void)
 | |
| {
 | |
| 	struct app_context *ctx = g_ctx;
 | |
| 	struct input *self = ctx->input;
 | |
| 
 | |
| 	// XXX: maybe use rl_make_bare_keymap() and start from there;
 | |
| 	//   our dear user could potentionally rig things up in a way that might
 | |
| 	//   result in some funny unspecified behaviour
 | |
| 
 | |
| 	// For vi mode, enabling "show-mode-in-prompt" is recommended as there is
 | |
| 	// no easy way to indicate mode changes otherwise.
 | |
| 
 | |
| 	rl_add_defun ("send-line", on_readline_return, -1);
 | |
| 	bind_common_keys (ctx);
 | |
| 
 | |
| 	// Move native history commands
 | |
| 	CALL_ (self, bind_meta, 'p', "previous-history");
 | |
| 	CALL_ (self, bind_meta, 'n', "next-history");
 | |
| 
 | |
| 	// We need to hide the prompt and input first
 | |
| 	rl_bind_key (RETURN, rl_named_function ("send-line"));
 | |
| 	CALL_ (self, bind_control, 'j', "send-line");
 | |
| 
 | |
| 	rl_variable_bind ("completion-ignore-case", "on");
 | |
| 	rl_bind_key (TAB, rl_named_function ("menu-complete"));
 | |
| 	if (key_btab)
 | |
| 		CALL_ (self, bind, key_btab, "menu-complete-backward");
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| #endif // HAVE_READLINE
 | |
| 
 | |
| // --- BSD Editline user actions -----------------------------------------------
 | |
| 
 | |
| #ifdef HAVE_EDITLINE
 | |
| 
 | |
| static unsigned char
 | |
| on_editline_complete (EditLine *editline, int key)
 | |
| {
 | |
| 	(void) key;
 | |
| 	(void) editline;
 | |
| 
 | |
| 	struct app_context *ctx = g_ctx;
 | |
| 
 | |
| 	// First prepare what Readline would have normally done for us...
 | |
| 	const LineInfo *info_mb = el_line (editline);
 | |
| 	int len = info_mb->lastchar - info_mb->buffer;
 | |
| 	int point = info_mb->cursor - info_mb->buffer;
 | |
| 	char *copy = xstrndup (info_mb->buffer, len);
 | |
| 
 | |
| 	// XXX: possibly incorrect wrt. shift state encodings
 | |
| 	int el_start = point, el_end = point;
 | |
| 	while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
 | |
| 		el_start--;
 | |
| 
 | |
| 	char **completions = make_completions (ctx, copy, el_start, el_end);
 | |
| 
 | |
| 	// XXX: possibly incorrect wrt. shift state encodings
 | |
| 	copy[el_end] = '\0';
 | |
| 	int el_len = mbstowcs (NULL, copy + el_start, 0);
 | |
| 	free (copy);
 | |
| 
 | |
| 	if (!completions)
 | |
| 		return CC_REFRESH_BEEP;
 | |
| 
 | |
| 	// Remove the original word
 | |
| 	el_wdeletestr (editline, el_len);
 | |
| 
 | |
| 	// Insert the best match instead
 | |
| 	el_insertstr (editline, completions[0]);
 | |
| 	bool only_match = !completions[1];
 | |
| 	for (char **p = completions; *p; p++)
 | |
| 		free (*p);
 | |
| 	free (completions);
 | |
| 
 | |
| 	// I'm not sure if Readline's menu-complete can at all be implemented
 | |
| 	// with Editline.  Spamming the terminal with possible completions
 | |
| 	// probably isn't what the user wants and we have no way of detecting
 | |
| 	// what the last executed handler was.
 | |
| 	if (!only_match)
 | |
| 		return CC_REFRESH_BEEP;
 | |
| 
 | |
| 	// But if there actually is just one match, finish the word
 | |
| 	el_insertstr (editline, " ");
 | |
| 	return CC_REFRESH;
 | |
| }
 | |
| 
 | |
| static unsigned char
 | |
| on_editline_return (EditLine *editline, int key)
 | |
| {
 | |
| 	(void) key;
 | |
| 	struct app_context *ctx = g_ctx;
 | |
| 	struct input_el *self = (struct input_el *) ctx->input;
 | |
| 
 | |
| 	const LineInfoW *info = el_wline (editline);
 | |
| 	int len = info->lastchar - info->buffer;
 | |
| 	int point = info->cursor - info->buffer;
 | |
| 
 | |
| 	wchar_t *line = calloc (sizeof *info->buffer, len + 1);
 | |
| 	memcpy (line, info->buffer, sizeof *info->buffer * len);
 | |
| 
 | |
| 	// XXX: Editline seems to remember its position in history,
 | |
| 	//   so it's not going to work as you'd expect it to
 | |
| 	if (*line)
 | |
| 	{
 | |
| 		HistEventW ev;
 | |
| 		history_w (self->current->history, &ev, H_ENTER, line);
 | |
| 		print_debug ("history: %d %ls", ev.num, ev.str);
 | |
| 	}
 | |
| 	free (line);
 | |
| 
 | |
| 	// process_input() expects a multibyte string
 | |
| 	const LineInfo *info_mb = el_line (editline);
 | |
| 	str_vector_add_owned (&ctx->pending_input,
 | |
| 		xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
 | |
| 	poller_idle_set (&ctx->input_event);
 | |
| 
 | |
| 	el_cursor (editline, len - point);
 | |
| 	el_wdeletestr (editline, len);
 | |
| 	return CC_REFRESH;
 | |
| }
 | |
| 
 | |
| static void
 | |
| app_editline_init (struct input_el *self)
 | |
| {
 | |
| 	// el_set() leaks memory in 20150325 and other versions, we need wchar_t
 | |
| 	el_wset (self->editline, EL_ADDFN,
 | |
| 		L"send-line", L"Send line",     on_editline_return);
 | |
| 	el_wset (self->editline, EL_ADDFN,
 | |
| 		L"complete",  L"Complete word", on_editline_complete);
 | |
| 
 | |
| 	struct input *input = &self->super;
 | |
| 	input->add_functions (input->user_data);
 | |
| 	bind_common_keys (g_ctx);
 | |
| 
 | |
| 	// Move native history commands
 | |
| 	CALL_ (input, bind_meta, 'p', "ed-prev-history");
 | |
| 	CALL_ (input, bind_meta, 'n', "ed-next-history");
 | |
| 
 | |
| 	// No, editline, it's not supposed to kill the entire line
 | |
| 	CALL_ (input, bind_control, 'w', "ed-delete-prev-word");
 | |
| 	// Just what are you doing?
 | |
| 	CALL_ (input, bind_control, 'u', "vi-kill-line-prev");
 | |
| 
 | |
| 	// We need to hide the prompt and input first
 | |
| 	CALL_ (input, bind, "\n", "send-line");
 | |
| 
 | |
| 	CALL_ (input, bind_control, 'i', "complete");
 | |
| 
 | |
| 	// Source the user's defaults file
 | |
| 	el_source (self->editline, NULL);
 | |
| }
 | |
| 
 | |
| #endif // HAVE_EDITLINE
 | |
| 
 | |
| // --- Configuration loading ---------------------------------------------------
 | |
| 
 | |
| static struct config_item *
 | |
| load_configuration_file (const char *filename, struct error **e)
 | |
| {
 | |
| 	struct config_item *root = NULL;
 | |
| 
 | |
| 	struct str data;
 | |
| 	str_init (&data);
 | |
| 	if (!read_file (filename, &data, e))
 | |
| 		goto end;
 | |
| 
 | |
| 	struct error *error = NULL;
 | |
| 	if (!(root = config_item_parse (data.str, data.len, false, &error)))
 | |
| 	{
 | |
| 		error_set (e, "Parsing `%s' failed: %s", filename, error->message);
 | |
| 		error_free (error);
 | |
| 	}
 | |
| end:
 | |
| 	str_free (&data);
 | |
| 	return root;
 | |
| }
 | |
| 
 | |
| static const char *g_first_time_help[] =
 | |
| {
 | |
| 	"",
 | |
| 	"\x02Welcome to degesch!",
 | |
| 	"",
 | |
| 	"To get a list of all commands, type \x02/help\x02.  To obtain",
 | |
| 	"more information on a command or option, simply add it as",
 | |
| 	"a parameter, e.g. \x02/help set\x02 or \x02/help behaviour.logging\x02.",
 | |
| 	"",
 | |
| 	"To switch between buffers, press \x02"
 | |
| 		"F5/Ctrl-P\x02 or \x02" "F6/Ctrl-N\x02.",
 | |
| 	"",
 | |
| 	"Finally, adding a network is as simple as:",
 | |
| 	" - \x02/server add freenode\x02",
 | |
| 	" - \x02/set servers.freenode.addresses = \"chat.freenode.net\"\x02",
 | |
| 	" - \x02/connect freenode\x02",
 | |
| 	"",
 | |
| 	"That should be enough to get you started.  Have fun!",
 | |
| 	""
 | |
| };
 | |
| 
 | |
| static void
 | |
| show_first_time_help (struct app_context *ctx)
 | |
| {
 | |
| 	for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++)
 | |
| 		log_global_indent (ctx, "#m", g_first_time_help[i]);
 | |
| }
 | |
| 
 | |
| const char *g_default_aliases[][2] =
 | |
| {
 | |
| 	{ "c",     "/buffer clear" }, { "close", "/buffer close" },
 | |
| 	{ "j",     "/join $*"      }, { "p",     "/part $*"      },
 | |
| 	{ "k",     "/kick $*"      }, { "kb",    "/kickban $*"   },
 | |
| 	{ "m",     "/msg $*"       }, { "q",     "/query $*"     },
 | |
| 	{ "n",     "/names $*"     }, { "t",     "/topic $*"     },
 | |
| 	{ "w",     "/who $*"       }, { "wi",    "/whois $*"     },
 | |
| 	{ "ww",    "/whowas $*"    },
 | |
| };
 | |
| 
 | |
| static void
 | |
| load_default_aliases (struct app_context *ctx)
 | |
| {
 | |
| 	struct str_map *aliases = get_aliases_config (ctx);
 | |
| 	for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
 | |
| 	{
 | |
| 		const char **pair = g_default_aliases[i];
 | |
| 		str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1]));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| load_configuration (struct app_context *ctx)
 | |
| {
 | |
| 	// In theory, we could ensure that only one instance is running by locking
 | |
| 	// the configuration file and ensuring here that it exists.  This is
 | |
| 	// however brittle, as it may be unlinked without the application noticing.
 | |
| 
 | |
| 	struct config_item *root = NULL;
 | |
| 	struct error *e = NULL;
 | |
| 
 | |
| 	char *filename = resolve_filename
 | |
| 		(PROGRAM_NAME ".conf", resolve_relative_config_filename);
 | |
| 	if (filename)
 | |
| 		root = load_configuration_file (filename, &e);
 | |
| 	else
 | |
| 		log_global_error (ctx, "Configuration file not found");
 | |
| 	free (filename);
 | |
| 
 | |
| 	if (e)
 | |
| 	{
 | |
| 		log_global_error (ctx, "#s", e->message);
 | |
| 		log_global_error (ctx,
 | |
| 			"Please either fix the configuration file or remove it", filename);
 | |
| 		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_init (&iter, get_servers_config (ctx));
 | |
| 
 | |
| 	struct config_item *subtree;
 | |
| 	while ((subtree = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		const char *name = iter.link->key;
 | |
| 		const char *err;
 | |
| 		if (subtree->type != CONFIG_ITEM_OBJECT)
 | |
| 			log_global_error (ctx, "Error in configuration: "
 | |
| 				"ignoring server `#s' as it's not an object", name);
 | |
| 		else if ((err = check_server_name_for_addition (ctx, name)))
 | |
| 			log_global_error (ctx, "Cannot load server `#s': #s", name, err);
 | |
| 		else
 | |
| 			server_add (ctx, name, subtree);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // --- Signals -----------------------------------------------------------------
 | |
| 
 | |
| static int g_signal_pipe[2];            ///< A pipe used to signal... signals
 | |
| 
 | |
| /// Program termination has been requested by a signal
 | |
| static volatile sig_atomic_t g_termination_requested;
 | |
| /// The window has changed in size
 | |
| static volatile sig_atomic_t g_winch_received;
 | |
| 
 | |
| static void
 | |
| postpone_signal_handling (char id)
 | |
| {
 | |
| 	int original_errno = errno;
 | |
| 	if (write (g_signal_pipe[1], &id, 1) == -1)
 | |
| 		soft_assert (errno == EAGAIN);
 | |
| 	errno = original_errno;
 | |
| }
 | |
| 
 | |
| static void
 | |
| signal_superhandler (int signum)
 | |
| {
 | |
| 	switch (signum)
 | |
| 	{
 | |
| 	case SIGWINCH:
 | |
| 		g_winch_received = true;
 | |
| 		postpone_signal_handling ('w');
 | |
| 		break;
 | |
| 	case SIGINT:
 | |
| 	case SIGTERM:
 | |
| 		g_termination_requested = true;
 | |
| 		postpone_signal_handling ('t');
 | |
| 		break;
 | |
| 	case SIGCHLD:
 | |
| 		postpone_signal_handling ('c');
 | |
| 		break;
 | |
| 	case SIGTSTP:
 | |
| 		postpone_signal_handling ('s');
 | |
| 		break;
 | |
| 	default:
 | |
| 		hard_assert (!"unhandled signal");
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| setup_signal_handlers (void)
 | |
| {
 | |
| 	if (pipe (g_signal_pipe) == -1)
 | |
| 		exit_fatal ("%s: %s", "pipe", strerror (errno));
 | |
| 
 | |
| 	set_cloexec (g_signal_pipe[0]);
 | |
| 	set_cloexec (g_signal_pipe[1]);
 | |
| 
 | |
| 	// So that the pipe cannot overflow; it would make write() block within
 | |
| 	// the signal handler, which is something we really don't want to happen.
 | |
| 	// The same holds true for read().
 | |
| 	set_blocking (g_signal_pipe[0], false);
 | |
| 	set_blocking (g_signal_pipe[1], false);
 | |
| 
 | |
| 	signal (SIGPIPE, SIG_IGN);
 | |
| 
 | |
| 	// So that we can write to the terminal while we're running a backlog
 | |
| 	// helper.  This is also inherited by the child so that it doesn't stop
 | |
| 	// when it calls tcsetpgrp().
 | |
| 	signal (SIGTTOU, SIG_IGN);
 | |
| 
 | |
| 	struct sigaction sa;
 | |
| 	sa.sa_flags = SA_RESTART;
 | |
| 	sa.sa_handler = signal_superhandler;
 | |
| 	sigemptyset (&sa.sa_mask);
 | |
| 
 | |
| 	if (sigaction (SIGWINCH, &sa, NULL) == -1
 | |
| 	 || sigaction (SIGINT,   &sa, NULL) == -1
 | |
| 	 || sigaction (SIGTERM,  &sa, NULL) == -1
 | |
| 	 || sigaction (SIGTSTP,  &sa, NULL) == -1
 | |
| 	 || sigaction (SIGCHLD,  &sa, NULL) == -1)
 | |
| 		exit_fatal ("sigaction: %s", strerror (errno));
 | |
| }
 | |
| 
 | |
| // --- I/O event handlers ------------------------------------------------------
 | |
| 
 | |
| static bool
 | |
| try_reap_child (struct app_context *ctx)
 | |
| {
 | |
| 	int status;
 | |
| 	pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED);
 | |
| 
 | |
| 	if (zombie == -1)
 | |
| 	{
 | |
| 		if (errno == ECHILD)  return false;
 | |
| 		if (errno == EINTR)   return true;
 | |
| 		exit_fatal ("%s: %s", "waitpid", strerror (errno));
 | |
| 	}
 | |
| 	if (!zombie)
 | |
| 		return false;
 | |
| 
 | |
| 	if (WIFSTOPPED (status))
 | |
| 	{
 | |
| 		// We could also send SIGCONT but what's the point
 | |
| 		print_debug ("a child has been stopped, killing its process group");
 | |
| 		kill (-zombie, SIGKILL);
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	if (ctx->running_backlog_helper)
 | |
| 		ctx->running_backlog_helper = false;
 | |
| 	else if (!ctx->running_editor)
 | |
| 	{
 | |
| 		log_global_debug (ctx, "An unknown child has died");
 | |
| 		return true;
 | |
| 	}
 | |
| 
 | |
| 	hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
 | |
| 	resume_terminal (ctx);
 | |
| 
 | |
| 	if (WIFSIGNALED (status))
 | |
| 		log_global_error (ctx,
 | |
| 			"Child died from signal #d", WTERMSIG (status));
 | |
| 	else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
 | |
| 		log_global_error (ctx,
 | |
| 			"Child returned status #d", WEXITSTATUS (status));
 | |
| 	else if (ctx->running_editor)
 | |
| 		input_editor_process (ctx);
 | |
| 
 | |
| 	if (ctx->running_editor)
 | |
| 		input_editor_cleanup (ctx);
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
 | |
| {
 | |
| 	char id = 0;
 | |
| 	(void) read (fd->fd, &id, 1);
 | |
| 
 | |
| 	// Stop ourselves cleanly, even if it makes little sense to do this
 | |
| 	if (id == 's')
 | |
| 	{
 | |
| 		suspend_terminal (ctx);
 | |
| 		kill (getpid (), SIGSTOP);
 | |
| 		g_winch_received = true;
 | |
| 		resume_terminal (ctx);
 | |
| 	}
 | |
| 
 | |
| 	// Reap all dead children (since the signal pipe may overflow etc. we run
 | |
| 	// waitpid() in a loop to return all the zombies it knows about).
 | |
| 	while (try_reap_child (ctx))
 | |
| 		;
 | |
| 
 | |
| 	if (g_termination_requested && !ctx->quitting)
 | |
| 		// TODO: this way we don't send a QUIT message but just close the
 | |
| 		//   connection from our side and wait for a full close.
 | |
| 		//   Once we allow for custom quit messages, we will probably want to
 | |
| 		//   call irc_initiate_disconnect() for all servers.
 | |
| 		initiate_quit (ctx);
 | |
| 
 | |
| 	if (g_winch_received)
 | |
| 	{
 | |
| 		redraw_screen (ctx);
 | |
| 		g_winch_received = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| process_mirc_escape (const struct pollfd *fd, struct app_context *ctx)
 | |
| {
 | |
| 	// There's no other way with libedit, as both el_getc() in a function
 | |
| 	// handler and CC_ARGHACK would block execution
 | |
| 	struct str *buf = &ctx->input_buffer;
 | |
| 	str_ensure_space (buf, 1);
 | |
| 	if (read (fd->fd, buf->str + buf->len, 1) != 1)
 | |
| 		goto error;
 | |
| 	buf->str[++buf->len] = '\0';
 | |
| 
 | |
| 	mbstate_t state;
 | |
| 	memset (&state, 0, sizeof state);
 | |
| 
 | |
| 	size_t len = mbrlen (buf->str, buf->len, &state);
 | |
| 
 | |
| 	// Illegal sequence
 | |
| 	if (len == (size_t) -1)
 | |
| 		goto error;
 | |
| 
 | |
| 	// Incomplete multibyte character
 | |
| 	if (len == (size_t) -2)
 | |
| 		return;
 | |
| 
 | |
| 	if (buf->len != 1)
 | |
| 		goto error;
 | |
| 	switch (buf->str[0])
 | |
| 	{
 | |
| 	case 'b': CALL_ (ctx->input, insert, "\x02"); break;
 | |
| 	case 'c': CALL_ (ctx->input, insert, "\x03"); break;
 | |
| 	case 'i':
 | |
| 	case ']': CALL_ (ctx->input, insert, "\x1d"); break;
 | |
| 	case 'u':
 | |
| 	case '_': CALL_ (ctx->input, insert, "\x1f"); break;
 | |
| 	case 'v': CALL_ (ctx->input, insert, "\x16"); break;
 | |
| 	case 'o': CALL_ (ctx->input, insert, "\x0f"); break;
 | |
| 
 | |
| 	default:
 | |
| 		goto error;
 | |
| 	}
 | |
| 	goto done;
 | |
| 
 | |
| error:
 | |
| 	CALL (ctx->input, ding);
 | |
| done:
 | |
| 	str_reset (buf);
 | |
| 	ctx->awaiting_mirc_escape = false;
 | |
| }
 | |
| 
 | |
| #define BRACKETED_PASTE_LIMIT 102400    ///< How much text can be pasted
 | |
| 
 | |
| static void
 | |
| process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
 | |
| {
 | |
| 	struct str *buf = &ctx->input_buffer;
 | |
| 	str_ensure_space (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 %zu bytes",
 | |
| 			(text_len = BRACKETED_PASTE_LIMIT));
 | |
| 
 | |
| 	buf->str[text_len] = '\0';
 | |
| 	if (CALL_ (ctx->input, insert, buf->str))
 | |
| 		goto done;
 | |
| 
 | |
| error:
 | |
| 	CALL (ctx->input, ding);
 | |
| 	log_global_error (ctx, "Paste failed");
 | |
| done:
 | |
| 	str_reset (buf);
 | |
| 	ctx->in_bracketed_paste = false;
 | |
| }
 | |
| 
 | |
| static void
 | |
| reset_autoaway (struct app_context *ctx)
 | |
| {
 | |
| 	// Stop the last one if it's been disabled altogether in the meantime
 | |
| 	poller_timer_reset (&ctx->autoaway_tmr);
 | |
| 
 | |
| 	// Unset any automated statuses that are active right at this moment
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &ctx->servers);
 | |
| 	struct server *s;
 | |
| 	while ((s = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		if (s->autoaway_active
 | |
| 		 && s->irc_user
 | |
| 		 && s->irc_user->away)
 | |
| 			irc_send (s, "AWAY");
 | |
| 
 | |
| 		s->autoaway_active = false;
 | |
| 	}
 | |
| 
 | |
| 	// And potentially start a new auto-away timer
 | |
| 	int64_t delay = get_config_integer
 | |
| 		(ctx->config.root, "behaviour.autoaway_delay");
 | |
| 	if (delay)
 | |
| 		poller_timer_set (&ctx->autoaway_tmr, delay * 1000);
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_autoaway_timer (struct app_context *ctx)
 | |
| {
 | |
| 	// An empty message would unset any away status, so let's ignore that
 | |
| 	const char *message = get_config_string
 | |
| 		(ctx->config.root, "behaviour.autoaway_message");
 | |
| 	if (!message || !*message)
 | |
| 		return;
 | |
| 
 | |
| 	struct str_map_iter iter;
 | |
| 	str_map_iter_init (&iter, &ctx->servers);
 | |
| 	struct server *s;
 | |
| 	while ((s = str_map_iter_next (&iter)))
 | |
| 	{
 | |
| 		// If the user has already been marked as away,
 | |
| 		// don't override his current away status
 | |
| 		if (s->irc_user
 | |
| 		 && s->irc_user->away)
 | |
| 			continue;
 | |
| 
 | |
| 		irc_send (s, "AWAY :%s", message);
 | |
| 		s->autoaway_active = true;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
 | |
| {
 | |
| 	if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
 | |
| 		print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
 | |
| 
 | |
| 	if (ctx->awaiting_mirc_escape)
 | |
| 		process_mirc_escape (fd, ctx);
 | |
| 	else if (ctx->in_bracketed_paste)
 | |
| 		process_bracketed_paste (fd, ctx);
 | |
| 	else if (!ctx->quitting)
 | |
| 		CALL (ctx->input, on_tty_readable);
 | |
| 
 | |
| 	// User activity detected, stop current auto-away and start anew;
 | |
| 	// since they might have just changed the settings, do this last
 | |
| 	reset_autoaway (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| rearm_flush_timer (struct app_context *ctx)
 | |
| {
 | |
| 	poller_timer_set (&ctx->flush_timer, 60 * 1000);
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_flush_timer (struct app_context *ctx)
 | |
| {
 | |
| 	// I guess we don't need to do anything more complicated
 | |
| 	fflush (NULL);
 | |
| 	rearm_flush_timer (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| rearm_date_change_timer (struct app_context *ctx)
 | |
| {
 | |
| 	struct tm tm_;
 | |
| 	const time_t now = time (NULL);
 | |
| 	if (!soft_assert (localtime_r (&now, &tm_)))
 | |
| 		return;
 | |
| 
 | |
| 	tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0;
 | |
| 	tm_.tm_mday++;
 | |
| 	tm_.tm_isdst = -1;
 | |
| 
 | |
| 	const time_t midnight = mktime (&tm_);
 | |
| 	if (!soft_assert (midnight != (time_t) -1))
 | |
| 		return;
 | |
| 	poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000);
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_date_change_timer (struct app_context *ctx)
 | |
| {
 | |
| 	if (ctx->terminal_suspended <= 0)
 | |
| 	{
 | |
| 		CALL (ctx->input, hide);
 | |
| 		buffer_update_time (ctx, time (NULL), stdout, 0);
 | |
| 		CALL (ctx->input, show);
 | |
| 	}
 | |
| 	rearm_date_change_timer (ctx);
 | |
| }
 | |
| 
 | |
| static void
 | |
| on_pending_input (struct app_context *ctx)
 | |
| {
 | |
| 	poller_idle_reset (&ctx->input_event);
 | |
| 	for (size_t i = 0; i < ctx->pending_input.len; i++)
 | |
| 		process_input (ctx, ctx->pending_input.vector[i]);
 | |
| 	str_vector_reset (&ctx->pending_input);
 | |
| }
 | |
| 
 | |
| static void
 | |
| init_poller_events (struct app_context *ctx)
 | |
| {
 | |
| 	poller_fd_init (&ctx->signal_event, &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);
 | |
| 
 | |
| 	poller_fd_init (&ctx->tty_event, &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);
 | |
| 
 | |
| 	poller_timer_init (&ctx->flush_timer, &ctx->poller);
 | |
| 	ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
 | |
| 	ctx->flush_timer.user_data = ctx;
 | |
| 	rearm_flush_timer (ctx);
 | |
| 
 | |
| 	poller_timer_init (&ctx->date_chg_tmr, &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);
 | |
| 
 | |
| 	poller_timer_init (&ctx->autoaway_tmr, &ctx->poller);
 | |
| 	ctx->autoaway_tmr.dispatcher = (poller_timer_fn) on_autoaway_timer;
 | |
| 	ctx->autoaway_tmr.user_data = ctx;
 | |
| 
 | |
| 	poller_idle_init (&ctx->input_event, &ctx->poller);
 | |
| 	ctx->input_event.dispatcher = (poller_idle_fn) on_pending_input;
 | |
| 	ctx->input_event.user_data = ctx;
 | |
| }
 | |
| 
 | |
| // --- Tests -------------------------------------------------------------------
 | |
| 
 | |
| // The application is quite monolithic and can only be partially unit-tested.
 | |
| // Locale-, terminal- and filesystem-dependent tests are also somewhat tricky.
 | |
| 
 | |
| #ifdef TESTING
 | |
| 
 | |
| static void
 | |
| on_test_config_foo_change (struct config_item *item)
 | |
| {
 | |
| 	*(bool *) item->user_data = item->value.boolean;
 | |
| }
 | |
| 
 | |
| static struct config_schema g_config_test[] =
 | |
| {
 | |
| 	{ .name      = "foo",
 | |
| 	  .comment   = "baz",
 | |
| 	  .type      = CONFIG_ITEM_BOOLEAN,
 | |
| 	  .default_  = "off",
 | |
| 	  .on_change = on_test_config_foo_change },
 | |
| 	{ .name      = "bar",
 | |
| 	  .type      = CONFIG_ITEM_INTEGER,
 | |
| 	  .validate  = config_validate_nonnegative,
 | |
| 	  .default_  = "1" },
 | |
| 	{ .name      = "foobar",
 | |
| 	  .type      = CONFIG_ITEM_STRING,
 | |
| 	  .default_  = "\"qux\"" },
 | |
| 	{}
 | |
| };
 | |
| 
 | |
| static void
 | |
| test_config_load (struct config_item *subtree, void *user_data)
 | |
| {
 | |
| 	config_schema_apply_to_object (g_config_test, subtree, user_data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| test_config (void)
 | |
| {
 | |
| 	struct config config;
 | |
| 	config_init (&config);
 | |
| 
 | |
| 	bool b = true;
 | |
| 	config_register_module (&config, "top", test_config_load, &b);
 | |
| 	config_load (&config, config_item_object ());
 | |
| 	config_schema_call_changed (config.root);
 | |
| 	hard_assert (b == false);
 | |
| 
 | |
| 	struct config_item *invalid = config_item_integer (-1);
 | |
| 	hard_assert (!config_item_set_from (config_item_get (config.root,
 | |
| 		"top.bar", NULL), invalid, NULL));
 | |
| 	config_item_destroy (invalid);
 | |
| 
 | |
| 	struct str s;
 | |
| 	str_init (&s);
 | |
| 	serialize_configuration (config.root, &s);
 | |
| 	config_load (&config, config_item_parse (s.str, s.len, false, NULL));
 | |
| 	str_free (&s);
 | |
| 
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	dump_matching_options (config.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 = \"qux\""));
 | |
| 	str_vector_free (&v);
 | |
| 
 | |
| 	config_free (&config);
 | |
| }
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| static void
 | |
| test_aliases (void)
 | |
| {
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	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"));
 | |
| 	str_vector_free (&v);
 | |
| }
 | |
| 
 | |
| static void
 | |
| test_wrapping (void)
 | |
| {
 | |
| 	static const char *message = " foo bar foobar fóóbárbáz";
 | |
| 	static const char *split[] =
 | |
| 		{ " foo", "bar", "foob", "ar", "fó", "ób", "árb", "áz" };
 | |
| 
 | |
| 	struct str_vector v;
 | |
| 	str_vector_init (&v);
 | |
| 	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]));
 | |
| 	str_vector_free (&v);
 | |
| }
 | |
| 
 | |
| static void
 | |
| test_utf8_prefix (void)
 | |
| {
 | |
| 	static const char *a[] = { "fřoo", "Fřooř", "fřOOŘ" };
 | |
| 	hard_assert (utf8_common_prefix (a, N_ELEMENTS (a)) == 5);
 | |
| }
 | |
| 
 | |
| 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-prefix", NULL, test_utf8_prefix);
 | |
| 	return test_run (&test);
 | |
| }
 | |
| 
 | |
| #define main main_shadowed
 | |
| #endif // TESTING
 | |
| 
 | |
| // --- Main program ------------------------------------------------------------
 | |
| 
 | |
| static const char *g_logo[] =
 | |
| {
 | |
| 	"     __                    __   ",
 | |
| 	"  __/ /___________________/ /   ",
 | |
| 	" /   / , /   / , / __/ __/ _ \\  ",
 | |
| 	"/ / / __/ / / __/_  / /_/ // /  ",
 | |
| 	"\\__/\\__/_  /\\__/___/\\__/_//_/   " PROGRAM_VERSION,
 | |
| 	"      /___/",
 | |
| 	""
 | |
| };
 | |
| 
 | |
| static void
 | |
| show_logo (struct app_context *ctx)
 | |
| {
 | |
| 	for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
 | |
| 		log_global_indent (ctx, "#m", g_logo[i]);
 | |
| }
 | |
| 
 | |
| static void
 | |
| format_input_and_die (struct app_context *ctx)
 | |
| {
 | |
| 	char buf[513];
 | |
| 	while (fgets (buf, sizeof buf, stdin))
 | |
| 	{
 | |
| 		struct formatter f;
 | |
| 		formatter_init (&f, ctx, NULL);
 | |
| 		formatter_add (&f, "#m", buf);
 | |
| 		formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
 | |
| 		formatter_free (&f);
 | |
| 	}
 | |
| 	exit (EXIT_SUCCESS);
 | |
| }
 | |
| 
 | |
| int
 | |
| main (int argc, char *argv[])
 | |
| {
 | |
| 	// We include a generated file from kike including this array we don't use;
 | |
| 	// let's just keep it there and silence the compiler warning instead
 | |
| 	(void) g_default_replies;
 | |
| 
 | |
| 	static const struct opt opts[] =
 | |
| 	{
 | |
| 		{ 'h', "help", NULL, 0, "display this help and exit" },
 | |
| 		{ 'V', "version", NULL, 0, "output version information and exit" },
 | |
| 		// This is mostly intended for previewing formatted MOTD files
 | |
| 		{ 'f', "format", NULL, OPT_LONG_ONLY, "format IRC text from stdin" },
 | |
| 		{ 0, NULL, NULL, 0, NULL }
 | |
| 	};
 | |
| 
 | |
| 	struct opt_handler oh;
 | |
| 	opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental 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);
 | |
| 	}
 | |
| 	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;
 | |
| 
 | |
| 	SSL_library_init ();
 | |
| 	atexit (EVP_cleanup);
 | |
| 	SSL_load_error_strings ();
 | |
| 	atexit (ERR_free_strings);
 | |
| 
 | |
| 	// Bootstrap configuration, so that we can access schema items at all
 | |
| 	register_config_modules (&ctx);
 | |
| 	config_load (&ctx.config, config_item_object ());
 | |
| 
 | |
| 	// The following part is a bit brittle because of interdependencies
 | |
| 	init_colors (&ctx);
 | |
| 	if (format_mode)  format_input_and_die (&ctx);
 | |
| 	init_global_buffer (&ctx);
 | |
| 	show_logo (&ctx);
 | |
| 	setup_signal_handlers ();
 | |
| 	init_poller_events (&ctx);
 | |
| 	load_configuration (&ctx);
 | |
| 
 | |
| 	// At this moment we can safely call any "on_change" callbacks
 | |
| 	config_schema_call_changed (ctx.config.root);
 | |
| 
 | |
| 	// Initialize input so that we can switch to new buffers
 | |
| 	refresh_prompt (&ctx);
 | |
| 	ctx.input->add_functions = input_add_functions;
 | |
| 	CALL_ (ctx.input, start, argv[0]);
 | |
| 	toggle_bracketed_paste (true);
 | |
| 	reset_autoaway (&ctx);
 | |
| 
 | |
| 	// Finally, we juice the configuration for some servers to create
 | |
| 	load_plugins (&ctx);
 | |
| 	load_servers (&ctx);
 | |
| 
 | |
| 	ctx.polling = true;
 | |
| 	while (ctx.polling)
 | |
| 		poller_run (&ctx.poller);
 | |
| 
 | |
| 	CALL (ctx.input, stop);
 | |
| 
 | |
| 	if (get_config_boolean (ctx.config.root, "behaviour.save_on_quit"))
 | |
| 		save_configuration (&ctx);
 | |
| 
 | |
| 	app_context_free (&ctx);
 | |
| 	toggle_bracketed_paste (false);
 | |
| 	free_terminal ();
 | |
| 	return EXIT_SUCCESS;
 | |
| }
 |