/*
* degesch.c: the experimental IRC client
*
* Copyright (c) 2015 - 2016, Přemysl Janouch
*
* 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( 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
#include
#include
#include
#include
#include
#ifdef TIOCGWINSZ
#include
#endif // ! TIOCGWINSZ
#include
#include
// Literally cancer
#undef lines
#undef columns
#ifdef HAVE_READLINE
#include
#include
#endif // HAVE_READLINE
#ifdef HAVE_EDITLINE
#include
#endif // HAVE_EDITLINE
#ifdef HAVE_LUA
#include
#include
#include
#endif // HAVE_LUA
/// Some arbitrary limit for the history file
#define HISTORY_LIMIT 10000
/// How many lines of backlog to store in memory
#define BACKLOG_LIMIT 1000
/// Characters that separate words
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
// --- 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.
struct input_buffer
{
#ifdef HAVE_READLINE
HISTORY_STATE *history; ///< Saved history state
char *saved_line; ///< Saved line content
int saved_mark; ///< Saved mark
#elif defined HAVE_EDITLINE
HistoryW *history; ///< The history object
wchar_t *saved_line; ///< Saved line content
int saved_len; ///< Length of the saved line
#endif // HAVE_EDITLINE
int saved_point; ///< Saved cursor position
};
static struct input_buffer *
input_buffer_new (void)
{
struct input_buffer *self = xcalloc (1, sizeof *self);
#ifdef HAVE_EDITLINE
self->history = history_winit ();
HistEventW ev;
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
#endif // HAVE_EDITLINE
return self;
}
static void
input_buffer_destroy (struct input_buffer *self)
{
#ifdef HAVE_READLINE
// Can't really free "history" contents from here
free (self->history);
#elif defined HAVE_EDITLINE
history_wend (self->history);
#endif // HAVE_EDITLINE
free (self->saved_line);
free (self);
}
struct input
{
bool active; ///< Are we a thing?
#if defined HAVE_READLINE
char *saved_line; ///< Saved line content
int saved_point; ///< Saved cursor position
int saved_mark; ///< Saved mark
#elif defined HAVE_EDITLINE
EditLine *editline; ///< The EditLine object
#endif // HAVE_EDITLINE
char *prompt; ///< The prompt we use
int prompt_shown; ///< Whether the prompt is shown now
struct input_buffer *current; ///< Current input buffer
};
static void
input_init (struct input *self)
{
memset (self, 0, sizeof *self);
}
static void
input_free (struct input *self)
{
#ifdef HAVE_READLINE
free (self->saved_line);
#endif // HAVE_READLINE
free (self->prompt);
}
// --- GNU Readline ------------------------------------------------------------
#ifdef HAVE_READLINE
#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE
#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE
#define input_ding(self) rl_ding ()
static void
input_on_terminal_resized (struct input *self)
{
(void) self;
// 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_on_readable (struct input *self)
{
(void) self;
rl_callback_read_char ();
}
static void
input_set_prompt (struct input *self, char *prompt)
{
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_erase_content (struct input *self)
{
(void) self;
rl_replace_line ("", false);
rl_redisplay ();
}
static void
input_erase (struct input *self)
{
(void) self;
rl_set_prompt ("");
input_erase_content (self);
}
static void
input_bind (struct input *self, const char *seq, const char *function_name)
{
(void) self;
rl_bind_keyseq (seq, rl_named_function (function_name));
}
static void
input_bind_meta (struct input *self, char key, const char *function_name)
{
// This one seems to actually work
char keyseq[] = { '\\', 'e', key, 0 };
input_bind (self, 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- behaves exactly the same
rl_bind_key (META (key), rl_named_function (function_name));
#endif
}
static void
input_bind_control (struct input *self, char key, const char *function_name)
{
char keyseq[] = { '\\', 'C', '-', key, 0 };
input_bind (self, keyseq, function_name);
}
static bool
input_insert (struct input *self, const char *s)
{
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_get_content (struct input *self)
{
(void) self;
return rl_copy_text (0, rl_end);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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_start (struct input *self, const char *program_name)
{
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);
rl_callback_handler_install (self->prompt, on_readline_input);
self->prompt_shown = 1;
self->active = true;
}
static void
input_stop (struct input *self)
{
if (self->prompt_shown > 0)
input_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;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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_save_buffer (struct input *self, struct input_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_restore_buffer (struct input *self, struct input_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_switch_buffer (struct input *self, struct input_buffer *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_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_restore_buffer (self, buffer);
self->current = buffer;
}
static void
input_destroy_buffer (struct input *self, struct input_buffer *buffer)
{
(void) self;
// 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_switch_buffer() 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_buffer_destroy (buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_save (struct input *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_restore (struct input *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_hide (struct input *self)
{
if (!self->active || self->prompt_shown-- < 1)
return;
input_save (self);
input_erase (self);
}
static void
input_show (struct input *self)
{
if (!self->active || ++self->prompt_shown < 1)
return;
input_restore (self);
rl_redisplay ();
}
#endif // HAVE_READLINE
// --- BSD Editline ------------------------------------------------------------
#ifdef HAVE_EDITLINE
#define INPUT_START_IGNORE '\x01'
#define INPUT_END_IGNORE '\x01'
static void app_editline_init (struct input *self);
static void
input_ding (struct input *self)
{
(void) self;
// XXX: this isn't probably very portable;
// we could use "bell" from terminfo but that creates a dependency
write (STDOUT_FILENO, "\a", 1);
}
static void
input_on_terminal_resized (struct input *self)
{
el_resize (self->editline);
}
static void
input_bind (struct input *self, const char *seq, const char *function_name)
{
el_set (self->editline, EL_BIND, seq, function_name, NULL);
}
static void
input_bind_meta (struct input *self, char key, const char *function_name)
{
char keyseq[] = { 'M', '-', key, 0 };
input_bind (self, keyseq, function_name);
}
static void
input_bind_control (struct input *self, char key, const char *function_name)
{
char keyseq[] = { '^', key, 0 };
input_bind (self, keyseq, function_name);
}
static void
input_redisplay (struct input *self)
{
// See rl_redisplay()
// The character is VREPRINT (usually C-r)
// TODO: read it from terminal info
// XXX: could we potentially break UTF-8 with this?
char x[] = { ('R' - 'A' + 1), 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 void
input_set_prompt (struct input *self, char *prompt)
{
free (self->prompt);
self->prompt = prompt;
if (self->prompt_shown > 0)
input_redisplay (self);
}
static char *
input_make_prompt (EditLine *editline)
{
struct input *self;
el_get (editline, EL_CLIENTDATA, &self);
if (!self->prompt)
return "";
return self->prompt;
}
static char *
input_make_empty_prompt (EditLine *editline)
{
(void) editline;
return "";
}
static void
input_erase_content (struct input *self)
{
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_redisplay (self);
}
static void
input_erase (struct input *self)
{
el_set (self->editline, EL_PROMPT, input_make_empty_prompt);
input_erase_content (self);
}
static bool
input_insert (struct input *self, const char *s)
{
bool success = !*s || !el_insertstr (self->editline, s);
if (self->prompt_shown > 0)
input_redisplay (self);
return success;
}
static char *
input_get_content (struct input *self)
{
const LineInfo *info = el_line (self->editline);
return xstrndup (info->buffer, info->lastchar - info->buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_start (struct input *self, const char *program_name)
{
self->editline = el_init (program_name, stdin, stdout, stderr);
el_set (self->editline, EL_CLIENTDATA, self);
el_set (self->editline, EL_PROMPT_ESC,
input_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_stop (struct input *self)
{
if (self->prompt_shown > 0)
input_erase (self);
el_end (self->editline);
self->editline = NULL;
self->active = false;
self->prompt_shown = false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_save_buffer (struct input *self, struct input_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_restore_buffer (struct input *self, struct input_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_switch_buffer (struct input *self, struct input_buffer *buffer)
{
if (self->current)
input_save_buffer (self, self->current);
input_restore_buffer (self, buffer);
el_wset (self->editline, EL_HIST, history, buffer->history);
self->current = buffer;
}
static void
input_destroy_buffer (struct input *self, struct input_buffer *buffer)
{
(void) self;
input_buffer_destroy (buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_save (struct input *self)
{
if (self->current)
input_save_buffer (self, self->current);
}
static void
input_restore (struct input *self)
{
if (self->current)
input_restore_buffer (self, self->current);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_hide (struct input *self)
{
if (!self->active || self->prompt_shown-- < 1)
return;
input_save (self);
input_erase (self);
}
static void
input_show (struct input *self)
{
if (!self->active || ++self->prompt_shown < 1)
return;
input_restore (self);
// XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539
el_set (self->editline,
EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE);
input_redisplay (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_on_readable (struct input *self)
{
// We bind the return key to process it how we need to
// 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;
// The character is VEOF (usually C-d)
// TODO: read it from terminal info
if (count == 0 && buf[0] == ('D' - 'A' + 1))
{
el_deletestr (self->editline, 1);
input_redisplay (self);
input_ding (self);
}
}
#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.
// XXX: this doesn't really have to be reference countable
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_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
{
LIST_HEADER (struct formatter_item)
enum formatter_item_type type; ///< Type of this item
int color; ///< Color
int attribute; ///< Attribute ID
char *text; ///< Either text or an attribute string
};
static struct formatter_item *
formatter_item_new (void)
{
struct formatter_item *self = xcalloc (1, sizeof *self);
return self;
}
static void
formatter_item_destroy (struct formatter_item *self)
{
free (self->text);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct formatter
{
struct app_context *ctx; ///< Application context
struct server *s; ///< Server
struct formatter_item *items; ///< Items
struct formatter_item *items_tail; ///< Tail of items
};
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;
}
static void
formatter_free (struct formatter *self)
{
LIST_FOR_EACH (struct formatter_item, iter, self->items)
formatter_item_destroy (iter);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 *formatter; ///< Line data
};
struct buffer_line *
buffer_line_new (void)
{
struct buffer_line *self = xcalloc (1, sizeof *self);
return self;
}
static void
buffer_line_destroy (struct buffer_line *self)
{
if (self->formatter)
formatter_free (self->formatter);
free (self->formatter);
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_buffer *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 unseen_messages_count; ///< # messages since last visited
unsigned unseen_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 (void)
{
struct buffer *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
self->input_data = input_buffer_new ();
return self;
}
static void
buffer_destroy (struct buffer *self)
{
free (self->name);
if (self->input_data)
input_buffer_destroy (self->input_data);
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 app_context
{
bool no_colors; ///< Disable attribute printing
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
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
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
iconv_t latin1_to_utf8; ///< ISO Latin 1 to UTF-8
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
}
*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 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;
self->last_displayed_msg_time = time (NULL);
char *encoding = nl_langinfo (CODESET);
// FIXME: put a check for "//TRANSLIT" in CMakeLists.txt
#ifdef __linux__
encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
#else // ! __linux__
encoding = xstrdup (encoding);
#endif // ! __linux__
if ((self->term_from_utf8 =
iconv_open (encoding, "UTF-8")) == (iconv_t) -1
|| (self->latin1_to_utf8 =
iconv_open ("UTF-8", "ISO-8859-1")) == (iconv_t) -1
|| (self->term_to_utf8 =
iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1)
exit_fatal ("creating the UTF-8 conversion object failed: %s",
strerror (errno));
free (encoding);
input_init (&self->input);
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
input_destroy_buffer (&self->input, 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->latin1_to_utf8);
iconv_close (self->term_from_utf8);
iconv_close (self->term_to_utf8);
input_free (&self->input);
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_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)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 },
{ .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 = "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_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))
static struct
{
bool initialized; ///< Terminal is available
bool stdout_is_tty; ///< `stdout' is a terminal
bool stderr_is_tty; ///< `stderr' is a terminal
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)
{
del_curterm (cur_term);
return false;
}
// Make sure newlines are output correctly
struct termios termios;
if (!tcgetattr (tty_fd, &termios))
{
termios.c_oflag |= ONLCR;
(void) tcsetattr (tty_fd, TCSADRAIN, &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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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;
input_hide (&ctx->input);
print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
fputs ("\n", stream);
input_show (&ctx->input);
}
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 (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 = xstrdup_printf ("%s%s%s",
g_terminal.color_set_fg[COLOR_YELLOW],
g_terminal.color_set_bg[COLOR_MAGENTA],
enter_bold_mode);
INIT_ATTR (HIGHLIGHT, highlight);
free (highlight);
#undef INIT_ATTR
if (ctx->no_colors)
{
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 (struct app_context *ctx, const char *text)
{
if (!text)
return NULL;
size_t len = strlen (text) + 1;
if (utf8_validate (text, len))
return xstrdup (text);
return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL);
}
// 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 (ctx, 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);
struct formatter_item *item = formatter_item_new ();
*item = template_;
LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
}
#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))
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 (self->ctx, 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 (self->ctx, 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 (self->ctx, 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 (self->ctx, (s = va_arg (*ap, char *)));
str_append (buf, tmp);
free (tmp);
break;
case 'm':
tmp = irc_to_utf8 (self->ctx, (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);
}
static void
formatter_add_from (struct formatter *self, struct formatter *other)
{
for (struct formatter_item *iter = other->items; iter; iter = iter->next)
formatter_add_item (self, *iter);
}
static bool
formatter_flush_attr
(struct attribute_printer *state, struct formatter_item *item)
{
switch (item->type)
{
case FORMATTER_ITEM_ATTR:
attribute_printer_apply (state, item->attribute);
state->want = 0;
state->want_foreground = -1;
state->want_background = -1;
return true;
case FORMATTER_ITEM_SIMPLE:
state->want ^= item->attribute;
attribute_printer_update (state);
return true;
case FORMATTER_ITEM_FG_COLOR:
state->want_foreground = item->color;
attribute_printer_update (state);
return true;
case FORMATTER_ITEM_BG_COLOR:
state->want_background = item->color;
attribute_printer_update (state);
return true;
default:
return false;
}
}
static void
formatter_flush_text (struct app_context *ctx, const char *text, FILE *stream)
{
struct str sanitized;
str_init (&sanitized);
// Throw away any potentially harmful control characters
char *term = iconv_xstrdup (ctx->term_from_utf8, (char *) text, -1, NULL);
for (char *p = term; *p; p++)
if (!strchr ("\a\b\x1b", *p))
str_append_c (&sanitized, *p);
free (term);
fputs (sanitized.str, stream);
str_free (&sanitized);
}
static void
formatter_flush (struct formatter *self, FILE *stream, bool raw_attributes)
{
if (!raw_attributes && !get_attribute_printer (stream))
{
LIST_FOR_EACH (struct formatter_item, iter, self->items)
if (iter->type == FORMATTER_ITEM_TEXT)
fputs (iter->text, stream);
return;
}
struct attribute_printer state;
attribute_printer_init (&state, self->ctx, stream);
attribute_printer_reset (&state);
int attribute_ignore = 0;
LIST_FOR_EACH (struct formatter_item, iter, self->items)
{
if (iter->type == FORMATTER_ITEM_TEXT)
formatter_flush_text (self->ctx, iter->text, stream);
else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR)
attribute_ignore += iter->attribute;
else if (attribute_ignore <= 0
&& !formatter_flush_attr (&state, iter))
hard_assert (!"unhandled formatter item type");
}
attribute_printer_reset (&state);
}
// --- Buffers -----------------------------------------------------------------
static void
buffer_update_time (struct app_context *ctx, time_t now)
{
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[32] = "";
if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t)))
print_status ("%s", buf);
// Else the buffer was too small, which is pretty weird
}
static void
buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output,
bool raw_attributes)
{
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);
formatter_add_from (f, line->formatter);
formatter_add (f, "\n");
formatter_flush (f, output, raw_attributes);
formatter_free (f);
}
static void
buffer_line_display (struct app_context *ctx,
struct buffer_line *line, bool is_external)
{
// 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 (ctx, line->when);
struct formatter f;
formatter_init (&f, ctx, NULL);
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);
// 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);
}
input_hide (&ctx->input);
buffer_line_flush (line, &f, stdout, false);
// Flush the trailing formatting reset item
fflush (stdout);
input_show (&ctx->input);
}
static void
buffer_line_write_to_backlog (struct app_context *ctx,
struct buffer_line *line, FILE *log_file)
{
struct formatter f;
formatter_init (&f, ctx, NULL);
struct tm current;
char buf[20];
if (!localtime_r (&line->when, ¤t))
print_error ("%s: %s", "localtime_r", strerror (errno));
else if (!strftime (buf, sizeof buf, "%F %T", ¤t))
print_error ("%s: %s", "strftime", "buffer too small");
else
formatter_add (&f, "#a#s#r ", ATTR_TIMESTAMP, buf);
buffer_line_flush (line, &f, log_file, !get_config_boolean
(ctx->config.root, "behaviour.backlog_helper_strip_formatting"));
}
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);
buffer_line_flush (line, &f, log_file, false);
}
static void
log_formatter (struct app_context *ctx,
struct buffer *buffer, int flags, struct formatter *f)
{
if (!buffer)
buffer = ctx->global_buffer;
if (buffer->lines_count >= BACKLOG_LIMIT)
{
struct buffer_line *popped = buffer->lines;
LIST_UNLINK_WITH_TAIL (buffer->lines, buffer->lines_tail, popped);
buffer_line_destroy (popped);
buffer->lines_count--;
}
struct buffer_line *line = buffer_line_new ();
line->flags = flags;
line->when = time (NULL);
// Move the formatter inside
line->formatter = xmalloc (sizeof *line->formatter);
*line->formatter = *f;
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)
input_ding (&ctx->input);
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;
if (!displayed)
{
buffer->unseen_messages_count++;
if (flags & BUFFER_LINE_UNIMPORTANT)
buffer->unseen_unimportant_count++;
buffer->highlighted |= important;
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);
input_destroy_buffer (&ctx->input, 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)
{
struct formatter f;
formatter_init (&f, ctx, NULL);
formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
formatter_flush (&f, stdout, false);
// Flush the trailing formatting reset item
fflush (stdout);
formatter_free (&f);
}
static void
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
{
// The prompt can take considerable time to redraw
input_hide (&ctx->input);
char *buffer_name_localized =
iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL);
print_status ("%s", buffer_name_localized);
free (buffer_name_localized);
// That is, minus the buffer switch line and the readline prompt
int display_limit = MAX (10, g_terminal.lines - 2);
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->unseen_messages_count;
for (; line; line = line->next)
{
if (until_marker-- == 0)
buffer_print_read_marker (ctx);
buffer_line_display (ctx, line, false);
}
buffer->unseen_messages_count = 0;
buffer->unseen_unimportant_count = 0;
buffer->highlighted = false;
// So that it is obvious if the last line in the buffer is not from today
buffer_update_time (ctx, time (NULL));
refresh_prompt (ctx);
input_show (&ctx->input);
}
static void
buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
if (ctx->current_buffer == buffer)
return;
buffer_print_backlog (ctx, buffer);
input_switch_buffer (&ctx->input, 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_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 ();
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 ();
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_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);
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
input_hide (&ctx->input);
// 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_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);
const char *ca_file = get_config_string (s->config, "tls_ca_file");
const char *ca_path = get_config_string (s->config, "tls_ca_path");
ERR_clear_error ();
struct error *error = NULL;
if (ca_file || ca_path)
{
if (SSL_CTX_load_verify_locations (ssl_ctx, ca_file, ca_path))
return true;
error_set (&error, "%s: %s",
"Failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
{
error_set (&error, "%s: %s",
"Couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
return true;
ca_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->unseen_messages_count - iter->unseen_unimportant_count))
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
if (self->prompt && !strcmp (new_prompt, self->prompt))
free (new_prompt);
else
input_set_prompt (self, 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)
{
// 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;
const char *all_prefixes = self->s->irc_chanuser_prefixes;
const char *all_modes = self->s->irc_chanuser_modes;
char prefix = all_prefixes[strchr (all_modes, self->mode_char) - 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);
}
// --- 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);
// 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];
if (!strcasecmp_ascii (cap, "multi-prefix")
|| !strcasecmp_ascii (cap, "invite-notify")
|| !strcasecmp_ascii (cap, "echo-message"))
str_vector_add (&chosen, cap);
}
char *chosen_str = join_str_vector (&chosen, ' ');
str_vector_free (&chosen);
irc_send (s, "CAP REQ :%s", chosen_str);
log_server_status (s, s->buffer,
"#s: #S", "Capabilities requested", 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 ();
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);
if (!s->cap_echo_message)
log_ctcp_reply (s, recipient, str_steal (&m));
else
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
{
const char *name;
void (*handler) (struct server *s, const struct irc_message *msg);
}
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 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 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)
{
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)
{
// :!@
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;
}
struct send_autosplit_args;
typedef void (*send_autosplit_logger_fn) (struct server *s,
struct send_autosplit_args *args, struct buffer *buffer, const char *line);
struct send_autosplit_args
{
const char *command; ///< E.g. PRIVMSG or NOTICE
const char *target; ///< User or channel
const char *message; ///< A message to be autosplit
send_autosplit_logger_fn logger; ///< Logger for all resulting lines
const char *prefix; ///< E.g. "\x01ACTION"
const char *suffix; ///< E.g. "\x01"
};
static void
send_autosplit_message (struct server *s, struct send_autosplit_args a)
{
struct buffer *buffer = str_map_find (&s->irc_buffer_map, a.target);
int fixed_part = strlen (a.command) + 1 + strlen (a.target) + 1 + 1
+ strlen (a.prefix) + strlen (a.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, a.message, fixed_part, &lines, &e))
{
log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message);
error_free (e);
goto end;
}
for (size_t i = 0; i < lines.len; i++)
{
irc_send (s, "%s %s :%s%s%s", a.command, a.target,
a.prefix, lines.vector[i], a.suffix);
if (!s->cap_echo_message)
a.logger (s, &a, buffer, lines.vector[i]);
}
end:
str_vector_free (&lines);
}
static void
log_autosplit_action (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
(void) a;
if (buffer && soft_assert (s->irc_user))
log_outcoming_action (s, buffer, s->irc_user->nickname, line);
// This can only be sent from a user or channel buffer
}
#define SEND_AUTOSPLIT_ACTION(s, target, message) \
send_autosplit_message ((s), (struct send_autosplit_args) \
{ "PRIVMSG", (target), (message), log_autosplit_action, \
"\x01" "ACTION ", "\x01" })
static void
log_autosplit_privmsg (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, a->target);
if (buffer && soft_assert (s->irc_user))
log_outcoming_privmsg (s, buffer,
prefixes, s->irc_user->nickname, line);
else
log_outcoming_orphan_privmsg (s, a->target, line);
free (prefixes);
}
#define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \
send_autosplit_message ((s), (struct send_autosplit_args) \
{ "PRIVMSG", (target), (message), log_autosplit_privmsg, "", "" })
static void
log_autosplit_notice (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
if (buffer && soft_assert (s->irc_user))
log_outcoming_notice (s, buffer, s->irc_user->nickname, line);
else
log_outcoming_orphan_notice (s, a->target, line);
}
#define SEND_AUTOSPLIT_NOTICE(s, target, message) \
send_autosplit_message ((s), (struct send_autosplit_args) \
{ "NOTICE", (target), (message), log_autosplit_notice, "", "" })
// --- 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 ();
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, ".");
return len ? xstrndup (begin, len) : NULL;
}
// --- 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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define XLUA_BUFFER_METATABLE "buffer" ///< Identifier for the Lua metatable
struct lua_buffer
{
struct lua_plugin *plugin; ///< The plugin we belong to
struct buffer *buffer; ///< The buffer
struct weak_ref_link *weak_ref; ///< A weak reference link
};
static void
lua_buffer_invalidate (void *object, void *user_data)
{
struct lua_buffer *wrapper = user_data;
wrapper->buffer = 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_plugin_push_buffer (struct lua_plugin *plugin, struct buffer *buffer)
{
lua_State *L = plugin->L;
if (lua_cache_get (L, buffer))
return;
struct lua_buffer *wrapper = lua_newuserdata (L, sizeof *wrapper);
luaL_setmetatable (L, XLUA_BUFFER_METATABLE);
wrapper->plugin = plugin;
wrapper->buffer = buffer;
wrapper->weak_ref = buffer_weak_ref
(buffer, lua_buffer_invalidate, wrapper);
lua_cache_store (L, buffer, -1);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define XLUA_SERVER_METATABLE "server" ///< Identifier for the Lua metatable
struct lua_server
{
struct lua_plugin *plugin; ///< The plugin we belong to
struct server *server; ///< The server
struct weak_ref_link *weak_ref; ///< A weak reference link
};
static void
lua_server_invalidate (void *object, void *user_data)
{
struct lua_server *wrapper = user_data;
wrapper->server = 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_plugin_push_server (struct lua_plugin *plugin, struct server *server)
{
lua_State *L = plugin->L;
if (lua_cache_get (L, server))
return;
struct lua_server *wrapper = lua_newuserdata (L, sizeof *wrapper);
luaL_setmetatable (L, XLUA_SERVER_METATABLE);
wrapper->plugin = plugin;
wrapper->server = server;
wrapper->weak_ref = server_weak_ref
(server, lua_server_invalidate, wrapper);
lua_cache_store (L, server, -1);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_buffer_gc (lua_State *L)
{
struct lua_buffer *wrapper = luaL_checkudata (L, 1, XLUA_BUFFER_METATABLE);
if (wrapper->buffer)
{
lua_cache_invalidate (L, wrapper->buffer);
buffer_weak_unref (wrapper->buffer, &wrapper->weak_ref);
wrapper->buffer = NULL;
}
return 0;
}
static int
lua_buffer_get_server (lua_State *L)
{
struct lua_buffer *wrapper = luaL_checkudata (L, 1, XLUA_BUFFER_METATABLE);
luaL_argcheck (L, wrapper->buffer, 1, "dead reference used");
if (wrapper->buffer->server)
lua_plugin_push_server (wrapper->plugin, wrapper->buffer->server);
else
lua_pushnil (L);
return 1;
}
static int
lua_buffer_log (lua_State *L)
{
struct lua_buffer *wrapper = luaL_checkudata (L, 1, XLUA_BUFFER_METATABLE);
luaL_argcheck (L, wrapper->buffer, 1, "dead reference used");
const char *message = luaL_checkstring (L, 2);
struct buffer *buffer = wrapper->buffer;
log_full (wrapper->plugin->ctx, buffer->server, buffer,
BUFFER_LINE_STATUS, "#s", message);
return 0;
}
static luaL_Reg lua_buffer_table[] =
{
// TODO: some useful methods or values
{ "__gc", lua_buffer_gc },
{ "get_server", lua_buffer_get_server },
{ "log", lua_buffer_log },
{ NULL, NULL }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_server_gc (lua_State *L)
{
struct lua_server *wrapper = luaL_checkudata (L, 1, XLUA_SERVER_METATABLE);
if (wrapper->server)
{
lua_cache_invalidate (L, wrapper->server);
server_weak_unref (wrapper->server, &wrapper->weak_ref);
wrapper->server = NULL;
}
return 0;
}
static int
lua_server_get_buffer (lua_State *L)
{
struct lua_server *wrapper = luaL_checkudata (L, 1, XLUA_SERVER_METATABLE);
luaL_argcheck (L, wrapper->server, 1, "dead reference used");
if (wrapper->server->buffer)
lua_plugin_push_buffer (wrapper->plugin, wrapper->server->buffer);
else
lua_pushnil (L);
return 1;
}
static int
lua_server_send (lua_State *L)
{
struct lua_server *wrapper = luaL_checkudata (L, 1, XLUA_SERVER_METATABLE);
luaL_argcheck (L, wrapper->server, 1, "dead reference used");
const char *line = luaL_checkstring (L, 2);
irc_send (wrapper->server, "%s", line);
return 0;
}
static luaL_Reg lua_server_table[] =
{
// TODO: some useful methods or values
{ "__gc", lua_server_gc },
{ "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_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 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_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 }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// 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))
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 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 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_plugin_push_buffer (plugin, buffer); // 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_plugin_push_server (plugin, s); // 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_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_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, -2), lua_typename (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;
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 this for SNI once TLS is implemented
(void) hostname;
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
struct error *e = NULL;
if (!lua_plugin_call (self->plugin, 1, 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[] =
{
{ "hook_input", lua_plugin_hook_input },
{ "hook_irc", lua_plugin_hook_irc },
{ "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_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);
int ret;
if (!(ret = luaL_loadfile (L, filename))
&& !(ret = lua_pcall (L, 0, 0, 0)))
return &plugin->super;
error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
lua_close (L);
free (plugin);
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);
// TODO: partial matches
return buffer;
}
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)
log_global_indent (ctx, " [#d] #s", i++, iter->name);
}
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 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;
unsigned long total = 0;
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
total++;
if (request == 0 || request > total)
{
log_global_error (ctx, "#s: #s",
"Can't move buffer", "requested position is out of range");
return true;
}
struct buffer *buffer = a->buffer;
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
struct buffer *following = ctx->buffers;
while (--request && following)
following = following->next;
LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
refresh_prompt (ctx);
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);
// XXX: clear screen?
buffer_print_backlog (ctx, a->buffer);
}
else if (!strcasecmp_ascii (action, "move"))
result = handle_buffer_move (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)
{
bool result = false;
struct str_vector items;
str_vector_init (&items);
cstr_split (item->value.string.str, ",", &items);
if (items.len == 1 && !*items.vector[0])
str_vector_reset (&items);
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)
{
bool result = false;
struct str_vector items;
str_vector_init (&items);
cstr_split (item->value.string.str, ",", &items);
if (items.len == 1 && !*items.vector[0])
str_vector_reset (&items);
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) && item->type != CONFIG_ITEM_STRING_ARRAY)
// FIXME: it can also be null, which makes this message confusing
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);
else
config_item_set_from (item, config_item_clone (new_), &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);
if (!a->s->cap_echo_message)
log_ctcp_query (a->s, 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",
"[ |