xK/degesch.c

7199 lines
186 KiB
C

/*
* degesch.c: the experimental IRC client
*
* Copyright (c) 2015, Přemysl Janouch <p.janouch@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
// A table of all attributes we use for output
// FIXME: awful naming, collides with ATTRIBUTE_*
#define ATTR_TABLE(XX) \
XX( PROMPT, "prompt", "Terminal attributes for the prompt" ) \
XX( RESET, "reset", "String to reset terminal attributes" ) \
XX( WARNING, "warning", "Terminal attributes for warnings" ) \
XX( ERROR, "error", "Terminal attributes for errors" ) \
XX( EXTERNAL, "external", "Terminal attributes for external lines" ) \
XX( TIMESTAMP, "timestamp", "Terminal attributes for timestamps" ) \
XX( HIGHLIGHT, "highlight", "Terminal attributes for highlights" ) \
XX( ACTION, "action", "Terminal attributes for user actions" ) \
XX( USERHOST, "userhost", "Terminal attributes for user@host" ) \
XX( JOIN, "join", "Terminal attributes for joins" ) \
XX( PART, "part", "Terminal attributes for parts" )
enum
{
#define XX(x, y, z) ATTR_ ## x,
ATTR_TABLE (XX)
#undef XX
ATTR_COUNT
};
// User data for logger functions to enable formatted logging
#define print_fatal_data ((void *) ATTR_ERROR)
#define print_error_data ((void *) ATTR_ERROR)
#define print_warning_data ((void *) ATTR_WARNING)
#include "config.h"
#define PROGRAM_NAME "degesch"
#include "common.c"
#include "kike-replies.c"
#include <langinfo.h>
#include <locale.h>
#include <pwd.h>
#include <sys/utsname.h>
#include <wchar.h>
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
#include <curses.h>
#include <term.h>
// Literally cancer
#undef lines
#undef columns
#ifdef HAVE_READLINE
#include <readline/readline.h>
#include <readline/history.h>
#endif // HAVE_READLINE
#ifdef HAVE_EDITLINE
#include <histedit.h>
#endif // HAVE_EDITLINE
/// Some arbitrary limit for the history file
#define HISTORY_LIMIT 10000
/// 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
char *(*saved_prompt) (EditLine *); ///< Saved prompt function
char saved_char; ///< Saved char for the prompt
#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)
rl_redisplay ();
rl_set_prompt (self->prompt);
if (self->prompt_shown)
rl_redisplay ();
}
static void
input_erase (struct input *self)
{
(void) self;
rl_set_prompt ("");
rl_replace_line ("", 0);
rl_point = rl_mark = 0;
rl_redisplay ();
}
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-<key> 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 void
input_insert_c (struct input *self, int c)
{
char s[2] = { c, 0 };
rl_insert_text (s);
if (self->prompt_shown)
rl_redisplay ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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;
}
static void
input_restore_buffer (struct input *self, struct input_buffer *buffer)
{
// Restore the target buffer's history
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;
history_set_history_state (state);
free (state);
}
// Try to restore the target buffer's readline state
if (buffer->saved_line)
{
rl_replace_line (buffer->saved_line, 0);
rl_point = buffer->saved_point;
rl_mark = buffer->saved_mark;
free (buffer->saved_line);
buffer->saved_line = NULL;
if (self->prompt_shown)
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);
free (buffer->history);
rl_clear_history ();
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, 0);
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)
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 (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);
// XXX: this doesn't seem to save the escape character
el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char);
el_set (self->editline, EL_PROMPT, input_make_empty_prompt);
input_redisplay (self);
}
static void
input_insert_c (struct input *self, int c)
{
char s[2] = { c, 0 };
el_insertstr (self->editline, s);
if (self->prompt_shown)
input_redisplay (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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);
// Would have used "saved_char" but it doesn't seem to work.
// And it doesn't even when it does anyway (it seems to just strip it).
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
/// Callback just before a reference counted object is destroyed
typedef void (*destroy_cb_fn) (void *object, void *user_data);
#define REF_COUNTABLE_HEADER \
size_t ref_count; /**< Reference count */ \
destroy_cb_fn on_destroy; /**< To remove any weak references */ \
void *user_data; /**< User data for callbacks */
#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; \
if (self->on_destroy) \
self->on_destroy (self, self->user_data); \
name ## _destroy (self); \
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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
// TODO: eventually a reference to the server
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
char *modes; ///< Op/voice/... characters
};
static struct channel_user *
channel_user_new (void)
{
struct channel_user *self = xcalloc (1, sizeof *self);
return self;
}
static void
channel_user_destroy (struct channel_user *self)
{
user_unref (self->user);
free (self->modes);
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
// TODO: eventually a reference to the server
char *name; ///< Channel name
char *mode; ///< Channel mode
char *topic; ///< Channel topic
struct channel_user *users; ///< Channel users
struct str_vector names_buf; ///< Buffer for RPL_NAMREPLY
};
static struct channel *
channel_new (void)
{
struct channel *self = xcalloc (1, sizeof *self);
self->ref_count = 1;
str_vector_init (&self->names_buf);
return self;
}
static void
channel_destroy (struct channel *self)
{
free (self->name);
free (self->mode);
free (self->topic);
// 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 buffer_line_flags
{
BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this
};
enum buffer_line_type
{
BUFFER_LINE_PRIVMSG, ///< PRIVMSG
BUFFER_LINE_ACTION, ///< PRIVMSG ACTION
BUFFER_LINE_NOTICE, ///< NOTICE
BUFFER_LINE_JOIN, ///< JOIN
BUFFER_LINE_PART, ///< PART
BUFFER_LINE_KICK, ///< KICK
BUFFER_LINE_NICK, ///< NICK
BUFFER_LINE_TOPIC, ///< TOPIC
BUFFER_LINE_QUIT, ///< QUIT
BUFFER_LINE_STATUS, ///< Whatever status messages
BUFFER_LINE_ERROR ///< Whatever error messages
};
struct buffer_line_args
{
char *who; ///< Name of the origin or NULL (user)
char *object; ///< Object of action
char *text; ///< Text of message
char *reason; ///< Reason for PART, KICK, QUIT
};
struct buffer_line
{
LIST_HEADER (struct buffer_line)
// We use the "type" and "flags" mostly just as formatting hints
enum buffer_line_type type; ///< Type of the event
int flags; ///< Flags
time_t when; ///< Time of the event
struct buffer_line_args args; ///< Arguments
};
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)
{
free (self->args.who);
free (self->args.object);
free (self->args.text);
free (self->args.reason);
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)
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
// 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->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->user)
user_unref (self->user);
if (self->channel)
channel_unref (self->channel);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum server_state
{
IRC_DISCONNECTED, ///< Not connected
IRC_CONNECTING, ///< Connecting to the server
IRC_CONNECTED, ///< Trying to register
IRC_REGISTERED ///< We can chat now
};
struct server
{
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
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 poller_fd read_event; ///< We can read from the socket
SSL_CTX *ssl_ctx; ///< SSL context
SSL *ssl; ///< SSL connection
// TODO: an output queue to prevent excess floods (this will be needed
// especially for away status polling)
// IRC:
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
char *irc_user_mode; ///< Our current user mode
char *irc_user_host; ///< Our current user@host
// 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
};
static void on_irc_timeout (void *user_data);
static void on_irc_ping_timeout (void *user_data);
static void irc_initiate_connect (struct server *s);
static void
server_init (struct server *self, struct poller *poller)
{
memset (self, 0, sizeof *self);
self->socket = -1;
str_init (&self->read_buffer);
self->state = IRC_DISCONNECTED;
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;
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;
}
static void
server_free (struct server *self)
{
if (self->connector)
{
connector_free (self->connector);
free (self->connector);
}
if (self->socket != -1)
{
xclose (self->socket);
poller_fd_reset (&self->read_event);
}
str_free (&self->read_buffer);
if (self->ssl)
SSL_free (self->ssl);
if (self->ssl_ctx)
SSL_CTX_free (self->ssl_ctx);
free (self->name);
if (self->irc_user)
user_unref (self->irc_user);
free (self->irc_user_mode);
free (self->irc_user_host);
str_map_free (&self->irc_users);
str_map_free (&self->irc_channels);
str_map_free (&self->irc_buffer_map);
}
static void
server_destroy (void *self)
{
server_free (self);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct app_context
{
// Configuration:
struct config config; ///< Program configuration
char *attrs[ATTR_COUNT]; ///< Terminal attributes
bool no_colors; ///< Colour output mode
bool isolate_buffers; ///< Isolate global/server buffers
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 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
// TODO: So that we always output proper date change messages
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
bool awaiting_mirc_escape; ///< Awaiting a mIRC attribute escape
char char_buf[MB_LEN_MAX + 1]; ///< Buffered multibyte char
size_t char_buf_len; ///< How much of an MB char is buffered
}
*g_ctx;
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 = server_destroy;
self->servers.key_xfrm = irc_strxfrm;
str_map_init (&self->buffers_by_name);
self->buffers_by_name.key_xfrm = irc_strxfrm;
self->last_displayed_msg_time = time (NULL);
char *encoding = nl_langinfo (CODESET);
#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);
}
static void
app_context_free (struct app_context *self)
{
config_free (&self->config);
for (size_t i = 0; i < ATTR_COUNT; i++)
free (self->attrs[i]);
// FIXME: this doesn't free the history state
LIST_FOR_EACH (struct buffer, iter, self->buffers)
buffer_destroy (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);
}
static void refresh_prompt (struct app_context *ctx);
static char *irc_cut_nickname (const char *prefix);
static const char *irc_find_userhost (const char *prefix);
// --- Configuration -----------------------------------------------------------
// TODO: eventually add "on_change" callbacks
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 = "nickname",
.comment = "IRC nickname",
.type = CONFIG_ITEM_STRING,
.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 = "ssl",
.comment = "Whether to use SSL/TLS",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off" },
{ .name = "ssl_cert",
.comment = "Client SSL certificate (PEM)",
.type = CONFIG_ITEM_STRING },
{ .name = "ssl_verify",
.comment = "Whether to verify certificates",
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "on" },
{ .name = "ssl_ca_file",
.comment = "OpenSSL CA bundle file",
.type = CONFIG_ITEM_STRING },
{ .name = "ssl_ca_path",
.comment = "OpenSSL CA bundle path",
.type = CONFIG_ITEM_STRING },
{ .name = "autojoin",
.comment = "Channels to join on start",
.type = CONFIG_ITEM_STRING_ARRAY,
.validate = config_validate_nonjunk_string },
{ .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" },
{}
};
static struct config_schema g_config_attributes[] =
{
#define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING },
ATTR_TABLE (XX)
#undef XX
{}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_config_server (struct config_item_ *subtree, void *user_data)
{
(void) user_data;
// This will eventually iterate over the object and create servers
config_schema_apply_to_object (g_config_server, subtree);
}
static void
load_config_behaviour (struct config_item_ *subtree, void *user_data)
{
(void) user_data;
config_schema_apply_to_object (g_config_behaviour, subtree);
}
static void
load_config_attributes (struct config_item_ *subtree, void *user_data)
{
(void) user_data;
config_schema_apply_to_object (g_config_attributes, subtree);
}
static void
register_config_modules (struct app_context *ctx)
{
struct config *config = &ctx->config;
config_register_module (config,
"server", load_config_server, ctx);
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 str s;
str_init (&s);
str_append (&s, value);
struct config_item_ *new_ = config_item_string (&s);
str_free (&s);
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 char *
write_configuration_file (const struct str *data, struct error **e)
{
struct str path;
str_init (&path);
get_xdg_home_dir (&path, "XDG_CONFIG_HOME", ".config");
str_append (&path, "/" PROGRAM_NAME);
if (!mkdir_with_parents (path.str, e))
goto error;
str_append (&path, "/" PROGRAM_NAME ".conf");
FILE *fp = fopen (path.str, "w");
if (!fp)
{
error_set (e, "could not open `%s' for writing: %s",
path.str, strerror (errno));
goto error;
}
errno = 0;
fwrite (data->str, data->len, 1, fp);
fclose (fp);
if (errno)
{
error_set (e, "writing to `%s' failed: %s", path.str, strerror (errno));
goto error;
}
return str_steal (&path);
error:
str_free (&path);
return NULL;
}
static void
serialize_configuration (struct app_context *ctx, 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 (ctx->config.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;
}
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 void
init_attribute (struct app_context *ctx, int id, const char *default_)
{
static const char *table[ATTR_COUNT] =
{
#define XX(x, y, z) [ATTR_ ## x] = "attributes." y,
ATTR_TABLE (XX)
#undef XX
};
const char *user = get_config_string (ctx->config.root, table[id]);
if (user)
ctx->attrs[id] = xstrdup (user);
else
ctx->attrs[id] = xstrdup (default_);
}
static void
init_colors (struct app_context *ctx)
{
bool have_ti = init_terminal ();
#define INIT_ATTR(id, ti) init_attribute (ctx, ATTR_ ## id, have_ti ? (ti) : "")
INIT_ATTR (PROMPT, enter_bold_mode);
INIT_ATTR (RESET, exit_attribute_mode);
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;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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
terminal_printer_fn printer; ///< Terminal printer
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_reset (struct attribute_printer *self)
{
if (self->dirty)
tputs (self->ctx->attrs[ATTR_RESET], 1, self->printer);
self->dirty = false;
}
static void
attribute_printer_init (struct attribute_printer *self,
struct app_context *ctx, terminal_printer_fn printer)
{
self->ctx = ctx;
self->printer = printer;
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)
{
tputs (self->ctx->attrs[attribute], 1, self->printer);
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);
// TODO: (INVERSE | BOLD) should be used for bright backgrounds
// when possible, i.e. when the foreground shouldn't be bright as well
// and when the BOLD attribute hasn't already been set
int attributes = self->want;
if (attributes & ATTRIBUTE_INVERSE)
{
bool tmp = fg_is_bright;
fg_is_bright = bg_is_bright;
bg_is_bright = tmp;
}
if (fg_is_bright) attributes |= ATTRIBUTE_BOLD;
if (bg_is_bright) attributes |= ATTRIBUTE_BLINK;
attribute_printer_reset (self);
if (attributes)
tputs (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
, 1, self->printer);
if (enter_italics_mode && (attributes & ATTRIBUTE_ITALIC))
tputs (enter_italics_mode, 1, self->printer);
if (fg >= 0)
tputs (g_terminal.color_set_fg[fg], 1, self->printer);
if (bg >= 0)
tputs (g_terminal.color_set_bg[bg], 1, self->printer);
self->dirty = true;
}
// --- 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
// #d inserts a signed integer; also supports the #<N> and #0<N> notation
// #m inserts a mIRC-formatted string (auto-resets at boundaries)
//
// #a inserts named attributes (auto-resets)
// #r resets terminal attributes
// #c sets foreground color
// #C sets background color
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 ///< For mIRC formatting only so far
};
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
bool ignore_new_attributes; ///< Whether to ignore new attributes
struct formatter_item *items; ///< Items
struct formatter_item *items_tail; ///< Tail of items
};
static void
formatter_init (struct formatter *self, struct app_context *ctx)
{
memset (self, 0, sizeof *self);
self->ctx = ctx;
}
static void
formatter_free (struct formatter *self)
{
LIST_FOR_EACH (struct formatter_item, iter, self->items)
formatter_item_destroy (iter);
}
static void
formatter_add_item (struct formatter *self, struct formatter_item template_)
{
if (template_.type != FORMATTER_ITEM_TEXT && self->ignore_new_attributes)
return;
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 const char *
formatter_parse_field (struct formatter *self,
const char *field, struct str *buf, va_list *ap)
{
size_t width = 0;
bool zero_padded = false;
int c;
restart:
switch ((c = *field++))
{
char *s;
// We can push boring text content to the caller's buffer
// and let it flush the buffer only when it's actually needed
case 's':
s = va_arg (*ap, char *);
for (size_t len = strlen (s); len < width; len++)
str_append_c (buf, ' ');
str_append (buf, s);
break;
case 'd':
s = xstrdup_printf ("%d", va_arg (*ap, int));
for (size_t len = strlen (s); len < width; len++)
str_append_c (buf, " 0"[zero_padded]);
str_append (buf, s);
free (s);
break;
case 'm':
formatter_parse_mirc (self, 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 == '0' && !zero_padded)
zero_padded = true;
else if (isdigit_ascii (c))
width = width * 10 + (c - '0');
else if (c)
hard_assert (!"unexpected format specifier");
else
hard_assert (!"unexpected end of format string");
goto restart;
}
return field;
}
static void
formatter_add (struct formatter *self, const char *format, ...)
{
struct str buf;
str_init (&buf);
va_list ap;
va_start (ap, format);
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);
va_end (ap);
}
static void
formatter_flush (struct formatter *self, FILE *stream)
{
terminal_printer_fn printer = get_attribute_printer (stream);
if (!printer)
{
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, printer);
attribute_printer_reset (&state);
LIST_FOR_EACH (struct formatter_item, iter, self->items)
{
switch (iter->type)
{
char *term;
case FORMATTER_ITEM_TEXT:
term = iconv_xstrdup
(self->ctx->term_from_utf8, iter->text, -1, NULL);
fputs (term, stream);
free (term);
break;
case FORMATTER_ITEM_ATTR:
attribute_printer_apply (&state, iter->attribute);
state.want = 0;
state.want_foreground = -1;
state.want_background = -1;
break;
case FORMATTER_ITEM_SIMPLE:
state.want |= iter->attribute;
attribute_printer_update (&state);
break;
case FORMATTER_ITEM_FG_COLOR:
state.want_foreground = iter->color;
attribute_printer_update (&state);
break;
case FORMATTER_ITEM_BG_COLOR:
state.want_background = iter->color;
attribute_printer_update (&state);
break;
}
}
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, &current))
{
// Strange but nonfatal
print_error ("%s: %s", "localtime_r", strerror (errno));
return;
}
ctx->last_displayed_msg_time = now;
if (last.tm_year == current.tm_year
&& last.tm_mon == current.tm_mon
&& last.tm_mday == current.tm_mday)
return;
char buf[32] = "";
if (soft_assert (strftime (buf, sizeof buf, "%F", &current)))
print_status ("%s", buf);
// Else the buffer was too small, which is pretty weird
}
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 buffer_line_args *a = &line->args;
char *nick = NULL;
const char *userhost = NULL;
// TODO: always assign the default colour to ourselves
// FIXME: never use the black colour, use the default instead
int nick_color = -1;
int object_color = -1;
if (a->who)
{
nick = irc_cut_nickname (a->who);
userhost = irc_find_userhost (a->who);
nick_color = str_map_hash (nick, strlen (nick)) % 8;
}
if (a->object)
object_color = str_map_hash (a->object, strlen (a->object)) % 8;
struct formatter f;
formatter_init (&f, ctx);
struct tm current;
if (!localtime_r (&line->when, &current))
print_error ("%s: %s", "localtime_r", strerror (errno));
else
formatter_add (&f, "#a#02d:#02d:#02d#r ",
ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec);
// 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);
f.ignore_new_attributes = true;
}
switch (line->type)
{
case BUFFER_LINE_PRIVMSG:
if (line->flags & BUFFER_LINE_HIGHLIGHT)
formatter_add (&f, "#a<#s>#r #m", ATTR_HIGHLIGHT, nick, a->text);
else
formatter_add (&f, "<#c#s#r> #m", nick_color, nick, a->text);
break;
case BUFFER_LINE_ACTION:
if (line->flags & BUFFER_LINE_HIGHLIGHT)
formatter_add (&f, " #a*#r ", ATTR_HIGHLIGHT);
else
formatter_add (&f, " #a*#r ", ATTR_ACTION);
formatter_add (&f, "#c#s#r #m", nick_color, nick, a->text);
break;
case BUFFER_LINE_NOTICE:
formatter_add (&f, " - ");
if (line->flags & BUFFER_LINE_HIGHLIGHT)
formatter_add (&f, "#a#s(#s)#r: #m",
ATTR_HIGHLIGHT, "Notice", nick, a->text);
else
formatter_add (&f, "#s(#c#s#r): #m",
"Notice", nick_color, nick, a->text);
break;
case BUFFER_LINE_JOIN:
formatter_add (&f, "#a-->#r ", ATTR_JOIN);
formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #s",
nick_color, nick, ATTR_USERHOST, userhost,
ATTR_JOIN, "has joined", a->object);
break;
case BUFFER_LINE_PART:
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #s",
nick_color, nick, ATTR_USERHOST, userhost,
ATTR_PART, "has left", a->object);
if (a->reason)
formatter_add (&f, " (#m)", a->reason);
break;
case BUFFER_LINE_KICK:
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #c#s#r",
nick_color, nick, ATTR_USERHOST, userhost,
ATTR_PART, "has kicked", object_color, a->object);
if (a->reason)
formatter_add (&f, " (#m)", a->reason);
break;
case BUFFER_LINE_NICK:
formatter_add (&f, " - ");
if (a->who)
formatter_add (&f, "#c#s#r #s #c#s#r",
nick_color, nick,
"is now known as", object_color, a->object);
else
formatter_add (&f, "#s #s",
"You are now known as", a->object);
break;
case BUFFER_LINE_TOPIC:
formatter_add (&f, " - ");
formatter_add (&f, "#c#s#r #s \"#m\"",
nick_color, nick,
"has changed the topic to", a->text);
break;
case BUFFER_LINE_QUIT:
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r",
nick_color, nick, ATTR_USERHOST, userhost,
ATTR_PART, "has quit");
if (a->reason)
formatter_add (&f, " (#m)", a->reason);
break;
case BUFFER_LINE_STATUS:
formatter_add (&f, " - ");
formatter_add (&f, "#s", a->text);
break;
case BUFFER_LINE_ERROR:
formatter_add (&f, "#a=!=#r ", ATTR_ERROR);
formatter_add (&f, "#s", a->text);
}
free (nick);
input_hide (&ctx->input);
// TODO: write the line to a log file; note that the global and server
// buffers musn't collide with filenames
formatter_add (&f, "\n");
formatter_flush (&f, stdout);
formatter_free (&f);
input_show (&ctx->input);
}
static void
buffer_send_internal (struct app_context *ctx, struct buffer *buffer,
enum buffer_line_type type, int flags,
struct buffer_line_args a)
{
struct buffer_line *line = buffer_line_new ();
line->type = type;
line->flags = flags;
line->when = time (NULL);
line->args = a;
LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
buffer->lines_count++;
if (buffer == ctx->current_buffer)
{
buffer_line_display (ctx, line, false);
return;
}
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;
if (!ctx->isolate_buffers && can_leak)
buffer_line_display (ctx, line, true);
else
{
buffer->unseen_messages_count++;
refresh_prompt (ctx);
}
}
#define buffer_send(ctx, buffer, type, flags, ...) \
buffer_send_internal ((ctx), (buffer), (type), (flags), \
(struct buffer_line_args) { __VA_ARGS__ })
#define buffer_send_status(ctx, buffer, ...) \
buffer_send (ctx, buffer, BUFFER_LINE_STATUS, 0, \
.text = xstrdup_printf (__VA_ARGS__))
#define buffer_send_error(ctx, buffer, ...) \
buffer_send (ctx, buffer, BUFFER_LINE_ERROR, 0, \
.text = xstrdup_printf (__VA_ARGS__))
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);
// 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);
// TODO: part from the channel if needed
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);
// It's not a good idea to remove these buffers, but it's even a worse
// one to leave the pointers point to invalid memory
if (buffer == ctx->global_buffer)
ctx->global_buffer = NULL;
if (buffer->type == BUFFER_SERVER)
buffer->server->buffer = NULL;
if (buffer == ctx->last_buffer)
ctx->last_buffer = NULL;
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
buffer_destroy (buffer);
refresh_prompt (ctx);
}
static void
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
{
print_status ("%s", buffer->name);
// That is, minus the buffer switch line and the readline prompt
int to_display = MAX (10, g_terminal.lines - 2);
struct buffer_line *line = buffer->lines_tail;
while (line && line->prev && --to_display > 0)
line = line->prev;
// Once we've found where we want to start with the backlog, print it
for (; line; line = line->next)
buffer_line_display (ctx, line, false);
buffer->unseen_messages_count = 0;
refresh_prompt (ctx);
}
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)
{
// TODO: try to merge the buffers as best as we can
}
static void
buffer_rename (struct app_context *ctx,
struct buffer *buffer, const char *new_name)
{
hard_assert (buffer->type == BUFFER_PM);
struct buffer *collision =
str_map_find (&buffer->server->irc_buffer_map, new_name);
if (collision)
{
// TODO: use full weechat-style buffer names
// to prevent name collisions with the global buffer
hard_assert (collision->type == BUFFER_PM);
// When there's a collision, there's not much else we can do
// other than somehow trying to merge them
buffer_merge (ctx, collision, buffer);
// TODO: log a status message about the merge
if (ctx->current_buffer == buffer)
buffer_activate (ctx, collision);
buffer_remove (ctx, buffer);
}
else
{
// Otherwise we just rename the buffer and that's it
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
str_map_set (&ctx->buffers_by_name, new_name, buffer);
free (buffer->name);
buffer->name = xstrdup (new_name);
// We might have renamed the current buffer
refresh_prompt (ctx);
}
}
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
init_buffers (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);
}
// --- Users, channels ---------------------------------------------------------
static void
irc_user_on_destroy (void *object, void *user_data)
{
struct user *user = object;
struct server *s = user_data;
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 ();
user->on_destroy = irc_user_on_destroy;
user->user_data = 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_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);
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 ();
channel->on_destroy = irc_channel_on_destroy;
channel->user_data = s;
channel->name = name;
channel->mode = xstrdup ("");
channel->topic = NULL;
str_map_set (&s->irc_channels, channel->name, channel);
return channel;
}
static void
irc_remove_user_from_channel (struct user *user, struct channel *channel)
{
LIST_FOR_EACH (struct channel_user, iter, channel->users)
if (iter->user == user)
irc_channel_unlink_user (channel, iter);
}
static void
irc_left_channel (struct channel *channel)
{
// TODO: shouldn't we decrease reference count on the channel?
LIST_FOR_EACH (struct channel_user, iter, channel->users)
irc_channel_unlink_user (channel, iter);
}
// --- Core functionality ------------------------------------------------------
// Most of the core IRC code comes from ZyklonB which is mostly blocking.
// While it's fairly easy to follow, it also stinks. It needs to be rewritten
// to be as asynchronous as possible. See kike.c for reference.
static bool
irc_is_connected (struct server *s)
{
return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
}
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);
}
static void
irc_reset_connection_timeouts (struct server *s)
{
irc_cancel_timers (s);
poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
}
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;
int64_t delay = get_config_integer (s->config, "reconnect_delay");
// TODO: exponentional backoff
hard_assert (s->state == IRC_DISCONNECTED);
buffer_send_status (s->ctx, s->buffer,
"Trying to reconnect in %ld seconds...", delay);
poller_timer_set (&s->reconnect_tmr, delay * 1000);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
irc_initialize_ssl_ctx (struct server *s, struct error **e)
{
// XXX: maybe we should call SSL_CTX_set_options() for some workarounds
bool verify = get_config_boolean (s->config, "ssl_verify");
if (!verify)
SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL);
const char *ca_file = get_config_string (s->config, "ssl_ca_file");
const char *ca_path = get_config_string (s->config, "ssl_ca_path");
struct error *error = NULL;
if (ca_file || ca_path)
{
if (SSL_CTX_load_verify_locations (s->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 (s->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
buffer_send_error (s->ctx, s->buffer, "%s", error->message);
error_free (error);
return true;
}
static bool
irc_initialize_ssl (struct server *s, struct error **e)
{
const char *error_info = NULL;
s->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
if (!s->ssl_ctx)
goto error_ssl_1;
if (!irc_initialize_ssl_ctx (s, e))
goto error_ssl_2;
s->ssl = SSL_new (s->ssl_ctx);
if (!s->ssl)
goto error_ssl_2;
const char *ssl_cert = get_config_string (s->config, "ssl_cert");
if (ssl_cert)
{
char *path = resolve_config_filename (ssl_cert);
if (!path)
buffer_send_error (s->ctx, s->ctx->global_buffer,
"%s: %s", "Cannot open file", ssl_cert);
// XXX: perhaps we should read the file ourselves for better messages
else if (!SSL_use_certificate_file (s->ssl, path, SSL_FILETYPE_PEM)
|| !SSL_use_PrivateKey_file (s->ssl, path, SSL_FILETYPE_PEM))
buffer_send_error (s->ctx, s->ctx->global_buffer,
"%s: %s", "Setting the SSL client certificate failed",
ERR_error_string (ERR_get_error (), NULL));
free (path);
}
SSL_set_connect_state (s->ssl);
if (!SSL_set_fd (s->ssl, s->socket))
goto error_ssl_3;
// Avoid SSL_write() returning SSL_ERROR_WANT_READ
SSL_set_mode (s->ssl, SSL_MODE_AUTO_RETRY);
switch (xssl_get_error (s->ssl, SSL_connect (s->ssl), &error_info))
{
case SSL_ERROR_NONE:
return true;
case SSL_ERROR_ZERO_RETURN:
error_info = "server closed the connection";
default:
break;
}
error_ssl_3:
SSL_free (s->ssl);
s->ssl = NULL;
error_ssl_2:
SSL_CTX_free (s->ssl_ctx);
s->ssl_ctx = NULL;
error_ssl_1:
// XXX: these error strings are really nasty; also there could be
// multiple errors on the OpenSSL stack.
if (!error_info)
error_info = ERR_error_string (ERR_get_error (), NULL);
error_set (e, "%s: %s", "could not initialize SSL", error_info);
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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)
{
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;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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)))
{
print_debug ("tried sending a message to a dead server connection");
return;
}
va_list ap;
va_start (ap, format);
struct str str;
str_init (&str);
str_append_vprintf (&str, format, ap);
va_end (ap);
if (g_debug_mode)
{
input_hide (&s->ctx->input);
char *term = irc_to_term (s->ctx, str.str);
fprintf (stderr, "[IRC] <== \"%s\"\n", term);
free (term);
input_show (&s->ctx->input);
}
str_append (&str, "\r\n");
if (s->ssl)
{
// TODO: call SSL_get_error() to detect if a clean shutdown has occured
if (SSL_write (s->ssl, str.str, str.len) != (int) str.len)
LOG_FUNC_FAILURE ("SSL_write",
ERR_error_string (ERR_get_error (), NULL));
}
else if (write (s->socket, str.str, str.len) != (ssize_t) str.len)
LOG_LIBC_FAILURE ("write");
str_free (&str);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
irc_shutdown (struct server *s)
{
// TODO: set a timer after which we cut the connection?
// Generally non-critical
if (s->ssl)
soft_assert (SSL_shutdown (s->ssl) != -1);
else
soft_assert (shutdown (s->socket, SHUT_WR) == 0);
}
static void
irc_destroy_connector (struct server *s)
{
connector_free (s->connector);
free (s->connector);
s->connector = 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)
{
// Destroy the user interface
input_stop (&ctx->input);
buffer_send_status (ctx, ctx->global_buffer, "Shutting down");
// 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)))
{
if (irc_is_connected (s))
// XXX: when we go async, we'll have to flush output buffers first
irc_shutdown (s);
else if (s->state == IRC_CONNECTING)
irc_destroy_connector (s);
}
ctx->quitting = true;
try_finish_quit (ctx);
}
static void
on_irc_disconnected (struct server *s)
{
hard_assert (irc_is_connected (s));
// Get rid of the dead socket and related things
if (s->ssl)
{
SSL_free (s->ssl);
s->ssl = NULL;
SSL_CTX_free (s->ssl_ctx);
s->ssl_ctx = NULL;
}
xclose (s->socket);
s->socket = -1;
s->state = IRC_DISCONNECTED;
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;
}
free (s->irc_user_mode);
s->irc_user_mode = NULL;
free (s->irc_user_host);
s->irc_user_host = NULL;
s->read_event.closed = true;
poller_fd_reset (&s->read_event);
// All of our timers have lost their meaning now
irc_cancel_timers (s);
if (s->ctx->quitting)
try_finish_quit (s->ctx);
else if (s->manual_disconnect)
s->manual_disconnect = false;
else
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;
buffer_send_error (s->ctx, s->buffer, "Connection timeout");
on_irc_disconnected (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));
}
// --- Processing server output ------------------------------------------------
static void irc_process_message
(const struct irc_message *msg, const char *raw, void *user_data);
enum irc_read_result
{
IRC_READ_OK, ///< Some data were read successfully
IRC_READ_EOF, ///< The server has closed connection
IRC_READ_AGAIN, ///< No more data at the moment
IRC_READ_ERROR ///< General connection failure
};
static enum irc_read_result
irc_fill_read_buffer_ssl (struct server *s, struct str *buf)
{
int n_read;
start:
n_read = SSL_read (s->ssl, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */);
const char *error_info = NULL;
switch (xssl_get_error (s->ssl, n_read, &error_info))
{
case SSL_ERROR_NONE:
buf->str[buf->len += n_read] = '\0';
return IRC_READ_OK;
case SSL_ERROR_ZERO_RETURN:
return IRC_READ_EOF;
case SSL_ERROR_WANT_READ:
return IRC_READ_AGAIN;
case SSL_ERROR_WANT_WRITE:
{
// Let it finish the handshake as we don't poll for writability;
// any errors are to be collected by SSL_read() in the next iteration
struct pollfd pfd = { .fd = s->socket, .events = POLLOUT };
soft_assert (poll (&pfd, 1, 0) > 0);
goto start;
}
case XSSL_ERROR_TRY_AGAIN:
goto start;
default:
LOG_FUNC_FAILURE ("SSL_read", error_info);
return IRC_READ_ERROR;
}
}
static enum irc_read_result
irc_fill_read_buffer (struct server *s, struct str *buf)
{
ssize_t n_read;
start:
n_read = recv (s->socket, buf->str + buf->len,
buf->alloc - buf->len - 1 /* null byte */, 0);
if (n_read > 0)
{
buf->str[buf->len += n_read] = '\0';
return IRC_READ_OK;
}
if (n_read == 0)
return IRC_READ_EOF;
if (errno == EAGAIN)
return IRC_READ_AGAIN;
if (errno == EINTR)
goto start;
LOG_LIBC_FAILURE ("recv");
return IRC_READ_ERROR;
}
static void
on_irc_readable (const struct pollfd *fd, struct server *s)
{
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
(void) set_blocking (s->socket, false);
struct str *buf = &s->read_buffer;
enum irc_read_result (*fill_buffer)(struct server *, struct str *)
= s->ssl
? irc_fill_read_buffer_ssl
: irc_fill_read_buffer;
bool disconnected = false;
while (true)
{
str_ensure_space (buf, 512);
switch (fill_buffer (s, buf))
{
case IRC_READ_AGAIN:
goto end;
case IRC_READ_ERROR:
buffer_send_error (s->ctx, s->buffer,
"Reading from the IRC server failed");
disconnected = true;
goto end;
case IRC_READ_EOF:
buffer_send_error (s->ctx, s->buffer,
"The IRC server closed the connection");
disconnected = true;
goto end;
case IRC_READ_OK:
break;
}
if (buf->len >= (1 << 20))
{
buffer_send_error (s->ctx, s->buffer,
"The IRC server seems to spew out data frantically");
irc_shutdown (s);
goto end;
}
}
end:
(void) set_blocking (s->socket, true);
irc_process_buffer (buf, irc_process_message, s);
if (disconnected)
on_irc_disconnected (s);
else
irc_reset_connection_timeouts (s);
}
// --- Connection establishment ------------------------------------------------
static bool
irc_autofill_user_info (struct server *s, struct error **e)
{
const char *nickname = get_config_string (s->config, "nickname");
const char *username = get_config_string (s->config, "username");
const char *realname = get_config_string (s->config, "realname");
if (nickname && 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 (!nickname)
set_config_string (s->config, "nickname", pwd->pw_name);
if (!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 void
irc_register (struct server *s)
{
// Fill in user information automatically if needed
irc_autofill_user_info (s, NULL);
const char *nickname = get_config_string (s->config, "nickname");
const char *username = get_config_string (s->config, "username");
const char *realname = get_config_string (s->config, "realname");
hard_assert (nickname && username && realname);
const char *password = get_config_string (s->config, "password");
if (password)
irc_send (s, "PASS :%s", password);
irc_send (s, "NICK %s", 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)
{
struct app_context *ctx = s->ctx;
s->socket = socket;
struct error *e = NULL;
bool use_ssl = get_config_boolean (s->config, "ssl");
if (use_ssl && !irc_initialize_ssl (s, &e))
{
buffer_send_error (ctx, s->buffer, "Connection failed: %s", e->message);
error_free (e);
xclose (s->socket);
s->socket = -1;
irc_queue_reconnect (s);
return;
}
buffer_send_status (ctx, s->buffer, "Connection established");
s->state = IRC_CONNECTED;
poller_fd_init (&s->read_event, &ctx->poller, s->socket);
s->read_event.dispatcher = (poller_fd_fn) on_irc_readable;
s->read_event.user_data = s;
poller_fd_set (&s->read_event, POLLIN);
irc_reset_connection_timeouts (s);
irc_register (s);
refresh_prompt (s->ctx);
}
static void
irc_on_connector_connecting (void *user_data, const char *address)
{
struct server *s = user_data;
buffer_send_status (s->ctx, s->buffer, "Connecting to %s...", address);
}
static void
irc_on_connector_error (void *user_data, const char *error)
{
struct server *s = user_data;
buffer_send_error (s->ctx, 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)
{
struct server *s = user_data;
irc_destroy_connector (s);
irc_finish_connection (s, socket);
}
static void
irc_split_host_port (char *s, char **host, char **port)
{
char *colon = strchr (s, ':');
if (colon)
{
*colon = '\0';
*port = ++colon;
}
else
*port = "6667";
*host = s;
}
static bool
irc_setup_connector (struct server *s,
const struct str_vector *addresses, struct error **e)
{
struct connector *connector = xmalloc (sizeof *connector);
connector_init (connector, &s->ctx->poller);
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;
s->state = IRC_CONNECTING;
s->connector = connector;
for (size_t i = 0; i < addresses->len; i++)
{
char *host, *port;
irc_split_host_port (addresses->vector[i], &host, &port);
if (!connector_add_target (connector, host, port, e))
{
irc_destroy_connector (s);
return false;
}
}
connector_step (connector);
return true;
}
static bool
irc_initiate_connect_socks (struct server *s,
const struct str_vector *addresses, struct error **e)
{
struct app_context *ctx = s->ctx;
const char *socks_host = get_config_string (s->config, "socks_host");
int64_t socks_port_int = get_config_integer (s->config, "socks_port");
const char *socks_username =
get_config_string (s->config, "socks_username");
const char *socks_password =
get_config_string (s->config, "socks_password");
if (!socks_host)
return false;
// FIXME: we only try the first address (still better than nothing)
char *irc_host, *irc_port;
irc_split_host_port (addresses->vector[0], &irc_host, &irc_port);
char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int);
char *address = format_host_port_pair (irc_host, irc_port);
char *socks_address = format_host_port_pair (socks_host, socks_port);
buffer_send_status (ctx, s->buffer,
"Connecting to %s via %s...", address, socks_address);
free (socks_address);
free (address);
// TODO: the SOCKS code needs a rewrite so that we don't block on it either;
// perhaps it could act as a special kind of connector
struct error *error = NULL;
bool result = true;
int fd = socks_connect (socks_host, socks_port, irc_host, irc_port,
socks_username, socks_password, &error);
if (fd != -1)
irc_finish_connection (s, fd);
else
{
error_set (e, "%s: %s", "SOCKS connection failed", error->message);
error_free (error);
result = false;
}
free (socks_port);
return true;
}
static void
irc_initiate_connect (struct server *s)
{
hard_assert (s->state == IRC_DISCONNECTED);
struct app_context *ctx = s->ctx;
const char *addresses = get_config_string (s->config, "addresses");
if (!addresses || !addresses[strspn (addresses, ",")])
{
// No sense in trying to reconnect
buffer_send_error (ctx, s->buffer,
"No addresses specified in configuration");
return;
}
struct str_vector servers;
str_vector_init (&servers);
split_str_ignore_empty (addresses, ',', &servers);
struct error *e = NULL;
if (!irc_initiate_connect_socks (s, &servers, &e) && !e)
irc_setup_connector (s, &servers, &e);
str_vector_free (&servers);
if (e)
{
buffer_send_error (s->ctx, s->buffer, "%s", e->message);
error_free (e);
irc_queue_reconnect (s);
}
}
// --- Input prompt ------------------------------------------------------------
static char *
make_unseen_prefix (struct app_context *ctx)
{
struct str active_buffers;
str_init (&active_buffers);
size_t i = 0;
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
{
i++;
if (!iter->unseen_messages_count)
continue;
if (active_buffers.len)
str_append_c (&active_buffers, ',');
str_append_printf (&active_buffers, "%zu", i);
}
if (active_buffers.len)
return str_steal (&active_buffers);
str_free (&active_buffers);
return NULL;
}
static void
make_prompt (struct app_context *ctx, struct str *output)
{
struct buffer *buffer = ctx->current_buffer;
if (!buffer)
return;
str_append_c (output, '[');
char *unseen_prefix = make_unseen_prefix (ctx);
if (unseen_prefix)
str_append_printf (output, "(%s) ", unseen_prefix);
free (unseen_prefix);
str_append_printf (output, "%d:%s",
buffer_get_index (ctx, buffer), buffer->name);
if (buffer->type == BUFFER_CHANNEL && *buffer->channel->mode)
str_append_printf (output, "(%s)", buffer->channel->mode);
if (buffer != ctx->global_buffer)
{
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
{
str_append (output, s->irc_user->nickname);
if (*s->irc_user_mode)
str_append_printf (output, "(%s)", s->irc_user_mode);
}
}
str_append_c (output, ']');
}
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, ' ');
if (have_attributes)
{
// XXX: to be completely correct, we should use tputs, but we cannot
input_set_prompt (&ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c",
INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
INPUT_END_IGNORE,
prompt.str,
INPUT_START_IGNORE, ctx->attrs[ATTR_RESET],
INPUT_END_IGNORE));
}
else
input_set_prompt (&ctx->input, xstrdup (prompt.str));
str_free (&prompt);
}
// --- Helpers -----------------------------------------------------------------
static char *
irc_cut_nickname (const char *prefix)
{
return xstrndup (prefix, strcspn (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_strcmp (nick, s->irc_user->nickname);
free (nick);
return result;
}
static bool
irc_is_channel (struct server *s, const char *ident)
{
(void) s; // TODO: parse prefixes from server features
return *ident && !!strchr ("#&+!", *ident);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct buffer *
irc_get_buffer_for_message (struct server *s,
const struct irc_message *msg, const char *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)
{
// Implying that the target is us
// 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);
buffer = irc_get_or_make_user_buffer (s, nickname);
free (nickname);
}
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);
for (char *p = copy; *p; p++)
*p = irc_tolower (*p);
char *nick = xstrdup (s->irc_user->nickname);
for (char *p = nick; *p; p++)
*p = irc_tolower (*p);
// 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;
}
// --- Input handling ----------------------------------------------------------
// TODO: we will need a proper mode parser; to be shared with kike
// TODO: we alse definitely need to parse server capability messages
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);
}
// This is weird, ignoring
if (!channel)
return;
// Get or make a user object
char *nickname = irc_cut_nickname (msg->prefix);
struct user *user = str_map_find (&s->irc_users, nickname);
if (!user)
user = irc_make_user (s, nickname);
else
{
user = user_ref (user);
free (nickname);
}
// Link the user with the channel
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;
channel_user->modes = xstrdup ("");
LIST_PREPEND (channel->users, channel_user);
// Finally log the message
if (buffer)
{
buffer_send (s->ctx, buffer, BUFFER_LINE_JOIN, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.object = irc_to_utf8 (s->ctx, 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 = "";
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 is 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)
{
buffer_send (s->ctx, buffer, BUFFER_LINE_KICK, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.object = irc_to_utf8 (s->ctx, target),
.reason = irc_to_utf8 (s->ctx, message));
}
}
static void
irc_handle_mode (struct server *s, const struct irc_message *msg)
{
if (!msg->prefix || msg->params.len < 1)
return;
char *who = irc_cut_nickname (msg->prefix);
const char *context = msg->params.vector[0];
// Join the modes back to a single string
struct str_vector copy;
str_vector_init (&copy);
str_vector_add_vector (&copy, msg->params.vector + 1);
char *reconstructed = join_str_vector (&copy, ' ');
str_vector_free (&copy);
char *modes = irc_to_utf8 (s->ctx, reconstructed);
free (reconstructed);
// TODO: parse the mode change and apply it
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));
// FIXME: logging
if (buffer)
{
buffer_send_status (s->ctx, buffer,
"Mode %s [%s] by %s", context, modes, who);
}
}
else if (irc_is_this_us (s, context))
{
// FIXME: logging
buffer_send_status (s->ctx, s->buffer,
"User mode [%s] by %s", modes, who);
}
else
{
// XXX: this shouldn't happen, reconnect?
}
free (who);
free (modes);
}
static void
irc_handle_nick (struct server *s, const struct irc_message *msg)
{
if (!msg->prefix || msg->params.len < 1)
return;
const char *new_nickname = msg->params.vector[0];
char *nickname = irc_cut_nickname (msg->prefix);
struct user *user = str_map_find (&s->irc_users, nickname);
free (nickname);
if (!user)
return;
// What the fuck
// TODO: probably log a message and force a reconnect
if (str_map_find (&s->irc_users, new_nickname))
return;
// Log a message in any PM buffer and rename it;
// we may even have one for ourselves
struct buffer *pm_buffer =
str_map_find (&s->irc_buffer_map, user->nickname);
if (pm_buffer)
{
str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);
str_map_set (&s->irc_buffer_map, user->nickname, NULL);
char *who = irc_is_this_us (s, msg->prefix)
? irc_to_utf8 (s->ctx, msg->prefix)
: NULL;
buffer_send (s->ctx, pm_buffer, BUFFER_LINE_NICK, 0,
.who = who,
.object = irc_to_utf8 (s->ctx, new_nickname));
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 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)
continue;
buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0,
.object = irc_to_utf8 (s->ctx, 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);
buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.object = irc_to_utf8 (s->ctx, new_nickname));
}
}
// Finally rename the user
str_map_set (&s->irc_users, new_nickname, user_ref (user));
str_map_set (&s->irc_users, user->nickname, NULL);
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)
{
char *nickname = irc_cut_nickname (msg->prefix);
char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname);
char *tag_utf8 = irc_to_utf8 (s->ctx, chunk->tag.str);
char *text_utf8 = irc_to_utf8 (s->ctx, chunk->text.str);
buffer_send_status (s->ctx, s->buffer,
"CTCP reply from %s: %s %s", nickname_utf8, tag_utf8, text_utf8);
free (nickname);
free (nickname_utf8);
free (tag_utf8);
free (text_utf8);
}
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)
{
// TODO: some more obvious indication of highlights
int flags = irc_is_highlight (s, text->str)
? BUFFER_LINE_HIGHLIGHT
: 0;
buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, flags,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.text = irc_to_utf8 (s->ctx, text->str));
}
}
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 = "";
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)
{
buffer_send (s->ctx, buffer, BUFFER_LINE_PART, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.object = irc_to_utf8 (s->ctx, channel_name),
.reason = irc_to_utf8 (s->ctx, message));
}
}
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);
char *text_utf8 = irc_to_utf8 (s->ctx, m.str);
char *recipient_utf8 = irc_to_utf8 (s->ctx, recipient);
str_free (&m);
buffer_send_status (s->ctx, s->buffer,
"CTCP reply to %s: %s", recipient_utf8, text_utf8);
free (text_utf8);
free (recipient_utf8);
}
static void
irc_handle_ctcp_request (struct server *s,
const struct irc_message *msg, struct ctcp_chunk *chunk)
{
char *nickname = irc_cut_nickname (msg->prefix);
char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname);
char *tag_utf8 = irc_to_utf8 (s->ctx, chunk->tag.str);
buffer_send_status (s->ctx, s->buffer,
"CTCP requested by %s: %s", nickname_utf8, tag_utf8);
const char *target = msg->params.vector[0];
const char *recipient = nickname;
if (irc_is_channel (s, target))
recipient = target;
if (!strcmp (chunk->tag.str, "CLIENTINFO"))
irc_send_ctcp_reply (s, recipient, "CLIENTINFO %s %s %s %s",
"PING", "VERSION", "TIME", "CLIENTINFO");
else if (!strcmp (chunk->tag.str, "PING"))
irc_send_ctcp_reply (s, recipient, "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, recipient, "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, recipient, "TIME %s", buf);
}
free (nickname);
free (nickname_utf8);
free (tag_utf8);
}
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)
{
// TODO: some more obvious indication of highlights
int flags = irc_is_highlight (s, text->str)
? BUFFER_LINE_HIGHLIGHT
: 0;
enum buffer_line_type type = is_action
? BUFFER_LINE_ACTION
: BUFFER_LINE_PRIVMSG;
buffer_send (s->ctx, buffer, type, flags,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.text = irc_to_utf8 (s->ctx, text->str));
}
}
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
irc_handle_quit (struct server *s, const struct irc_message *msg)
{
if (!msg->prefix)
return;
// What the fuck
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 = "";
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)
{
buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.reason = irc_to_utf8 (s->ctx, message));
// TODO: set some kind of a flag in the buffer and when the user
// reappers 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)
{
buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
if (buffer)
buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.reason = irc_to_utf8 (s->ctx, 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)
{
buffer_send (s->ctx, buffer, BUFFER_LINE_TOPIC, 0,
.who = irc_to_utf8 (s->ctx, msg->prefix),
.text = irc_to_utf8 (s->ctx, topic));
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct irc_handler
{
char *name;
void (*handler) (struct server *s, const struct irc_message *msg);
}
g_irc_handlers[] =
{
// This list needs to stay sorted
{ "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);
split_str_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 void
irc_on_registered (struct server *s, const char *nickname)
{
s->irc_user = irc_get_or_make_user (s, nickname);
s->irc_user_mode = xstrdup ("");
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);
const char *autojoin = get_config_string (s->config, "autojoin");
if (autojoin)
irc_send (s, "JOIN :%s", autojoin);
// TODO: rejoin all current channels (mark those we've left manually?)
}
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);
split_str_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_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 for later processing
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
if (channel)
split_str_ignore_empty (nicks, ' ', &channel->names_buf);
}
static void
irc_process_names (struct server *s, struct channel *channel)
{
// TODO: overwrite users with "channel->names_buf", which contains
// [@+]-prefixed nicknames; take care to combine channel user modes
str_vector_reset (&channel->names_buf);
}
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_process_numeric (struct server *s,
const struct irc_message *msg, unsigned long numeric)
{
// Numerics typically have human-readable information
// TODO: try to output certain replies in more specific buffers
// TODO: fail the connection if there's no first parameter
// 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 (&copy);
str_vector_add_vector (&copy, msg->params.vector + !!msg->params.len);
// Join the parameter vector back, recode it to our internal encoding
// and send it to the server buffer
char *reconstructed = join_str_vector (&copy, ' ');
str_vector_free (&copy);
buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
.text = irc_to_utf8 (s->ctx, reconstructed));
free (reconstructed);
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:
// TODO: parse this, mainly PREFIX; see
// http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
// TODO: initialize key_strxfrm according to server properties;
// note that collisions may arise on reconnecting
break;
case IRC_RPL_USERHOST: irc_handle_rpl_userhost (s, msg); break;
case IRC_RPL_NAMREPLY: irc_handle_rpl_namreply (s, msg); break;
case IRC_RPL_ENDOFNAMES: irc_handle_rpl_endofnames (s, msg); break;
case IRC_ERR_NICKNAMEINUSE:
// TODO: if (state == IRC_CONNECTED), use a different nick;
// either use a number suffix, or accept commas in "nickname" config
break;
}
}
static void
irc_process_message (const struct irc_message *msg,
const char *raw, void *user_data)
{
struct server *s = user_data;
if (g_debug_mode)
{
input_hide (&s->ctx->input);
char *term = irc_to_term (s->ctx, raw);
fprintf (stderr, "[IRC] ==> \"%s\"\n", term);
free (term);
input_show (&s->ctx->input);
}
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)
{
int 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
while (true)
{
const char *next = utf8_next (text, text_len - eaten, NULL);
hard_assert (next);
size_t char_len = next - text;
if (char_len > line_len)
break;
str_append_data (output, text, char_len);
text += char_len;
eaten += char_len;
line_len -= char_len;
}
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;
for (size_t message_left = strlen (message); message_left; )
{
struct str m;
str_init (&m);
size_t eaten = wrap_text_for_single_line (message,
MIN ((size_t) line_max, message_left), message_left, &m);
if (!eaten)
{
str_free (&m);
goto error;
}
str_vector_add_owned (output, str_steal (&m));
message += eaten;
message_left -= eaten;
}
return true;
error:
// Well, that's just weird
error_set (e,
"Message splitting was unsuccessful as there was "
"too little room for UTF-8 characters");
return false;
}
/// Automatically splits messages that arrive at other clients with our prefix
/// so that they don't arrive cut off by the server
static bool
irc_autosplit_message (struct server *s, const char *message,
int fixed_part, struct str_vector *output, struct error **e)
{
// :<nick>!<user>@<host> <fixed-part><message>
int space_in_one_message = 0;
if (s->irc_user && s->irc_user_host)
space_in_one_message = 510
- 1 - (int) strlen (s->irc_user->nickname)
- 1 - (int) strlen (s->irc_user_host)
- 1 - fixed_part;
// However we don't always have the full info for message splitting
if (!space_in_one_message)
str_vector_add (output, message);
else if (!wrap_message (message, space_in_one_message, output, e))
return false;
return true;
}
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))
{
buffer_send_error (s->ctx,
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);
a.logger (s, &a, buffer, lines.vector[i]);
}
end:
str_vector_free (&lines);
}
static void
log_outcoming_action (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
(void) a;
if (buffer && soft_assert (s->irc_user))
buffer_send (s->ctx, buffer, BUFFER_LINE_ACTION, 0,
.who = irc_to_utf8 (s->ctx, s->irc_user->nickname),
.text = irc_to_utf8 (s->ctx, 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_outcoming_action, \
"\x01" "ACTION ", "\x01" })
static void
log_outcoming_privmsg (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
if (buffer && soft_assert (s->irc_user))
buffer_send (s->ctx, buffer, BUFFER_LINE_PRIVMSG, 0,
.who = irc_to_utf8 (s->ctx, s->irc_user->nickname),
.text = irc_to_utf8 (s->ctx, line));
else
// TODO: fix logging and encoding
buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
.text = xstrdup_printf ("MSG(%s): %s", a->target, line));
}
#define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \
send_autosplit_message ((s), (struct send_autosplit_args) \
{ "PRIVMSG", (target), (message), log_outcoming_privmsg, "", "" })
static void
log_outcoming_notice (struct server *s,
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
if (buffer && soft_assert (s->irc_user))
buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, 0,
.who = irc_to_utf8 (s->ctx, s->irc_user->nickname),
.text = irc_to_utf8 (s->ctx, line));
else
// TODO: fix logging and encoding
buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
.text = xstrdup_printf ("Notice -> %s: %s", a->target, line));
}
#define SEND_AUTOSPLIT_NOTICE(s, target, message) \
send_autosplit_message ((s), (struct send_autosplit_args) \
{ "NOTICE", (target), (message), log_outcoming_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)
{
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);
// Empty objects will show as such
if (item->type == CONFIG_ITEM_OBJECT
&& item->value.object.len)
{
config_dump_children (item, data);
return;
}
// Don't bother writing out null values everywhere
struct config_schema *schema = item->schema;
bool has_default = schema && schema->default_;
if (item->type != CONFIG_ITEM_NULL || has_default)
{
str_append (&line, " = ");
struct str value;
str_init (&value);
config_item_write (item, false, &value);
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 app_context *ctx, const char *mask, struct str_vector *output)
{
config_dump (ctx->config.root, output);
str_vector_sort (output);
// Filter out results by wildcard matching
for (size_t i = 0; i < output->len; i++)
{
// Yeah, I know
const char *line = output->vector[i];
char *key = xstrndup (line, strcspn (line, " "));
if (fnmatch (mask, key, 0))
str_vector_remove (output, i--);
free (key);
}
}
// --- User input handling -----------------------------------------------------
/// 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;
}
static char *
maybe_cut_word (char **s, bool (*validator) (void *, char *), 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 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))
buffer_send_error (ctx, ctx->global_buffer,
"%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 bool
server_command_check (struct app_context *ctx, const char *action,
bool need_registration)
{
// "need_registration" is primarily for message sending commands,
// as they may want to log buffer lines and use our current nickname
if (ctx->current_buffer->type == BUFFER_GLOBAL)
// XXX: couldn't we just pass the name of the user command here?
// That doesn't actually concern the function but rather its callers.
buffer_send_error (ctx, ctx->current_buffer,
"Can't do this from a global buffer (%s)", action);
else
{
struct server *s = ctx->current_buffer->server;
if (!irc_is_connected (s))
buffer_send_error (ctx, s->buffer, "Not connected");
else if (s->state != IRC_REGISTERED && need_registration)
buffer_send_error (ctx, s->buffer, "Not registered");
else
return true;
}
return false;
}
static bool
validate_channel_name (void *user_data, char *word)
{
struct server *s = user_data;
return irc_is_channel (s, word);
}
static char *
try_get_channel (struct app_context *ctx, char **arguments)
{
struct server *s = ctx->current_buffer->server;
char *channel_name = maybe_cut_word (arguments, validate_channel_name, s);
if (channel_name)
return channel_name;
if (ctx->current_buffer->type == BUFFER_CHANNEL)
return ctx->current_buffer->channel->name;
return NULL;
}
static void
show_buffers_list (struct app_context *ctx)
{
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
buffer_send_status (ctx, ctx->global_buffer, "Buffers list:");
int i = 1;
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
buffer_send_status (ctx, ctx->global_buffer,
" [%d] %s", i++, iter->name);
}
static void
handle_buffer_close (struct app_context *ctx, char *arguments)
{
struct buffer *buffer = NULL;
const char *which = NULL;
if (!*arguments)
buffer = ctx->current_buffer;
else
buffer = try_decode_buffer (ctx, (which = cut_word (&arguments)));
if (!buffer)
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s", "No such buffer", which);
else if (buffer == ctx->global_buffer)
buffer_send_error (ctx, ctx->global_buffer,
"Can't close the global buffer");
else if (buffer->type == BUFFER_SERVER)
buffer_send_error (ctx, ctx->global_buffer,
"Can't close a server buffer");
else
{
if (buffer == ctx->current_buffer)
buffer_activate (ctx, buffer_next (ctx, 1));
buffer_remove (ctx, buffer);
}
}
static bool
handle_command_buffer (struct app_context *ctx, char *arguments)
{
char *action = cut_word (&arguments);
if (try_handle_buffer_goto (ctx, action))
return true;
// XXX: also build a prefix map?
// TODO: some subcommand to print N last lines from the buffer
if (!strcasecmp_ascii (action, "list"))
show_buffers_list (ctx);
else if (!strcasecmp_ascii (action, "clear"))
{
// TODO
}
else if (!strcasecmp_ascii (action, "move"))
{
// TODO: unlink the buffer and link it back at index;
// we will probably need to extend liberty for this
}
else if (!strcasecmp_ascii (action, "close"))
handle_buffer_close (ctx, arguments);
else
return false;
return true;
}
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);
split_str (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);
split_str (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)
{
buffer_send_error (ctx, ctx->global_buffer,
"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, key, &tmp);
buffer_send_status (ctx, ctx->global_buffer,
"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)
{
buffer_send_error (ctx, ctx->global_buffer,
"Invalid value: %s", e->message);
error_free (e);
return true;
}
if ((add | remove) && !config_item_type_is_string (new_->type))
{
buffer_send_error (ctx, ctx->global_buffer,
"+= / -= operators need a string argument");
config_item_destroy (new_);
return true;
}
for (size_t i = 0; i < all->len; i++)
{
char *key = xstrndup (all->vector[i], strcspn (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 app_context *ctx, char *arguments)
{
char *option = "*";
if (*arguments)
option = cut_word (&arguments);
struct str_vector all;
str_vector_init (&all);
dump_matching_options (ctx, option, &all);
bool result = true;
if (!all.len)
buffer_send_error (ctx, ctx->global_buffer, "No matches: %s", option);
else if (!*arguments)
{
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
for (size_t i = 0; i < all.len; i++)
buffer_send_status (ctx, ctx->global_buffer, "%s", all.vector[i]);
}
else
result = handle_command_set_assign (ctx, &all, arguments);
str_vector_free (&all);
return result;
}
static bool
handle_command_save (struct app_context *ctx, char *arguments)
{
if (*arguments)
return false;
struct str data;
str_init (&data);
serialize_configuration (ctx, &data);
struct error *e = NULL;
char *filename = write_configuration_file (&data, &e);
str_free (&data);
if (!filename)
{
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s", "Saving configuration failed", e->message);
error_free (e);
}
else
buffer_send_status (ctx, ctx->global_buffer,
"Configuration written to `%s'", filename);
free (filename);
return true;
}
static bool
handle_command_msg (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "send messages", true))
return true;
if (!*arguments)
return false;
struct server *s = ctx->current_buffer->server;
char *target = cut_word (&arguments);
if (!*arguments)
buffer_send_error (ctx, s->buffer, "No text to send");
else
SEND_AUTOSPLIT_PRIVMSG (s, target, arguments);
return true;
}
static bool
handle_command_query (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "send messages", true))
return true;
if (!*arguments)
return false;
struct server *s = ctx->current_buffer->server;
char *target = cut_word (&arguments);
if (irc_is_channel (s, target))
buffer_send_error (ctx, s->buffer, "Cannot query a channel");
else if (!*arguments)
buffer_send_error (ctx, s->buffer, "No text to send");
else
{
buffer_activate (ctx, irc_get_or_make_user_buffer (s, target));
SEND_AUTOSPLIT_PRIVMSG (s, target, arguments);
}
return true;
}
static bool
handle_command_notice (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "send messages", true))
return true;
if (!*arguments)
return false;
struct server *s = ctx->current_buffer->server;
char *target = cut_word (&arguments);
if (!*arguments)
buffer_send_error (ctx, s->buffer, "No text to send");
else
SEND_AUTOSPLIT_NOTICE (s, target, arguments);
return true;
}
static bool
handle_command_ctcp (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "send messages", true))
return true;
if (!*arguments)
return false;
char *target = cut_word (&arguments);
if (!*arguments)
return false;
char *tag = cut_word (&arguments);
for (char *p = tag; *p; p++)
*p = toupper_ascii (*p);
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "PRIVMSG %s :\x01%s %s\x01", target, tag, arguments);
else
irc_send (s, "PRIVMSG %s :\x01%s\x01", target, tag);
buffer_send_status (ctx, s->buffer,
"CTCP query to %s: %s", target, tag);
return true;
}
static bool
handle_command_me (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "send messages", true))
return true;
struct server *s = ctx->current_buffer->server;
if (ctx->current_buffer->type == BUFFER_CHANNEL)
SEND_AUTOSPLIT_ACTION (s,
ctx->current_buffer->channel->name, arguments);
else if (ctx->current_buffer->type == BUFFER_PM)
SEND_AUTOSPLIT_ACTION (s,
ctx->current_buffer->user->nickname, arguments);
else
buffer_send_error (ctx, s->buffer,
"Can't do this from a server buffer (%s)",
"send CTCP actions");
return true;
}
static bool
handle_command_quit (struct app_context *ctx, char *arguments)
{
struct str_map_iter iter;
str_map_iter_init (&iter, &ctx->servers);
struct server *s;
while ((s = str_map_iter_next (&iter)))
{
if (irc_is_connected (s))
irc_initiate_disconnect (s, *arguments ? arguments : NULL);
}
initiate_quit (ctx);
return true;
}
static bool
handle_command_join (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "join", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
// TODO: check if the arguments are in the form of
// "channel(,channel)* key(,key)*"
irc_send (s, "JOIN %s", arguments);
else if (ctx->current_buffer->type != BUFFER_CHANNEL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't join",
"no argument given and this buffer is not a channel");
// TODO: have a better way of checking if we're on the channel
else if (ctx->current_buffer->channel->users)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't join",
"you already are on the channel");
else
// TODO: send the key if known
irc_send (s, "JOIN %s", ctx->current_buffer->channel->name);
return true;
}
static bool
handle_command_part (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "part", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
{
// TODO: check if the arguments are in the form of "channel(,channel)*"
char *channels = cut_word (&arguments);
if (*arguments)
irc_send (s, "PART %s :%s", channels, arguments);
else
irc_send (s, "PART %s", channels);
}
else if (ctx->current_buffer->type != BUFFER_CHANNEL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't part",
"no argument given and this buffer is not a channel");
// TODO: have a better way of checking if we're on the channel
else if (!ctx->current_buffer->channel->users)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't part", "you're not on the channel");
else
irc_send (s, "PART %s", ctx->current_buffer->channel->name);
return true;
}
static bool
handle_command_cycle (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "cycle", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
{
// TODO: check if the arguments are in the form of "channel(,channel)*"
char *channels = cut_word (&arguments);
if (*arguments)
irc_send (s, "PART %s :%s", channels, arguments);
else
irc_send (s, "PART %s", channels);
// TODO: send the key if known
irc_send (s, "JOIN %s", channels);
}
else if (ctx->current_buffer->type != BUFFER_CHANNEL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't cycle",
"no argument given and this buffer is not a channel");
// TODO: have a better way of checking if we're on the channel
else if (!ctx->current_buffer->channel->users)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't cycle", "you're not on the channel");
else
{
irc_send (s, "PART %s", ctx->current_buffer->channel->name);
// TODO: send the key if known
irc_send (s, "JOIN %s", ctx->current_buffer->channel->name);
}
return true;
}
static bool
handle_command_mode (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "mode", true))
return true;
// FIXME: allow usernames as well, not only channels
// FIXME: +channels collide with setting modes
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't set mode",
"no channel name given and this buffer is not a channel");
else if (*arguments)
irc_send (s, "MODE %s %s", channel_name, arguments);
else
irc_send (s, "MODE %s", channel_name);
return true;
}
static bool
handle_command_topic (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "topic", true))
return true;
// FIXME: currently the topic can't start with a channel name
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't change topic",
"no channel name given and this buffer is not a channel");
else if (*arguments)
// FIXME: there's no way to unset the topic
irc_send (s, "TOPIC %s :%s", channel_name, arguments);
else
irc_send (s, "TOPIC %s", channel_name);
return true;
}
static bool
handle_command_kick (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "kick", true))
return true;
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't kick",
"no channel name given and this buffer is not a channel");
else if (*arguments)
// FIXME: the reason should be one argument
irc_send (s, "KICK %s %s", channel_name, arguments);
else
return false;
return true;
}
static bool
handle_command_kickban (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "kickban", true))
return true;
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't kickban",
"no channel name given and this buffer is not a channel");
else if (*arguments)
{
// FIXME: don't include the reason
irc_send (s, "MODE %s +b %s", channel_name, arguments);
// FIXME: the reason should be one argument
irc_send (s, "KICK %s %s", channel_name, arguments);
}
else
return false;
return true;
}
static bool
handle_command_ban (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "ban", true))
return true;
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't ban",
"no channel name given and this buffer is not a channel");
else if (*arguments)
irc_send (s, "MODE %s +b %s", channel_name, arguments);
else
return false;
return true;
}
static bool
handle_command_invite (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "invite", true))
return true;
// XXX: the order of arguments should probably be reverse
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't invite",
"no channel name given and this buffer is not a channel");
else if (*arguments)
irc_send (s, "INVITE %s %s", arguments, channel_name);
else
return false;
return true;
}
static bool
handle_command_connect (struct app_context *ctx, char *arguments)
{
struct server *s = NULL;
if (*arguments)
{
char *name = cut_word (&arguments);
if (!(s = str_map_find (&ctx->servers, name)))
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s: %s", "Can't connect", "no such server", name);
}
else if (ctx->current_buffer->type == BUFFER_GLOBAL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't connect",
"no server name given and this buffer is global");
else
s = ctx->current_buffer->server;
if (!s)
return true;
if (irc_is_connected (s))
{
buffer_send_error (ctx, s->buffer, "Already connected");
return true;
}
if (s->state == IRC_CONNECTING)
irc_destroy_connector (s);
irc_cancel_timers (s);
irc_initiate_connect (s);
return true;
}
static bool
handle_command_disconnect (struct app_context *ctx, char *arguments)
{
// TODO: try to take server name from arguments
struct server *s = NULL;
if (ctx->current_buffer->type == BUFFER_GLOBAL)
buffer_send_error (ctx, ctx->current_buffer,
"%s: %s", "Can't disconnect", "this buffer is global");
else
s = ctx->current_buffer->server;
if (!s)
return true;
if (s->state == IRC_CONNECTING)
{
buffer_send_status (ctx, s->buffer, "Connecting aborted");
irc_destroy_connector (s);
}
else if (!irc_is_connected (s))
buffer_send_error (ctx, s->buffer, "Not connected");
else
irc_initiate_disconnect (s, *arguments ? arguments : NULL);
return true;
}
static bool
handle_command_list (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "list channels", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "LIST %s", arguments);
else
irc_send (s, "LIST");
return true;
}
static bool
handle_command_names (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "names", true))
return true;
struct server *s = ctx->current_buffer->server;
char *channel_name = try_get_channel (ctx, &arguments);
if (!channel_name)
irc_send (s, "NAMES");
else
irc_send (s, "NAMES %s", channel_name);
return true;
}
static bool
handle_command_who (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "who", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "WHO %s", arguments);
else
irc_send (s, "WHO");
return true;
}
static bool
handle_command_whois (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "whois", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "WHOIS %s", arguments);
else
irc_send (s, "WHOIS");
return true;
}
static bool
handle_command_whowas (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "whowas", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "WHOWAS %s", arguments);
else
irc_send (s, "WHOWAS");
return true;
}
static bool
handle_command_motd (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "motd", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "MOTD %s", arguments);
else
irc_send (s, "MOTD");
return true;
}
static bool
handle_command_away (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "away", true))
return true;
struct server *s = ctx->current_buffer->server;
if (*arguments)
irc_send (s, "AWAY %s", arguments);
else
irc_send (s, "AWAY");
return true;
}
static bool
handle_command_nick (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "change nickname", false))
return true;
if (!*arguments)
return false;
struct server *s = ctx->current_buffer->server;
irc_send (s, "NICK %s", cut_word (&arguments));
return true;
}
static bool
handle_command_quote (struct app_context *ctx, char *arguments)
{
if (!server_command_check (ctx, "quote", false))
return true;
struct server *s = ctx->current_buffer->server;
irc_send (s, "%s", arguments);
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool handle_command_help (struct app_context *, char *);
static struct command_handler
{
const char *name;
const char *description;
const char *usage;
bool (*handler) (struct app_context *ctx, char *arguments);
}
g_command_handlers[] =
{
{ "help", "Show help",
"[<command> | <option>]",
handle_command_help },
{ "quit", "Quit the program",
"[<message>]",
handle_command_quit },
{ "buffer", "Manage buffers",
"list | clear | move | { close [<number> | <name>] } | <number>",
handle_command_buffer },
{ "set", "Manage configuration",
"[<option>]",
handle_command_set },
{ "save", "Save configuration",
NULL,
handle_command_save },
{ "msg", "Send message to a nick or channel",
"<target> <message>",
handle_command_msg },
{ "query", "Send a private message to a nick",
"<nick> <message>",
handle_command_query },
{ "notice", "Send notice to a nick or channel",
"<target> <message>",
handle_command_notice },
{ "ctcp", "Send a CTCP query",
"<target> <tag>",
handle_command_ctcp },
{ "me", "Send a CTCP action",
"<message>",
handle_command_me },
{ "join", "Join channels",
"[<channel>[,<channel>...]]",
handle_command_join },
{ "part", "Leave channels",
"[<channel>[,<channel>...]] [<reason>]",
handle_command_part },
{ "cycle", "Rejoin channels",
"[<channel>[,<channel>...]] [<reason>]",
handle_command_cycle },
// TODO: /op, /voice, /hop
{ "mode", "Change mode",
"[<channel>] [<mode>...]",
handle_command_mode },
{ "topic", "Change topic",
"[<channel>] [<topic>]",
handle_command_topic },
{ "kick", "Kick user from channel",
"[<channel>] <user> [<reason>]",
handle_command_kick },
{ "kickban", "Kick and ban user from channel",
"[<channel>] <user> [<reason>]",
handle_command_kickban },
{ "ban", "Ban user from channel",
"[<channel>] <mask>",
handle_command_ban },
{ "invite", "Invite user to channel",
"[<channel>] <user>",
handle_command_invite },
{ "connect", "Connect to the server",
"[server]",
handle_command_connect },
{ "disconnect", "Disconnect from the server",
"[reason]",
handle_command_disconnect },
{ "list", "List channels and their topic",
"[<channel>[,<channel>...]] [<server>]",
handle_command_list },
{ "names", "List users on channel",
"[<channel>[,<channel>...]]",
handle_command_names },
{ "who", "List users",
"[<mask>]",
handle_command_who },
{ "whois", "Get user information",
"<mask>",
handle_command_whois },
{ "whowas", "Get user information",
"<mask>",
handle_command_whowas },
{ "motd", "Get the Message of The Day",
NULL,
handle_command_motd },
{ "away", "Set away status",
"[<text>]",
handle_command_away },
{ "nick", "Change current nick",
"<nickname>",
handle_command_nick },
{ "quote", "Send a raw command to the server",
"<command>",
handle_command_quote },
};
static bool
try_handle_command_help_option (struct app_context *ctx, const char *name)
{
struct config_item_ *item =
config_item_get (ctx->config.root, name, NULL);
if (!item)
return false;
struct config_schema *schema = item->schema;
if (!schema)
{
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s", "Option not recognized", name);
return true;
}
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
buffer_send_status (ctx, ctx->global_buffer,
"Option \"%s\":", name);
buffer_send_status (ctx, ctx->global_buffer,
" Description: %s", schema->comment);
buffer_send_status (ctx, ctx->global_buffer,
" Type: %s", config_item_type_name (schema->type));
buffer_send_status (ctx, ctx->global_buffer,
" Default: %s", schema->default_ ? schema->default_ : "null");
struct str tmp;
str_init (&tmp);
config_item_write (item, false, &tmp);
buffer_send_status (ctx, ctx->global_buffer,
" Current value: %s", tmp.str);
str_free (&tmp);
return true;
}
static bool
handle_command_help (struct app_context *ctx, char *arguments)
{
if (!*arguments)
{
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
buffer_send_status (ctx, ctx->global_buffer, "Commands:");
int longest = 0;
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
{
int len = strlen (g_command_handlers[i].name);
longest = MAX (longest, len);
}
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
{
struct command_handler *handler = &g_command_handlers[i];
buffer_send_status (ctx, ctx->global_buffer, " %-*s %s",
longest, handler->name, handler->description);
}
return true;
}
char *command = cut_word (&arguments);
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
{
struct command_handler *handler = &g_command_handlers[i];
if (strcasecmp_ascii (command, handler->name))
continue;
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
buffer_send_status (ctx, ctx->global_buffer, "%s: %s",
handler->name, handler->description);
buffer_send_status (ctx, ctx->global_buffer, " Arguments: %s",
handler->usage ? handler->usage : "(none)");
return true;
}
if (!try_handle_command_help_option (ctx, command))
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s", "No such command or option", command);
return true;
}
static int
command_handler_cmp_by_length (const void *a, const void *b)
{
const struct command_handler *first = a;
const struct command_handler *second = b;
return strlen (first->name) - strlen (second->name);
}
static void
init_partial_matching_user_command_map (struct str_map *partial)
{
// Trivially create a partial matching map
str_map_init (partial);
partial->key_xfrm = tolower_ascii_strxfrm;
// We process them from the longest to the shortest one,
// so that common prefixes favor shorter entries
struct command_handler *by_length[N_ELEMENTS (g_command_handlers)];
for (size_t i = 0; i < N_ELEMENTS (by_length); i++)
by_length[i] = &g_command_handlers[i];
qsort (by_length, N_ELEMENTS (by_length), sizeof *by_length,
command_handler_cmp_by_length);
for (size_t i = N_ELEMENTS (by_length); i--; )
{
char *copy = xstrdup (by_length[i]->name);
for (size_t part = strlen (copy); part; part--)
{
copy[part] = '\0';
str_map_set (partial, copy, by_length[i]);
}
free (copy);
}
}
static void
process_user_command (struct app_context *ctx, char *command)
{
static bool initialized = false;
static struct str_map partial;
if (!initialized)
{
init_partial_matching_user_command_map (&partial);
initialized = true;
}
char *name = cut_word (&command);
if (try_handle_buffer_goto (ctx, name))
return;
struct command_handler *handler = str_map_find (&partial, name);
if (!handler)
buffer_send_error (ctx, ctx->global_buffer,
"%s: %s", "No such command", name);
else if (!handler->handler (ctx, command))
buffer_send_error (ctx, ctx->global_buffer,
"%s: /%s %s", "Usage", handler->name, handler->usage);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
send_message_to_target (struct server *s,
const char *target, char *message, struct buffer *buffer)
{
if (!irc_is_connected (s))
{
buffer_send_error (s->ctx, buffer, "Not connected");
return;
}
SEND_AUTOSPLIT_PRIVMSG (s, target, message);
}
static void
send_message_to_current_buffer (struct app_context *ctx, char *message)
{
struct buffer *buffer = ctx->current_buffer;
hard_assert (buffer != NULL);
switch (buffer->type)
{
case BUFFER_GLOBAL:
case BUFFER_SERVER:
buffer_send_error (ctx, buffer, "This buffer is not a channel");
break;
case BUFFER_CHANNEL:
send_message_to_target (buffer->server,
buffer->channel->name, message, buffer);
break;
case BUFFER_PM:
send_message_to_target (buffer->server,
buffer->user->nickname, message, buffer);
break;
}
}
static void
process_input (struct app_context *ctx, char *user_input)
{
char *input;
size_t len;
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
print_error ("character conversion failed for `%s'", "user input");
else if (input[0] != '/')
send_message_to_current_buffer (ctx, input);
else if (input[1] == '/')
send_message_to_current_buffer (ctx, input + 1);
else
process_user_command (ctx, input + 1);
free (input);
}
// --- Word completion ---------------------------------------------------------
// The amount of crap that goes into this is truly insane.
// It's mostly because of Editline's total ignorance of this task.
struct completion_word
{
size_t start; ///< Offset to start of word
size_t end; ///< Offset to end of word
};
struct completion
{
char *line; ///< The line which is being completed
struct completion_word *words; ///< Word locations
size_t words_len; ///< Number of words
size_t words_alloc; ///< Number of words allocated
size_t location; ///< Which word is being completed
};
static void
completion_init (struct completion *self)
{
memset (self, 0, sizeof *self);
}
static void
completion_free (struct completion *self)
{
free (self->line);
free (self->words);
}
static void
completion_add_word (struct completion *self, size_t start, size_t end)
{
if (!self->words)
self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
if (self->words_len == self->words_alloc)
self->words = xreallocarray (self->words,
(self->words_alloc <<= 1), sizeof *self->words);
self->words[self->words_len++] = (struct completion_word) { start, end };
}
static void
completion_parse (struct completion *self, const char *line, size_t len)
{
self->line = xstrndup (line, len);
// The first and the last word may be empty
const char *s = self->line;
while (true)
{
const char *start = s;
size_t word_len = strcspn (s, WORD_BREAKING_CHARS);
const char *end = start + word_len;
s = end + strspn (end, WORD_BREAKING_CHARS);
completion_add_word (self, start - self->line, end - self->line);
if (s == end)
break;
}
}
static void
completion_locate (struct completion *self, size_t offset)
{
size_t i = 0;
for (; i < self->words_len; i++)
if (self->words[i].start > offset)
break;
self->location = i - 1;
}
static bool
completion_matches (struct completion *self, int word, const char *pattern)
{
hard_assert (word >= 0 && word < (int) self->words_len);
char *text = xstrndup (self->line + self->words[word].start,
self->words[word].end - self->words[word].start);
bool result = !fnmatch (pattern, text, 0);
free (text);
return result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX: this isn't completely right because Unicode, but let's keep it simple.
// At worst it will stop before a combining mark, or fail to compare
// non-ASCII identifiers case-insensitively.
static size_t
utf8_common_prefix (const char **vector, size_t len)
{
size_t prefix = 0;
if (!vector || !vector[0])
return 0;
struct utf8_iter a[len];
for (size_t i = 0; i < len; i++)
utf8_iter_init (&a[i], vector[i]);
size_t ch_len;
int32_t ch;
while ((ch = utf8_iter_next (&a[0], &ch_len)) != -1)
{
for (size_t i = 1; i < len; i++)
{
int32_t other = utf8_iter_next (&a[i], NULL);
if (ch == other)
continue;
// Not bothering with lowercasing non-ASCII
if (ch >= 0x80 || other >= 0x80
|| tolower_ascii (ch) != tolower_ascii (other))
return prefix;
}
prefix += ch_len;
}
return prefix;
}
static void
complete_command (struct app_context *ctx, struct completion *data,
const char *word, struct str_vector *output)
{
(void) ctx;
(void) data;
const char *prefix = "";
if (*word == '/')
{
word++;
prefix = "/";
}
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
{
struct command_handler *handler = &g_command_handlers[i];
// FIXME: we want an ASCII version
if (!strncasecmp (word, handler->name, strlen (word)))
str_vector_add_owned (output,
xstrdup_printf ("%s%s", prefix, handler->name));
}
}
static void
complete_option (struct app_context *ctx, struct completion *data,
const char *word, struct str_vector *output)
{
(void) data;
struct str_vector options;
str_vector_init (&options);
config_dump (ctx->config.root, &options);
str_vector_sort (&options);
// Wildcard expansion is an interesting side-effect
char *mask = xstrdup_printf ("%s*", word);
for (size_t i = 0; i < options.len; i++)
{
const char *line = options.vector[i];
char *key = xstrndup (line, strcspn (line, " "));
if (!fnmatch (mask, key, 0))
str_vector_add_owned (output, key);
else
free (key);
}
free (mask);
str_vector_free (&options);
}
static void
complete_nicknames (struct app_context *ctx, struct completion *data,
const char *word, struct str_vector *output)
{
if (ctx->current_buffer->type != BUFFER_CHANNEL)
return;
struct channel *channel = ctx->current_buffer->channel;
// XXX: this is a bit hackish and doesn't respect server case mapping
char *mask = xstrdup_printf ("%s*", word);
LIST_FOR_EACH (struct channel_user, iter, channel->users)
{
const char *nickname = iter->user->nickname;
if (fnmatch (mask, nickname, 0))
continue;
str_vector_add_owned (output, data->location == 0
? xstrdup_printf ("%s:", nickname)
: xstrdup (nickname));
}
free (mask);
}
static char **
complete_word (struct app_context *ctx, struct completion *data,
const char *word)
{
// First figure out what exactly do we need to complete
bool try_commands = false;
bool try_options = false;
bool try_nicknames = false;
if (data->location == 0 && completion_matches (data, 0, "/*"))
try_commands = true;
else if (data->location == 1 && completion_matches (data, 0, "/set"))
try_options = true;
else if (data->location == 1 && completion_matches (data, 0, "/help"))
try_commands = try_options = true;
else
try_nicknames = true;
struct str_vector words;
str_vector_init (&words);
// Add placeholder
str_vector_add_owned (&words, NULL);
if (try_commands) complete_command (ctx, data, word, &words);
if (try_options) complete_option (ctx, data, word, &words);
if (try_nicknames) complete_nicknames (ctx, data, word, &words);
if (words.len == 1)
{
// Nothing matched
str_vector_free (&words);
return NULL;
}
if (words.len == 2)
{
words.vector[0] = words.vector[1];
words.vector[1] = NULL;
}
else
{
size_t prefix = utf8_common_prefix
((const char **) words.vector + 1, words.len - 1);
if (!prefix)
words.vector[0] = xstrdup (word);
else
words.vector[0] = xstrndup (words.vector[1], prefix);
}
return words.vector;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// A special wrapper for iconv_xstrdup() that also fixes indexes into the
/// original string to point to the right location in the output.
/// Thanks, Readline! Without you I would have never needed to deal with this.
static char *
locale_to_utf8 (struct app_context *ctx, const char *locale,
int *indexes[], size_t n_indexes)
{
struct str utf8; str_init (&utf8);
mbstate_t state; memset (&state, 0, sizeof state);
size_t remaining = strlen (locale) + 1;
const char *p = locale;
// Reset the shift state, FWIW
(void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL);
bool fixed[n_indexes];
memset (fixed, 0, sizeof fixed);
while (true)
{
size_t len = mbrlen (p, remaining, &state);
// Incomplete multibyte character or illegal sequence (probably)
if (len == (size_t) -2
|| len == (size_t) -1)
{
str_free (&utf8);
return NULL;
}
// Convert indexes into the multibyte string to UTF-8
for (size_t i = 0; i < n_indexes; i++)
if (!fixed[i] && *indexes[i] <= p - locale)
{
*indexes[i] = utf8.len;
fixed[i] = true;
}
// End of string
if (!len)
break;
// EINVAL (incomplete sequence) should never happen and
// EILSEQ neither because we've already checked for that with mbrlen().
// E2BIG is what iconv_xstrdup solves. This must succeed.
size_t ch_len;
char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len);
hard_assert (ch != NULL);
str_append_data (&utf8, ch, ch_len);
free (ch);
p += len;
remaining -= len;
}
return str_steal (&utf8);
}
static void
utf8_vector_to_locale (struct app_context *ctx, char **vector)
{
for (; *vector; vector++)
{
char *converted = iconv_xstrdup
(ctx->term_from_utf8, *vector, -1, NULL);
if (!soft_assert (converted))
converted = xstrdup ("");
free (*vector);
*vector = converted;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Takes a line in locale-specific encoding and position of a word to complete,
/// returns a vector of matches in locale-specific encoding.
static char **
make_completions (struct app_context *ctx, char *line, int start, int end)
{
int *fixes[] = { &start, &end };
char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
if (!line_utf8)
return NULL;
hard_assert (start >= 0 && end >= 0 && start <= end);
struct completion c;
completion_init (&c);
completion_parse (&c, line, strlen (line));
completion_locate (&c, start);
char *word = xstrndup (line + start, end - start);
char **completions = complete_word (ctx, &c, word);
free (word);
completion_free (&c);
if (completions)
utf8_vector_to_locale (ctx, completions);
free (line_utf8);
return completions;
}
// --- Common code for user actions --------------------------------------------
static bool
redraw_screen (struct app_context *ctx)
{
if (!soft_assert (clear_screen != NULL))
return false;
input_hide (&ctx->input);
terminal_printer_fn printer = get_attribute_printer (stdout);
tputs (clear_screen, 1, printer);
fflush (stdout);
buffer_print_backlog (ctx, ctx->current_buffer);
input_show (&ctx->input);
return true;
}
static bool
jump_to_buffer (struct app_context *ctx, int n)
{
if (n < 0 || n > 9)
return false;
// There's no buffer zero
if (n == 0)
n = 10;
if (ctx->last_buffer && buffer_get_index (ctx, ctx->current_buffer) == n)
// Fast switching between two buffers
buffer_activate (ctx, ctx->last_buffer);
else if (!buffer_goto (ctx, n))
return false;
return true;
}
static void
await_mirc_escape (struct app_context *ctx)
{
ctx->awaiting_mirc_escape = true;
ctx->char_buf_len = 0;
}
static void
bind_common_keys (struct app_context *ctx)
{
struct input *self = &ctx->input;
input_bind_control (self, 'p', "previous-buffer");
input_bind_control (self, 'n', "next-buffer");
// Redefine M-0 through M-9 to switch buffers
for (int i = 0; i <= 9; i++)
input_bind_meta (self, '0' + i, "goto-buffer");
input_bind_meta (self, 'm', "insert-attribute");
if (key_f5)
input_bind (self, key_f5, "previous-buffer");
if (key_f6)
input_bind (self, key_f6, "next-buffer");
if (clear_screen)
input_bind_control (self, 'l', "redraw-screen");
}
// --- GNU Readline user actions -----------------------------------------------
#ifdef HAVE_READLINE
static int
on_readline_goto_buffer (int count, int key)
{
(void) count;
struct app_context *ctx = g_ctx;
if (!jump_to_buffer (ctx, UNMETA (key) - '0'))
input_ding (&ctx->input);
return 0;
}
static int
on_readline_previous_buffer (int count, int key)
{
(void) key;
struct app_context *ctx = g_ctx;
buffer_activate (ctx, buffer_previous (ctx, count));
return 0;
}
static int
on_readline_next_buffer (int count, int key)
{
(void) key;
struct app_context *ctx = g_ctx;
buffer_activate (ctx, buffer_next (ctx, count));
return 0;
}
static int
on_readline_redraw_screen (int count, int key)
{
(void) count;
(void) key;
struct app_context *ctx = g_ctx;
if (!redraw_screen (ctx))
input_ding (&ctx->input);
return 0;
}
static int
on_readline_insert_attribute (int count, int key)
{
(void) count;
(void) key;
struct app_context *ctx = g_ctx;
await_mirc_escape (ctx);
return 0;
}
static int
on_readline_return (int count, int key)
{
(void) count;
(void) key;
struct app_context *ctx = g_ctx;
// Let readline pass the line to our input handler
rl_done = 1;
// Hide the line, don't redisplay it
input_hide (&ctx->input);
input_restore (&ctx->input);
return 0;
}
static void
on_readline_input (char *line)
{
struct app_context *ctx = g_ctx;
if (line)
{
if (*line)
add_history (line);
process_input (ctx, line);
free (line);
}
else
{
input_hide (&ctx->input);
input_restore (&ctx->input);
input_ding (&ctx->input);
}
if (ctx->input.active)
// Readline automatically redisplays it
ctx->input.prompt_shown = 1;
}
static char **
app_readline_completion (const char *text, int start, int end)
{
// We will reconstruct that ourselves
(void) text;
// Don't iterate over filenames and stuff
rl_attempted_completion_over = true;
return make_completions (g_ctx, rl_line_buffer, start, end);
}
static int
app_readline_init (void)
{
struct app_context *ctx = g_ctx;
struct input *self = &ctx->input;
// XXX: maybe use rl_make_bare_keymap() and start from there;
// our dear user could potentionally rig things up in a way that might
// result in some funny unspecified behaviour
rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1);
rl_add_defun ("next-buffer", on_readline_next_buffer, -1);
rl_add_defun ("goto-buffer", on_readline_goto_buffer, -1);
rl_add_defun ("redraw-screen", on_readline_redraw_screen, -1);
rl_add_defun ("insert-attribute", on_readline_insert_attribute, -1);
rl_add_defun ("send-line", on_readline_return, -1);
bind_common_keys (ctx);
// Move native history commands
input_bind_meta (self, 'p', "previous-history");
input_bind_meta (self, 'n', "next-history");
// We need to hide the prompt and input first
rl_bind_key (RETURN, rl_named_function ("send-line"));
rl_variable_bind ("completion-ignore-case", "on");
rl_bind_key (TAB, rl_named_function ("menu-complete"));
if (key_btab)
input_bind (self, key_btab, "menu-complete-backward");
return 0;
}
#endif // HAVE_READLINE
// --- BSD Editline user actions -----------------------------------------------
#ifdef HAVE_EDITLINE
static unsigned char
on_editline_goto_buffer (EditLine *editline, int key)
{
(void) editline;
struct app_context *ctx = g_ctx;
if (!jump_to_buffer (ctx, key - '0'))
return CC_ERROR;
return CC_NORM;
}
static unsigned char
on_editline_previous_buffer (EditLine *editline, int key)
{
(void) editline;
(void) key;
struct app_context *ctx = g_ctx;
buffer_activate (ctx, buffer_previous (ctx, 1));
return CC_NORM;
}
static unsigned char
on_editline_next_buffer (EditLine *editline, int key)
{
(void) editline;
(void) key;
struct app_context *ctx = g_ctx;
buffer_activate (ctx, buffer_next (ctx, 1));
return CC_NORM;
}
static unsigned char
on_editline_redraw_screen (EditLine *editline, int key)
{
(void) editline;
(void) key;
if (!redraw_screen (g_ctx))
return CC_ERROR;
return CC_NORM;
}
static unsigned char
on_editline_insert_attribute (EditLine *editline, int key)
{
(void) editline;
(void) key;
await_mirc_escape (g_ctx);
return CC_NORM;
}
static unsigned char
on_editline_complete (EditLine *editline, int key)
{
(void) key;
(void) editline;
struct app_context *ctx = g_ctx;
// First prepare what Readline would have normally done for us...
const LineInfo *info_mb = el_line (editline);
int len = info_mb->lastchar - info_mb->buffer;
int point = info_mb->cursor - info_mb->buffer;
char *copy = xstrndup (info_mb->buffer, len);
// XXX: possibly incorrect wrt. shift state encodings
int el_start = point, el_end = point;
while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
el_start--;
char **completions = make_completions (ctx, copy, el_start, el_end);
// XXX: possibly incorrect wrt. shift state encodings
copy[el_end] = '\0';
int el_len = mbstowcs (NULL, copy + el_start, 0);
free (copy);
if (!completions)
return CC_REFRESH_BEEP;
// Remove the original word
el_wdeletestr (editline, el_len);
// Insert the best match instead
el_insertstr (editline, completions[0]);
bool only_match = !completions[1];
for (char **p = completions; *p; p++)
free (*p);
free (completions);
// I'm not sure if Readline's menu-complete can at all be implemented
// with Editline. Spamming the terminal with possible completions
// probably isn't what the user wants and we have no way of detecting
// what the last executed handler was.
if (!only_match)
return CC_REFRESH_BEEP;
// But if there actually is just one match, finish the word
el_insertstr (editline, " ");
return CC_REFRESH;
}
static unsigned char
on_editline_return (EditLine *editline, int key)
{
(void) key;
struct app_context *ctx = g_ctx;
const LineInfoW *info = el_wline (editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
wchar_t *line = calloc (sizeof *info->buffer, len + 1);
memcpy (line, info->buffer, sizeof *info->buffer * len);
// XXX: Editline seems to remember its position in history,
// so it's not going to work as you'd expect it to
if (*line)
{
HistEventW ev;
history_w (ctx->input.current->history, &ev, H_ENTER, line);
print_debug ("history: %d %ls", ev.num, ev.str);
}
free (line);
// process_input() expects a multibyte string
const LineInfo *info_mb = el_line (editline);
char copy[info_mb->lastchar - info_mb->buffer + 1];
memcpy (copy, info_mb->buffer, sizeof copy - 1);
copy[sizeof copy - 1] = '\0';
process_input (ctx, copy);
el_cursor (editline, len - point);
el_wdeletestr (editline, len);
return CC_REFRESH;
}
static void
app_editline_init (struct input *self)
{
static const struct { const char *name; const char *help;
unsigned char (*func) (EditLine *, int); } x[] =
{
{ "goto-buffer", "Go to buffer", on_editline_goto_buffer },
{ "previous-buffer", "Previous buffer", on_editline_previous_buffer },
{ "next-buffer", "Next buffer", on_editline_next_buffer },
{ "redraw-screen", "Redraw screen", on_editline_redraw_screen },
{ "insert-attribute", "mIRC formatting", on_editline_insert_attribute },
{ "send-line", "Send line", on_editline_return },
{ "complete", "Complete word", on_editline_complete },
};
for (size_t i = 0; i < N_ELEMENTS (x); i++)
el_set (self->editline, EL_ADDFN, x[i].name, x[i].help, x[i].func);
bind_common_keys (g_ctx);
// Move native history commands
input_bind_meta (self, 'p', "ed-prev-history");
input_bind_meta (self, 'n', "ed-next-history");
// No, editline, it's not supposed to kill the entire line
input_bind_control (self, 'w', "ed-delete-prev-word");
// Just what are you doing?
input_bind_control (self, 'u', "vi-kill-line-prev");
// We need to hide the prompt and input first
input_bind (self, "\n", "send-line");
input_bind_control (self, 'i', "complete");
// Source the user's defaults file
el_source (self->editline, NULL);
}
#endif // HAVE_EDITLINE
// --- Configuration loading ---------------------------------------------------
static bool
read_file (const char *filename, struct str *output, struct error **e)
{
FILE *fp = fopen (filename, "rb");
if (!fp)
{
error_set (e, "could not open `%s' for reading: %s",
filename, strerror (errno));
return false;
}
char buf[BUFSIZ];
size_t len;
while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
str_append_data (output, buf, len);
str_append_data (output, buf, len);
bool success = !ferror (fp);
fclose (fp);
if (success)
return true;
error_set (e, "error while reading `%s': %s",
filename, strerror (errno));
return false;
}
static struct config_item_ *
load_configuration_file (const char *filename, struct error **e)
{
struct config_item_ *root = NULL;
struct str data;
str_init (&data);
if (!read_file (filename, &data, e))
goto end;
struct error *error = NULL;
if (!(root = config_item_parse (data.str, data.len, false, &error)))
{
error_set (e, "configuration parse error: %s", error->message);
error_free (error);
}
end:
str_free (&data);
return root;
}
static void
load_configuration (struct app_context *ctx)
{
struct config_item_ *root = NULL;
struct error *e = NULL;
char *filename = resolve_config_filename (PROGRAM_NAME ".conf");
if (filename)
root = load_configuration_file (filename, &e);
else
print_status ("configuration file not found");
free (filename);
if (e)
{
print_error ("%s", e->message);
error_free (e);
e = NULL;
}
config_load (&ctx->config, root ? root : config_item_object ());
ctx->isolate_buffers =
get_config_boolean (ctx->config.root, "behaviour.isolate_buffers");
}
// --- Signals -----------------------------------------------------------------
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
/// Program termination has been requested by a signal
static volatile sig_atomic_t g_termination_requested;
/// The window has changed in size
static volatile sig_atomic_t g_winch_received;
static void
sigterm_handler (int signum)
{
(void) signum;
g_termination_requested = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "t", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
sigwinch_handler (int signum)
{
(void) signum;
g_winch_received = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "w", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
setup_signal_handlers (void)
{
if (pipe (g_signal_pipe) == -1)
exit_fatal ("%s: %s", "pipe", strerror (errno));
set_cloexec (g_signal_pipe[0]);
set_cloexec (g_signal_pipe[1]);
// So that the pipe cannot overflow; it would make write() block within
// the signal handler, which is something we really don't want to happen.
// The same holds true for read().
set_blocking (g_signal_pipe[0], false);
set_blocking (g_signal_pipe[1], false);
signal (SIGPIPE, SIG_IGN);
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigwinch_handler;
sigemptyset (&sa.sa_mask);
if (sigaction (SIGWINCH, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
sa.sa_handler = sigterm_handler;
if (sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
}
// --- I/O event handlers ------------------------------------------------------
// FIXME: merge this with initiate_quit()
static void
preinitiate_quit (struct app_context *ctx)
{
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
// TODO: a faster timer for quitting
// XXX: why do we do this? Just to reset the reconnect timer?
irc_reset_connection_timeouts (s);
if (irc_is_connected (s))
irc_initiate_disconnect (s, NULL);
}
}
static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{
char dummy;
(void) read (fd->fd, &dummy, 1);
if (g_termination_requested && !ctx->quitting)
{
preinitiate_quit (ctx);
initiate_quit (ctx);
}
if (g_winch_received)
{
if (ctx->input.active)
input_on_terminal_resized (&ctx->input);
update_screen_size ();
g_winch_received = false;
}
}
static void
process_mirc_escape (const struct pollfd *fd, struct app_context *ctx)
{
// There's no other way with libedit, as both el_getc() in a function
// handler and CC_ARGHACK would block execution
if (read (fd->fd, ctx->char_buf + ctx->char_buf_len, 1) != 1)
goto error;
mbstate_t state;
memset (&state, 0, sizeof state);
size_t len = mbrlen (ctx->char_buf, ++ctx->char_buf_len, &state);
// Illegal sequence
if (len == (size_t) -1)
goto error;
// Incomplete multibyte character
if (len == (size_t) -2)
return;
if (ctx->char_buf_len != 1)
goto error;
switch (ctx->char_buf[0])
{
case 'b': input_insert_c (&ctx->input, '\x02'); break;
case 'c': input_insert_c (&ctx->input, '\x03'); break;
case 'i':
case ']': input_insert_c (&ctx->input, '\x1d'); break;
case 'u':
case '_': input_insert_c (&ctx->input, '\x1f'); break;
case 'v': input_insert_c (&ctx->input, '\x16'); break;
case 'o': input_insert_c (&ctx->input, '\x0f'); break;
default:
goto error;
}
goto done;
error:
input_ding (&ctx->input);
done:
ctx->awaiting_mirc_escape = false;
}
static void
on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
{
(void) ctx;
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
if (ctx->awaiting_mirc_escape)
{
process_mirc_escape (fd, ctx);
return;
}
// XXX: this may loop for a bit: stop the event or eat the input?
// (This prevents a segfault when the input has been stopped.)
if (ctx->input.active)
input_on_readable (&ctx->input);
}
static void
init_poller_events (struct app_context *ctx)
{
poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]);
ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
ctx->signal_event.user_data = ctx;
poller_fd_set (&ctx->signal_event, POLLIN);
poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO);
ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
ctx->tty_event.user_data = ctx;
poller_fd_set (&ctx->tty_event, POLLIN);
}
// --- Main program ------------------------------------------------------------
static void
display_logo (void)
{
const char *logo =
" __ __ \n"
" __/ / ____ ____ ____ ____ ____ / /_ \n"
" / / / , / / / / , / / __/ / __/ / __ \\ \n"
" / / / / __/ / / / / __/ /_ / / /_ / / / / \n"
" /___/ /___/ /_ / /___/ /___/ /___/ /_/ /_/ \n"
" /___/ \n"
" ";
struct str_vector v;
str_vector_init (&v);
char *x = xstrdup_printf ("%s%s",
logo, PROGRAM_NAME " " PROGRAM_VERSION " starting");
split_str (x, '\n', &v);
free (x);
for (size_t i = 0; i < v.len; i++)
print_status ("%s", v.vector[i]);
str_vector_free (&v);
}
static void
create_server (struct app_context *ctx)
{
struct server *s = xmalloc (sizeof *s);
server_init (s, &ctx->poller);
s->ctx = ctx;
s->name = xstrdup ("server");
str_map_set (&ctx->servers, s->name, s);
// Load configuration
s->config = config_item_get (ctx->config.root, "server", NULL);
hard_assert (s->config != NULL);
struct error *e = NULL;
if (!irc_autofill_user_info (s, &e))
{
print_error ("%s: %s", "failed to fill in user details", e->message);
error_free (e);
}
// 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);
// Connect to the server ASAP
poller_timer_set (&s->reconnect_tmr, 0);
}
int
main (int argc, char *argv[])
{
// We include a generated file from kike including this array we don't use;
// let's just keep it there and silence the compiler warning instead
(void) g_default_replies;
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh;
opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental IRC client.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'd':
g_debug_mode = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
display_logo ();
// We only need to convert to and from the terminal encoding
setlocale (LC_CTYPE, "");
struct app_context ctx;
app_context_init (&ctx);
g_ctx = &ctx;
SSL_library_init ();
atexit (EVP_cleanup);
SSL_load_error_strings ();
atexit (ERR_free_strings);
setup_signal_handlers ();
register_config_modules (&ctx);
load_configuration (&ctx);
init_colors (&ctx);
init_poller_events (&ctx);
init_buffers (&ctx);
refresh_prompt (&ctx);
input_start (&ctx.input, argv[0]);
// TODO: finish multi-server
create_server (&ctx);
ctx.polling = true;
while (ctx.polling)
poller_run (&ctx.poller);
app_context_free (&ctx);
free_terminal ();
return EXIT_SUCCESS;
}