9537 lines
253 KiB
C
9537 lines
253 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 attrs for the prompt" ) \
|
|
XX( RESET, "reset", "String to reset terminal attributes" ) \
|
|
XX( READ_MARKER, "read_marker", "Terminal attrs for the read marker" ) \
|
|
XX( WARNING, "warning", "Terminal attrs for warnings" ) \
|
|
XX( ERROR, "error", "Terminal attrs for errors" ) \
|
|
XX( EXTERNAL, "external", "Terminal attrs for external lines" ) \
|
|
XX( TIMESTAMP, "timestamp", "Terminal attrs for timestamps" ) \
|
|
XX( HIGHLIGHT, "highlight", "Terminal attrs for highlights" ) \
|
|
XX( ACTION, "action", "Terminal attrs for user actions" ) \
|
|
XX( USERHOST, "userhost", "Terminal attrs for user@host" ) \
|
|
XX( JOIN, "join", "Terminal attrs for joins" ) \
|
|
XX( PART, "part", "Terminal attrs for parts" )
|
|
|
|
enum
|
|
{
|
|
#define XX(x, y, z) ATTR_ ## x,
|
|
ATTR_TABLE (XX)
|
|
#undef XX
|
|
ATTR_COUNT
|
|
};
|
|
|
|
// User data for logger functions to enable formatted logging
|
|
#define print_fatal_data ((void *) ATTR_ERROR)
|
|
#define print_error_data ((void *) ATTR_ERROR)
|
|
#define print_warning_data ((void *) ATTR_WARNING)
|
|
|
|
#include "config.h"
|
|
#define PROGRAM_NAME "degesch"
|
|
|
|
#include "common.c"
|
|
#include "kike-replies.c"
|
|
|
|
#include <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
|
|
|
|
/// How many lines of backlog to store in memory
|
|
#define BACKLOG_LIMIT 1000
|
|
|
|
/// Characters that separate words
|
|
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
|
|
|
|
// --- User interface ----------------------------------------------------------
|
|
|
|
// I'm not sure which one of these backends is worse: whether it's GNU Readline
|
|
// or BSD Editline. They both have their own annoying problems.
|
|
|
|
struct input_buffer
|
|
{
|
|
#ifdef HAVE_READLINE
|
|
HISTORY_STATE *history; ///< Saved history state
|
|
char *saved_line; ///< Saved line content
|
|
int saved_mark; ///< Saved mark
|
|
#elif defined HAVE_EDITLINE
|
|
HistoryW *history; ///< The history object
|
|
wchar_t *saved_line; ///< Saved line content
|
|
int saved_len; ///< Length of the saved line
|
|
#endif // HAVE_EDITLINE
|
|
int saved_point; ///< Saved cursor position
|
|
};
|
|
|
|
static struct input_buffer *
|
|
input_buffer_new (void)
|
|
{
|
|
struct input_buffer *self = xcalloc (1, sizeof *self);
|
|
#ifdef HAVE_EDITLINE
|
|
self->history = history_winit ();
|
|
|
|
HistEventW ev;
|
|
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
|
|
#endif // HAVE_EDITLINE
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
input_buffer_destroy (struct input_buffer *self)
|
|
{
|
|
#ifdef HAVE_READLINE
|
|
// Can't really free "history" contents from here
|
|
free (self->history);
|
|
#elif defined HAVE_EDITLINE
|
|
history_wend (self->history);
|
|
#endif // HAVE_EDITLINE
|
|
free (self->saved_line);
|
|
free (self);
|
|
}
|
|
|
|
struct input
|
|
{
|
|
bool active; ///< Are we a thing?
|
|
|
|
#if defined HAVE_READLINE
|
|
char *saved_line; ///< Saved line content
|
|
int saved_point; ///< Saved cursor position
|
|
int saved_mark; ///< Saved mark
|
|
#elif defined HAVE_EDITLINE
|
|
EditLine *editline; ///< The EditLine object
|
|
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 > 0)
|
|
rl_redisplay ();
|
|
|
|
rl_set_prompt (self->prompt);
|
|
if (self->prompt_shown > 0)
|
|
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 > 0)
|
|
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;
|
|
state->entries = NULL;
|
|
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 > 0)
|
|
rl_redisplay ();
|
|
}
|
|
}
|
|
|
|
static void
|
|
input_switch_buffer (struct input *self, struct input_buffer *buffer)
|
|
{
|
|
// There could possibly be occurences of the current undo list in some
|
|
// history entry. We either need to free the undo list, or move it
|
|
// somewhere else to load back later, as the buffer we're switching to
|
|
// has its own history state.
|
|
rl_free_undo_list ();
|
|
|
|
// Save this buffer's history so that it's independent for each buffer
|
|
if (self->current)
|
|
input_save_buffer (self, self->current);
|
|
else
|
|
// Just throw it away; there should always be an active buffer however
|
|
#if RL_READLINE_VERSION >= 0x0603
|
|
rl_clear_history ();
|
|
#else // RL_READLINE_VERSION < 0x0603
|
|
// At least something... this may leak undo entries
|
|
clear_history ();
|
|
#endif // RL_READLINE_VERSION < 0x0603
|
|
|
|
input_restore_buffer (self, buffer);
|
|
self->current = buffer;
|
|
}
|
|
|
|
static void
|
|
input_destroy_buffer (struct input *self, struct input_buffer *buffer)
|
|
{
|
|
(void) self;
|
|
|
|
// rl_clear_history, being the only way I know of to get rid of the complete
|
|
// history including attached data, is a pretty recent addition. *sigh*
|
|
#if RL_READLINE_VERSION >= 0x0603
|
|
if (buffer->history)
|
|
{
|
|
// See input_switch_buffer() for why we need to do this BS
|
|
rl_free_undo_list ();
|
|
|
|
// This is probably the only way we can free the history fully
|
|
HISTORY_STATE *state = history_get_history_state ();
|
|
|
|
history_set_history_state (buffer->history);
|
|
rl_clear_history ();
|
|
// rl_clear_history just removes history entries,
|
|
// we have to reclaim memory for their actual container ourselves
|
|
free (buffer->history->entries);
|
|
free (buffer->history);
|
|
buffer->history = NULL;
|
|
|
|
history_set_history_state (state);
|
|
free (state);
|
|
}
|
|
#endif // RL_READLINE_VERSION
|
|
|
|
input_buffer_destroy (buffer);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
input_save (struct input *self)
|
|
{
|
|
hard_assert (!self->saved_line);
|
|
|
|
self->saved_point = rl_point;
|
|
self->saved_mark = rl_mark;
|
|
self->saved_line = rl_copy_text (0, rl_end);
|
|
}
|
|
|
|
static void
|
|
input_restore (struct input *self)
|
|
{
|
|
hard_assert (self->saved_line);
|
|
|
|
rl_set_prompt (self->prompt);
|
|
rl_replace_line (self->saved_line, 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 > 0)
|
|
input_redisplay (self);
|
|
}
|
|
|
|
static char *
|
|
input_make_prompt (EditLine *editline)
|
|
{
|
|
struct input *self;
|
|
el_get (editline, EL_CLIENTDATA, &self);
|
|
if (!self->prompt)
|
|
return "";
|
|
return self->prompt;
|
|
}
|
|
|
|
static char *
|
|
input_make_empty_prompt (EditLine *editline)
|
|
{
|
|
(void) editline;
|
|
return "";
|
|
}
|
|
|
|
static void
|
|
input_erase (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 > 0)
|
|
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
|
|
|
|
char *nickname; ///< Literal nickname
|
|
// TODO: write code to poll for the away status
|
|
bool away; ///< User is away
|
|
|
|
struct user_channel *channels; ///< Channels the user is on
|
|
};
|
|
|
|
static struct user *
|
|
user_new (void)
|
|
{
|
|
struct user *self = xcalloc (1, sizeof *self);
|
|
self->ref_count = 1;
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
user_destroy (struct user *self)
|
|
{
|
|
free (self->nickname);
|
|
LIST_FOR_EACH (struct user_channel, iter, self->channels)
|
|
user_channel_destroy (iter);
|
|
free (self);
|
|
}
|
|
|
|
REF_COUNTABLE_METHODS (user)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct channel_user
|
|
{
|
|
LIST_HEADER (struct channel_user)
|
|
|
|
struct user *user; ///< Reference to user
|
|
struct str prefixes; ///< Ordered @+... characters
|
|
};
|
|
|
|
static struct channel_user *
|
|
channel_user_new (void)
|
|
{
|
|
struct channel_user *self = xcalloc (1, sizeof *self);
|
|
str_init (&self->prefixes);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
channel_user_destroy (struct channel_user *self)
|
|
{
|
|
user_unref (self->user);
|
|
str_free (&self->prefixes);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// We keep references to channels in their buffers,
|
|
// and weak references in their users and the name lookup table.
|
|
|
|
// XXX: this doesn't really have to be reference countable
|
|
|
|
struct channel
|
|
{
|
|
REF_COUNTABLE_HEADER
|
|
|
|
char *name; ///< Channel name
|
|
char *topic; ///< Channel topic
|
|
|
|
// XXX: write something like an ordered set of characters object?
|
|
struct str no_param_modes; ///< No parameter channel modes
|
|
struct str_map param_modes; ///< Parametrized channel modes
|
|
|
|
struct channel_user *users; ///< Channel users
|
|
struct str_vector names_buf; ///< Buffer for RPL_NAMREPLY
|
|
|
|
bool left_manually; ///< Don't rejoin on reconnect
|
|
};
|
|
|
|
static struct channel *
|
|
channel_new (void)
|
|
{
|
|
struct channel *self = xcalloc (1, sizeof *self);
|
|
self->ref_count = 1;
|
|
str_init (&self->no_param_modes);
|
|
str_map_init (&self->param_modes);
|
|
self->param_modes.free = free;
|
|
str_vector_init (&self->names_buf);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
channel_destroy (struct channel *self)
|
|
{
|
|
free (self->name);
|
|
free (self->topic);
|
|
str_free (&self->no_param_modes);
|
|
str_map_free (&self->param_modes);
|
|
// Owner has to make sure we have no users by now
|
|
hard_assert (!self->users);
|
|
str_vector_free (&self->names_buf);
|
|
free (self);
|
|
}
|
|
|
|
REF_COUNTABLE_METHODS (channel)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum formatter_item_type
|
|
{
|
|
FORMATTER_ITEM_TEXT, ///< Text
|
|
FORMATTER_ITEM_ATTR, ///< Formatting attributes
|
|
FORMATTER_ITEM_FG_COLOR, ///< Foreground color
|
|
FORMATTER_ITEM_BG_COLOR, ///< Background color
|
|
FORMATTER_ITEM_SIMPLE, ///< For mIRC formatting only so far
|
|
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
|
|
};
|
|
|
|
struct formatter_item
|
|
{
|
|
LIST_HEADER (struct formatter_item)
|
|
|
|
enum formatter_item_type type; ///< Type of this item
|
|
int color; ///< Color
|
|
int attribute; ///< Attribute ID
|
|
char *text; ///< Either text or an attribute string
|
|
};
|
|
|
|
static struct formatter_item *
|
|
formatter_item_new (void)
|
|
{
|
|
struct formatter_item *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
formatter_item_destroy (struct formatter_item *self)
|
|
{
|
|
free (self->text);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct formatter
|
|
{
|
|
struct app_context *ctx; ///< Application context
|
|
struct server *s; ///< Server
|
|
|
|
struct formatter_item *items; ///< Items
|
|
struct formatter_item *items_tail; ///< Tail of items
|
|
};
|
|
|
|
static void
|
|
formatter_init (struct formatter *self,
|
|
struct app_context *ctx, struct server *s)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
self->ctx = ctx;
|
|
self->s = s;
|
|
}
|
|
|
|
static void
|
|
formatter_free (struct formatter *self)
|
|
{
|
|
LIST_FOR_EACH (struct formatter_item, iter, self->items)
|
|
formatter_item_destroy (iter);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum buffer_line_flags
|
|
{
|
|
BUFFER_LINE_STATUS = 1 << 0, ///< Status message
|
|
BUFFER_LINE_ERROR = 1 << 1, ///< Error message
|
|
BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this
|
|
BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file
|
|
BUFFER_LINE_INDENT = 1 << 4 ///< Just indent the line
|
|
};
|
|
|
|
struct buffer_line
|
|
{
|
|
LIST_HEADER (struct buffer_line)
|
|
|
|
int flags; ///< Flags
|
|
time_t when; ///< Time of the event
|
|
struct formatter *formatter; ///< Line data
|
|
};
|
|
|
|
struct buffer_line *
|
|
buffer_line_new (void)
|
|
{
|
|
struct buffer_line *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
buffer_line_destroy (struct buffer_line *self)
|
|
{
|
|
if (self->formatter)
|
|
formatter_free (self->formatter);
|
|
free (self->formatter);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum buffer_type
|
|
{
|
|
BUFFER_GLOBAL, ///< Global information
|
|
BUFFER_SERVER, ///< Server-related messages
|
|
BUFFER_CHANNEL, ///< Channels
|
|
BUFFER_PM ///< Private messages (query)
|
|
};
|
|
|
|
struct buffer
|
|
{
|
|
LIST_HEADER (struct buffer)
|
|
|
|
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
|
|
bool highlighted; ///< We've been highlighted
|
|
|
|
FILE *log_file; ///< Log file
|
|
|
|
// Origin information:
|
|
|
|
struct server *server; ///< Reference to server
|
|
struct channel *channel; ///< Reference to channel
|
|
struct user *user; ///< Reference to user
|
|
};
|
|
|
|
static struct buffer *
|
|
buffer_new (void)
|
|
{
|
|
struct buffer *self = xcalloc (1, sizeof *self);
|
|
self->input_data = input_buffer_new ();
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
buffer_destroy (struct buffer *self)
|
|
{
|
|
free (self->name);
|
|
if (self->input_data)
|
|
input_buffer_destroy (self->input_data);
|
|
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
|
|
buffer_line_destroy (iter);
|
|
if (self->log_file)
|
|
(void) fclose (self->log_file);
|
|
if (self->user)
|
|
user_unref (self->user);
|
|
if (self->channel)
|
|
channel_unref (self->channel);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum transport_io_result
|
|
{
|
|
TRANSPORT_IO_OK = 0, ///< Completed successfully
|
|
TRANSPORT_IO_EOF, ///< Connection shut down by peer
|
|
TRANSPORT_IO_ERROR ///< Connection error
|
|
};
|
|
|
|
// The only real purpose of this is to abstract away TLS
|
|
struct transport
|
|
{
|
|
/// Initialize the transport
|
|
bool (*init) (struct server *s, struct error **e);
|
|
/// Destroy the user data pointer
|
|
void (*cleanup) (struct server *s);
|
|
|
|
/// The underlying socket may have become readable, update `read_buffer'
|
|
enum transport_io_result (*try_read) (struct server *s);
|
|
/// The underlying socket may have become writeable, flush `write_buffer'
|
|
enum transport_io_result (*try_write) (struct server *s);
|
|
/// Return event mask to use in the poller
|
|
int (*get_poll_events) (struct server *s);
|
|
|
|
/// Called just before closing the connection from our side
|
|
void (*in_before_shutdown) (struct server *s);
|
|
};
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum server_state
|
|
{
|
|
IRC_DISCONNECTED, ///< Not connected
|
|
IRC_CONNECTING, ///< Connecting to the server
|
|
IRC_CONNECTED, ///< Trying to register
|
|
IRC_REGISTERED, ///< We can chat now
|
|
IRC_CLOSING, ///< Flushing output before shutdown
|
|
IRC_HALF_CLOSED ///< Connection shutdown from our side
|
|
};
|
|
|
|
/// Convert an IRC identifier character to lower-case
|
|
typedef int (*irc_tolower_fn) (int);
|
|
|
|
/// Key conversion function for hashmap lookups
|
|
typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t);
|
|
|
|
struct server
|
|
{
|
|
struct app_context *ctx; ///< Application context
|
|
|
|
char *name; ///< Server identifier
|
|
struct buffer *buffer; ///< The buffer for this server
|
|
struct config_item_ *config; ///< Configuration root
|
|
|
|
// Connection:
|
|
|
|
enum server_state state; ///< Connection state
|
|
struct connector *connector; ///< Connection establisher
|
|
struct socks_connector *socks_conn; ///< SOCKS connection establisher
|
|
unsigned reconnect_attempt; ///< Number of reconnect attempt
|
|
bool manual_disconnect; ///< Don't reconnect after disconnect
|
|
|
|
int socket; ///< Socket FD of the server
|
|
struct str read_buffer; ///< Input yet to be processed
|
|
struct str write_buffer; ///< Outut yet to be be sent out
|
|
struct poller_fd socket_event; ///< We can read from the socket
|
|
|
|
struct transport *transport; ///< Transport method
|
|
void *transport_data; ///< Transport data
|
|
|
|
// Events:
|
|
|
|
struct poller_timer ping_tmr; ///< We should send a ping
|
|
struct poller_timer timeout_tmr; ///< Connection seems to be dead
|
|
struct poller_timer reconnect_tmr; ///< We should reconnect now
|
|
struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate
|
|
|
|
// IRC:
|
|
|
|
// TODO: an output queue to prevent excess floods (this will be needed
|
|
// especially for away status polling)
|
|
|
|
bool rehashing; ///< Rehashing IRC identifiers
|
|
|
|
struct str_map irc_users; ///< IRC user data
|
|
struct str_map irc_channels; ///< IRC channel data
|
|
struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers
|
|
|
|
struct user *irc_user; ///< Our own user
|
|
int nick_counter; ///< Iterates "nicks" when registering
|
|
struct str irc_user_mode; ///< Our current user modes
|
|
char *irc_user_host; ///< Our current user@host
|
|
|
|
bool cap_echo_message; ///< Whether the server echos messages
|
|
|
|
// Server-specific information (from RPL_ISUPPORT):
|
|
|
|
irc_tolower_fn irc_tolower; ///< Server tolower()
|
|
irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm()
|
|
|
|
char *irc_chantypes; ///< Channel types (name prefixes)
|
|
char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
|
|
char *irc_statusmsg; ///< Prefixes for channel targets
|
|
|
|
char *irc_chanmodes_list; ///< Channel modes for lists
|
|
char *irc_chanmodes_param_always; ///< Channel modes with mandatory param
|
|
char *irc_chanmodes_param_when_set; ///< Channel modes with param when set
|
|
char *irc_chanmodes_param_never; ///< Channel modes without param
|
|
|
|
char *irc_chanuser_prefixes; ///< Channel user prefixes
|
|
char *irc_chanuser_modes; ///< Channel user modes
|
|
|
|
unsigned irc_max_modes; ///< Max parametrized modes per command
|
|
};
|
|
|
|
static void on_irc_timeout (void *user_data);
|
|
static void on_irc_ping_timeout (void *user_data);
|
|
static void on_irc_autojoin_timeout (void *user_data);
|
|
static void irc_initiate_connect (struct server *s);
|
|
|
|
static void
|
|
server_init_specifics (struct server *self)
|
|
{
|
|
// Defaults as per the RPL_ISUPPORT drafts, or RFC 1459
|
|
|
|
self->irc_tolower = irc_tolower;
|
|
self->irc_strxfrm = irc_strxfrm;
|
|
|
|
self->irc_chantypes = xstrdup ("#&");
|
|
self->irc_idchan_prefixes = xstrdup ("");
|
|
self->irc_statusmsg = xstrdup ("");
|
|
|
|
self->irc_chanmodes_list = xstrdup ("b");
|
|
self->irc_chanmodes_param_always = xstrdup ("k");
|
|
self->irc_chanmodes_param_when_set = xstrdup ("l");
|
|
self->irc_chanmodes_param_never = xstrdup ("imnpst");
|
|
|
|
self->irc_chanuser_prefixes = xstrdup ("@+");
|
|
self->irc_chanuser_modes = xstrdup ("ov");
|
|
|
|
self->irc_max_modes = 3;
|
|
}
|
|
|
|
static void
|
|
server_free_specifics (struct server *self)
|
|
{
|
|
free (self->irc_chantypes);
|
|
free (self->irc_idchan_prefixes);
|
|
free (self->irc_statusmsg);
|
|
|
|
free (self->irc_chanmodes_list);
|
|
free (self->irc_chanmodes_param_always);
|
|
free (self->irc_chanmodes_param_when_set);
|
|
free (self->irc_chanmodes_param_never);
|
|
|
|
free (self->irc_chanuser_prefixes);
|
|
free (self->irc_chanuser_modes);
|
|
}
|
|
|
|
static void
|
|
server_init (struct server *self, struct poller *poller)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
|
|
self->socket = -1;
|
|
str_init (&self->read_buffer);
|
|
str_init (&self->write_buffer);
|
|
self->state = IRC_DISCONNECTED;
|
|
|
|
poller_timer_init (&self->timeout_tmr, poller);
|
|
self->timeout_tmr.dispatcher = on_irc_timeout;
|
|
self->timeout_tmr.user_data = self;
|
|
|
|
poller_timer_init (&self->ping_tmr, poller);
|
|
self->ping_tmr.dispatcher = on_irc_ping_timeout;
|
|
self->ping_tmr.user_data = self;
|
|
|
|
poller_timer_init (&self->reconnect_tmr, poller);
|
|
self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
|
|
self->reconnect_tmr.user_data = self;
|
|
|
|
poller_timer_init (&self->autojoin_tmr, poller);
|
|
self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
|
|
self->autojoin_tmr.user_data = self;
|
|
|
|
str_map_init (&self->irc_users);
|
|
self->irc_users.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->irc_channels);
|
|
self->irc_channels.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->irc_buffer_map);
|
|
self->irc_buffer_map.key_xfrm = irc_strxfrm;
|
|
|
|
str_init (&self->irc_user_mode);
|
|
|
|
server_free_specifics (self);
|
|
server_init_specifics (self);
|
|
}
|
|
|
|
static void
|
|
server_free (struct server *self)
|
|
{
|
|
free (self->name);
|
|
|
|
if (self->connector)
|
|
{
|
|
connector_free (self->connector);
|
|
free (self->connector);
|
|
}
|
|
if (self->socks_conn)
|
|
{
|
|
socks_connector_free (self->socks_conn);
|
|
free (self->socks_conn);
|
|
}
|
|
|
|
if (self->transport
|
|
&& self->transport->cleanup)
|
|
self->transport->cleanup (self);
|
|
|
|
if (self->socket != -1)
|
|
{
|
|
xclose (self->socket);
|
|
self->socket_event.closed = true;
|
|
poller_fd_reset (&self->socket_event);
|
|
}
|
|
str_free (&self->read_buffer);
|
|
str_free (&self->write_buffer);
|
|
|
|
poller_timer_reset (&self->ping_tmr);
|
|
poller_timer_reset (&self->timeout_tmr);
|
|
poller_timer_reset (&self->reconnect_tmr);
|
|
poller_timer_reset (&self->autojoin_tmr);
|
|
|
|
str_map_free (&self->irc_users);
|
|
str_map_free (&self->irc_channels);
|
|
str_map_free (&self->irc_buffer_map);
|
|
|
|
if (self->irc_user)
|
|
user_unref (self->irc_user);
|
|
str_free (&self->irc_user_mode);
|
|
free (self->irc_user_host);
|
|
|
|
server_free_specifics (self);
|
|
}
|
|
|
|
static void
|
|
server_destroy (void *self)
|
|
{
|
|
server_free (self);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct app_context
|
|
{
|
|
bool no_colors; ///< Disable attribute printing
|
|
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
|
|
|
|
// Configuration:
|
|
|
|
struct config config; ///< Program configuration
|
|
char *attrs[ATTR_COUNT]; ///< Terminal attributes
|
|
bool isolate_buffers; ///< Isolate global/server buffers
|
|
bool beep_on_highlight; ///< Beep on highlight
|
|
bool logging; ///< Logging to file enabled
|
|
|
|
struct str_map servers; ///< Our servers
|
|
|
|
// Events:
|
|
|
|
struct poller_fd tty_event; ///< Terminal input event
|
|
struct poller_fd signal_event; ///< Signal FD event
|
|
|
|
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
|
|
struct poller_timer date_chg_tmr; ///< Print a date change
|
|
|
|
struct poller poller; ///< Manages polled descriptors
|
|
bool quitting; ///< User requested quitting
|
|
bool polling; ///< The event loop is running
|
|
|
|
// Buffers:
|
|
|
|
struct buffer *buffers; ///< All our buffers in order
|
|
struct buffer *buffers_tail; ///< The tail of our buffers
|
|
|
|
struct buffer *global_buffer; ///< The global buffer
|
|
struct buffer *current_buffer; ///< The current buffer
|
|
struct buffer *last_buffer; ///< Last used buffer
|
|
|
|
// TODO: make buffer names fully unique like weechat does
|
|
struct str_map buffers_by_name; ///< Buffers by name
|
|
|
|
time_t last_displayed_msg_time; ///< Time of last displayed message
|
|
|
|
// Terminal:
|
|
|
|
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
|
|
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
|
|
iconv_t latin1_to_utf8; ///< ISO Latin 1 to UTF-8
|
|
|
|
struct input input; ///< User interface
|
|
|
|
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 = tolower_ascii_strxfrm;
|
|
|
|
str_map_init (&self->buffers_by_name);
|
|
self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
|
|
|
|
self->last_displayed_msg_time = time (NULL);
|
|
|
|
char *encoding = nl_langinfo (CODESET);
|
|
#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_defaults[i]);
|
|
free (self->attrs[i]);
|
|
}
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, self->buffers)
|
|
{
|
|
#ifdef HAVE_READLINE
|
|
input_destroy_buffer (&self->input, iter->input_data);
|
|
iter->input_data = NULL;
|
|
#endif // HAVE_READLINE
|
|
buffer_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);
|
|
|
|
// --- Configuration -----------------------------------------------------------
|
|
|
|
static void
|
|
on_config_debug_mode_change (struct config_item_ *item)
|
|
{
|
|
g_debug_mode = item->value.boolean;
|
|
}
|
|
|
|
static void on_config_attribute_change (struct config_item_ *item);
|
|
static void on_config_logging_change (struct config_item_ *item);
|
|
|
|
#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \
|
|
static void \
|
|
on_config_ ## name ## _change (struct config_item_ *item) \
|
|
{ \
|
|
struct app_context *ctx = item->user_data; \
|
|
ctx->name = item->value.boolean; \
|
|
}
|
|
|
|
TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers)
|
|
TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
config_validate_nonjunk_string
|
|
(const struct config_item_ *item, struct error **e)
|
|
{
|
|
if (item->type == CONFIG_ITEM_NULL)
|
|
return true;
|
|
|
|
hard_assert (config_item_type_is_string (item->type));
|
|
for (size_t i = 0; i < item->value.string.len; i++)
|
|
{
|
|
// Not even a tabulator
|
|
unsigned char c = item->value.string.str[i];
|
|
if (c < 32)
|
|
{
|
|
error_set (e, "control characters are not allowed");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
config_validate_addresses
|
|
(const struct config_item_ *item, struct error **e)
|
|
{
|
|
if (item->type == CONFIG_ITEM_NULL)
|
|
return true;
|
|
if (!config_validate_nonjunk_string (item, e))
|
|
return false;
|
|
|
|
// Comma-separated list of "host[:port]" pairs
|
|
regex_t re;
|
|
int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?"
|
|
"(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB);
|
|
hard_assert (!err);
|
|
|
|
bool result = !regexec (&re, item->value.string.str, 0, NULL, 0);
|
|
if (!result)
|
|
error_set (e, "invalid address list string");
|
|
|
|
regfree (&re);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
config_validate_nonnegative
|
|
(const struct config_item_ *item, struct error **e)
|
|
{
|
|
if (item->type == CONFIG_ITEM_NULL)
|
|
return true;
|
|
|
|
hard_assert (item->type == CONFIG_ITEM_INTEGER);
|
|
if (item->value.integer >= 0)
|
|
return true;
|
|
|
|
error_set (e, "must be non-negative");
|
|
return false;
|
|
}
|
|
|
|
static struct config_schema g_config_server[] =
|
|
{
|
|
{ .name = "nicks",
|
|
.comment = "IRC nickname",
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
.validate = config_validate_nonjunk_string },
|
|
{ .name = "username",
|
|
.comment = "IRC user name",
|
|
.type = CONFIG_ITEM_STRING,
|
|
.validate = config_validate_nonjunk_string },
|
|
{ .name = "realname",
|
|
.comment = "IRC real name/e-mail",
|
|
.type = CONFIG_ITEM_STRING,
|
|
.validate = config_validate_nonjunk_string },
|
|
|
|
{ .name = "addresses",
|
|
.comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
.validate = config_validate_addresses },
|
|
{ .name = "password",
|
|
.comment = "Password to connect to the server, if any",
|
|
.type = CONFIG_ITEM_STRING,
|
|
.validate = config_validate_nonjunk_string },
|
|
|
|
{ .name = "ssl",
|
|
.comment = "Whether to use TLS",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "off" },
|
|
{ .name = "ssl_cert",
|
|
.comment = "Client TLS 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 = "ssl_ciphers",
|
|
.comment = "OpenSSL cipher preference list",
|
|
.type = CONFIG_ITEM_STRING,
|
|
.default_ = "\"DEFAULT:!MEDIUM:!LOW\"" },
|
|
|
|
{ .name = "autoconnect",
|
|
.comment = "Connect automatically on startup",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "on" },
|
|
{ .name = "autojoin",
|
|
.comment = "Channels to join on start",
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
.validate = config_validate_nonjunk_string },
|
|
{ .name = "command",
|
|
.comment = "Command to execute after a successful connect",
|
|
.type = CONFIG_ITEM_STRING },
|
|
{ .name = "command_delay",
|
|
.comment = "Delay between executing \"command\" and joining channels",
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
.validate = config_validate_nonnegative,
|
|
.default_ = "0" },
|
|
{ .name = "reconnect",
|
|
.comment = "Whether to reconnect on error",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "on" },
|
|
{ .name = "reconnect_delay",
|
|
.comment = "Time between reconnecting",
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
.validate = config_validate_nonnegative,
|
|
.default_ = "5" },
|
|
|
|
{ .name = "socks_host",
|
|
.comment = "Address of a SOCKS 4a/5 proxy",
|
|
.type = CONFIG_ITEM_STRING,
|
|
.validate = config_validate_nonjunk_string },
|
|
{ .name = "socks_port",
|
|
.comment = "SOCKS port number",
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
.validate = config_validate_nonnegative,
|
|
.default_ = "1080" },
|
|
{ .name = "socks_username",
|
|
.comment = "SOCKS auth. username",
|
|
.type = CONFIG_ITEM_STRING },
|
|
{ .name = "socks_password",
|
|
.comment = "SOCKS auth. password",
|
|
.type = CONFIG_ITEM_STRING },
|
|
{}
|
|
};
|
|
|
|
static struct config_schema g_config_behaviour[] =
|
|
{
|
|
{ .name = "isolate_buffers",
|
|
.comment = "Don't leak messages from the server and global buffers",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "off",
|
|
.on_change = on_config_isolate_buffers_change },
|
|
{ .name = "beep_on_highlight",
|
|
.comment = "Beep when highlighted or on a new invisible PM",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "on",
|
|
.on_change = on_config_beep_on_highlight_change },
|
|
{ .name = "logging",
|
|
.comment = "Log buffer contents to file",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "off",
|
|
.on_change = on_config_logging_change },
|
|
{ .name = "save_on_quit",
|
|
.comment = "Save configuration before quitting",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "on" },
|
|
{ .name = "debug_mode",
|
|
.comment = "Produce some debugging output",
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
.default_ = "off",
|
|
.on_change = on_config_debug_mode_change },
|
|
|
|
{ .name = "reconnect_delay_growing",
|
|
.comment = "Growing factor for reconnect delay",
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
.validate = config_validate_nonnegative,
|
|
.default_ = "2" },
|
|
{ .name = "reconnect_delay_max",
|
|
.comment = "Maximum reconnect delay in seconds",
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
.validate = config_validate_nonnegative,
|
|
.default_ = "600" },
|
|
{}
|
|
};
|
|
|
|
static struct config_schema g_config_attributes[] =
|
|
{
|
|
#define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING, \
|
|
.on_change = on_config_attribute_change },
|
|
ATTR_TABLE (XX)
|
|
#undef XX
|
|
{}
|
|
};
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
load_config_behaviour (struct config_item_ *subtree, void *user_data)
|
|
{
|
|
config_schema_apply_to_object (g_config_behaviour, subtree, user_data);
|
|
}
|
|
|
|
static void
|
|
load_config_attributes (struct config_item_ *subtree, void *user_data)
|
|
{
|
|
config_schema_apply_to_object (g_config_attributes, subtree, user_data);
|
|
}
|
|
|
|
static void
|
|
register_config_modules (struct app_context *ctx)
|
|
{
|
|
struct config *config = &ctx->config;
|
|
// The servers are loaded later when we can create buffers for them
|
|
config_register_module (config, "servers", NULL, NULL);
|
|
config_register_module (config, "aliases", NULL, NULL);
|
|
config_register_module (config, "behaviour", load_config_behaviour, ctx);
|
|
config_register_module (config, "attributes", load_config_attributes, ctx);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static const char *
|
|
get_config_string (struct config_item_ *root, const char *key)
|
|
{
|
|
struct config_item_ *item = config_item_get (root, key, NULL);
|
|
hard_assert (item);
|
|
if (item->type == CONFIG_ITEM_NULL)
|
|
return NULL;
|
|
hard_assert (config_item_type_is_string (item->type));
|
|
return item->value.string.str;
|
|
}
|
|
|
|
static bool
|
|
set_config_string
|
|
(struct config_item_ *root, const char *key, const char *value)
|
|
{
|
|
struct config_item_ *item = config_item_get (root, key, NULL);
|
|
hard_assert (item);
|
|
|
|
struct config_item_ *new_ = config_item_string_from_cstr (value);
|
|
struct error *e = NULL;
|
|
if (config_item_set_from (item, new_, &e))
|
|
return true;
|
|
|
|
config_item_destroy (new_);
|
|
print_error ("couldn't set `%s' in configuration: %s", key, e->message);
|
|
error_free (e);
|
|
return false;
|
|
}
|
|
|
|
static int64_t
|
|
get_config_integer (struct config_item_ *root, const char *key)
|
|
{
|
|
struct config_item_ *item = config_item_get (root, key, NULL);
|
|
hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
|
|
return item->value.integer;
|
|
}
|
|
|
|
static bool
|
|
get_config_boolean (struct config_item_ *root, const char *key)
|
|
{
|
|
struct config_item_ *item = config_item_get (root, key, NULL);
|
|
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
|
|
return item->value.boolean;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static struct str_map *
|
|
get_servers_config (struct app_context *ctx)
|
|
{
|
|
return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
|
|
}
|
|
|
|
static struct str_map *
|
|
get_aliases_config (struct app_context *ctx)
|
|
{
|
|
return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static 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
|
|
apply_attribute_change (struct config_item_ *item, int id)
|
|
{
|
|
struct app_context *ctx = item->user_data;
|
|
free (ctx->attrs[id]);
|
|
ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
|
|
? ctx->attrs_defaults[id]
|
|
: item->value.string.str);
|
|
}
|
|
|
|
static void
|
|
on_config_attribute_change (struct config_item_ *item)
|
|
{
|
|
static const char *table[ATTR_COUNT] =
|
|
{
|
|
#define XX(x, y, z) [ATTR_ ## x] = y,
|
|
ATTR_TABLE (XX)
|
|
#undef XX
|
|
};
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (table); i++)
|
|
if (!strcmp (item->schema->name, table[i]))
|
|
{
|
|
apply_attribute_change (item, i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void
|
|
init_colors (struct app_context *ctx)
|
|
{
|
|
bool have_ti = init_terminal ();
|
|
char **defaults = ctx->attrs_defaults;
|
|
|
|
#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "")
|
|
|
|
INIT_ATTR (PROMPT, enter_bold_mode);
|
|
INIT_ATTR (RESET, exit_attribute_mode);
|
|
INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]);
|
|
INIT_ATTR (WARNING, g_terminal.color_set_fg[COLOR_YELLOW]);
|
|
INIT_ATTR (ERROR, g_terminal.color_set_fg[COLOR_RED]);
|
|
|
|
INIT_ATTR (EXTERNAL, g_terminal.color_set_fg[COLOR_WHITE]);
|
|
INIT_ATTR (TIMESTAMP, g_terminal.color_set_fg[COLOR_WHITE]);
|
|
INIT_ATTR (ACTION, g_terminal.color_set_fg[COLOR_RED]);
|
|
INIT_ATTR (USERHOST, g_terminal.color_set_fg[COLOR_CYAN]);
|
|
INIT_ATTR (JOIN, g_terminal.color_set_fg[COLOR_GREEN]);
|
|
INIT_ATTR (PART, g_terminal.color_set_fg[COLOR_RED]);
|
|
|
|
char *highlight = xstrdup_printf ("%s%s%s",
|
|
g_terminal.color_set_fg[COLOR_YELLOW],
|
|
g_terminal.color_set_bg[COLOR_MAGENTA],
|
|
enter_bold_mode);
|
|
INIT_ATTR (HIGHLIGHT, highlight);
|
|
free (highlight);
|
|
|
|
#undef INIT_ATTR
|
|
|
|
if (ctx->no_colors)
|
|
{
|
|
g_terminal.stdout_is_tty = false;
|
|
g_terminal.stderr_is_tty = false;
|
|
}
|
|
|
|
g_log_message_real = log_message_attributed;
|
|
|
|
// Apply the default values so that we start with any formatting at all
|
|
config_schema_call_changed
|
|
(config_item_get (ctx->config.root, "attributes", NULL));
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// A little tool that tries to make the most of the terminal's capabilities
|
|
// to set up text attributes. It mostly targets just terminal emulators as that
|
|
// is what people are using these days. At least no stupid ncurses limits us
|
|
// with color pairs.
|
|
|
|
enum
|
|
{
|
|
ATTRIBUTE_BOLD = 1 << 0,
|
|
ATTRIBUTE_ITALIC = 1 << 1,
|
|
ATTRIBUTE_UNDERLINE = 1 << 2,
|
|
ATTRIBUTE_INVERSE = 1 << 3,
|
|
ATTRIBUTE_BLINK = 1 << 4
|
|
};
|
|
|
|
struct attribute_printer
|
|
{
|
|
struct app_context *ctx; ///< Application context
|
|
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;
|
|
}
|
|
|
|
// --- Helpers -----------------------------------------------------------------
|
|
|
|
static int
|
|
irc_server_strcmp (struct server *s, const char *a, const char *b)
|
|
{
|
|
int x;
|
|
while (*a || *b)
|
|
if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
|
|
return x;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n)
|
|
{
|
|
int x;
|
|
while (n-- && (*a || *b))
|
|
if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
|
|
return x;
|
|
return 0;
|
|
}
|
|
|
|
static char *
|
|
irc_cut_nickname (const char *prefix)
|
|
{
|
|
return cstr_cut_until (prefix, "!@");
|
|
}
|
|
|
|
static const char *
|
|
irc_find_userhost (const char *prefix)
|
|
{
|
|
const char *p = strchr (prefix, '!');
|
|
return p ? p + 1 : NULL;
|
|
}
|
|
|
|
static bool
|
|
irc_is_this_us (struct server *s, const char *prefix)
|
|
{
|
|
// This shouldn't be called before successfully registering.
|
|
// Better safe than sorry, though.
|
|
if (!s->irc_user)
|
|
return false;
|
|
|
|
char *nick = irc_cut_nickname (prefix);
|
|
bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname);
|
|
free (nick);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
irc_is_channel (struct server *s, const char *ident)
|
|
{
|
|
return *ident
|
|
&& (!!strchr (s->irc_chantypes, *ident) ||
|
|
!!strchr (s->irc_idchan_prefixes, *ident));
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// As of 2015, everything should be in UTF-8. And if it's not, we'll decode it
|
|
// as ISO Latin 1. This function should not be called on the whole message.
|
|
static char *
|
|
irc_to_utf8 (struct app_context *ctx, const char *text)
|
|
{
|
|
if (!text)
|
|
return NULL;
|
|
size_t len = strlen (text) + 1;
|
|
if (utf8_validate (text, len))
|
|
return xstrdup (text);
|
|
return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL);
|
|
}
|
|
|
|
// This function is used to output debugging IRC traffic to the terminal.
|
|
// It's far from ideal, as any non-UTF-8 text degrades the entire line to
|
|
// ISO Latin 1. But it should work good enough most of the time.
|
|
static char *
|
|
irc_to_term (struct app_context *ctx, const char *text)
|
|
{
|
|
char *utf8 = irc_to_utf8 (ctx, text);
|
|
char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
|
|
free (utf8);
|
|
return term;
|
|
}
|
|
|
|
// --- Output formatter --------------------------------------------------------
|
|
|
|
// This complicated piece of code makes attributed text formatting simple.
|
|
// We use a printf-inspired syntax to push attributes and text to the object,
|
|
// then flush it either to a terminal, or a log file with formatting stripped.
|
|
//
|
|
// Format strings use a #-quoted notation, to differentiate from printf:
|
|
// #s inserts a string (expected to be in UTF-8)
|
|
// #d inserts a signed integer
|
|
//
|
|
// #S inserts a string from the server with unknown encoding
|
|
// #m inserts a mIRC-formatted string (auto-resets at boundaries)
|
|
// #n cuts the nickname from a string and automatically colours it
|
|
// #N is like #n but also appends userhost, if present
|
|
//
|
|
// #a inserts named attributes (auto-resets)
|
|
// #r resets terminal attributes
|
|
// #c sets foreground color
|
|
// #C sets background color
|
|
//
|
|
// Modifiers:
|
|
// & free() the string argument after using it
|
|
|
|
static void
|
|
formatter_add_item (struct formatter *self, struct formatter_item template_)
|
|
{
|
|
if (template_.text)
|
|
template_.text = xstrdup (template_.text);
|
|
|
|
struct formatter_item *item = formatter_item_new ();
|
|
*item = template_;
|
|
LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
|
|
}
|
|
|
|
#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
|
|
(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
|
|
|
|
#define FORMATTER_ADD_RESET(self) \
|
|
FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
|
|
#define FORMATTER_ADD_TEXT(self, text_) \
|
|
FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
|
|
#define FORMATTER_ADD_SIMPLE(self, attribute_) \
|
|
FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = ATTRIBUTE_ ## attribute_)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum
|
|
{
|
|
MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN,
|
|
MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE,
|
|
MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN,
|
|
MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY,
|
|
};
|
|
|
|
// We use estimates from the 16 color terminal palette, or the 256 color cube,
|
|
// which is not always available. The mIRC orange colour is only in the cube.
|
|
|
|
static const int g_mirc_to_terminal[] =
|
|
{
|
|
[MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231),
|
|
[MIRC_BLACK] = COLOR_256 (BLACK, 16),
|
|
[MIRC_BLUE] = COLOR_256 (BLUE, 19),
|
|
[MIRC_GREEN] = COLOR_256 (GREEN, 34),
|
|
[MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196),
|
|
[MIRC_RED] = COLOR_256 (RED, 124),
|
|
[MIRC_PURPLE] = COLOR_256 (MAGENTA, 127),
|
|
[MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214),
|
|
[MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226),
|
|
[MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46),
|
|
[MIRC_CYAN] = COLOR_256 (CYAN, 37),
|
|
[MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51),
|
|
[MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21),
|
|
[MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201),
|
|
[MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244),
|
|
[MIRC_L_GRAY] = COLOR_256 (WHITE, 252),
|
|
};
|
|
|
|
static const char *
|
|
formatter_parse_mirc_color (struct formatter *self, const char *s)
|
|
{
|
|
if (!isdigit_ascii (*s))
|
|
return s;
|
|
|
|
int fg = *s++ - '0';
|
|
if (isdigit_ascii (*s))
|
|
fg = fg * 10 + (*s++ - '0');
|
|
if (fg >= 0 && fg < 16)
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
|
|
|
|
if (*s != ',' || !isdigit_ascii (s[1]))
|
|
return s;
|
|
s++;
|
|
|
|
int bg = *s++ - '0';
|
|
if (isdigit_ascii (*s))
|
|
bg = bg * 10 + (*s++ - '0');
|
|
if (bg >= 0 && bg < 16)
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
|
|
|
|
return s;
|
|
}
|
|
|
|
static void
|
|
formatter_parse_mirc (struct formatter *self, const char *s)
|
|
{
|
|
struct str buf;
|
|
str_init (&buf);
|
|
|
|
FORMATTER_ADD_RESET (self);
|
|
|
|
unsigned char c;
|
|
while ((c = *s++))
|
|
{
|
|
if (buf.len && c < 0x20)
|
|
{
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
|
str_reset (&buf);
|
|
}
|
|
|
|
switch (c)
|
|
{
|
|
case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD); break;
|
|
case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC); break;
|
|
case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break;
|
|
case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE); break;
|
|
|
|
case '\x03':
|
|
s = formatter_parse_mirc_color (self, s);
|
|
break;
|
|
case '\x0f':
|
|
FORMATTER_ADD_RESET (self);
|
|
break;
|
|
default:
|
|
str_append_c (&buf, c);
|
|
}
|
|
}
|
|
|
|
if (buf.len)
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
|
|
|
str_free (&buf);
|
|
FORMATTER_ADD_RESET (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
formatter_parse_nick (struct formatter *self, char *s)
|
|
{
|
|
char *nick = irc_cut_nickname (s);
|
|
int color = siphash_wrapper (nick, strlen (nick)) % 8;
|
|
|
|
// We always use the default color for ourselves
|
|
if (self->s && irc_is_this_us (self->s, nick))
|
|
color = -1;
|
|
|
|
// Never use the black colour, could become transparent on black terminals
|
|
if (color == COLOR_BLACK)
|
|
color = -1;
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
|
|
|
|
char *x = irc_to_utf8 (self->ctx, nick);
|
|
free (nick);
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
free (x);
|
|
|
|
// Need to reset the color afterwards
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
|
|
}
|
|
|
|
static void
|
|
formatter_parse_nick_full (struct formatter *self, char *s)
|
|
{
|
|
formatter_parse_nick (self, s);
|
|
|
|
const char *userhost;
|
|
if (!(userhost = irc_find_userhost (s)))
|
|
return;
|
|
|
|
FORMATTER_ADD_TEXT (self, " (");
|
|
FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);
|
|
|
|
char *x = irc_to_utf8 (self->ctx, userhost);
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
free (x);
|
|
|
|
FORMATTER_ADD_RESET (self);
|
|
FORMATTER_ADD_TEXT (self, ")");
|
|
}
|
|
|
|
static const char *
|
|
formatter_parse_field (struct formatter *self,
|
|
const char *field, struct str *buf, va_list *ap)
|
|
{
|
|
bool free_string = false;
|
|
char *s = NULL;
|
|
char *tmp = NULL;
|
|
int c;
|
|
|
|
restart:
|
|
switch ((c = *field++))
|
|
{
|
|
// We can push boring text content to the caller's buffer
|
|
// and let it flush the buffer only when it's actually needed
|
|
case 'd':
|
|
tmp = xstrdup_printf ("%d", va_arg (*ap, int));
|
|
str_append (buf, tmp);
|
|
free (tmp);
|
|
break;
|
|
case 's':
|
|
str_append (buf, (s = va_arg (*ap, char *)));
|
|
break;
|
|
|
|
case 'S':
|
|
tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *)));
|
|
str_append (buf, tmp);
|
|
free (tmp);
|
|
break;
|
|
case 'm':
|
|
tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *)));
|
|
formatter_parse_mirc (self, tmp);
|
|
free (tmp);
|
|
break;
|
|
case 'n':
|
|
formatter_parse_nick (self, (s = va_arg (*ap, char *)));
|
|
break;
|
|
case 'N':
|
|
formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
|
|
break;
|
|
|
|
case 'a':
|
|
FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
|
|
break;
|
|
case 'c':
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
|
|
break;
|
|
case 'C':
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
|
|
break;
|
|
case 'r':
|
|
FORMATTER_ADD_RESET (self);
|
|
break;
|
|
|
|
default:
|
|
if (c == '&' && !free_string)
|
|
free_string = true;
|
|
else if (c)
|
|
hard_assert (!"unexpected format specifier");
|
|
else
|
|
hard_assert (!"unexpected end of format string");
|
|
goto restart;
|
|
}
|
|
|
|
if (free_string)
|
|
free (s);
|
|
return field;
|
|
}
|
|
|
|
// I was unable to take a pointer of a bare "va_list" when it was passed in
|
|
// as a function argument, so it has to be a pointer from the beginning
|
|
static void
|
|
formatter_addv (struct formatter *self, const char *format, va_list *ap)
|
|
{
|
|
struct str buf;
|
|
str_init (&buf);
|
|
|
|
while (*format)
|
|
{
|
|
if (*format != '#' || *++format == '#')
|
|
{
|
|
str_append_c (&buf, *format++);
|
|
continue;
|
|
}
|
|
if (buf.len)
|
|
{
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
|
str_reset (&buf);
|
|
}
|
|
|
|
format = formatter_parse_field (self, format, &buf, ap);
|
|
}
|
|
|
|
if (buf.len)
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
|
|
|
str_free (&buf);
|
|
}
|
|
|
|
static void
|
|
formatter_add (struct formatter *self, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
formatter_addv (self, format, &ap);
|
|
va_end (ap);
|
|
}
|
|
|
|
static void
|
|
formatter_add_from (struct formatter *self, struct formatter *other)
|
|
{
|
|
for (struct formatter_item *iter = other->items; iter; iter = iter->next)
|
|
formatter_add_item (self, *iter);
|
|
}
|
|
|
|
static bool
|
|
formatter_flush_attr
|
|
(struct attribute_printer *state, struct formatter_item *item)
|
|
{
|
|
switch (item->type)
|
|
{
|
|
case FORMATTER_ITEM_ATTR:
|
|
attribute_printer_apply (state, item->attribute);
|
|
state->want = 0;
|
|
state->want_foreground = -1;
|
|
state->want_background = -1;
|
|
return true;
|
|
case FORMATTER_ITEM_SIMPLE:
|
|
state->want |= item->attribute;
|
|
attribute_printer_update (state);
|
|
return true;
|
|
case FORMATTER_ITEM_FG_COLOR:
|
|
state->want_foreground = item->color;
|
|
attribute_printer_update (state);
|
|
return true;
|
|
case FORMATTER_ITEM_BG_COLOR:
|
|
state->want_background = item->color;
|
|
attribute_printer_update (state);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void
|
|
formatter_flush_text (struct app_context *ctx, const char *text, FILE *stream)
|
|
{
|
|
struct str sanitized;
|
|
str_init (&sanitized);
|
|
|
|
// Throw away any potentially harmful control characters
|
|
char *term = iconv_xstrdup (ctx->term_from_utf8, (char *) text, -1, NULL);
|
|
for (char *p = term; *p; p++)
|
|
if (!strchr ("\a\b\x1b", *p))
|
|
str_append_c (&sanitized, *p);
|
|
free (term);
|
|
|
|
fputs (sanitized.str, stream);
|
|
str_free (&sanitized);
|
|
}
|
|
|
|
static void
|
|
formatter_flush (struct formatter *self, FILE *stream)
|
|
{
|
|
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);
|
|
|
|
int attribute_ignore = 0;
|
|
LIST_FOR_EACH (struct formatter_item, iter, self->items)
|
|
{
|
|
if (iter->type == FORMATTER_ITEM_TEXT)
|
|
formatter_flush_text (self->ctx, iter->text, stream);
|
|
else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR)
|
|
attribute_ignore += iter->attribute;
|
|
else if (attribute_ignore <= 0
|
|
&& !formatter_flush_attr (&state, iter))
|
|
hard_assert (!"unhandled formatter item type");
|
|
}
|
|
attribute_printer_reset (&state);
|
|
}
|
|
|
|
// --- Buffers -----------------------------------------------------------------
|
|
|
|
static void
|
|
buffer_update_time (struct app_context *ctx, time_t now)
|
|
{
|
|
struct tm last, current;
|
|
if (!localtime_r (&ctx->last_displayed_msg_time, &last)
|
|
|| !localtime_r (&now, ¤t))
|
|
{
|
|
// Strange but nonfatal
|
|
print_error ("%s: %s", "localtime_r", strerror (errno));
|
|
return;
|
|
}
|
|
|
|
ctx->last_displayed_msg_time = now;
|
|
if (last.tm_year == current.tm_year
|
|
&& last.tm_mon == current.tm_mon
|
|
&& last.tm_mday == current.tm_mday)
|
|
return;
|
|
|
|
char buf[32] = "";
|
|
if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t)))
|
|
print_status ("%s", buf);
|
|
// Else the buffer was too small, which is pretty weird
|
|
}
|
|
|
|
static void
|
|
buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output)
|
|
{
|
|
int flags = line->flags;
|
|
if (flags & BUFFER_LINE_INDENT) formatter_add (f, " ");
|
|
if (flags & BUFFER_LINE_STATUS) formatter_add (f, " - ");
|
|
if (flags & BUFFER_LINE_ERROR) formatter_add (f, "#a=!=#r ", ATTR_ERROR);
|
|
|
|
formatter_add_from (f, line->formatter);
|
|
formatter_add (f, "\n");
|
|
formatter_flush (f, output);
|
|
formatter_free (f);
|
|
}
|
|
|
|
static void
|
|
buffer_line_display (struct app_context *ctx,
|
|
struct buffer_line *line, bool is_external)
|
|
{
|
|
// Normal timestamps don't include the date, this way the user won't be
|
|
// confused as to when an event has happened
|
|
buffer_update_time (ctx, line->when);
|
|
|
|
struct formatter f;
|
|
formatter_init (&f, ctx, NULL);
|
|
|
|
struct tm current;
|
|
char buf[9];
|
|
if (!localtime_r (&line->when, ¤t))
|
|
print_error ("%s: %s", "localtime_r", strerror (errno));
|
|
else if (!strftime (buf, sizeof buf, "%T", ¤t))
|
|
print_error ("%s: %s", "strftime", "buffer too small");
|
|
else
|
|
formatter_add (&f, "#a#s#r ", ATTR_TIMESTAMP, buf);
|
|
|
|
// Ignore all formatting for messages coming from other buffers, that is
|
|
// either from the global or server buffer. Instead print them in grey.
|
|
if (is_external)
|
|
{
|
|
formatter_add (&f, "#a", ATTR_EXTERNAL);
|
|
FORMATTER_ADD_ITEM (&f, IGNORE_ATTR, .attribute = 1);
|
|
}
|
|
|
|
input_hide (&ctx->input);
|
|
buffer_line_flush (line, &f, stdout);
|
|
input_show (&ctx->input);
|
|
}
|
|
|
|
static void
|
|
buffer_line_write_to_log (struct app_context *ctx,
|
|
struct buffer_line *line, FILE *log_file)
|
|
{
|
|
if (line->flags & BUFFER_LINE_SKIP_FILE)
|
|
return;
|
|
|
|
struct formatter f;
|
|
formatter_init (&f, ctx, NULL);
|
|
|
|
struct tm current;
|
|
char buf[20];
|
|
if (!gmtime_r (&line->when, ¤t))
|
|
print_error ("%s: %s", "gmtime_r", strerror (errno));
|
|
else if (!strftime (buf, sizeof buf, "%F %T", ¤t))
|
|
print_error ("%s: %s", "strftime", "buffer too small");
|
|
else
|
|
formatter_add (&f, "#s ", buf);
|
|
|
|
buffer_line_flush (line, &f, log_file);
|
|
}
|
|
|
|
static void
|
|
log_formatter (struct app_context *ctx,
|
|
struct buffer *buffer, int flags, struct formatter *f)
|
|
{
|
|
if (!buffer)
|
|
buffer = ctx->global_buffer;
|
|
|
|
if (buffer->lines_count >= BACKLOG_LIMIT)
|
|
{
|
|
struct buffer_line *popped = buffer->lines;
|
|
LIST_UNLINK_WITH_TAIL (buffer->lines, buffer->lines_tail, popped);
|
|
buffer_line_destroy (popped);
|
|
buffer->lines_count--;
|
|
}
|
|
|
|
struct buffer_line *line = buffer_line_new ();
|
|
line->flags = flags;
|
|
line->when = time (NULL);
|
|
|
|
// Move the formatter inside
|
|
line->formatter = xmalloc (sizeof *line->formatter);
|
|
*line->formatter = *f;
|
|
|
|
LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
|
|
buffer->lines_count++;
|
|
|
|
if (buffer->log_file)
|
|
buffer_line_write_to_log (ctx, line, buffer->log_file);
|
|
|
|
if (ctx->beep_on_highlight)
|
|
if ((flags & BUFFER_LINE_HIGHLIGHT)
|
|
|| (buffer->type == BUFFER_PM && buffer != ctx->current_buffer))
|
|
input_ding (&ctx->input);
|
|
|
|
bool can_leak = false;
|
|
if ((buffer == ctx->global_buffer)
|
|
|| (ctx->current_buffer->type == BUFFER_GLOBAL
|
|
&& buffer->type == BUFFER_SERVER)
|
|
|| (ctx->current_buffer->type != BUFFER_GLOBAL
|
|
&& buffer == ctx->current_buffer->server->buffer))
|
|
can_leak = true;
|
|
|
|
if (buffer == ctx->current_buffer)
|
|
buffer_line_display (ctx, line, false);
|
|
else if (!ctx->isolate_buffers && can_leak)
|
|
buffer_line_display (ctx, line, true);
|
|
else
|
|
{
|
|
buffer->unseen_messages_count++;
|
|
if (flags & BUFFER_LINE_HIGHLIGHT)
|
|
buffer->highlighted = true;
|
|
|
|
refresh_prompt (ctx);
|
|
}
|
|
}
|
|
|
|
static void
|
|
log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
|
|
int flags, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
|
|
struct formatter f;
|
|
formatter_init (&f, ctx, s);
|
|
formatter_addv (&f, format, &ap);
|
|
log_formatter (ctx, buffer, flags, &f);
|
|
|
|
va_end (ap);
|
|
}
|
|
|
|
#define log_global(ctx, flags, ...) \
|
|
log_full ((ctx), NULL, (ctx)->global_buffer, flags, __VA_ARGS__)
|
|
#define log_server(s, buffer, flags, ...) \
|
|
log_full ((s)->ctx, s, (buffer), flags, __VA_ARGS__)
|
|
|
|
#define log_global_status(ctx, ...) \
|
|
log_global ((ctx), BUFFER_LINE_STATUS, __VA_ARGS__)
|
|
#define log_global_error(ctx, ...) \
|
|
log_global ((ctx), BUFFER_LINE_ERROR, __VA_ARGS__)
|
|
#define log_global_indent(ctx, ...) \
|
|
log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__)
|
|
|
|
#define log_server_status(s, buffer, ...) \
|
|
log_server ((s), (buffer), BUFFER_LINE_STATUS, __VA_ARGS__)
|
|
#define log_server_error(s, buffer, ...) \
|
|
log_server ((s), (buffer), BUFFER_LINE_ERROR, __VA_ARGS__)
|
|
|
|
#define log_global_debug(ctx, ...) \
|
|
BLOCK_START \
|
|
if (g_debug_mode) \
|
|
log_global ((ctx), 0, "(*) " __VA_ARGS__); \
|
|
BLOCK_END
|
|
|
|
#define log_server_debug(s, ...) \
|
|
BLOCK_START \
|
|
if (g_debug_mode) \
|
|
log_server ((s), (s)->buffer, 0, "(*) " __VA_ARGS__); \
|
|
BLOCK_END
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// Lines that are used in more than one place
|
|
|
|
#define log_nick_self(s, buffer, new_) \
|
|
log_server_status ((s), (buffer), "You are now known as #n", (new_))
|
|
#define log_nick(s, buffer, old, new_) \
|
|
log_server_status ((s), (buffer), "#n is now known as #n", (old), (new_))
|
|
|
|
#define log_outcoming_notice(s, buffer, who, text) \
|
|
log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text))
|
|
#define log_outcoming_privmsg(s, buffer, prefixes, who, text) \
|
|
log_server ((s), (buffer), 0, "<#s#n> #m", (prefixes), (who), (text))
|
|
#define log_outcoming_action(s, buffer, who, text) \
|
|
log_server ((s), (buffer), 0, " #a*#r #n #m", ATTR_ACTION, (who), (text))
|
|
|
|
#define log_outcoming_orphan_notice(s, target, text) \
|
|
log_server_status ((s), (s)->buffer, "Notice -> #n: #m", (target), (text))
|
|
#define log_outcoming_orphan_privmsg(s, target, text) \
|
|
log_server_status ((s), (s)->buffer, "MSG(#n): #m", (target), (text))
|
|
|
|
#define log_ctcp_query(s, target, tag) \
|
|
log_server_status ((s), (s)->buffer, "CTCP query to #S: #S", target, tag)
|
|
#define log_ctcp_reply(s, target, reply /* freed! */) \
|
|
log_server_status ((s), (s)->buffer, "CTCP reply to #S: #&S", target, reply)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
make_log_filename (const char *filename, struct str *output)
|
|
{
|
|
for (const char *p = filename; *p; p++)
|
|
// XXX: anything more to replace?
|
|
if (strchr ("/\\ ", *p))
|
|
str_append_c (output, '_');
|
|
else
|
|
str_append_c (output, tolower_ascii (*p));
|
|
}
|
|
|
|
static void
|
|
buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
if (!ctx->logging || buffer->log_file)
|
|
return;
|
|
|
|
struct str path;
|
|
str_init (&path);
|
|
get_xdg_home_dir (&path, "XDG_DATA_HOME", ".local/share");
|
|
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, "logs");
|
|
|
|
(void) mkdir_with_parents (path.str, NULL);
|
|
|
|
// TODO: make sure global and server buffers don't collide with filenames
|
|
str_append_c (&path, '/');
|
|
make_log_filename (buffer->name, &path);
|
|
str_append (&path, ".log");
|
|
|
|
if (!(buffer->log_file = fopen (path.str, "ab")))
|
|
log_global_error (ctx, "Couldn't open log file `#s': #s",
|
|
path.str, strerror (errno));
|
|
str_free (&path);
|
|
}
|
|
|
|
static void
|
|
buffer_close_log_file (struct buffer *buffer)
|
|
{
|
|
if (buffer->log_file)
|
|
(void) fclose (buffer->log_file);
|
|
buffer->log_file = NULL;
|
|
}
|
|
|
|
static void
|
|
on_config_logging_change (struct config_item_ *item)
|
|
{
|
|
struct app_context *ctx = item->user_data;
|
|
ctx->logging = item->value.boolean;
|
|
|
|
for (struct buffer *buffer = ctx->buffers; buffer; buffer = buffer->next)
|
|
if (ctx->logging)
|
|
buffer_open_log_file (ctx, buffer);
|
|
else
|
|
buffer_close_log_file (buffer);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static struct buffer *
|
|
buffer_by_name (struct app_context *ctx, const char *name)
|
|
{
|
|
return str_map_find (&ctx->buffers_by_name, name);
|
|
}
|
|
|
|
static void
|
|
buffer_add (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
hard_assert (!buffer_by_name (ctx, buffer->name));
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
|
|
buffer_open_log_file (ctx, buffer);
|
|
|
|
// In theory this can't cause changes in the prompt
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_remove (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
hard_assert (buffer != ctx->current_buffer);
|
|
hard_assert (buffer != ctx->global_buffer);
|
|
|
|
input_destroy_buffer (&ctx->input, buffer->input_data);
|
|
buffer->input_data = NULL;
|
|
|
|
// And make sure to unlink the buffer from "irc_buffer_map"
|
|
struct server *s = buffer->server;
|
|
if (buffer->channel)
|
|
str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL);
|
|
if (buffer->user)
|
|
str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);
|
|
|
|
if (buffer == ctx->last_buffer)
|
|
ctx->last_buffer = NULL;
|
|
if (buffer->type == BUFFER_SERVER)
|
|
buffer->server->buffer = NULL;
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
buffer_destroy (buffer);
|
|
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_print_read_marker (struct app_context *ctx)
|
|
{
|
|
struct formatter f;
|
|
formatter_init (&f, ctx, NULL);
|
|
formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
|
|
formatter_flush (&f, stdout);
|
|
formatter_free (&f);
|
|
}
|
|
|
|
static void
|
|
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
// The prompt can take considerable time to redraw
|
|
input_hide (&ctx->input);
|
|
|
|
char *buffer_name_localized =
|
|
iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL);
|
|
print_status ("%s", buffer_name_localized);
|
|
free (buffer_name_localized);
|
|
|
|
// That is, minus the buffer switch line and the readline prompt
|
|
int display_limit = MAX (MAX (10,
|
|
(int) buffer->unseen_messages_count), g_terminal.lines - 2);
|
|
|
|
struct buffer_line *line = buffer->lines_tail;
|
|
int to_display = line != NULL;
|
|
for (; line && line->prev && --display_limit > 0; line = line->prev)
|
|
to_display++;
|
|
|
|
// Once we've found where we want to start with the backlog, print it
|
|
int until_marker = to_display - (int) buffer->unseen_messages_count;
|
|
for (; line; line = line->next)
|
|
{
|
|
if (until_marker-- == 0)
|
|
buffer_print_read_marker (ctx);
|
|
buffer_line_display (ctx, line, false);
|
|
}
|
|
|
|
buffer->unseen_messages_count = 0;
|
|
buffer->highlighted = false;
|
|
|
|
// So that it is obvious if the last line in the buffer is not from today
|
|
buffer_update_time (ctx, time (NULL));
|
|
|
|
refresh_prompt (ctx);
|
|
input_show (&ctx->input);
|
|
}
|
|
|
|
static void
|
|
buffer_activate (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
if (ctx->current_buffer == buffer)
|
|
return;
|
|
|
|
buffer_print_backlog (ctx, buffer);
|
|
input_switch_buffer (&ctx->input, buffer->input_data);
|
|
|
|
// Now at last we can switch the pointers
|
|
ctx->last_buffer = ctx->current_buffer;
|
|
ctx->current_buffer = buffer;
|
|
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_merge (struct app_context *ctx,
|
|
struct buffer *buffer, struct buffer *merged)
|
|
{
|
|
// XXX: anything better to do? This situation is arguably rare and I'm
|
|
// not entirely sure what action to take.
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS,
|
|
"Buffer #s was merged into this buffer", merged->name);
|
|
|
|
// Find all lines from "merged" newer than the newest line in "buffer"
|
|
struct buffer_line *start = merged->lines;
|
|
if (buffer->lines_tail)
|
|
while (start && start->when < buffer->lines_tail->when)
|
|
start = start->next;
|
|
if (!start)
|
|
return;
|
|
|
|
// Count how many of them we have
|
|
size_t n = 0;
|
|
for (struct buffer_line *iter = start; iter; iter = iter->next)
|
|
n++;
|
|
struct buffer_line *tail = merged->lines_tail;
|
|
|
|
// Cut them from the original buffer
|
|
if (start == merged->lines)
|
|
merged->lines = NULL;
|
|
else if (start->prev)
|
|
start->prev->next = NULL;
|
|
if (start == merged->lines_tail)
|
|
merged->lines_tail = start->prev;
|
|
merged->lines_count -= n;
|
|
|
|
// And append them to current lines in the buffer
|
|
buffer->lines_tail->next = start;
|
|
start->prev = buffer->lines_tail;
|
|
buffer->lines_tail = tail;
|
|
buffer->lines_count += n;
|
|
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE,
|
|
"End of merged content");
|
|
}
|
|
|
|
static void
|
|
buffer_rename (struct app_context *ctx,
|
|
struct buffer *buffer, const char *new_name)
|
|
{
|
|
struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name);
|
|
if (collision == buffer)
|
|
return;
|
|
|
|
hard_assert (!collision);
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
str_map_set (&ctx->buffers_by_name, new_name, buffer);
|
|
|
|
buffer_close_log_file (buffer);
|
|
buffer_open_log_file (ctx, buffer);
|
|
|
|
free (buffer->name);
|
|
buffer->name = xstrdup (new_name);
|
|
|
|
// We might have renamed the current buffer
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_clear (struct buffer *buffer)
|
|
{
|
|
LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
|
|
buffer_line_destroy (iter);
|
|
|
|
buffer->lines = buffer->lines_tail = NULL;
|
|
buffer->lines_count = 0;
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_at_index (struct app_context *ctx, int n)
|
|
{
|
|
int i = 0;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
if (++i == n)
|
|
return iter;
|
|
return NULL;
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_next (struct app_context *ctx, int count)
|
|
{
|
|
struct buffer *new_buffer = ctx->current_buffer;
|
|
while (count-- > 0)
|
|
if (!(new_buffer = new_buffer->next))
|
|
new_buffer = ctx->buffers;
|
|
return new_buffer;
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_previous (struct app_context *ctx, int count)
|
|
{
|
|
struct buffer *new_buffer = ctx->current_buffer;
|
|
while (count-- > 0)
|
|
if (!(new_buffer = new_buffer->prev))
|
|
new_buffer = ctx->buffers_tail;
|
|
return new_buffer;
|
|
}
|
|
|
|
static bool
|
|
buffer_goto (struct app_context *ctx, int n)
|
|
{
|
|
struct buffer *buffer = buffer_at_index (ctx, n);
|
|
if (!buffer)
|
|
return false;
|
|
|
|
buffer_activate (ctx, buffer);
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
buffer_get_index (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
int index = 1;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
{
|
|
if (iter == buffer)
|
|
return index;
|
|
index++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
buffer_remove_safe (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
if (buffer == ctx->current_buffer)
|
|
buffer_activate (ctx, ctx->last_buffer
|
|
? ctx->last_buffer
|
|
: buffer_next (ctx, 1));
|
|
buffer_remove (ctx, buffer);
|
|
}
|
|
|
|
static void
|
|
init_global_buffer (struct app_context *ctx)
|
|
{
|
|
struct buffer *global = ctx->global_buffer = buffer_new ();
|
|
global->type = BUFFER_GLOBAL;
|
|
global->name = xstrdup (PROGRAM_NAME);
|
|
|
|
buffer_add (ctx, global);
|
|
buffer_activate (ctx, global);
|
|
}
|
|
|
|
// --- Users, channels ---------------------------------------------------------
|
|
|
|
static void
|
|
irc_user_on_destroy (void *object, void *user_data)
|
|
{
|
|
struct user *user = object;
|
|
struct server *s = user_data;
|
|
if (!s->rehashing)
|
|
str_map_set (&s->irc_users, user->nickname, NULL);
|
|
}
|
|
|
|
static struct user *
|
|
irc_make_user (struct server *s, char *nickname)
|
|
{
|
|
hard_assert (!str_map_find (&s->irc_users, nickname));
|
|
|
|
struct user *user = user_new ();
|
|
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;
|
|
}
|
|
|
|
// Note that this eats the user reference
|
|
static void
|
|
irc_channel_link_user (struct channel *channel, struct user *user,
|
|
const char *prefixes)
|
|
{
|
|
struct user_channel *user_channel = user_channel_new ();
|
|
user_channel->channel = channel;
|
|
LIST_PREPEND (user->channels, user_channel);
|
|
|
|
struct channel_user *channel_user = channel_user_new ();
|
|
channel_user->user = user;
|
|
str_append (&channel_user->prefixes, prefixes);
|
|
LIST_PREPEND (channel->users, channel_user);
|
|
}
|
|
|
|
static void
|
|
irc_channel_unlink_user
|
|
(struct channel *channel, struct channel_user *channel_user)
|
|
{
|
|
// First destroy the user's weak references to the channel
|
|
struct user *user = channel_user->user;
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
if (iter->channel == channel)
|
|
{
|
|
LIST_UNLINK (user->channels, iter);
|
|
user_channel_destroy (iter);
|
|
}
|
|
|
|
// Then just unlink the user from the channel
|
|
LIST_UNLINK (channel->users, channel_user);
|
|
channel_user_destroy (channel_user);
|
|
}
|
|
|
|
static void
|
|
irc_channel_on_destroy (void *object, void *user_data)
|
|
{
|
|
struct channel *channel = object;
|
|
struct server *s = user_data;
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
irc_channel_unlink_user (channel, iter);
|
|
if (!s->rehashing)
|
|
str_map_set (&s->irc_channels, channel->name, NULL);
|
|
}
|
|
|
|
static struct channel *
|
|
irc_make_channel (struct server *s, char *name)
|
|
{
|
|
hard_assert (!str_map_find (&s->irc_channels, name));
|
|
|
|
struct channel *channel = channel_new ();
|
|
channel->on_destroy = irc_channel_on_destroy;
|
|
channel->user_data = s;
|
|
channel->name = name;
|
|
channel->topic = NULL;
|
|
str_map_set (&s->irc_channels, channel->name, channel);
|
|
return channel;
|
|
}
|
|
|
|
static struct channel_user *
|
|
irc_channel_get_user (struct channel *channel, struct user *user)
|
|
{
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
if (iter->user == user)
|
|
return iter;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
irc_remove_user_from_channel (struct user *user, struct channel *channel)
|
|
{
|
|
struct channel_user *channel_user = irc_channel_get_user (channel, user);
|
|
if (channel_user)
|
|
irc_channel_unlink_user (channel, channel_user);
|
|
}
|
|
|
|
static void
|
|
irc_left_channel (struct channel *channel)
|
|
{
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
irc_channel_unlink_user (channel, iter);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
remove_conflicting_buffer (struct server *s, struct buffer *buffer)
|
|
{
|
|
log_server_status (s, s->buffer,
|
|
"Removed buffer #s because of casemapping conflict", buffer->name);
|
|
if (s->ctx->current_buffer == buffer)
|
|
buffer_activate (s->ctx, s->buffer);
|
|
buffer_remove (s->ctx, buffer);
|
|
}
|
|
|
|
static void
|
|
irc_try_readd_user (struct server *s,
|
|
struct user *user, struct buffer *buffer)
|
|
{
|
|
if (str_map_find (&s->irc_users, user->nickname))
|
|
{
|
|
// Remove user from all channels and destroy any PM buffer
|
|
user_ref (user);
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
irc_remove_user_from_channel (user, iter->channel);
|
|
if (buffer)
|
|
remove_conflicting_buffer (s, buffer);
|
|
user_unref (user);
|
|
}
|
|
else
|
|
{
|
|
str_map_set (&s->irc_users, user->nickname, user);
|
|
str_map_set (&s->irc_buffer_map, user->nickname, buffer);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_try_readd_channel (struct server *s,
|
|
struct channel *channel, struct buffer *buffer)
|
|
{
|
|
if (str_map_find (&s->irc_channels, channel->name))
|
|
{
|
|
// Remove all users from channel and destroy any channel buffer
|
|
channel_ref (channel);
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
irc_channel_unlink_user (channel, iter);
|
|
if (buffer)
|
|
remove_conflicting_buffer (s, buffer);
|
|
channel_unref (channel);
|
|
}
|
|
else
|
|
{
|
|
str_map_set (&s->irc_channels, channel->name, channel);
|
|
str_map_set (&s->irc_buffer_map, channel->name, buffer);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_rehash_and_fix_conflicts (struct server *s)
|
|
{
|
|
// Save the old maps and initialize new ones
|
|
struct str_map old_users = s->irc_users;
|
|
struct str_map old_channels = s->irc_channels;
|
|
struct str_map old_buffer_map = s->irc_buffer_map;
|
|
|
|
str_map_init (&s->irc_users);
|
|
str_map_init (&s->irc_channels);
|
|
str_map_init (&s->irc_buffer_map);
|
|
|
|
s->irc_users .key_xfrm = s->irc_strxfrm;
|
|
s->irc_channels .key_xfrm = s->irc_strxfrm;
|
|
s->irc_buffer_map.key_xfrm = s->irc_strxfrm;
|
|
|
|
// Prevent channels and users from unsetting themselves
|
|
// from server maps upon removing the last reference to them
|
|
s->rehashing = true;
|
|
|
|
// XXX: to be perfectly sure, we should also check
|
|
// whether any users collide with channels and vice versa
|
|
|
|
// Our own user always takes priority, add him first
|
|
if (s->irc_user)
|
|
irc_try_readd_user (s, s->irc_user,
|
|
str_map_find (&old_buffer_map, s->irc_user->nickname));
|
|
|
|
struct str_map_iter iter;
|
|
struct user *user;
|
|
struct channel *channel;
|
|
|
|
str_map_iter_init (&iter, &old_users);
|
|
while ((user = str_map_iter_next (&iter)))
|
|
irc_try_readd_user (s, user,
|
|
str_map_find (&old_buffer_map, user->nickname));
|
|
|
|
str_map_iter_init (&iter, &old_channels);
|
|
while ((channel = str_map_iter_next (&iter)))
|
|
irc_try_readd_channel (s, channel,
|
|
str_map_find (&old_buffer_map, channel->name));
|
|
|
|
// Hopefully we've either moved or destroyed all the old content
|
|
s->rehashing = false;
|
|
|
|
str_map_free (&old_users);
|
|
str_map_free (&old_channels);
|
|
str_map_free (&old_buffer_map);
|
|
}
|
|
|
|
static void
|
|
irc_set_casemapping (struct server *s,
|
|
irc_tolower_fn tolower, irc_strxfrm_fn strxfrm)
|
|
{
|
|
if (tolower == s->irc_tolower
|
|
&& strxfrm == s->irc_strxfrm)
|
|
return;
|
|
|
|
s->irc_tolower = tolower;
|
|
s->irc_strxfrm = strxfrm;
|
|
|
|
// Ideally we would never have to do this but I can't think of a workaround
|
|
irc_rehash_and_fix_conflicts (s);
|
|
}
|
|
|
|
// --- Core functionality ------------------------------------------------------
|
|
|
|
static bool
|
|
irc_is_connected (struct server *s)
|
|
{
|
|
return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
|
|
}
|
|
|
|
static void
|
|
irc_update_poller (struct server *s, const struct pollfd *pfd)
|
|
{
|
|
int new_events = s->transport->get_poll_events (s);
|
|
hard_assert (new_events != 0);
|
|
|
|
if (!pfd || pfd->events != new_events)
|
|
poller_fd_set (&s->socket_event, new_events);
|
|
}
|
|
|
|
static void
|
|
irc_cancel_timers (struct server *s)
|
|
{
|
|
poller_timer_reset (&s->timeout_tmr);
|
|
poller_timer_reset (&s->ping_tmr);
|
|
poller_timer_reset (&s->reconnect_tmr);
|
|
poller_timer_reset (&s->autojoin_tmr);
|
|
}
|
|
|
|
static void
|
|
irc_reset_connection_timeouts (struct server *s)
|
|
{
|
|
poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
|
|
poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
|
|
poller_timer_reset (&s->reconnect_tmr);
|
|
}
|
|
|
|
static int64_t
|
|
irc_get_reconnect_delay (struct server *s)
|
|
{
|
|
int64_t delay = get_config_integer (s->config, "reconnect_delay");
|
|
|
|
int64_t delay_factor = get_config_integer (s->ctx->config.root,
|
|
"behaviour.reconnect_delay_growing");
|
|
for (unsigned i = 0; i < s->reconnect_attempt; i++)
|
|
delay *= delay_factor;
|
|
|
|
int64_t delay_max = get_config_integer (s->ctx->config.root,
|
|
"behaviour.reconnect_delay_max");
|
|
return (delay > delay_max || delay < 0) ? delay_max : delay;
|
|
}
|
|
|
|
static void
|
|
irc_queue_reconnect (struct server *s)
|
|
{
|
|
// As long as the user wants us to, that is
|
|
if (!get_config_boolean (s->config, "reconnect"))
|
|
return;
|
|
|
|
// XXX: maybe add a state for when a connect is queued?
|
|
hard_assert (s->state == IRC_DISCONNECTED);
|
|
|
|
int64_t delay = irc_get_reconnect_delay (s);
|
|
s->reconnect_attempt++;
|
|
|
|
log_server_status (s, s->buffer,
|
|
"Trying to reconnect in #&s seconds...",
|
|
xstrdup_printf ("%" PRId64, delay));
|
|
poller_timer_set (&s->reconnect_tmr, delay * 1000);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void irc_send (struct server *s,
|
|
const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
|
|
|
|
static void
|
|
irc_send (struct server *s, const char *format, ...)
|
|
{
|
|
if (!soft_assert (irc_is_connected (s)))
|
|
{
|
|
log_server_debug (s, "sending a message to a dead server connection");
|
|
return;
|
|
}
|
|
|
|
if (s->state == IRC_CLOSING
|
|
|| s->state == IRC_HALF_CLOSED)
|
|
return;
|
|
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
struct str str;
|
|
str_init (&str);
|
|
str_append_vprintf (&str, format, ap);
|
|
va_end (ap);
|
|
|
|
log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);
|
|
|
|
str_append_str (&s->write_buffer, &str);
|
|
str_free (&str);
|
|
str_append (&s->write_buffer, "\r\n");
|
|
irc_update_poller (s, NULL);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
irc_real_shutdown (struct server *s)
|
|
{
|
|
hard_assert (irc_is_connected (s) && s->state != IRC_HALF_CLOSED);
|
|
|
|
if (s->transport
|
|
&& s->transport->in_before_shutdown)
|
|
s->transport->in_before_shutdown (s);
|
|
|
|
while (shutdown (s->socket, SHUT_WR) == -1)
|
|
if (!soft_assert (errno == EINTR))
|
|
break;
|
|
|
|
s->state = IRC_HALF_CLOSED;
|
|
}
|
|
|
|
static void
|
|
irc_shutdown (struct server *s)
|
|
{
|
|
if (s->state == IRC_CLOSING
|
|
|| s->state == IRC_HALF_CLOSED)
|
|
return;
|
|
|
|
// TODO: set a timer to cut the connection if we don't receive an EOF
|
|
s->state = IRC_CLOSING;
|
|
|
|
// Either there's still some data in the write buffer and we wait
|
|
// until they're sent, or we send an EOF to the server right away
|
|
if (!s->write_buffer.len)
|
|
irc_real_shutdown (s);
|
|
}
|
|
|
|
static void
|
|
irc_destroy_connector (struct server *s)
|
|
{
|
|
if (s->connector)
|
|
connector_free (s->connector);
|
|
free (s->connector);
|
|
s->connector = NULL;
|
|
|
|
if (s->socks_conn)
|
|
socks_connector_free (s->socks_conn);
|
|
free (s->socks_conn);
|
|
s->socks_conn = NULL;
|
|
|
|
// Not connecting anymore
|
|
s->state = IRC_DISCONNECTED;
|
|
}
|
|
|
|
static void
|
|
try_finish_quit (struct app_context *ctx)
|
|
{
|
|
if (!ctx->quitting)
|
|
return;
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &ctx->servers);
|
|
|
|
bool disconnected_all = true;
|
|
struct server *s;
|
|
while ((s = str_map_iter_next (&iter)))
|
|
if (irc_is_connected (s))
|
|
disconnected_all = false;
|
|
|
|
if (disconnected_all)
|
|
ctx->polling = false;
|
|
}
|
|
|
|
static void
|
|
initiate_quit (struct app_context *ctx)
|
|
{
|
|
log_global_status (ctx, "Shutting down");
|
|
|
|
// Destroy the user interface
|
|
input_stop (&ctx->input);
|
|
|
|
// Initiate a connection close
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &ctx->servers);
|
|
struct server *s;
|
|
while ((s = str_map_iter_next (&iter)))
|
|
{
|
|
// There may be a timer set to reconnect to the server
|
|
poller_timer_reset (&s->reconnect_tmr);
|
|
|
|
if (irc_is_connected (s))
|
|
{
|
|
irc_shutdown (s);
|
|
s->manual_disconnect = true;
|
|
}
|
|
else if (s->state == IRC_CONNECTING)
|
|
irc_destroy_connector (s);
|
|
}
|
|
|
|
ctx->quitting = true;
|
|
try_finish_quit (ctx);
|
|
}
|
|
|
|
static void
|
|
irc_destroy_transport (struct server *s)
|
|
{
|
|
if (s->transport
|
|
&& s->transport->cleanup)
|
|
s->transport->cleanup (s);
|
|
s->transport = NULL;
|
|
|
|
xclose (s->socket);
|
|
s->socket = -1;
|
|
s->state = IRC_DISCONNECTED;
|
|
|
|
s->socket_event.closed = true;
|
|
poller_fd_reset (&s->socket_event);
|
|
|
|
str_reset (&s->read_buffer);
|
|
str_reset (&s->write_buffer);
|
|
}
|
|
|
|
static void
|
|
irc_destroy_state (struct server *s)
|
|
{
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_channels);
|
|
struct channel *channel;
|
|
while ((channel = str_map_iter_next (&iter)))
|
|
irc_left_channel (channel);
|
|
|
|
if (s->irc_user)
|
|
{
|
|
user_unref (s->irc_user);
|
|
s->irc_user = NULL;
|
|
}
|
|
|
|
str_reset (&s->irc_user_mode);
|
|
free (s->irc_user_host);
|
|
s->irc_user_host = NULL;
|
|
|
|
s->cap_echo_message = false;
|
|
|
|
// Need to call this before server_init_specifics()
|
|
irc_set_casemapping (s, irc_tolower, irc_strxfrm);
|
|
|
|
server_free_specifics (s);
|
|
server_init_specifics (s);
|
|
}
|
|
|
|
static void
|
|
irc_disconnect (struct server *s)
|
|
{
|
|
hard_assert (irc_is_connected (s));
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_buffer_map);
|
|
struct buffer *buffer;
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
log_server_status (s, buffer, "Disconnected from server");
|
|
|
|
irc_cancel_timers (s);
|
|
irc_destroy_transport (s);
|
|
irc_destroy_state (s);
|
|
|
|
// Take any relevant actions
|
|
if (s->ctx->quitting)
|
|
try_finish_quit (s->ctx);
|
|
else if (s->manual_disconnect)
|
|
s->manual_disconnect = false;
|
|
else
|
|
{
|
|
s->reconnect_attempt = 0;
|
|
irc_queue_reconnect (s);
|
|
}
|
|
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static void
|
|
irc_initiate_disconnect (struct server *s, const char *reason)
|
|
{
|
|
hard_assert (irc_is_connected (s));
|
|
|
|
s->manual_disconnect = true;
|
|
if (reason)
|
|
irc_send (s, "QUIT :%s", reason);
|
|
else
|
|
irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
on_irc_ping_timeout (void *user_data)
|
|
{
|
|
struct server *s = user_data;
|
|
log_server_error (s, s->buffer, "Connection timeout");
|
|
irc_disconnect (s);
|
|
}
|
|
|
|
static void
|
|
on_irc_timeout (void *user_data)
|
|
{
|
|
// Provoke a response from the server
|
|
struct server *s = user_data;
|
|
irc_send (s, "PING :%" PRIi64, (int64_t) time (NULL));
|
|
}
|
|
|
|
static void
|
|
on_irc_autojoin_timeout (void *user_data)
|
|
{
|
|
struct server *s = user_data;
|
|
|
|
// TODO: split autojoin at commas and make a joined set with regular rejoins
|
|
const char *autojoin = get_config_string (s->config, "autojoin");
|
|
if (autojoin)
|
|
irc_send (s, "JOIN :%s", autojoin);
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_channels);
|
|
|
|
struct channel *channel;
|
|
while ((channel = str_map_iter_next (&iter)))
|
|
if (!channel->left_manually)
|
|
irc_send (s, "JOIN :%s", channel->name);
|
|
}
|
|
|
|
// --- Server I/O --------------------------------------------------------------
|
|
|
|
static void irc_process_message
|
|
(const struct irc_message *msg, const char *raw, void *user_data);
|
|
|
|
static enum transport_io_result
|
|
irc_try_read (struct server *s)
|
|
{
|
|
enum transport_io_result result = s->transport->try_read (s);
|
|
if (result == TRANSPORT_IO_OK)
|
|
{
|
|
if (s->read_buffer.len >= (1 << 20))
|
|
{
|
|
// XXX: this is stupid; if anything, count it in dependence of time
|
|
log_server_error (s, s->buffer,
|
|
"The IRC server seems to spew out data frantically");
|
|
return TRANSPORT_IO_ERROR;
|
|
}
|
|
if (s->read_buffer.len)
|
|
irc_process_buffer (&s->read_buffer, irc_process_message, s);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static enum transport_io_result
|
|
irc_try_write (struct server *s)
|
|
{
|
|
enum transport_io_result result = s->transport->try_write (s);
|
|
if (result == TRANSPORT_IO_OK)
|
|
{
|
|
// If we're flushing the write buffer and our job is complete, we send
|
|
// an EOF to the server, changing the state to IRC_HALF_CLOSED
|
|
if (s->state == IRC_CLOSING && !s->write_buffer.len)
|
|
irc_real_shutdown (s);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
irc_try_read_write (struct server *s)
|
|
{
|
|
enum transport_io_result read_result;
|
|
enum transport_io_result write_result;
|
|
if ((read_result = irc_try_read (s)) == TRANSPORT_IO_ERROR
|
|
|| (write_result = irc_try_write (s)) == TRANSPORT_IO_ERROR)
|
|
{
|
|
log_server_error (s, s->buffer, "Server connection failed");
|
|
return false;
|
|
}
|
|
|
|
// FIXME: this may probably fire multiple times when we're flushing,
|
|
// we should probably store a flag next to the state
|
|
if (read_result == TRANSPORT_IO_EOF
|
|
|| write_result == TRANSPORT_IO_EOF)
|
|
log_server_error (s, s->buffer, "Server closed the connection");
|
|
|
|
// If the write needs to read and we receive an EOF, we can't flush
|
|
if (write_result == TRANSPORT_IO_EOF)
|
|
return false;
|
|
|
|
if (read_result == TRANSPORT_IO_EOF)
|
|
{
|
|
// Eventually initiate shutdown to flush the write buffer
|
|
irc_shutdown (s);
|
|
|
|
// If there's nothing to write, we can disconnect now
|
|
if (s->state == IRC_HALF_CLOSED)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
on_irc_ready (const struct pollfd *pfd, struct server *s)
|
|
{
|
|
if (irc_try_read_write (s))
|
|
{
|
|
// XXX: shouldn't we rather wait for PONG messages?
|
|
irc_reset_connection_timeouts (s);
|
|
irc_update_poller (s, pfd);
|
|
}
|
|
else
|
|
// We don't want to keep the socket anymore
|
|
irc_disconnect (s);
|
|
}
|
|
|
|
// --- Plain transport ---------------------------------------------------------
|
|
|
|
static enum transport_io_result
|
|
transport_plain_try_read (struct server *s)
|
|
{
|
|
struct str *buf = &s->read_buffer;
|
|
ssize_t n_read;
|
|
|
|
while (true)
|
|
{
|
|
str_ensure_space (buf, 512);
|
|
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';
|
|
continue;
|
|
}
|
|
if (n_read == 0)
|
|
return TRANSPORT_IO_EOF;
|
|
|
|
if (errno == EAGAIN)
|
|
return TRANSPORT_IO_OK;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LOG_LIBC_FAILURE ("recv");
|
|
return TRANSPORT_IO_ERROR;
|
|
}
|
|
}
|
|
|
|
static enum transport_io_result
|
|
transport_plain_try_write (struct server *s)
|
|
{
|
|
struct str *buf = &s->write_buffer;
|
|
ssize_t n_written;
|
|
|
|
while (buf->len)
|
|
{
|
|
n_written = send (s->socket, buf->str, buf->len, 0);
|
|
if (n_written >= 0)
|
|
{
|
|
str_remove_slice (buf, 0, n_written);
|
|
continue;
|
|
}
|
|
|
|
if (errno == EAGAIN)
|
|
return TRANSPORT_IO_OK;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
LOG_LIBC_FAILURE ("send");
|
|
return TRANSPORT_IO_ERROR;
|
|
}
|
|
return TRANSPORT_IO_OK;
|
|
}
|
|
|
|
static int
|
|
transport_plain_get_poll_events (struct server *s)
|
|
{
|
|
int events = POLLIN;
|
|
if (s->write_buffer.len)
|
|
events |= POLLOUT;
|
|
return events;
|
|
}
|
|
|
|
static struct transport g_transport_plain =
|
|
{
|
|
.try_read = transport_plain_try_read,
|
|
.try_write = transport_plain_try_write,
|
|
.get_poll_events = transport_plain_get_poll_events,
|
|
};
|
|
|
|
// --- TLS transport -----------------------------------------------------------
|
|
|
|
struct transport_tls_data
|
|
{
|
|
SSL_CTX *ssl_ctx; ///< SSL context
|
|
SSL *ssl; ///< SSL connection
|
|
bool ssl_rx_want_tx; ///< SSL_read() wants to write
|
|
bool ssl_tx_want_rx; ///< SSL_write() wants to read
|
|
};
|
|
|
|
/// The index in SSL_CTX user data for a reference to the server
|
|
static int g_transport_tls_data_index = -1;
|
|
|
|
static int
|
|
transport_tls_verify_callback (int preverify_ok, X509_STORE_CTX *ctx)
|
|
{
|
|
SSL *ssl = X509_STORE_CTX_get_ex_data
|
|
(ctx, SSL_get_ex_data_X509_STORE_CTX_idx ());
|
|
struct server *s = SSL_CTX_get_ex_data
|
|
(SSL_get_SSL_CTX (ssl), g_transport_tls_data_index);
|
|
|
|
X509 *cert = X509_STORE_CTX_get_current_cert (ctx);
|
|
char *subject = X509_NAME_oneline (X509_get_subject_name (cert), NULL, 0);
|
|
char *issuer = X509_NAME_oneline (X509_get_issuer_name (cert), NULL, 0);
|
|
|
|
log_server_status (s, s->buffer, "Certificate subject: #s", subject);
|
|
log_server_status (s, s->buffer, "Certificate issuer: #s", issuer);
|
|
|
|
if (!preverify_ok)
|
|
{
|
|
log_server_error (s, s->buffer,
|
|
"Certificate verification failed: #s",
|
|
X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx)));
|
|
}
|
|
|
|
free (subject);
|
|
free (issuer);
|
|
return preverify_ok;
|
|
}
|
|
|
|
static bool
|
|
transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
|
|
{
|
|
bool verify = get_config_boolean (s->config, "ssl_verify");
|
|
SSL_CTX_set_verify (ssl_ctx, verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE,
|
|
transport_tls_verify_callback);
|
|
|
|
if (g_transport_tls_data_index == -1)
|
|
g_transport_tls_data_index =
|
|
SSL_CTX_get_ex_new_index (0, "server", NULL, NULL, NULL);
|
|
SSL_CTX_set_ex_data (ssl_ctx, g_transport_tls_data_index, s);
|
|
|
|
const char *ciphers = get_config_string (s->config, "ssl_ciphers");
|
|
if (ciphers && !SSL_CTX_set_cipher_list (ssl_ctx, ciphers))
|
|
log_server_error (s, s->buffer,
|
|
"Failed to select any cipher from the cipher list");
|
|
SSL_CTX_set_mode (ssl_ctx,
|
|
SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
|
|
|
|
// Disable deprecated protocols (see RFC 7568)
|
|
SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
|
|
|
|
const char *ca_file = get_config_string (s->config, "ssl_ca_file");
|
|
const char *ca_path = get_config_string (s->config, "ssl_ca_path");
|
|
|
|
ERR_clear_error ();
|
|
|
|
struct error *error = NULL;
|
|
if (ca_file || ca_path)
|
|
{
|
|
if (SSL_CTX_load_verify_locations (ssl_ctx, ca_file, ca_path))
|
|
return true;
|
|
|
|
error_set (&error, "%s: %s",
|
|
"Failed to set locations for the CA certificate bundle",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
goto ca_error;
|
|
}
|
|
|
|
if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
|
|
{
|
|
error_set (&error, "%s: %s",
|
|
"Couldn't load the default CA certificate bundle",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
goto ca_error;
|
|
}
|
|
return true;
|
|
|
|
ca_error:
|
|
if (verify)
|
|
{
|
|
error_propagate (e, error);
|
|
return false;
|
|
}
|
|
|
|
// Only inform the user if we're not actually verifying
|
|
log_server_error (s, s->buffer, "#s", error->message);
|
|
error_free (error);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
transport_tls_init_cert (struct server *s, SSL *ssl, struct error **e)
|
|
{
|
|
const char *ssl_cert = get_config_string (s->config, "ssl_cert");
|
|
if (!ssl_cert)
|
|
return true;
|
|
|
|
ERR_clear_error ();
|
|
|
|
bool result = false;
|
|
char *path = resolve_filename (ssl_cert, resolve_relative_config_filename);
|
|
if (!path)
|
|
error_set (e, "%s: %s", "Cannot open file", ssl_cert);
|
|
// XXX: perhaps we should read the file ourselves for better messages
|
|
else if (!SSL_use_certificate_file (ssl, path, SSL_FILETYPE_PEM)
|
|
|| !SSL_use_PrivateKey_file (ssl, path, SSL_FILETYPE_PEM))
|
|
error_set (e, "%s: %s", "Setting the SSL client certificate failed",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
else
|
|
result = true;
|
|
free (path);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
transport_tls_init (struct server *s, struct error **e)
|
|
{
|
|
ERR_clear_error ();
|
|
|
|
struct error *error = NULL;
|
|
SSL_CTX *ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
|
|
if (!ssl_ctx)
|
|
goto error_ssl_1;
|
|
if (!transport_tls_init_ctx (s, ssl_ctx, &error))
|
|
goto error_ssl_2;
|
|
|
|
SSL *ssl = SSL_new (ssl_ctx);
|
|
if (!ssl)
|
|
goto error_ssl_2;
|
|
|
|
if (!transport_tls_init_cert (s, ssl, &error))
|
|
{
|
|
// XXX: is this a reason to abort the connection?
|
|
log_server_error (s, s->buffer, "#s", error->message);
|
|
error_free (error);
|
|
}
|
|
|
|
SSL_set_connect_state (ssl);
|
|
if (!SSL_set_fd (ssl, s->socket))
|
|
goto error_ssl_3;
|
|
|
|
struct transport_tls_data *data = xcalloc (1, sizeof *data);
|
|
data->ssl_ctx = ssl_ctx;
|
|
data->ssl = ssl;
|
|
|
|
// Forces a handshake even if neither side wants to transmit data
|
|
data->ssl_rx_want_tx = true;
|
|
|
|
s->transport_data = data;
|
|
return true;
|
|
|
|
error_ssl_3:
|
|
SSL_free (ssl);
|
|
error_ssl_2:
|
|
SSL_CTX_free (ssl_ctx);
|
|
error_ssl_1:
|
|
if (!error)
|
|
error_set (&error, "%s: %s", "Could not initialize TLS",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
|
|
error_propagate (e, error);
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
transport_tls_cleanup (struct server *s)
|
|
{
|
|
struct transport_tls_data *data = s->transport_data;
|
|
if (data->ssl)
|
|
SSL_free (data->ssl);
|
|
if (data->ssl_ctx)
|
|
SSL_CTX_free (data->ssl_ctx);
|
|
free (data);
|
|
}
|
|
|
|
static enum transport_io_result
|
|
transport_tls_try_read (struct server *s)
|
|
{
|
|
struct transport_tls_data *data = s->transport_data;
|
|
if (data->ssl_tx_want_rx)
|
|
return TRANSPORT_IO_OK;
|
|
|
|
struct str *buf = &s->read_buffer;
|
|
data->ssl_rx_want_tx = false;
|
|
while (true)
|
|
{
|
|
ERR_clear_error ();
|
|
str_ensure_space (buf, 512);
|
|
int n_read = SSL_read (data->ssl, buf->str + buf->len,
|
|
buf->alloc - buf->len - 1 /* null byte */);
|
|
|
|
const char *error_info = NULL;
|
|
switch (xssl_get_error (data->ssl, n_read, &error_info))
|
|
{
|
|
case SSL_ERROR_NONE:
|
|
buf->str[buf->len += n_read] = '\0';
|
|
continue;
|
|
case SSL_ERROR_ZERO_RETURN:
|
|
return TRANSPORT_IO_EOF;
|
|
case SSL_ERROR_WANT_READ:
|
|
return TRANSPORT_IO_OK;
|
|
case SSL_ERROR_WANT_WRITE:
|
|
data->ssl_rx_want_tx = true;
|
|
return TRANSPORT_IO_OK;
|
|
case XSSL_ERROR_TRY_AGAIN:
|
|
continue;
|
|
default:
|
|
LOG_FUNC_FAILURE ("SSL_read", error_info);
|
|
return TRANSPORT_IO_ERROR;
|
|
}
|
|
}
|
|
}
|
|
|
|
static enum transport_io_result
|
|
transport_tls_try_write (struct server *s)
|
|
{
|
|
struct transport_tls_data *data = s->transport_data;
|
|
if (data->ssl_rx_want_tx)
|
|
return TRANSPORT_IO_OK;
|
|
|
|
struct str *buf = &s->write_buffer;
|
|
data->ssl_tx_want_rx = false;
|
|
while (buf->len)
|
|
{
|
|
ERR_clear_error ();
|
|
int n_written = SSL_write (data->ssl, buf->str, buf->len);
|
|
|
|
const char *error_info = NULL;
|
|
switch (xssl_get_error (data->ssl, n_written, &error_info))
|
|
{
|
|
case SSL_ERROR_NONE:
|
|
str_remove_slice (buf, 0, n_written);
|
|
continue;
|
|
case SSL_ERROR_ZERO_RETURN:
|
|
return TRANSPORT_IO_EOF;
|
|
case SSL_ERROR_WANT_WRITE:
|
|
return TRANSPORT_IO_OK;
|
|
case SSL_ERROR_WANT_READ:
|
|
data->ssl_tx_want_rx = true;
|
|
return TRANSPORT_IO_OK;
|
|
case XSSL_ERROR_TRY_AGAIN:
|
|
continue;
|
|
default:
|
|
LOG_FUNC_FAILURE ("SSL_write", error_info);
|
|
return TRANSPORT_IO_ERROR;
|
|
}
|
|
}
|
|
return TRANSPORT_IO_OK;
|
|
}
|
|
|
|
static int
|
|
transport_tls_get_poll_events (struct server *s)
|
|
{
|
|
struct transport_tls_data *data = s->transport_data;
|
|
|
|
int events = POLLIN;
|
|
if (s->write_buffer.len || data->ssl_rx_want_tx)
|
|
events |= POLLOUT;
|
|
|
|
// While we're waiting for an opposite event, we ignore the original
|
|
if (data->ssl_rx_want_tx) events &= ~POLLIN;
|
|
if (data->ssl_tx_want_rx) events &= ~POLLOUT;
|
|
return events;
|
|
}
|
|
|
|
static void
|
|
transport_tls_in_before_shutdown (struct server *s)
|
|
{
|
|
struct transport_tls_data *data = s->transport_data;
|
|
(void) SSL_shutdown (data->ssl);
|
|
}
|
|
|
|
static struct transport g_transport_tls =
|
|
{
|
|
.init = transport_tls_init,
|
|
.cleanup = transport_tls_cleanup,
|
|
.try_read = transport_tls_try_read,
|
|
.try_write = transport_tls_try_write,
|
|
.get_poll_events = transport_tls_get_poll_events,
|
|
.in_before_shutdown = transport_tls_in_before_shutdown,
|
|
};
|
|
|
|
// --- Connection establishment ------------------------------------------------
|
|
|
|
static bool
|
|
irc_autofill_user_info (struct server *s, struct error **e)
|
|
{
|
|
const char *nicks = get_config_string (s->config, "nicks");
|
|
const char *username = get_config_string (s->config, "username");
|
|
const char *realname = get_config_string (s->config, "realname");
|
|
|
|
if (nicks && *nicks && username && *username && realname)
|
|
return true;
|
|
|
|
// Read POSIX user info and fill the configuration if needed
|
|
struct passwd *pwd = getpwuid (geteuid ());
|
|
if (!pwd)
|
|
FAIL ("cannot retrieve user information: %s", strerror (errno));
|
|
|
|
// FIXME: set_config_strings() writes errors on its own
|
|
if (!nicks || !*nicks)
|
|
set_config_string (s->config, "nicks", pwd->pw_name);
|
|
if (!username || !*username)
|
|
set_config_string (s->config, "username", pwd->pw_name);
|
|
|
|
// Not all systems have the GECOS field but the vast majority does
|
|
if (!realname)
|
|
{
|
|
char *gecos = pwd->pw_gecos;
|
|
|
|
// The first comma, if any, ends the user's real name
|
|
char *comma = strchr (gecos, ',');
|
|
if (comma)
|
|
*comma = '\0';
|
|
|
|
set_config_string (s->config, "realname", gecos);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static char *
|
|
irc_fetch_next_nickname (struct server *s)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (get_config_string (s->config, "nicks"), ',', &v);
|
|
|
|
char *result = NULL;
|
|
if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len)
|
|
result = xstrdup (v.vector[s->nick_counter++]);
|
|
if ((size_t) s->nick_counter >= v.len)
|
|
// Exhausted all nicknames
|
|
s->nick_counter = -1;
|
|
|
|
str_vector_free (&v);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
irc_register (struct server *s)
|
|
{
|
|
// Fill in user information automatically if needed
|
|
irc_autofill_user_info (s, NULL);
|
|
|
|
const char *username = get_config_string (s->config, "username");
|
|
const char *realname = get_config_string (s->config, "realname");
|
|
hard_assert (username && realname);
|
|
|
|
// Start IRCv3.1 capability negotiation;
|
|
// at worst the server will ignore this or send a harmless error message
|
|
irc_send (s, "CAP LS");
|
|
|
|
const char *password = get_config_string (s->config, "password");
|
|
if (password)
|
|
irc_send (s, "PASS :%s", password);
|
|
|
|
s->nick_counter = 0;
|
|
|
|
char *nickname = irc_fetch_next_nickname (s);
|
|
if (nickname)
|
|
irc_send (s, "NICK :%s", nickname);
|
|
else
|
|
log_server_error (s, s->buffer, "No nicks present in configuration");
|
|
free (nickname);
|
|
|
|
// IRC servers may ignore the last argument if it's empty
|
|
irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " ");
|
|
}
|
|
|
|
static void
|
|
irc_finish_connection (struct server *s, int socket)
|
|
{
|
|
struct app_context *ctx = s->ctx;
|
|
|
|
set_blocking (socket, false);
|
|
s->socket = socket;
|
|
s->transport = get_config_boolean (s->config, "ssl")
|
|
? &g_transport_tls
|
|
: &g_transport_plain;
|
|
|
|
struct error *e = NULL;
|
|
if (s->transport->init && !s->transport->init (s, &e))
|
|
{
|
|
log_server_error (s, s->buffer, "Connection failed: #s", e->message);
|
|
error_free (e);
|
|
|
|
xclose (s->socket);
|
|
s->socket = -1;
|
|
|
|
s->transport = NULL;
|
|
return;
|
|
}
|
|
|
|
log_server_status (s, s->buffer, "Connection established");
|
|
s->state = IRC_CONNECTED;
|
|
|
|
poller_fd_init (&s->socket_event, &ctx->poller, s->socket);
|
|
s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready;
|
|
s->socket_event.user_data = s;
|
|
|
|
irc_update_poller (s, NULL);
|
|
irc_reset_connection_timeouts (s);
|
|
irc_register (s);
|
|
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static void
|
|
irc_split_host_port (char *s, char **host, char **port)
|
|
{
|
|
char *colon = strrchr (s, ':');
|
|
if (colon)
|
|
{
|
|
*colon = '\0';
|
|
*port = ++colon;
|
|
}
|
|
else
|
|
*port = "6667";
|
|
|
|
// Unwrap IPv6 addresses in format_host_port_pair() format
|
|
size_t host_end = strlen (s) - 1;
|
|
if (*s == '[' && s[host_end] == ']')
|
|
s++[host_end] = '\0';
|
|
|
|
*host = s;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
irc_on_connector_connecting (void *user_data, const char *address)
|
|
{
|
|
struct server *s = user_data;
|
|
log_server_status (s, s->buffer, "Connecting to #s...", address);
|
|
}
|
|
|
|
static void
|
|
irc_on_connector_error (void *user_data, const char *error)
|
|
{
|
|
struct server *s = user_data;
|
|
log_server_error (s, s->buffer, "Connection failed: #s", error);
|
|
}
|
|
|
|
static void
|
|
irc_on_connector_failure (void *user_data)
|
|
{
|
|
struct server *s = user_data;
|
|
irc_destroy_connector (s);
|
|
irc_queue_reconnect (s);
|
|
}
|
|
|
|
static void
|
|
irc_on_connector_connected (void *user_data, int socket)
|
|
{
|
|
struct server *s = user_data;
|
|
irc_destroy_connector (s);
|
|
irc_finish_connection (s, socket);
|
|
}
|
|
|
|
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);
|
|
s->connector = connector;
|
|
|
|
connector->user_data = s;
|
|
connector->on_connecting = irc_on_connector_connecting;
|
|
connector->on_error = irc_on_connector_error;
|
|
connector->on_connected = irc_on_connector_connected;
|
|
connector->on_failure = irc_on_connector_failure;
|
|
|
|
for (size_t i = 0; i < addresses->len; i++)
|
|
{
|
|
char *host, *port;
|
|
irc_split_host_port (addresses->vector[i], &host, &port);
|
|
|
|
if (!connector_add_target (connector, host, port, e))
|
|
return false;
|
|
}
|
|
|
|
connector_step (connector);
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// TODO: see if we can further merge code for the two connectors, for example
|
|
// by making SOCKS 4A and 5 mere plugins for the connector, or by using
|
|
// a virtual interface common to them both (seems more likely)
|
|
|
|
static void
|
|
irc_on_socks_connecting (void *user_data,
|
|
const char *address, const char *via, const char *version)
|
|
{
|
|
struct server *s = user_data;
|
|
log_server_status (s, s->buffer,
|
|
"Connecting to #s via #s (#s)...", address, via, version);
|
|
}
|
|
|
|
static bool
|
|
irc_setup_connector_socks (struct server *s,
|
|
const struct str_vector *addresses, struct error **e)
|
|
{
|
|
const char *socks_host = get_config_string (s->config, "socks_host");
|
|
int64_t socks_port_int = get_config_integer (s->config, "socks_port");
|
|
|
|
if (!socks_host)
|
|
return false;
|
|
|
|
struct socks_connector *connector = xmalloc (sizeof *connector);
|
|
socks_connector_init (connector, &s->ctx->poller);
|
|
s->socks_conn = connector;
|
|
|
|
// FIXME: the SOCKS connector may outlive these values
|
|
connector->hostname = socks_host;
|
|
// FIXME: memory leak
|
|
connector->service = xstrdup_printf ("%" PRIi64, socks_port_int);
|
|
connector->username = get_config_string (s->config, "socks_username");
|
|
connector->password = get_config_string (s->config, "socks_password");
|
|
|
|
connector->user_data = s;
|
|
connector->on_connecting = irc_on_socks_connecting;
|
|
connector->on_error = irc_on_connector_error;
|
|
connector->on_connected = irc_on_connector_connected;
|
|
connector->on_failure = irc_on_connector_failure;
|
|
|
|
for (size_t i = 0; i < addresses->len; i++)
|
|
{
|
|
char *host, *port;
|
|
irc_split_host_port (addresses->vector[i], &host, &port);
|
|
|
|
if (!socks_connector_add_target (connector, host, port, e))
|
|
return false;
|
|
}
|
|
|
|
socks_connector_run (connector);
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
irc_initiate_connect (struct server *s)
|
|
{
|
|
hard_assert (s->state == IRC_DISCONNECTED);
|
|
|
|
const char *addresses = get_config_string (s->config, "addresses");
|
|
if (!addresses || !addresses[strspn (addresses, ",")])
|
|
{
|
|
// No sense in trying to reconnect
|
|
log_server_error (s, s->buffer,
|
|
"No addresses specified in configuration");
|
|
return;
|
|
}
|
|
|
|
struct str_vector servers;
|
|
str_vector_init (&servers);
|
|
cstr_split_ignore_empty (addresses, ',', &servers);
|
|
|
|
struct error *e = NULL;
|
|
if (!irc_setup_connector_socks (s, &servers, &e) && !e)
|
|
irc_setup_connector (s, &servers, &e);
|
|
|
|
str_vector_free (&servers);
|
|
|
|
if (!e)
|
|
s->state = IRC_CONNECTING;
|
|
else
|
|
{
|
|
irc_destroy_connector (s);
|
|
|
|
log_server_error (s, s->buffer, "#s", e->message);
|
|
error_free (e);
|
|
irc_queue_reconnect (s);
|
|
}
|
|
}
|
|
|
|
// --- Input prompt ------------------------------------------------------------
|
|
|
|
static void
|
|
make_unseen_prefix (struct app_context *ctx, struct str *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, ',');
|
|
if (iter->highlighted)
|
|
str_append_c (active_buffers, '!');
|
|
str_append_printf (active_buffers, "%zu", i);
|
|
}
|
|
}
|
|
|
|
static void
|
|
make_chanmode_postfix (struct channel *channel, struct str *modes)
|
|
{
|
|
if (channel->no_param_modes.len)
|
|
str_append (modes, channel->no_param_modes.str);
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &channel->param_modes);
|
|
|
|
char *param;
|
|
while ((param = str_map_iter_next (&iter)))
|
|
str_append_c (modes, iter.link->key[0]);
|
|
}
|
|
|
|
static void
|
|
make_server_postfix (struct buffer *buffer, struct str *output)
|
|
{
|
|
struct server *s = buffer->server;
|
|
str_append_c (output, ' ');
|
|
if (!irc_is_connected (s))
|
|
str_append (output, "(disconnected)");
|
|
else if (s->state != IRC_REGISTERED)
|
|
str_append (output, "(unregistered)");
|
|
else
|
|
{
|
|
if (buffer->type == BUFFER_CHANNEL)
|
|
{
|
|
struct channel_user *channel_user =
|
|
irc_channel_get_user (buffer->channel, s->irc_user);
|
|
if (channel_user)
|
|
str_append (output, channel_user->prefixes.str);
|
|
}
|
|
str_append (output, s->irc_user->nickname);
|
|
if (s->irc_user_mode.len)
|
|
str_append_printf (output, "(%s)", s->irc_user_mode.str);
|
|
}
|
|
}
|
|
static void
|
|
make_prompt (struct app_context *ctx, struct str *output)
|
|
{
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
if (!buffer)
|
|
return;
|
|
|
|
str_append_c (output, '[');
|
|
|
|
struct str active_buffers;
|
|
str_init (&active_buffers);
|
|
make_unseen_prefix (ctx, &active_buffers);
|
|
if (active_buffers.len)
|
|
str_append_printf (output, "(%s) ", active_buffers.str);
|
|
str_free (&active_buffers);
|
|
|
|
str_append_printf (output, "%d:%s",
|
|
buffer_get_index (ctx, buffer), buffer->name);
|
|
if (buffer->type == BUFFER_CHANNEL)
|
|
{
|
|
struct str modes;
|
|
str_init (&modes);
|
|
make_chanmode_postfix (buffer->channel, &modes);
|
|
if (modes.len)
|
|
str_append_printf (output, "(+%s)", modes.str);
|
|
str_free (&modes);
|
|
}
|
|
|
|
if (buffer != ctx->global_buffer)
|
|
make_server_postfix (buffer, output);
|
|
|
|
str_append_c (output, ']');
|
|
}
|
|
|
|
static void
|
|
refresh_prompt (struct app_context *ctx)
|
|
{
|
|
bool have_attributes = !!get_attribute_printer (stdout);
|
|
|
|
struct str prompt;
|
|
str_init (&prompt);
|
|
make_prompt (ctx, &prompt);
|
|
str_append_c (&prompt, ' ');
|
|
char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
|
|
str_free (&prompt);
|
|
|
|
if (have_attributes)
|
|
{
|
|
// XXX: to be completely correct, we should use tputs, but we cannot
|
|
input_set_prompt (&ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c",
|
|
INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
|
|
INPUT_END_IGNORE,
|
|
localized,
|
|
INPUT_START_IGNORE, ctx->attrs[ATTR_RESET],
|
|
INPUT_END_IGNORE));
|
|
free (localized);
|
|
}
|
|
else
|
|
input_set_prompt (&ctx->input, localized);
|
|
}
|
|
|
|
// --- Helpers -----------------------------------------------------------------
|
|
|
|
static struct buffer *
|
|
irc_get_buffer_for_message (struct server *s,
|
|
const struct irc_message *msg, const char *target)
|
|
{
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
|
|
if (irc_is_channel (s, target))
|
|
{
|
|
struct channel *channel = str_map_find (&s->irc_channels, target);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// This is weird
|
|
if (!channel)
|
|
return NULL;
|
|
}
|
|
else if (!buffer)
|
|
{
|
|
// Don't make user buffers for servers (they can send NOTICEs)
|
|
if (!irc_find_userhost (msg->prefix))
|
|
return s->buffer;
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
if (irc_is_this_us (s, target))
|
|
buffer = irc_get_or_make_user_buffer (s, nickname);
|
|
free (nickname);
|
|
|
|
// With the IRCv3.2 echo-message capability, we can receive messages
|
|
// as they are delivered to the target; in that case we return NULL
|
|
// and the caller should check the origin
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
static bool
|
|
irc_is_highlight (struct server *s, const char *message)
|
|
{
|
|
// This may be called by notices before even successfully registering
|
|
if (!s->irc_user)
|
|
return false;
|
|
|
|
// Well, this is rather crude but it should make most users happy.
|
|
// Ideally we could do this at least in proper Unicode.
|
|
char *copy = xstrdup (message);
|
|
cstr_transform (copy, s->irc_tolower);
|
|
|
|
char *nick = xstrdup (s->irc_user->nickname);
|
|
cstr_transform (nick, s->irc_tolower);
|
|
|
|
// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
|
|
// Also excluded from the ASCII: common user channel prefixes: +%@&~
|
|
const char *delimiters = ",.;:!?()<>/=#$* \t\r\n\v\f\"'";
|
|
|
|
bool result = false;
|
|
char *save = NULL;
|
|
for (char *token = strtok_r (copy, delimiters, &save);
|
|
token; token = strtok_r (NULL, delimiters, &save))
|
|
if (!strcmp (token, nick))
|
|
{
|
|
result = true;
|
|
break;
|
|
}
|
|
|
|
free (copy);
|
|
free (nick);
|
|
return result;
|
|
}
|
|
|
|
static const char *
|
|
irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
|
|
{
|
|
if (user && irc_is_channel (s, target))
|
|
{
|
|
struct channel *channel;
|
|
struct channel_user *channel_user;
|
|
if ((channel = str_map_find (&s->irc_channels, target))
|
|
&& (channel_user = irc_channel_get_user (channel, user)))
|
|
return channel_user->prefixes.str;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// --- Mode processor ----------------------------------------------------------
|
|
|
|
struct mode_processor
|
|
{
|
|
char **params; ///< Mode string parameters
|
|
bool adding; ///< Currently adding modes
|
|
char mode_char; ///< Currently processed mode char
|
|
|
|
// User data:
|
|
|
|
struct server *s; ///< Server
|
|
struct channel *channel; ///< The channel being modified
|
|
};
|
|
|
|
/// Process a single mode character
|
|
typedef bool (*mode_processor_apply_fn) (struct mode_processor *);
|
|
|
|
static const char *
|
|
mode_processor_next_param (struct mode_processor *self)
|
|
{
|
|
if (!*self->params)
|
|
return NULL;
|
|
return *self->params++;
|
|
}
|
|
|
|
static void
|
|
mode_processor_run (struct mode_processor *self,
|
|
char **params, mode_processor_apply_fn apply_cb)
|
|
{
|
|
self->params = params;
|
|
|
|
const char *mode_string;
|
|
while ((mode_string = mode_processor_next_param (self)))
|
|
{
|
|
self->adding = true;
|
|
while ((self->mode_char = *mode_string++))
|
|
{
|
|
if (self->mode_char == '+') self->adding = true;
|
|
else if (self->mode_char == '-') self->adding = false;
|
|
else if (!apply_cb (self))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
mode_char_cmp (const void *a, const void *b)
|
|
{
|
|
return *(const char *) a - *(const char *) b;
|
|
}
|
|
|
|
/// Add/remove the current mode character to/from the given ordered list
|
|
static void
|
|
mode_processor_toggle (struct mode_processor *self, struct str *modes)
|
|
{
|
|
const char *pos = strchr (modes->str, self->mode_char);
|
|
if (self->adding == !!pos)
|
|
return;
|
|
|
|
if (self->adding)
|
|
{
|
|
str_append_c (modes, self->mode_char);
|
|
qsort (modes->str, modes->len, 1, mode_char_cmp);
|
|
}
|
|
else
|
|
str_remove_slice (modes, pos - modes->str, 1);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
mode_processor_do_user (struct mode_processor *self)
|
|
{
|
|
const char *nickname;
|
|
struct user *user;
|
|
struct channel_user *channel_user;
|
|
if (!(nickname = mode_processor_next_param (self))
|
|
|| !(user = str_map_find (&self->s->irc_users, nickname))
|
|
|| !(channel_user = irc_channel_get_user (self->channel, user)))
|
|
return;
|
|
|
|
const char *all_prefixes = self->s->irc_chanuser_prefixes;
|
|
const char *all_modes = self->s->irc_chanuser_modes;
|
|
char prefix = all_prefixes[strchr (all_modes, self->mode_char) - all_modes];
|
|
|
|
struct str *prefixes = &channel_user->prefixes;
|
|
const char *pos = strchr (prefixes->str, prefix);
|
|
if (self->adding == !!pos)
|
|
return;
|
|
|
|
if (self->adding)
|
|
{
|
|
// Add the new mode prefix while retaining the right order
|
|
char *old_prefixes = str_steal (prefixes);
|
|
str_init (prefixes);
|
|
for (const char *p = all_prefixes; *p; p++)
|
|
if (*p == prefix || strchr (old_prefixes, *p))
|
|
str_append_c (prefixes, *p);
|
|
free (old_prefixes);
|
|
}
|
|
else
|
|
str_remove_slice (prefixes, pos - prefixes->str, 1);
|
|
}
|
|
|
|
static void
|
|
mode_processor_do_param_always (struct mode_processor *self)
|
|
{
|
|
const char *param = NULL;
|
|
if (!(param = mode_processor_next_param (self)))
|
|
return;
|
|
|
|
char key[2] = { self->mode_char, 0 };
|
|
str_map_set (&self->channel->param_modes, key,
|
|
self->adding ? xstrdup (param) : NULL);
|
|
}
|
|
|
|
static void
|
|
mode_processor_do_param_when_set (struct mode_processor *self)
|
|
{
|
|
const char *param = NULL;
|
|
if (self->adding && !(param = mode_processor_next_param (self)))
|
|
return;
|
|
|
|
char key[2] = { self->mode_char, 0 };
|
|
str_map_set (&self->channel->param_modes, key,
|
|
self->adding ? xstrdup (param) : NULL);
|
|
}
|
|
|
|
static bool
|
|
mode_processor_apply_channel (struct mode_processor *self)
|
|
{
|
|
if (strchr (self->s->irc_chanuser_modes, self->mode_char))
|
|
mode_processor_do_user (self);
|
|
else if (strchr (self->s->irc_chanmodes_list, self->mode_char))
|
|
// Nothing to do here, just skip the next argument if there's any
|
|
(void) mode_processor_next_param (self);
|
|
else if (strchr (self->s->irc_chanmodes_param_always, self->mode_char))
|
|
mode_processor_do_param_always (self);
|
|
else if (strchr (self->s->irc_chanmodes_param_when_set, self->mode_char))
|
|
mode_processor_do_param_when_set (self);
|
|
else if (strchr (self->s->irc_chanmodes_param_never, self->mode_char))
|
|
mode_processor_toggle (self, &self->channel->no_param_modes);
|
|
else
|
|
// It's not safe to continue, results could be undesired
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
irc_handle_mode_channel
|
|
(struct server *s, struct channel *channel, char **params)
|
|
{
|
|
struct mode_processor p = { .s = s, .channel = channel };
|
|
mode_processor_run (&p, params, mode_processor_apply_channel);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
mode_processor_apply_user (struct mode_processor *self)
|
|
{
|
|
mode_processor_toggle (self, &self->s->irc_user_mode);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
irc_handle_mode_user (struct server *s, char **params)
|
|
{
|
|
struct mode_processor p = { .s = s };
|
|
mode_processor_run (&p, params, mode_processor_apply_user);
|
|
}
|
|
|
|
// --- Input handling ----------------------------------------------------------
|
|
|
|
static void
|
|
irc_handle_cap (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
|
|
const char *args = "";
|
|
if (msg->params.len > 2)
|
|
cstr_split_ignore_empty ((args = msg->params.vector[2]), ' ', &v);
|
|
|
|
const char *subcommand = msg->params.vector[1];
|
|
if (!strcasecmp_ascii (subcommand, "ACK"))
|
|
{
|
|
log_server_status (s, s->buffer,
|
|
"#s: #S", "Capabilities acknowledged", args);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
{
|
|
const char *cap = v.vector[i];
|
|
bool active = true;
|
|
if (*cap == '-')
|
|
{
|
|
active = false;
|
|
cap++;
|
|
}
|
|
if (!strcasecmp_ascii (cap, "echo-message"))
|
|
s->cap_echo_message = active;
|
|
}
|
|
irc_send (s, "CAP END");
|
|
}
|
|
else if (!strcasecmp_ascii (subcommand, "NAK"))
|
|
{
|
|
log_server_error (s, s->buffer,
|
|
"#s: #S", "Capabilities not acknowledged", args);
|
|
irc_send (s, "CAP END");
|
|
}
|
|
else if (!strcasecmp_ascii (subcommand, "LS"))
|
|
{
|
|
log_server_status (s, s->buffer,
|
|
"#s: #S", "Capabilities supported", args);
|
|
|
|
struct str_vector chosen;
|
|
str_vector_init (&chosen);
|
|
|
|
// Filter server capabilities for ones we can make use of
|
|
for (size_t i = 0; i < v.len; i++)
|
|
{
|
|
const char *cap = v.vector[i];
|
|
if (!strcasecmp_ascii (cap, "multi-prefix")
|
|
|| !strcasecmp_ascii (cap, "invite-notify")
|
|
|| !strcasecmp_ascii (cap, "echo-message"))
|
|
str_vector_add (&chosen, cap);
|
|
}
|
|
|
|
char *chosen_str = join_str_vector (&chosen, ' ');
|
|
str_vector_free (&chosen);
|
|
irc_send (s, "CAP REQ :%s", chosen_str);
|
|
log_server_status (s, s->buffer,
|
|
"#s: #S", "Capabilities requested", chosen_str);
|
|
free (chosen_str);
|
|
}
|
|
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static void
|
|
irc_handle_invite (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
const char *target = msg->params.vector[0];
|
|
const char *channel_name = msg->params.vector[1];
|
|
|
|
struct buffer *buffer;
|
|
if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
|
|
buffer = s->buffer;
|
|
|
|
// IRCv3.2 invite-notify extension allows the target to be someone else
|
|
if (irc_is_this_us (s, target))
|
|
log_server_status (s, buffer,
|
|
"#n has invited you to #S", msg->prefix, channel_name);
|
|
else
|
|
log_server_status (s, buffer,
|
|
"#n has invited #n to #S", msg->prefix, target, channel_name);
|
|
}
|
|
|
|
static void
|
|
irc_handle_join (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
if (!irc_is_channel (s, channel_name))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// We've joined a new channel
|
|
if (!channel && irc_is_this_us (s, msg->prefix))
|
|
{
|
|
buffer = buffer_new ();
|
|
buffer->type = BUFFER_CHANNEL;
|
|
buffer->name = xstrdup_printf ("%s.%s", s->name, channel_name);
|
|
buffer->server = s;
|
|
buffer->channel = channel =
|
|
irc_make_channel (s, xstrdup (channel_name));
|
|
str_map_set (&s->irc_buffer_map, channel->name, buffer);
|
|
|
|
buffer_add (s->ctx, buffer);
|
|
buffer_activate (s->ctx, buffer);
|
|
|
|
// Request the channel mode as we don't get it automatically
|
|
irc_send (s, "MODE %s", channel_name);
|
|
}
|
|
|
|
// This is weird, ignoring
|
|
if (!channel)
|
|
return;
|
|
|
|
// Reset the field so that we rejoin the channel after reconnecting
|
|
channel->left_manually = false;
|
|
|
|
// Add the user to the channel
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
irc_channel_link_user (channel, irc_get_or_make_user (s, nickname), "");
|
|
free (nickname);
|
|
|
|
// Finally log the message
|
|
if (buffer)
|
|
{
|
|
log_server (s, buffer, 0, "#a-->#r #N #a#s#r #S",
|
|
ATTR_JOIN, msg->prefix, ATTR_JOIN, "has joined", channel_name);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_kick (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
const char *target = msg->params.vector[1];
|
|
if (!irc_is_channel (s, channel_name)
|
|
|| irc_is_channel (s, target))
|
|
return;
|
|
|
|
const char *message = NULL;
|
|
if (msg->params.len > 2)
|
|
message = msg->params.vector[2];
|
|
|
|
struct user *user = str_map_find (&s->irc_users, target);
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be 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)
|
|
{
|
|
struct formatter f;
|
|
formatter_init (&f, s->ctx, s);
|
|
formatter_add (&f, "#a<--#r #N #a#s#r #n",
|
|
ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target);
|
|
if (message)
|
|
formatter_add (&f, " (#m)", message);
|
|
log_formatter (s->ctx, buffer, 0, &f);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_mode (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
return;
|
|
|
|
const char *context = msg->params.vector[0];
|
|
|
|
// Join the modes back to a single string
|
|
struct str_vector copy;
|
|
str_vector_init (©);
|
|
str_vector_add_vector (©, msg->params.vector + 1);
|
|
char *modes = join_str_vector (©, ' ');
|
|
str_vector_free (©);
|
|
|
|
if (irc_is_channel (s, context))
|
|
{
|
|
struct channel *channel = str_map_find (&s->irc_channels, context);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, context);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
if (channel)
|
|
irc_handle_mode_channel (s, channel, msg->params.vector + 1);
|
|
|
|
if (buffer)
|
|
{
|
|
log_server_status (s, buffer,
|
|
"Mode #S [#S] by #n", context, modes, msg->prefix);
|
|
}
|
|
}
|
|
else if (irc_is_this_us (s, context))
|
|
{
|
|
irc_handle_mode_user (s, msg->params.vector + 1);
|
|
log_server_status (s, s->buffer,
|
|
"User mode [#S] by #n", modes, msg->prefix);
|
|
}
|
|
|
|
free (modes);
|
|
|
|
// Our own modes might have changed
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static void
|
|
irc_handle_nick (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
return;
|
|
|
|
const char *new_nickname = msg->params.vector[0];
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
free (nickname);
|
|
if (!user)
|
|
return;
|
|
|
|
bool lexicographically_different =
|
|
!!irc_server_strcmp (s, user->nickname, new_nickname);
|
|
|
|
// What the fuck, someone renamed themselves to ourselves
|
|
// TODO: probably log a message and force a reconnect
|
|
if (lexicographically_different
|
|
&& !irc_server_strcmp (s, new_nickname, s->irc_user->nickname))
|
|
return;
|
|
|
|
// Log a message in any PM buffer (we may even have one for ourselves)
|
|
struct buffer *pm_buffer =
|
|
str_map_find (&s->irc_buffer_map, user->nickname);
|
|
if (pm_buffer)
|
|
{
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
log_nick_self (s, pm_buffer, new_nickname);
|
|
else
|
|
log_nick (s, pm_buffer, msg->prefix, new_nickname);
|
|
}
|
|
|
|
// The new nickname may collide with a user referenced by a PM buffer,
|
|
// or in case of data inconsistency with the server, channels.
|
|
// In the latter case we need the colliding user to leave all of them.
|
|
struct user *user_collision = NULL;
|
|
if (lexicographically_different
|
|
&& (user_collision = str_map_find (&s->irc_users, new_nickname)))
|
|
LIST_FOR_EACH (struct user_channel, iter, user_collision->channels)
|
|
irc_remove_user_from_channel (user_collision, iter->channel);
|
|
|
|
struct buffer *buffer_collision = NULL;
|
|
if (lexicographically_different
|
|
&& (buffer_collision = str_map_find (&s->irc_buffer_map, new_nickname)))
|
|
{
|
|
hard_assert (buffer_collision->type == BUFFER_PM);
|
|
hard_assert (buffer_collision->user == user_collision);
|
|
|
|
user_unref (buffer_collision->user);
|
|
buffer_collision->user = user_ref (user);
|
|
}
|
|
|
|
if (pm_buffer && buffer_collision)
|
|
{
|
|
// There's not much else we can do other than somehow try to merge
|
|
// one buffer into the other. In our case, the original buffer wins.
|
|
buffer_merge (s->ctx, buffer_collision, pm_buffer);
|
|
if (s->ctx->current_buffer == pm_buffer)
|
|
buffer_activate (s->ctx, buffer_collision);
|
|
buffer_remove (s->ctx, pm_buffer);
|
|
pm_buffer = buffer_collision;
|
|
}
|
|
|
|
// The colliding user should be completely gone by now
|
|
if (lexicographically_different)
|
|
hard_assert (!str_map_find (&s->irc_users, new_nickname));
|
|
|
|
// Now we can rename the PM buffer to reflect the new nickname
|
|
if (pm_buffer)
|
|
{
|
|
str_map_set (&s->irc_buffer_map, user->nickname, NULL);
|
|
str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);
|
|
|
|
char *x = xstrdup_printf ("%s.%s", s->name, new_nickname);
|
|
buffer_rename (s->ctx, pm_buffer, x);
|
|
free (x);
|
|
}
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
{
|
|
log_nick_self (s, s->buffer, new_nickname);
|
|
|
|
// Log a message in all open buffers on this server
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_buffer_map);
|
|
struct buffer *buffer;
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
{
|
|
// We've already done that
|
|
if (buffer != pm_buffer)
|
|
log_nick_self (s, buffer, new_nickname);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Log a message in all channels the user is in
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
{
|
|
struct buffer *buffer =
|
|
str_map_find (&s->irc_buffer_map, iter->channel->name);
|
|
hard_assert (buffer != NULL);
|
|
log_nick (s, buffer, msg->prefix, new_nickname);
|
|
}
|
|
}
|
|
|
|
// Finally rename the user as it should be safe now
|
|
str_map_set (&s->irc_users, user->nickname, NULL);
|
|
str_map_set (&s->irc_users, new_nickname, user);
|
|
|
|
free (user->nickname);
|
|
user->nickname = xstrdup (new_nickname);
|
|
|
|
// We might have renamed ourselves
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static void
|
|
irc_handle_ctcp_reply (struct server *s,
|
|
const struct irc_message *msg, struct ctcp_chunk *chunk)
|
|
{
|
|
const char *target = msg->params.vector[0];
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
log_ctcp_reply (s, target,
|
|
xstrdup_printf ("%s %s", chunk->tag.str, chunk->text.str));
|
|
else
|
|
log_server_status (s, s->buffer, "CTCP reply from #n: #S #S",
|
|
msg->prefix, chunk->tag.str, chunk->text.str);
|
|
}
|
|
|
|
static void
|
|
irc_handle_notice_text (struct server *s,
|
|
const struct irc_message *msg, struct str *text)
|
|
{
|
|
const char *target = msg->params.vector[0];
|
|
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
|
|
if (!buffer)
|
|
{
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
log_outcoming_orphan_notice (s, target, text->str);
|
|
return;
|
|
}
|
|
|
|
char *nick = irc_cut_nickname (msg->prefix);
|
|
// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
|
|
if (!irc_is_this_us (s, msg->prefix) && irc_is_highlight (s, text->str))
|
|
log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_HIGHLIGHT,
|
|
"#a#s(#S)#r: #m", ATTR_HIGHLIGHT, "Notice", nick, text->str);
|
|
else
|
|
log_outcoming_notice (s, buffer, msg->prefix, text->str);
|
|
free (nick);
|
|
}
|
|
|
|
static void
|
|
irc_handle_notice (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
// This ignores empty messages which we should never receive anyway
|
|
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
|
|
if (!iter->is_extended)
|
|
irc_handle_notice_text (s, msg, &iter->text);
|
|
else
|
|
irc_handle_ctcp_reply (s, msg, iter);
|
|
ctcp_destroy (chunks);
|
|
}
|
|
|
|
static void
|
|
irc_handle_part (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
if (!irc_is_channel (s, channel_name))
|
|
return;
|
|
|
|
const char *message = NULL;
|
|
if (msg->params.len > 1)
|
|
message = msg->params.vector[1];
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
free (nickname);
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be is weird for this to be false
|
|
if (user && channel)
|
|
{
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
irc_left_channel (channel);
|
|
else
|
|
irc_remove_user_from_channel (user, channel);
|
|
}
|
|
|
|
if (buffer)
|
|
{
|
|
struct formatter f;
|
|
formatter_init (&f, s->ctx, s);
|
|
formatter_add (&f, "#a<--#r #N #a#s#r #S",
|
|
ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name);
|
|
if (message)
|
|
formatter_add (&f, " (#m)", message);
|
|
log_formatter (s->ctx, buffer, 0, &f);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_ping (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len)
|
|
irc_send (s, "PONG :%s", msg->params.vector[0]);
|
|
else
|
|
irc_send (s, "PONG");
|
|
}
|
|
|
|
static char *
|
|
ctime_now (char buf[26])
|
|
{
|
|
struct tm tm_;
|
|
time_t now = time (NULL);
|
|
if (!asctime_r (localtime_r (&now, &tm_), buf))
|
|
return NULL;
|
|
|
|
// Annoying thing
|
|
*strchr (buf, '\n') = '\0';
|
|
return buf;
|
|
}
|
|
|
|
static void irc_send_ctcp_reply (struct server *s, const char *recipient,
|
|
const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
|
|
|
|
static void
|
|
irc_send_ctcp_reply (struct server *s,
|
|
const char *recipient, const char *format, ...)
|
|
{
|
|
struct str m;
|
|
str_init (&m);
|
|
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
str_append_vprintf (&m, format, ap);
|
|
va_end (ap);
|
|
|
|
irc_send (s, "NOTICE %s :\x01%s\x01", recipient, m.str);
|
|
if (!s->cap_echo_message)
|
|
log_ctcp_reply (s, recipient, str_steal (&m));
|
|
else
|
|
str_free (&m);
|
|
}
|
|
|
|
static void
|
|
irc_handle_ctcp_request (struct server *s,
|
|
const struct irc_message *msg, struct ctcp_chunk *chunk)
|
|
{
|
|
const char *target = msg->params.vector[0];
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
log_ctcp_query (s, target, chunk->tag.str);
|
|
|
|
log_server_status (s, s->buffer,
|
|
"CTCP requested by #n: #S", msg->prefix, chunk->tag.str);
|
|
|
|
char *recipient = irc_is_channel (s, target)
|
|
? irc_cut_nickname (msg->prefix)
|
|
: xstrdup (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 (recipient);
|
|
}
|
|
|
|
static void
|
|
irc_handle_privmsg_text (struct server *s,
|
|
const struct irc_message *msg, struct str *text, bool is_action)
|
|
{
|
|
const char *target = msg->params.vector[0];
|
|
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
|
|
if (!buffer)
|
|
{
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
log_outcoming_orphan_privmsg (s, target, text->str);
|
|
return;
|
|
}
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
const char *prefixes = irc_get_privmsg_prefix
|
|
(s, str_map_find (&s->irc_users, nickname), target);
|
|
|
|
// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
|
|
if (irc_is_this_us (s, msg->prefix) || !irc_is_highlight (s, text->str))
|
|
{
|
|
if (is_action)
|
|
log_outcoming_action (s, buffer, nickname, text->str);
|
|
else
|
|
log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str);
|
|
}
|
|
else if (is_action)
|
|
log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
|
|
" #a*#r #n #m", ATTR_HIGHLIGHT, msg->prefix, text->str);
|
|
else
|
|
log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
|
|
"#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);
|
|
|
|
free (nickname);
|
|
}
|
|
|
|
static void
|
|
irc_handle_privmsg (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
// This ignores empty messages which we should never receive anyway
|
|
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
|
|
if (!iter->is_extended)
|
|
irc_handle_privmsg_text (s, msg, &iter->text, false);
|
|
else if (!strcmp (iter->tag.str, "ACTION"))
|
|
irc_handle_privmsg_text (s, msg, &iter->text, true);
|
|
else
|
|
irc_handle_ctcp_request (s, msg, iter);
|
|
ctcp_destroy (chunks);
|
|
}
|
|
|
|
static void
|
|
log_quit (struct server *s,
|
|
struct buffer *buffer, const char *prefix, const char *reason)
|
|
{
|
|
struct formatter f;
|
|
formatter_init (&f, s->ctx, s);
|
|
formatter_add (&f, "#a<--#r #N #a#s#r",
|
|
ATTR_PART, prefix, ATTR_PART, "has quit");
|
|
if (reason)
|
|
formatter_add (&f, " (#m)", reason);
|
|
log_formatter (s->ctx, buffer, 0, &f);
|
|
}
|
|
|
|
static void
|
|
irc_handle_quit (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix)
|
|
return;
|
|
|
|
// What the fuck, the server never sends this back
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
return;
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
free (nickname);
|
|
if (!user)
|
|
return;
|
|
|
|
const char *message = NULL;
|
|
if (msg->params.len > 0)
|
|
message = msg->params.vector[0];
|
|
|
|
// Log a message in any PM buffer
|
|
struct buffer *buffer =
|
|
str_map_find (&s->irc_buffer_map, user->nickname);
|
|
if (buffer)
|
|
{
|
|
log_quit (s, buffer, msg->prefix, message);
|
|
|
|
// TODO: set some kind of a flag in the buffer and when the user
|
|
// reappears on a channel (JOIN), log a "is back online" message.
|
|
// Also set this flag when we receive a "no such nick" numeric
|
|
// and reset it when we send something to the buffer.
|
|
}
|
|
|
|
// Log a message in all channels the user is in
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
{
|
|
if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name)))
|
|
log_quit (s, buffer, msg->prefix, message);
|
|
|
|
// This destroys "iter" which doesn't matter to us
|
|
irc_remove_user_from_channel (user, iter->channel);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_topic (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
const char *topic = msg->params.vector[1];
|
|
if (!irc_is_channel (s, channel_name))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be is weird for this to be false
|
|
if (channel)
|
|
{
|
|
free (channel->topic);
|
|
channel->topic = xstrdup (topic);
|
|
}
|
|
|
|
if (buffer)
|
|
{
|
|
log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"",
|
|
msg->prefix, "has changed the topic to", topic);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static struct irc_handler
|
|
{
|
|
const char *name;
|
|
void (*handler) (struct server *s, const struct irc_message *msg);
|
|
}
|
|
g_irc_handlers[] =
|
|
{
|
|
// This list needs to stay sorted
|
|
{ "CAP", irc_handle_cap },
|
|
{ "INVITE", irc_handle_invite },
|
|
{ "JOIN", irc_handle_join },
|
|
{ "KICK", irc_handle_kick },
|
|
{ "MODE", irc_handle_mode },
|
|
{ "NICK", irc_handle_nick },
|
|
{ "NOTICE", irc_handle_notice },
|
|
{ "PART", irc_handle_part },
|
|
{ "PING", irc_handle_ping },
|
|
{ "PRIVMSG", irc_handle_privmsg },
|
|
{ "QUIT", irc_handle_quit },
|
|
{ "TOPIC", irc_handle_topic },
|
|
};
|
|
|
|
static int
|
|
irc_handler_cmp_by_name (const void *a, const void *b)
|
|
{
|
|
const struct irc_handler *first = a;
|
|
const struct irc_handler *second = b;
|
|
return strcasecmp_ascii (first->name, second->name);
|
|
}
|
|
|
|
static bool
|
|
irc_try_parse_word_for_userhost (struct server *s, const char *word)
|
|
{
|
|
regex_t re;
|
|
int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
|
|
if (!soft_assert (!err))
|
|
return false;
|
|
|
|
regmatch_t matches[2];
|
|
bool result = false;
|
|
if (!regexec (&re, word, 2, matches, 0))
|
|
{
|
|
free (s->irc_user_host);
|
|
s->irc_user_host = xstrndup (word + matches[1].rm_so,
|
|
matches[1].rm_eo - matches[1].rm_so);
|
|
result = true;
|
|
}
|
|
regfree (&re);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (m, ' ', &v);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
if (irc_try_parse_word_for_userhost (s, v.vector[i]))
|
|
break;
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static bool process_input_utf8
|
|
(struct app_context *, struct buffer *, char *, int);
|
|
|
|
static void
|
|
irc_on_registered (struct server *s, const char *nickname)
|
|
{
|
|
s->irc_user = irc_get_or_make_user (s, nickname);
|
|
str_reset (&s->irc_user_mode);
|
|
s->irc_user_host = NULL;
|
|
|
|
s->state = IRC_REGISTERED;
|
|
refresh_prompt (s->ctx);
|
|
|
|
// XXX: we can also use WHOIS if it's not supported (optional by RFC 2812)
|
|
irc_send (s, "USERHOST %s", s->irc_user->nickname);
|
|
|
|
const char *command = get_config_string (s->config, "command");
|
|
if (command)
|
|
{
|
|
log_server_debug (s, "Executing \"#s\"", command);
|
|
|
|
char *copy = xstrdup (command);
|
|
process_input_utf8 (s->ctx, s->buffer, copy, 0);
|
|
free (copy);
|
|
}
|
|
|
|
int64_t command_delay = get_config_integer (s->config, "command_delay");
|
|
log_server_debug (s, "Autojoining channels in #&s seconds...",
|
|
xstrdup_printf ("%" PRId64, command_delay));
|
|
poller_timer_set (&s->autojoin_tmr, command_delay * 1000);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_userhost (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
const char *response = msg->params.vector[1];
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (response, ' ', &v);
|
|
|
|
for (size_t i = 0; i < v.len; i++)
|
|
{
|
|
char *nick = v.vector[i];
|
|
char *equals = strchr (nick, '=');
|
|
|
|
if (!equals || equals == nick)
|
|
continue;
|
|
|
|
// User is an IRC operator
|
|
if (equals[-1] == '*')
|
|
equals[-1] = '\0';
|
|
else
|
|
equals[ 0] = '\0';
|
|
|
|
// TODO: make use of this (away status polling?)
|
|
char away_status = equals[1];
|
|
if (!strchr ("+-", away_status))
|
|
continue;
|
|
|
|
char *userhost = equals + 2;
|
|
if (irc_is_this_us (s, nick))
|
|
{
|
|
free (s->irc_user_host);
|
|
s->irc_user_host = xstrdup (userhost);
|
|
}
|
|
}
|
|
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_umodeis (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
str_reset (&s->irc_user_mode);
|
|
irc_handle_mode_user (s, msg->params.vector + 1);
|
|
|
|
// XXX: do we want to log a message?
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_namreply (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 4)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[2];
|
|
const char *nicks = msg->params.vector[3];
|
|
|
|
// Just push the nicknames to a string vector to process later
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
if (channel)
|
|
cstr_split_ignore_empty (nicks, ' ', &channel->names_buf);
|
|
}
|
|
|
|
static void
|
|
irc_sync_channel_user (struct server *s, struct channel *channel,
|
|
const char *nickname, const char *prefixes)
|
|
{
|
|
struct user *user = irc_get_or_make_user (s, nickname);
|
|
struct channel_user *channel_user =
|
|
irc_channel_get_user (channel, user);
|
|
if (!channel_user)
|
|
{
|
|
irc_channel_link_user (channel, user, prefixes);
|
|
return;
|
|
}
|
|
|
|
user_unref (user);
|
|
|
|
// If our idea of the user's modes disagrees with what the server's
|
|
// sent us (the most powerful modes differ), use the latter one
|
|
if (channel_user->prefixes.str[0] != prefixes[0])
|
|
{
|
|
str_reset (&channel_user->prefixes);
|
|
str_append (&channel_user->prefixes, prefixes);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_process_names (struct server *s, struct channel *channel)
|
|
{
|
|
struct str_map present;
|
|
str_map_init (&present);
|
|
present.key_xfrm = s->irc_strxfrm;
|
|
|
|
struct str_vector *updates = &channel->names_buf;
|
|
for (size_t i = 0; i < updates->len; i++)
|
|
{
|
|
const char *item = updates->vector[i];
|
|
size_t n_prefixes = strspn (item, s->irc_chanuser_prefixes);
|
|
const char *nickname = item + n_prefixes;
|
|
|
|
// Store the nickname in a hashset
|
|
str_map_set (&present, nickname, (void *) 1);
|
|
|
|
char *prefixes = xstrndup (item, n_prefixes);
|
|
irc_sync_channel_user (s, channel, nickname, prefixes);
|
|
free (prefixes);
|
|
}
|
|
|
|
// Get rid of channel users missing from "updates"
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
if (!str_map_find (&present, iter->user->nickname))
|
|
irc_channel_unlink_user (channel, iter);
|
|
|
|
str_map_free (&present);
|
|
str_vector_reset (&channel->names_buf);
|
|
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
str_vector_add_owned (&v,
|
|
xstrdup_printf ("%s%s", iter->prefixes.str, iter->user->nickname));
|
|
char *all_users = join_str_vector (&v, ' ');
|
|
str_vector_free (&v);
|
|
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
|
|
if (buffer)
|
|
{
|
|
log_server_status (s, buffer, "Users on #S: #S",
|
|
channel->name, all_users);
|
|
}
|
|
|
|
free (all_users);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_endofnames (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
if (!strcmp (channel_name, "*"))
|
|
{
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_channels);
|
|
struct channel *channel;
|
|
while ((channel = str_map_iter_next (&iter)))
|
|
irc_process_names (s, channel);
|
|
}
|
|
else if (channel)
|
|
irc_process_names (s, channel);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_topic (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 3)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
const char *topic = msg->params.vector[2];
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
if (channel)
|
|
{
|
|
free (channel->topic);
|
|
channel->topic = xstrdup (topic);
|
|
}
|
|
|
|
if (buffer)
|
|
log_server_status (s, buffer, "The topic is: #m", topic);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_channelmodeis (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
if (channel)
|
|
{
|
|
str_reset (&channel->no_param_modes);
|
|
str_map_clear (&channel->param_modes);
|
|
|
|
irc_handle_mode_channel (s, channel, msg->params.vector + 1);
|
|
}
|
|
|
|
// XXX: do we want to log a message?
|
|
refresh_prompt (s->ctx);
|
|
}
|
|
|
|
static char *
|
|
make_time_string (time_t time)
|
|
{
|
|
char buf[32];
|
|
struct tm tm;
|
|
strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&time, &tm));
|
|
return xstrdup (buf);
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_creationtime (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 3)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
const char *creation_time = msg->params.vector[2];
|
|
|
|
unsigned long created;
|
|
if (!xstrtoul (&created, creation_time, 10))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
if (buffer)
|
|
{
|
|
log_server_status (s, buffer, "Channel created on #&s",
|
|
make_time_string (created));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_topicwhotime (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 4)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
const char *who = msg->params.vector[2];
|
|
const char *change_time = msg->params.vector[3];
|
|
|
|
unsigned long changed;
|
|
if (!xstrtoul (&changed, change_time, 10))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
if (buffer)
|
|
{
|
|
log_server_status (s, buffer, "Topic set by #N on #&s",
|
|
who, make_time_string (changed));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_inviting (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 3)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
const char *nickname = msg->params.vector[2];
|
|
|
|
struct buffer *buffer;
|
|
if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
|
|
buffer = s->buffer;
|
|
|
|
log_server_status (s, buffer,
|
|
"You have invited #n to #S", nickname, channel_name);
|
|
}
|
|
|
|
static void
|
|
irc_handle_err_nicknameinuse (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
log_server_error (s, s->buffer,
|
|
"Nickname is already in use: #S", msg->params.vector[1]);
|
|
|
|
// Only do this while we haven't successfully registered yet
|
|
if (s->state != IRC_CONNECTED)
|
|
return;
|
|
|
|
char *nickname = irc_fetch_next_nickname (s);
|
|
if (nickname)
|
|
{
|
|
log_server_status (s, s->buffer, "Retrying with #s...", nickname);
|
|
irc_send (s, "NICK :%s", nickname);
|
|
free (nickname);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
irc_handle_isupport_prefix (struct server *s, char *value)
|
|
{
|
|
char *modes = value;
|
|
char *prefixes = strchr (value, ')');
|
|
size_t n_prefixes = prefixes - modes;
|
|
if (*modes++ != '(' || !prefixes++ || strlen (value) != 2 * n_prefixes--)
|
|
return;
|
|
|
|
free (s->irc_chanuser_modes);
|
|
free (s->irc_chanuser_prefixes);
|
|
|
|
s->irc_chanuser_modes = xstrndup (modes, n_prefixes);
|
|
s->irc_chanuser_prefixes = xstrndup (prefixes, n_prefixes);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_casemapping (struct server *s, char *value)
|
|
{
|
|
if (!strcmp (value, "ascii"))
|
|
irc_set_casemapping (s, tolower_ascii, tolower_ascii_strxfrm);
|
|
else if (!strcmp (value, "rfc1459"))
|
|
irc_set_casemapping (s, irc_tolower, irc_strxfrm);
|
|
else if (!strcmp (value, "rfc1459-strict"))
|
|
irc_set_casemapping (s, irc_tolower_strict, irc_strxfrm_strict);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_chantypes (struct server *s, char *value)
|
|
{
|
|
free (s->irc_chantypes);
|
|
s->irc_chantypes = xstrdup (value);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_idchan (struct server *s, char *value)
|
|
{
|
|
struct str prefixes;
|
|
str_init (&prefixes);
|
|
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (value, ',', &v);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
{
|
|
// Not using or validating the numeric part
|
|
const char *pair = v.vector[i];
|
|
const char *colon = strchr (pair, ':');
|
|
if (colon)
|
|
str_append_data (&prefixes, pair, colon - pair);
|
|
}
|
|
str_vector_free (&v);
|
|
|
|
free (s->irc_idchan_prefixes);
|
|
s->irc_idchan_prefixes = str_steal (&prefixes);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_statusmsg (struct server *s, char *value)
|
|
{
|
|
free (s->irc_statusmsg);
|
|
s->irc_statusmsg = xstrdup (value);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_chanmodes (struct server *s, char *value)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (value, ',', &v);
|
|
if (v.len >= 4)
|
|
{
|
|
free (s->irc_chanmodes_list);
|
|
s->irc_chanmodes_list = xstrdup (v.vector[0]);
|
|
free (s->irc_chanmodes_param_always);
|
|
s->irc_chanmodes_param_always = xstrdup (v.vector[1]);
|
|
free (s->irc_chanmodes_param_when_set);
|
|
s->irc_chanmodes_param_when_set = xstrdup (v.vector[2]);
|
|
free (s->irc_chanmodes_param_never);
|
|
s->irc_chanmodes_param_never = xstrdup (v.vector[3]);
|
|
}
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static void
|
|
irc_handle_isupport_modes (struct server *s, char *value)
|
|
{
|
|
unsigned long modes;
|
|
if (!*value)
|
|
s->irc_max_modes = UINT_MAX;
|
|
else if (xstrtoul (&modes, value, 10) && modes && modes <= UINT_MAX)
|
|
s->irc_max_modes = modes;
|
|
}
|
|
|
|
static void
|
|
unescape_isupport_value (const char *value, struct str *output)
|
|
{
|
|
const char *alphabet = "0123456789abcdef", *a, *b;
|
|
for (const char *p = value; *p; p++)
|
|
{
|
|
if (p[0] == '\\'
|
|
&& p[1] == 'x'
|
|
&& p[2] && (a = strchr (alphabet, tolower_ascii (p[2])))
|
|
&& p[3] && (b = strchr (alphabet, tolower_ascii (p[3]))))
|
|
{
|
|
str_append_c (output, (a - alphabet) << 4 | (b - alphabet));
|
|
p += 3;
|
|
}
|
|
else
|
|
str_append_c (output, *p);
|
|
}
|
|
}
|
|
|
|
static void
|
|
dispatch_isupport (struct server *s, const char *name, char *value)
|
|
{
|
|
#define MATCH(from, to) if (!strcmp (name, (from))) { (to) (s, value); return; }
|
|
|
|
// TODO: also make use of TARGMAX to split client commands as necessary
|
|
|
|
MATCH ("PREFIX", irc_handle_isupport_prefix);
|
|
MATCH ("CASEMAPPING", irc_handle_isupport_casemapping);
|
|
MATCH ("CHANTYPES", irc_handle_isupport_chantypes);
|
|
MATCH ("IDCHAN", irc_handle_isupport_idchan);
|
|
MATCH ("STATUSMSG", irc_handle_isupport_statusmsg);
|
|
MATCH ("CHANMODES", irc_handle_isupport_chanmodes);
|
|
MATCH ("MODES", irc_handle_isupport_modes);
|
|
|
|
#undef MATCH
|
|
}
|
|
|
|
static void
|
|
irc_handle_rpl_isupport (struct server *s, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len < 2)
|
|
return;
|
|
|
|
for (size_t i = 1; i < msg->params.len - 1; i++)
|
|
{
|
|
// TODO: if the parameter starts with "-", it resets to default
|
|
char *param = msg->params.vector[i];
|
|
char *value = param + strcspn (param, "=");
|
|
if (*value) *value++ = '\0';
|
|
|
|
struct str value_unescaped;
|
|
str_init (&value_unescaped);
|
|
unescape_isupport_value (value, &value_unescaped);
|
|
dispatch_isupport (s, param, value_unescaped.str);
|
|
str_free (&value_unescaped);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
irc_process_numeric (struct server *s,
|
|
const struct irc_message *msg, unsigned long numeric)
|
|
{
|
|
// Numerics typically have human-readable information
|
|
|
|
// Get rid of the first parameter, if there's any at all,
|
|
// as it contains our nickname and is of no practical use to the user
|
|
struct str_vector copy;
|
|
str_vector_init (©);
|
|
str_vector_add_vector (©, msg->params.vector + !!msg->params.len);
|
|
|
|
struct buffer *buffer = s->buffer;
|
|
switch (numeric)
|
|
{
|
|
case IRC_RPL_WELCOME:
|
|
irc_on_registered (s, msg->params.vector[0]);
|
|
|
|
// We still issue a USERHOST anyway as this is in general unreliable
|
|
if (msg->params.len == 2)
|
|
irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]);
|
|
break;
|
|
|
|
case IRC_RPL_ISUPPORT:
|
|
irc_handle_rpl_isupport (s, msg); break;
|
|
case IRC_RPL_USERHOST:
|
|
irc_handle_rpl_userhost (s, msg); break;
|
|
case IRC_RPL_UMODEIS:
|
|
irc_handle_rpl_umodeis (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_NAMREPLY:
|
|
irc_handle_rpl_namreply (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_ENDOFNAMES:
|
|
irc_handle_rpl_endofnames (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_TOPIC:
|
|
irc_handle_rpl_topic (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_CHANNELMODEIS:
|
|
irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_CREATIONTIME:
|
|
irc_handle_rpl_creationtime (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_TOPICWHOTIME:
|
|
irc_handle_rpl_topicwhotime (s, msg); buffer = NULL; break;
|
|
case IRC_RPL_INVITING:
|
|
irc_handle_rpl_inviting (s, msg); buffer = NULL; break;
|
|
|
|
case IRC_ERR_NICKNAMEINUSE:
|
|
irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break;
|
|
|
|
case IRC_RPL_LIST:
|
|
case IRC_RPL_WHOREPLY:
|
|
case IRC_RPL_ENDOFWHO:
|
|
|
|
case IRC_ERR_UNKNOWNCOMMAND:
|
|
case IRC_ERR_NEEDMOREPARAMS:
|
|
// Just preventing these commands from getting printed in a more
|
|
// specific buffer as that would be unwanted
|
|
break;
|
|
|
|
default:
|
|
// If the second parameter is something we have a buffer for
|
|
// (a channel, a PM buffer), log it in that buffer. This is very basic.
|
|
// TODO: whitelist/blacklist a lot more replies in here.
|
|
// TODO: we should either strip the first parameter from the resulting
|
|
// buffer line, or at least put it in brackets
|
|
if (msg->params.len > 1)
|
|
{
|
|
struct buffer *x;
|
|
if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
|
|
buffer = x;
|
|
}
|
|
}
|
|
|
|
if (buffer)
|
|
{
|
|
// Join the parameter vector back and send it to the server buffer
|
|
log_server (s, buffer, BUFFER_LINE_STATUS,
|
|
"#&m", join_str_vector (©, ' '));
|
|
}
|
|
|
|
str_vector_free (©);
|
|
}
|
|
|
|
static void
|
|
irc_process_message (const struct irc_message *msg,
|
|
const char *raw, void *user_data)
|
|
{
|
|
struct server *s = user_data;
|
|
|
|
log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, raw);
|
|
|
|
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))
|
|
{
|
|
log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message);
|
|
error_free (e);
|
|
goto end;
|
|
}
|
|
|
|
for (size_t i = 0; i < lines.len; i++)
|
|
{
|
|
irc_send (s, "%s %s :%s%s%s", a.command, a.target,
|
|
a.prefix, lines.vector[i], a.suffix);
|
|
if (!s->cap_echo_message)
|
|
a.logger (s, &a, buffer, lines.vector[i]);
|
|
}
|
|
end:
|
|
str_vector_free (&lines);
|
|
}
|
|
|
|
static void
|
|
log_autosplit_action (struct server *s,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
(void) a;
|
|
|
|
if (buffer && soft_assert (s->irc_user))
|
|
log_outcoming_action (s, buffer, s->irc_user->nickname, line);
|
|
|
|
// This can only be sent from a user or channel buffer
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_ACTION(s, target, message) \
|
|
send_autosplit_message ((s), (struct send_autosplit_args) \
|
|
{ "PRIVMSG", (target), (message), log_autosplit_action, \
|
|
"\x01" "ACTION ", "\x01" })
|
|
|
|
static void
|
|
log_autosplit_privmsg (struct server *s,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
const char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, a->target);
|
|
if (buffer && soft_assert (s->irc_user))
|
|
log_outcoming_privmsg (s, buffer,
|
|
prefixes, s->irc_user->nickname, line);
|
|
else
|
|
log_outcoming_orphan_privmsg (s, a->target, line);
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \
|
|
send_autosplit_message ((s), (struct send_autosplit_args) \
|
|
{ "PRIVMSG", (target), (message), log_autosplit_privmsg, "", "" })
|
|
|
|
static void
|
|
log_autosplit_notice (struct server *s,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
if (buffer && soft_assert (s->irc_user))
|
|
log_outcoming_notice (s, buffer, s->irc_user->nickname, line);
|
|
else
|
|
log_outcoming_orphan_notice (s, a->target, line);
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_NOTICE(s, target, message) \
|
|
send_autosplit_message ((s), (struct send_autosplit_args) \
|
|
{ "NOTICE", (target), (message), log_autosplit_notice, "", "" })
|
|
|
|
// --- Configuration dumper ----------------------------------------------------
|
|
|
|
struct config_dump_level
|
|
{
|
|
struct config_dump_level *next; ///< Next print level
|
|
const char *name; ///< Name of the object
|
|
};
|
|
|
|
struct config_dump_data
|
|
{
|
|
struct config_dump_level *head; ///< The first level
|
|
struct config_dump_level **tail; ///< Where to place further levels
|
|
struct str_vector *output; ///< Where to place new entries
|
|
};
|
|
|
|
static void config_dump_item
|
|
(struct config_item_ *item, struct config_dump_data *data);
|
|
|
|
static void
|
|
config_dump_children
|
|
(struct config_item_ *object, struct config_dump_data *data)
|
|
{
|
|
hard_assert (object->type == CONFIG_ITEM_OBJECT);
|
|
|
|
struct config_dump_level level;
|
|
level.next = NULL;
|
|
|
|
struct config_dump_level **prev_tail = data->tail;
|
|
*data->tail = &level;
|
|
data->tail = &level.next;
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &object->value.object);
|
|
struct config_item_ *child;
|
|
while ((child = str_map_iter_next (&iter)))
|
|
{
|
|
level.name = iter.link->key;
|
|
config_dump_item (child, data);
|
|
}
|
|
|
|
data->tail = prev_tail;
|
|
*data->tail = NULL;
|
|
}
|
|
|
|
static void
|
|
config_dump_item (struct config_item_ *item, struct config_dump_data *data)
|
|
{
|
|
// Empty objects will show as such
|
|
if (item->type == CONFIG_ITEM_OBJECT
|
|
&& item->value.object.len)
|
|
{
|
|
config_dump_children (item, data);
|
|
return;
|
|
}
|
|
|
|
// Currently there's no reason for us to dump unknown items
|
|
struct config_schema *schema = item->schema;
|
|
if (!schema)
|
|
return;
|
|
|
|
struct str line;
|
|
str_init (&line);
|
|
|
|
struct config_dump_level *iter = data->head;
|
|
if (iter)
|
|
{
|
|
str_append (&line, iter->name);
|
|
iter = iter->next;
|
|
}
|
|
for (; iter; iter = iter->next)
|
|
str_append_printf (&line, ".%s", iter->name);
|
|
|
|
struct str value;
|
|
str_init (&value);
|
|
config_item_write (item, false, &value);
|
|
|
|
// Don't bother writing out null values everywhere
|
|
bool has_default = schema && schema->default_;
|
|
if (item->type != CONFIG_ITEM_NULL || has_default)
|
|
{
|
|
str_append (&line, " = ");
|
|
str_append_str (&line, &value);
|
|
}
|
|
|
|
if (!schema)
|
|
str_append (&line, " (unrecognized)");
|
|
else if (has_default && strcmp (schema->default_, value.str))
|
|
str_append_printf (&line, " (default: %s)", schema->default_);
|
|
else if (!has_default && item->type != CONFIG_ITEM_NULL)
|
|
str_append_printf (&line, " (default: %s)", "null");
|
|
|
|
str_free (&value);
|
|
str_vector_add_owned (data->output, str_steal (&line));
|
|
}
|
|
|
|
static void
|
|
config_dump (struct config_item_ *root, struct str_vector *output)
|
|
{
|
|
struct config_dump_data data;
|
|
data.head = NULL;
|
|
data.tail = &data.head;
|
|
data.output = output;
|
|
|
|
config_dump_item (root, &data);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static int
|
|
str_vector_sort_cb (const void *a, const void *b)
|
|
{
|
|
return strcmp (*(const char **) a, *(const char **) b);
|
|
}
|
|
|
|
static void
|
|
str_vector_sort (struct str_vector *self)
|
|
{
|
|
qsort (self->vector, self->len, sizeof *self->vector, str_vector_sort_cb);
|
|
}
|
|
|
|
static void
|
|
dump_matching_options
|
|
(struct 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
|
|
char *key = cstr_cut_until (output->vector[i], " ");
|
|
if (fnmatch (mask, key, 0))
|
|
str_vector_remove (output, i--);
|
|
free (key);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
save_configuration (struct app_context *ctx)
|
|
{
|
|
struct str data;
|
|
str_init (&data);
|
|
serialize_configuration (ctx, &data);
|
|
|
|
struct error *e = NULL;
|
|
char *filename = write_configuration_file (&data, &e);
|
|
str_free (&data);
|
|
|
|
if (!filename)
|
|
{
|
|
log_global_error (ctx,
|
|
"#s: #s", "Saving configuration failed", e->message);
|
|
error_free (e);
|
|
}
|
|
else
|
|
log_global_status (ctx, "Configuration written to `#s'", filename);
|
|
free (filename);
|
|
}
|
|
|
|
// --- Server management -------------------------------------------------------
|
|
|
|
static bool
|
|
validate_server_name (const char *name)
|
|
{
|
|
for (const unsigned char *p = (const unsigned char *) name; *p; p++)
|
|
if (*p < 32 || *p == '.')
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static const char *
|
|
check_server_name_for_addition (struct app_context *ctx, const char *name)
|
|
{
|
|
if (!strcasecmp_ascii (name, ctx->global_buffer->name))
|
|
return "name collides with the global buffer";
|
|
if (str_map_find (&ctx->servers, name))
|
|
return "server already exists";
|
|
if (!validate_server_name (name))
|
|
return "invalid server name";
|
|
return NULL;
|
|
}
|
|
|
|
static struct server *
|
|
server_add (struct app_context *ctx,
|
|
const char *name, struct config_item_ *subtree)
|
|
{
|
|
hard_assert (!str_map_find (&ctx->servers, name));
|
|
|
|
struct server *s = xmalloc (sizeof *s);
|
|
server_init (s, &ctx->poller);
|
|
|
|
s->ctx = ctx;
|
|
s->name = xstrdup (name);
|
|
str_map_set (&ctx->servers, s->name, s);
|
|
s->config = subtree;
|
|
|
|
// Add a buffer and activate it
|
|
struct buffer *buffer = s->buffer = buffer_new ();
|
|
buffer->type = BUFFER_SERVER;
|
|
buffer->name = xstrdup (s->name);
|
|
buffer->server = s;
|
|
|
|
buffer_add (ctx, buffer);
|
|
buffer_activate (ctx, buffer);
|
|
|
|
config_schema_apply_to_object (g_config_server, subtree, s);
|
|
config_schema_call_changed (subtree);
|
|
|
|
if (get_config_boolean (s->config, "autoconnect"))
|
|
// Connect to the server ASAP
|
|
poller_timer_set (&s->reconnect_tmr, 0);
|
|
return s;
|
|
}
|
|
|
|
static void
|
|
server_add_new (struct app_context *ctx, const char *name)
|
|
{
|
|
// Note that there may already be something in the configuration under
|
|
// that key that we've ignored earlier, and there may also be
|
|
// a case-insensitive conflict. Those things may only happen as a result
|
|
// of manual edits to the configuration, though, and they're not really
|
|
// going to break anything. They only cause surprises when loading.
|
|
struct str_map *servers = get_servers_config (ctx);
|
|
struct config_item_ *subtree = config_item_object ();
|
|
str_map_set (servers, name, subtree);
|
|
|
|
struct server *s = server_add (ctx, name, subtree);
|
|
struct error *e = NULL;
|
|
if (!irc_autofill_user_info (s, &e))
|
|
{
|
|
log_server_error (s, s->buffer,
|
|
"#s: #s", "Failed to fill in user details", e->message);
|
|
error_free (e);
|
|
}
|
|
}
|
|
|
|
static void
|
|
server_remove (struct app_context *ctx, struct server *s)
|
|
{
|
|
hard_assert (!irc_is_connected (s));
|
|
|
|
if (s->buffer)
|
|
buffer_remove_safe (ctx, s->buffer);
|
|
|
|
struct str_map_unset_iter iter;
|
|
str_map_unset_iter_init (&iter, &s->irc_buffer_map);
|
|
struct buffer *buffer;
|
|
while ((buffer = str_map_unset_iter_next (&iter)))
|
|
buffer_remove_safe (ctx, buffer);
|
|
str_map_unset_iter_free (&iter);
|
|
|
|
hard_assert (!s->buffer);
|
|
hard_assert (!s->irc_buffer_map.len);
|
|
hard_assert (!s->irc_channels.len);
|
|
soft_assert (!s->irc_users.len);
|
|
|
|
str_map_set (get_servers_config (ctx), s->name, NULL);
|
|
s->config = NULL;
|
|
|
|
// This actually destroys the server as it's owned by the map
|
|
str_map_set (&ctx->servers, s->name, NULL);
|
|
}
|
|
|
|
static void
|
|
server_rename (struct app_context *ctx, struct server *s, const char *new_name)
|
|
{
|
|
str_map_set (&ctx->servers, new_name,
|
|
str_map_steal (&ctx->servers, s->name));
|
|
|
|
struct str_map *servers = get_servers_config (ctx);
|
|
str_map_set (servers, new_name, str_map_steal (servers, s->name));
|
|
|
|
free (s->name);
|
|
s->name = xstrdup (new_name);
|
|
|
|
buffer_rename (ctx, s->buffer, new_name);
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &s->irc_buffer_map);
|
|
struct buffer *buffer;
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
{
|
|
// FIXME: creation of buffer names should be centralized
|
|
char *x = NULL;
|
|
switch (buffer->type)
|
|
{
|
|
case BUFFER_PM:
|
|
x = xstrdup_printf ("%s.%s", s->name, buffer->user->nickname);
|
|
break;
|
|
case BUFFER_CHANNEL:
|
|
x = xstrdup_printf ("%s.%s", s->name, buffer->channel->name);
|
|
break;
|
|
default:
|
|
hard_assert (!"unexpected type of server-related buffer");
|
|
}
|
|
buffer_rename (ctx, buffer, x);
|
|
free (x);
|
|
}
|
|
}
|
|
|
|
// --- User input handling -----------------------------------------------------
|
|
|
|
// HANDLER_NEEDS_REG is primarily for message sending commands,
|
|
// as they may want to log buffer lines and use our current nickname
|
|
|
|
enum handler_flags
|
|
{
|
|
HANDLER_SERVER = (1 << 0), ///< Server context required
|
|
HANDLER_NEEDS_REG = (1 << 1), ///< Server registration required
|
|
HANDLER_CHANNEL_FIRST = (1 << 2), ///< Channel required, first argument
|
|
HANDLER_CHANNEL_LAST = (1 << 3) ///< Channel required, last argument
|
|
};
|
|
|
|
struct handler_args
|
|
{
|
|
struct app_context *ctx; ///< Application context
|
|
struct buffer *buffer; ///< Current buffer
|
|
struct server *s; ///< Related server
|
|
const char *channel_name; ///< Related channel name
|
|
char *arguments; ///< Command arguments
|
|
};
|
|
|
|
/// Cuts the longest non-whitespace portion of text and advances the pointer
|
|
static char *
|
|
cut_word (char **s)
|
|
{
|
|
char *start = *s;
|
|
size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);
|
|
char *end = start + word_len;
|
|
*s = end + strspn (end, WORD_BREAKING_CHARS);
|
|
*end = '\0';
|
|
return start;
|
|
}
|
|
|
|
/// Validates a word to be cut from a string
|
|
typedef bool (*word_validator_fn) (void *, char *);
|
|
|
|
static char *
|
|
maybe_cut_word (char **s, word_validator_fn validator, void *user_data)
|
|
{
|
|
char *start = *s;
|
|
size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);
|
|
|
|
char *word = xstrndup (start, word_len);
|
|
bool ok = validator (user_data, word);
|
|
free (word);
|
|
|
|
if (!ok)
|
|
return NULL;
|
|
|
|
char *end = start + word_len;
|
|
*s = end + strspn (end, WORD_BREAKING_CHARS);
|
|
*end = '\0';
|
|
return start;
|
|
}
|
|
|
|
static char *
|
|
maybe_cut_word_from_end (char **s, word_validator_fn validator, void *user_data)
|
|
{
|
|
// Find the start and end of the last word
|
|
char *start = *s, *end = start + strlen (start);
|
|
while (end > start && strchr (WORD_BREAKING_CHARS, end [-1]))
|
|
end--;
|
|
char *word = end;
|
|
while (word > start && !strchr (WORD_BREAKING_CHARS, word[-1]))
|
|
word--;
|
|
|
|
// There's just one word at maximum, starting at the beginning
|
|
if (word == start)
|
|
return maybe_cut_word (s, validator, user_data);
|
|
|
|
char *tmp = xstrndup (word, word - start);
|
|
bool ok = validator (user_data, tmp);
|
|
free (tmp);
|
|
|
|
if (!ok)
|
|
return NULL;
|
|
|
|
// It doesn't start at the beginning, cut it off and return it
|
|
word[-1] = *end = '\0';
|
|
return word;
|
|
}
|
|
|
|
static bool
|
|
validate_channel_name (void *user_data, char *word)
|
|
{
|
|
return irc_is_channel (user_data, word);
|
|
}
|
|
|
|
static char *
|
|
try_get_channel (struct handler_args *a,
|
|
char *(*cutter) (char **, word_validator_fn, void *))
|
|
{
|
|
char *channel_name = cutter (&a->arguments, validate_channel_name, a->s);
|
|
if (channel_name)
|
|
return channel_name;
|
|
if (a->buffer->type == BUFFER_CHANNEL)
|
|
return a->buffer->channel->name;
|
|
return NULL;
|
|
}
|
|
|
|
static bool
|
|
try_handle_buffer_goto (struct app_context *ctx, const char *word)
|
|
{
|
|
unsigned long n;
|
|
if (!xstrtoul (&n, word, 10))
|
|
return false;
|
|
|
|
if (n > INT_MAX || !buffer_goto (ctx, n))
|
|
log_global_error (ctx, "#s: #s", "No such buffer", word);
|
|
return true;
|
|
}
|
|
|
|
static struct buffer *
|
|
try_decode_buffer (struct app_context *ctx, const char *word)
|
|
{
|
|
unsigned long n;
|
|
struct buffer *buffer = NULL;
|
|
if (xstrtoul (&n, word, 10) && n <= INT_MAX)
|
|
buffer = buffer_at_index (ctx, n);
|
|
if (!buffer)
|
|
buffer = buffer_by_name (ctx, word);
|
|
// TODO: partial matches
|
|
return buffer;
|
|
}
|
|
|
|
static void
|
|
show_buffers_list (struct app_context *ctx)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "Buffers list:");
|
|
|
|
int i = 1;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
log_global_indent (ctx, " [#d] #s", i++, iter->name);
|
|
}
|
|
|
|
static void
|
|
part_channel (struct server *s, const char *channel_name, const char *reason)
|
|
{
|
|
if (*reason)
|
|
irc_send (s, "PART %s :%s", channel_name, reason);
|
|
else
|
|
irc_send (s, "PART %s", channel_name);
|
|
|
|
struct channel *channel;
|
|
if ((channel = str_map_find (&s->irc_channels, channel_name)))
|
|
channel->left_manually = true;
|
|
}
|
|
|
|
static void
|
|
handle_buffer_close (struct app_context *ctx, struct handler_args *a)
|
|
{
|
|
struct buffer *buffer = NULL;
|
|
const char *which = NULL;
|
|
if (!*a->arguments)
|
|
buffer = a->buffer;
|
|
else
|
|
buffer = try_decode_buffer (ctx, (which = cut_word (&a->arguments)));
|
|
|
|
if (!buffer)
|
|
log_global_error (ctx, "#s: #s", "No such buffer", which);
|
|
else if (buffer == ctx->global_buffer)
|
|
log_global_error (ctx, "Can't close the global buffer");
|
|
else if (buffer->type == BUFFER_SERVER)
|
|
log_global_error (ctx, "Can't close a server buffer");
|
|
else
|
|
{
|
|
// The user would be unable to recreate the buffer otherwise
|
|
if (buffer->type == BUFFER_CHANNEL)
|
|
part_channel (buffer->server, buffer->channel->name, "");
|
|
buffer_remove_safe (ctx, buffer);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
handle_buffer_move (struct app_context *ctx, struct handler_args *a)
|
|
{
|
|
unsigned long request;
|
|
if (!xstrtoul (&request, a->arguments, 10))
|
|
return false;
|
|
|
|
unsigned long total = 0;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
total++;
|
|
|
|
if (request == 0 || request > total)
|
|
{
|
|
log_global_error (ctx, "#s: #s",
|
|
"Can't move buffer", "requested position is out of range");
|
|
return true;
|
|
}
|
|
|
|
struct buffer *buffer = a->buffer;
|
|
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
|
|
struct buffer *following = ctx->buffers;
|
|
while (--request && following)
|
|
following = following->next;
|
|
|
|
LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
|
|
|
|
refresh_prompt (ctx);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_buffer (struct handler_args *a)
|
|
{
|
|
struct app_context *ctx = a->ctx;
|
|
char *action = cut_word (&a->arguments);
|
|
if (try_handle_buffer_goto (ctx, action))
|
|
return true;
|
|
|
|
// TODO: some subcommand to print N last lines from the buffer
|
|
bool result = true;
|
|
if (!strcasecmp_ascii (action, "list"))
|
|
show_buffers_list (ctx);
|
|
else if (!strcasecmp_ascii (action, "clear"))
|
|
{
|
|
buffer_clear (a->buffer);
|
|
// XXX: clear screen?
|
|
buffer_print_backlog (ctx, a->buffer);
|
|
}
|
|
else if (!strcasecmp_ascii (action, "move"))
|
|
result = handle_buffer_move (ctx, a);
|
|
else if (!strcasecmp_ascii (action, "close"))
|
|
handle_buffer_close (ctx, a);
|
|
else
|
|
result = false;
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
replace_string_array
|
|
(struct config_item_ *item, struct str_vector *array, struct error **e)
|
|
{
|
|
char *changed = join_str_vector (array, ',');
|
|
struct str tmp = { .str = changed, .len = strlen (changed) };
|
|
bool result = config_item_set_from (item,
|
|
config_item_string_array (&tmp), e);
|
|
free (changed);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
handle_command_set_add
|
|
(struct config_item_ *item, const char *value, struct error **e)
|
|
{
|
|
bool result = false;
|
|
struct str_vector items;
|
|
str_vector_init (&items);
|
|
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)
|
|
{
|
|
log_global_error (ctx,
|
|
"Failed to set option \"#s\": #s", key, e->message);
|
|
error_free (e);
|
|
}
|
|
else
|
|
{
|
|
struct str_vector tmp;
|
|
str_vector_init (&tmp);
|
|
dump_matching_options (ctx, key, &tmp);
|
|
log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
|
|
str_vector_free (&tmp);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
handle_command_set_assign
|
|
(struct app_context *ctx, struct str_vector *all, char *arguments)
|
|
{
|
|
char *op = cut_word (&arguments);
|
|
bool add = false;
|
|
bool remove = false;
|
|
|
|
if (!strcmp (op, "+=")) add = true;
|
|
else if (!strcmp (op, "-=")) remove = true;
|
|
else if (strcmp (op, "=")) return false;
|
|
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
struct error *e = NULL;
|
|
struct config_item_ *new_ =
|
|
config_item_parse (arguments, strlen (arguments), true, &e);
|
|
if (e)
|
|
{
|
|
log_global_error (ctx, "Invalid value: #s", e->message);
|
|
error_free (e);
|
|
return true;
|
|
}
|
|
|
|
if ((add | remove) && !config_item_type_is_string (new_->type))
|
|
{
|
|
log_global_error (ctx, "+= / -= operators need a string argument");
|
|
config_item_destroy (new_);
|
|
return true;
|
|
}
|
|
for (size_t i = 0; i < all->len; i++)
|
|
{
|
|
char *key = cstr_cut_until (all->vector[i], " ");
|
|
handle_command_set_assign_item (ctx, key, new_, add, remove);
|
|
free (key);
|
|
}
|
|
config_item_destroy (new_);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_set (struct handler_args *a)
|
|
{
|
|
struct app_context *ctx = a->ctx;
|
|
char *option = "*";
|
|
if (*a->arguments)
|
|
option = cut_word (&a->arguments);
|
|
|
|
struct str_vector all;
|
|
str_vector_init (&all);
|
|
dump_matching_options (ctx, option, &all);
|
|
|
|
bool result = true;
|
|
if (!all.len)
|
|
log_global_error (ctx, "No matches: #s", option);
|
|
else if (!*a->arguments)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
for (size_t i = 0; i < all.len; i++)
|
|
log_global_indent (ctx, "#s", all.vector[i]);
|
|
}
|
|
else
|
|
result = handle_command_set_assign (ctx, &all, a->arguments);
|
|
|
|
str_vector_free (&all);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
handle_command_save (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
return false;
|
|
|
|
save_configuration (a->ctx);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
show_aliases_list (struct app_context *ctx)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "Aliases:");
|
|
|
|
struct str_map *aliases = get_aliases_config (ctx);
|
|
if (!aliases->len)
|
|
{
|
|
log_global_indent (ctx, " (none)");
|
|
return true;
|
|
}
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, aliases);
|
|
struct config_item_ *alias;
|
|
while ((alias = str_map_iter_next (&iter)))
|
|
{
|
|
struct str definition;
|
|
str_init (&definition);
|
|
if (config_item_type_is_string (alias->type))
|
|
config_item_write_string (&definition, &alias->value.string);
|
|
else
|
|
str_append (&definition, "alias definition is not a string");
|
|
log_global_indent (ctx, " /#s: #s", iter.link->key, definition.str);
|
|
str_free (&definition);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_alias (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return show_aliases_list (a->ctx);
|
|
|
|
// TODO: validate the name; maybe also while loading configuration
|
|
char *name = cut_word (&a->arguments);
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
struct config_item_ *alias = config_item_string_from_cstr (a->arguments);
|
|
|
|
struct str definition;
|
|
str_init (&definition);
|
|
config_item_write_string (&definition, &alias->value.string);
|
|
str_map_set (get_aliases_config (a->ctx), name, alias);
|
|
log_global_status (a->ctx, "Created alias /#s: #s", name, definition.str);
|
|
str_free (&definition);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_unalias (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
struct str_map *aliases = get_aliases_config (a->ctx);
|
|
while (*a->arguments)
|
|
{
|
|
char *name = cut_word (&a->arguments);
|
|
if (!str_map_find (aliases, name))
|
|
log_global_error (a->ctx, "No such alias: #s", name);
|
|
else
|
|
{
|
|
str_map_set (aliases, name, NULL);
|
|
log_global_status (a->ctx, "Alias removed: #s", name);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_msg (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (!*a->arguments)
|
|
log_server_error (a->s, a->s->buffer, "No text to send");
|
|
else
|
|
SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_query (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (irc_is_channel (a->s, target))
|
|
log_server_error (a->s, a->s->buffer, "Cannot query a channel");
|
|
else if (!*a->arguments)
|
|
log_server_error (a->s, a->s->buffer, "No text to send");
|
|
else
|
|
{
|
|
buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
|
|
SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_notice (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (!*a->arguments)
|
|
log_server_error (a->s, a->s->buffer, "No text to send");
|
|
else
|
|
SEND_AUTOSPLIT_NOTICE (a->s, target, a->arguments);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_ctcp (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *tag = cut_word (&a->arguments);
|
|
cstr_transform (tag, toupper_ascii);
|
|
|
|
if (*a->arguments)
|
|
irc_send (a->s, "PRIVMSG %s :\x01%s %s\x01", target, tag, a->arguments);
|
|
else
|
|
irc_send (a->s, "PRIVMSG %s :\x01%s\x01", target, tag);
|
|
|
|
if (!a->s->cap_echo_message)
|
|
log_ctcp_query (a->s, target, tag);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_me (struct handler_args *a)
|
|
{
|
|
if (a->buffer->type == BUFFER_CHANNEL)
|
|
SEND_AUTOSPLIT_ACTION (a->s,
|
|
a->buffer->channel->name, a->arguments);
|
|
else if (a->buffer->type == BUFFER_PM)
|
|
SEND_AUTOSPLIT_ACTION (a->s,
|
|
a->buffer->user->nickname, a->arguments);
|
|
else
|
|
log_server_error (a->s, a->s->buffer,
|
|
"Can't do this from a server buffer (#s)",
|
|
"send CTCP actions");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_quit (struct handler_args *a)
|
|
{
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &a->ctx->servers);
|
|
|
|
struct server *s;
|
|
while ((s = str_map_iter_next (&iter)))
|
|
{
|
|
if (irc_is_connected (s))
|
|
irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
|
|
}
|
|
|
|
initiate_quit (a->ctx);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_join (struct handler_args *a)
|
|
{
|
|
// XXX: send the last known channel key?
|
|
if (irc_is_channel (a->s, a->arguments))
|
|
// XXX: we may want to split the list of channels
|
|
irc_send (a->s, "JOIN %s", a->arguments);
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
|
|
"no channel name given and this buffer is not a channel");
|
|
// TODO: have a better way of checking if we're on the channel
|
|
else if (a->buffer->channel->users)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
|
|
"you already are on the channel");
|
|
else if (*a->arguments)
|
|
irc_send (a->s, "JOIN %s :%s", a->buffer->channel->name, a->arguments);
|
|
else
|
|
irc_send (a->s, "JOIN %s", a->buffer->channel->name);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_part (struct handler_args *a)
|
|
{
|
|
if (irc_is_channel (a->s, a->arguments))
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (cut_word (&a->arguments), ' ', &v);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
part_channel (a->s, v.vector[i], a->arguments);
|
|
str_vector_free (&v);
|
|
}
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
|
|
"no channel name given and this buffer is not a channel");
|
|
// TODO: have a better way of checking if we're on the channel
|
|
else if (!a->buffer->channel->users)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
|
|
"you're not on the channel");
|
|
else
|
|
part_channel (a->s, a->buffer->channel->name, a->arguments);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
cycle_channel (struct server *s, const char *channel_name, const char *reason)
|
|
{
|
|
// If a channel key is set, we must specify it when rejoining
|
|
const char *key = NULL;
|
|
struct channel *channel;
|
|
if ((channel = str_map_find (&s->irc_channels, channel_name)))
|
|
key = str_map_find (&channel->param_modes, "k");
|
|
|
|
if (*reason)
|
|
irc_send (s, "PART %s :%s", channel_name, reason);
|
|
else
|
|
irc_send (s, "PART %s", channel_name);
|
|
|
|
if (key)
|
|
irc_send (s, "JOIN %s :%s", channel_name, key);
|
|
else
|
|
irc_send (s, "JOIN %s", channel_name);
|
|
}
|
|
|
|
static bool
|
|
handle_command_cycle (struct handler_args *a)
|
|
{
|
|
if (irc_is_channel (a->s, a->arguments))
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (cut_word (&a->arguments), ' ', &v);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
cycle_channel (a->s, v.vector[i], a->arguments);
|
|
str_vector_free (&v);
|
|
}
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
|
|
"no channel name given and this buffer is not a channel");
|
|
// TODO: have a better way of checking if we're on the channel
|
|
else if (!a->buffer->channel->users)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
|
|
"you're not on the channel");
|
|
else
|
|
cycle_channel (a->s, a->buffer->channel->name, a->arguments);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_mode (struct handler_args *a)
|
|
{
|
|
// Channel names prefixed by "+" collide with mode strings,
|
|
// so we just disallow specifying these channels
|
|
char *target = NULL;
|
|
if (strchr ("+-\0", *a->arguments))
|
|
{
|
|
if (a->buffer->type == BUFFER_CHANNEL)
|
|
target = a->buffer->channel->name;
|
|
if (a->buffer->type == BUFFER_PM)
|
|
target = a->buffer->user->nickname;
|
|
if (a->buffer->type == BUFFER_SERVER)
|
|
target = a->s->irc_user->nickname;
|
|
}
|
|
else
|
|
// If there a->arguments and they don't begin with a mode string,
|
|
// they're either a user name or a channel name
|
|
target = cut_word (&a->arguments);
|
|
|
|
if (!target)
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't change mode",
|
|
"no target given and this buffer is neither a PM nor a channel");
|
|
else if (*a->arguments)
|
|
// XXX: split channel mode params as necessary using irc_max_modes?
|
|
irc_send (a->s, "MODE %s %s", target, a->arguments);
|
|
else
|
|
irc_send (a->s, "MODE %s", target);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_topic (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
// FIXME: there's no way to unset the topic
|
|
irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments);
|
|
else
|
|
irc_send (a->s, "TOPIC %s", a->channel_name);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_kick (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (*a->arguments)
|
|
irc_send (a->s, "KICK %s %s :%s",
|
|
a->channel_name, target, a->arguments);
|
|
else
|
|
irc_send (a->s, "KICK %s %s", a->channel_name, target);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_kickban (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
if (strpbrk (target, "!@*?"))
|
|
return false;
|
|
|
|
// XXX: how about other masks?
|
|
irc_send (a->s, "MODE %s +b %s!*@*", a->channel_name, target);
|
|
if (*a->arguments)
|
|
irc_send (a->s, "KICK %s %s :%s",
|
|
a->channel_name, target, a->arguments);
|
|
else
|
|
irc_send (a->s, "KICK %s %s", a->channel_name, target);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
mass_channel_mode (struct server *s, const char *channel_name,
|
|
bool adding, char mode_char, struct str_vector *v)
|
|
{
|
|
size_t n;
|
|
for (size_t i = 0; i < v->len; i += n)
|
|
{
|
|
struct str modes; str_init (&modes);
|
|
struct str params; str_init (¶ms);
|
|
|
|
n = MIN (v->len - i, s->irc_max_modes);
|
|
str_append_printf (&modes, "MODE %s %c", channel_name, "-+"[adding]);
|
|
for (size_t k = 0; k < n; k++)
|
|
{
|
|
str_append_c (&modes, mode_char);
|
|
str_append_printf (¶ms, " %s", v->vector[i + k]);
|
|
}
|
|
|
|
irc_send (s, "%s%s", modes.str, params.str);
|
|
|
|
str_free (&modes);
|
|
str_free (¶ms);
|
|
}
|
|
}
|
|
|
|
static void
|
|
mass_channel_mode_mask_list
|
|
(struct handler_args *a, bool adding, char mode_char)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (a->arguments, ' ', &v);
|
|
|
|
// XXX: this may be a bit too trivial; we could map also nicknames
|
|
// to information from WHO polling or userhost-in-names
|
|
for (size_t i = 0; i < v.len; i++)
|
|
{
|
|
char *target = v.vector[i];
|
|
if (strpbrk (target, "!@*?"))
|
|
continue;
|
|
|
|
v.vector[i] = xstrdup_printf ("%s!*@*", target);
|
|
free (target);
|
|
}
|
|
|
|
mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static bool
|
|
handle_command_ban (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
mass_channel_mode_mask_list (a, true, 'b');
|
|
else
|
|
irc_send (a->s, "MODE %s +b", a->channel_name);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_unban (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
mass_channel_mode_mask_list (a, false, 'b');
|
|
else
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_invite (struct handler_args *a)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (a->arguments, ' ', &v);
|
|
|
|
bool result = !!v.len;
|
|
for (size_t i = 0; i < v.len; i++)
|
|
irc_send (a->s, "INVITE %s %s", v.vector[i], a->channel_name);
|
|
|
|
str_vector_free (&v);
|
|
return result;
|
|
}
|
|
|
|
static struct server *
|
|
resolve_server (struct app_context *ctx, struct handler_args *a,
|
|
const char *command_name)
|
|
{
|
|
struct server *s = NULL;
|
|
if (*a->arguments)
|
|
{
|
|
char *server_name = cut_word (&a->arguments);
|
|
if (!(s = str_map_find (&ctx->servers, server_name)))
|
|
log_global_error (ctx, "/#s: #s: #s",
|
|
command_name, "no such server", server_name);
|
|
}
|
|
else if (a->buffer->type == BUFFER_GLOBAL)
|
|
log_global_error (ctx, "/#s: #s",
|
|
command_name, "no server name given and this buffer is global");
|
|
else
|
|
s = a->buffer->server;
|
|
return s;
|
|
}
|
|
|
|
static bool
|
|
handle_command_connect (struct handler_args *a)
|
|
{
|
|
struct server *s = NULL;
|
|
if (!(s = resolve_server (a->ctx, a, "connect")))
|
|
return true;
|
|
|
|
if (irc_is_connected (s))
|
|
{
|
|
log_server_error (s, s->buffer, "Already connected");
|
|
return true;
|
|
}
|
|
if (s->state == IRC_CONNECTING)
|
|
irc_destroy_connector (s);
|
|
|
|
irc_cancel_timers (s);
|
|
|
|
s->reconnect_attempt = 0;
|
|
irc_initiate_connect (s);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_disconnect (struct handler_args *a)
|
|
{
|
|
struct server *s = NULL;
|
|
if (!(s = resolve_server (a->ctx, a, "disconnect")))
|
|
return true;
|
|
|
|
if (s->state == IRC_CONNECTING)
|
|
{
|
|
log_server_status (s, s->buffer, "Connecting aborted");
|
|
irc_destroy_connector (s);
|
|
}
|
|
else if (poller_timer_is_active (&s->reconnect_tmr))
|
|
{
|
|
log_server_status (s, s->buffer, "Connecting aborted");
|
|
poller_timer_reset (&s->reconnect_tmr);
|
|
}
|
|
else if (!irc_is_connected (s))
|
|
log_server_error (s, s->buffer, "Not connected");
|
|
else
|
|
irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
show_servers_list (struct app_context *ctx)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "Servers list:");
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &ctx->servers);
|
|
struct server *s;
|
|
while ((s = str_map_iter_next (&iter)))
|
|
log_global_indent (ctx, " #s", s->name);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_server_add (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
struct app_context *ctx = a->ctx;
|
|
char *name = cut_word (&a->arguments);
|
|
const char *err;
|
|
if ((err = check_server_name_for_addition (ctx, name)))
|
|
log_global_error (ctx, "Cannot create server `#s': #s", name, err);
|
|
else
|
|
{
|
|
server_add_new (ctx, name);
|
|
log_global_status (ctx, "Server added: #s", name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_server_remove (struct handler_args *a)
|
|
{
|
|
struct app_context *ctx = a->ctx;
|
|
struct server *s = NULL;
|
|
if (!(s = resolve_server (ctx, a, "server")))
|
|
return true;
|
|
|
|
if (irc_is_connected (s))
|
|
log_server_error (s, s->buffer, "Can't remove a connected server");
|
|
else
|
|
{
|
|
char *name = xstrdup (s->name);
|
|
server_remove (ctx, s);
|
|
log_global_status (ctx, "Server removed: #s", name);
|
|
free (name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_server_rename (struct handler_args *a)
|
|
{
|
|
struct app_context *ctx = a->ctx;
|
|
if (!*a->arguments)
|
|
return false;
|
|
char *old_name = cut_word (&a->arguments);
|
|
if (!*a->arguments)
|
|
return false;
|
|
char *new_name = cut_word (&a->arguments);
|
|
|
|
struct server *s;
|
|
const char *err;
|
|
if (!(s = str_map_find (&ctx->servers, old_name)))
|
|
log_global_error (ctx, "/#s: #s: #s",
|
|
"server", "no such server", old_name);
|
|
else if ((err = check_server_name_for_addition (ctx, new_name)))
|
|
log_global_error (ctx,
|
|
"Cannot rename server to `#s': #s", new_name, err);
|
|
else
|
|
{
|
|
server_rename (ctx, s, new_name);
|
|
log_global_status (ctx, "Server renamed: #s to #s", old_name, new_name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_server (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return show_servers_list (a->ctx);
|
|
|
|
char *action = cut_word (&a->arguments);
|
|
if (!strcasecmp_ascii (action, "list"))
|
|
return show_servers_list (a->ctx);
|
|
if (!strcasecmp_ascii (action, "add"))
|
|
return handle_server_add (a);
|
|
if (!strcasecmp_ascii (action, "remove"))
|
|
return handle_server_remove (a);
|
|
if (!strcasecmp_ascii (action, "rename"))
|
|
return handle_server_rename (a);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
handle_command_names (struct handler_args *a)
|
|
{
|
|
char *channel_name = try_get_channel (a, maybe_cut_word);
|
|
if (channel_name)
|
|
irc_send (a->s, "NAMES %s", channel_name);
|
|
else
|
|
irc_send (a->s, "NAMES");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_whois (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
irc_send (a->s, "WHOIS %s", a->arguments);
|
|
else if (a->buffer->type == BUFFER_PM)
|
|
irc_send (a->s, "WHOIS %s", a->buffer->user->nickname);
|
|
else if (a->buffer->type == BUFFER_SERVER)
|
|
irc_send (a->s, "WHOIS %s", a->s->irc_user->nickname);
|
|
else
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
|
|
"no target given and this buffer is not a PM nor a server");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_whowas (struct handler_args *a)
|
|
{
|
|
if (*a->arguments)
|
|
irc_send (a->s, "WHOWAS %s", a->arguments);
|
|
else if (a->buffer->type == BUFFER_PM)
|
|
irc_send (a->s, "WHOWAS %s", a->buffer->user->nickname);
|
|
else
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
|
|
"no target given and this buffer is not a PM");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_nick (struct handler_args *a)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
irc_send (a->s, "NICK %s", cut_word (&a->arguments));
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_quote (struct handler_args *a)
|
|
{
|
|
irc_send (a->s, "%s", a->arguments);
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
handle_command_channel_mode
|
|
(struct handler_args *a, bool adding, char mode_char)
|
|
{
|
|
if (!*a->arguments)
|
|
return false;
|
|
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
cstr_split_ignore_empty (a->arguments, ' ', &v);
|
|
mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
|
|
str_vector_free (&v);
|
|
return true;
|
|
}
|
|
|
|
#define CHANMODE_HANDLER(name, adding, mode_char) \
|
|
static bool \
|
|
handle_command_ ## name (struct handler_args *a) \
|
|
{ \
|
|
return handle_command_channel_mode (a, (adding), (mode_char)); \
|
|
}
|
|
|
|
CHANMODE_HANDLER (op, true, 'o') CHANMODE_HANDLER (deop, false, 'o')
|
|
CHANMODE_HANDLER (voice, true, 'v') CHANMODE_HANDLER (devoice, false, 'v')
|
|
|
|
#define TRIVIAL_HANDLER(name, command) \
|
|
static bool \
|
|
handle_command_ ## name (struct handler_args *a) \
|
|
{ \
|
|
if (*a->arguments) \
|
|
irc_send (a->s, command " %s", a->arguments); \
|
|
else \
|
|
irc_send (a->s, command); \
|
|
return true; \
|
|
}
|
|
|
|
TRIVIAL_HANDLER (list, "LIST")
|
|
TRIVIAL_HANDLER (who, "WHO")
|
|
TRIVIAL_HANDLER (motd, "MOTD")
|
|
TRIVIAL_HANDLER (stats, "STATS")
|
|
TRIVIAL_HANDLER (away, "AWAY")
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool handle_command_help (struct handler_args *);
|
|
|
|
static struct command_handler
|
|
{
|
|
const char *name;
|
|
const char *description;
|
|
const char *usage;
|
|
bool (*handler) (struct handler_args *a);
|
|
enum handler_flags flags;
|
|
}
|
|
g_command_handlers[] =
|
|
{
|
|
{ "help", "Show help",
|
|
"[<command> | <option>]",
|
|
handle_command_help, 0 },
|
|
{ "quit", "Quit the program",
|
|
"[<message>]",
|
|
handle_command_quit, 0 },
|
|
{ "buffer", "Manage buffers",
|
|
"<N> | list | clear | move <N> | close [<N> | <name>]",
|
|
handle_command_buffer, 0 },
|
|
{ "set", "Manage configuration",
|
|
"[<option>]",
|
|
handle_command_set, 0 },
|
|
{ "save", "Save configuration",
|
|
NULL,
|
|
handle_command_save, 0 },
|
|
|
|
{ "alias", "List or set aliases",
|
|
"[<name> <definition>]",
|
|
handle_command_alias, 0 },
|
|
{ "unalias", "Unset aliases",
|
|
"<name>...",
|
|
handle_command_unalias, 0 },
|
|
|
|
{ "msg", "Send message to a nick or channel",
|
|
"<target> <message>",
|
|
handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
|
{ "query", "Send a private message to a nick",
|
|
"<nick> <message>",
|
|
handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
|
{ "notice", "Send notice to a nick or channel",
|
|
"<target> <message>",
|
|
handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
|
{ "ctcp", "Send a CTCP query",
|
|
"<target> <tag>",
|
|
handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
|
{ "me", "Send a CTCP action",
|
|
"<message>",
|
|
handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
|
|
|
{ "join", "Join channels",
|
|
"[<channel>[,<channel>...]] [<key>[,<key>...]]",
|
|
handle_command_join, HANDLER_SERVER },
|
|
{ "part", "Leave channels",
|
|
"[<channel>[,<channel>...]] [<reason>]",
|
|
handle_command_part, HANDLER_SERVER },
|
|
{ "cycle", "Rejoin channels",
|
|
"[<channel>[,<channel>...]] [<reason>]",
|
|
handle_command_cycle, HANDLER_SERVER },
|
|
|
|
{ "op", "Give channel operator status",
|
|
"<nick>...",
|
|
handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "deop", "Remove channel operator status",
|
|
"<nick>...",
|
|
handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "voice", "Give voice",
|
|
"<nick>...",
|
|
handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "devoice", "Remove voice",
|
|
"<nick>...",
|
|
handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
|
|
{ "mode", "Change mode",
|
|
"[<channel>] [<mode>...]",
|
|
handle_command_mode, HANDLER_SERVER },
|
|
{ "topic", "Change topic",
|
|
"[<channel>] [<topic>]",
|
|
handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "kick", "Kick user from channel",
|
|
"[<channel>] <user> [<reason>]",
|
|
handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "kickban", "Kick and ban user from channel",
|
|
"[<channel>] <user> [<reason>]",
|
|
handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "ban", "Ban user from channel",
|
|
"[<channel>] [<mask>...]",
|
|
handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "unban", "Unban user from channel",
|
|
"[<channel>] <mask>...",
|
|
handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
|
{ "invite", "Invite user to channel",
|
|
"<user>... [<channel>]",
|
|
handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST },
|
|
|
|
{ "server", "Manage servers",
|
|
"list | add <name> | delete <name> | rename <old> <new>",
|
|
handle_command_server, 0 },
|
|
{ "connect", "Connect to the server",
|
|
"[<server>]",
|
|
handle_command_connect, 0 },
|
|
{ "disconnect", "Disconnect from the server",
|
|
"[<server> [<reason>]]",
|
|
handle_command_disconnect, 0 },
|
|
|
|
{ "list", "List channels and their topic",
|
|
"[<channel>[,<channel>...]] [<target>]",
|
|
handle_command_list, HANDLER_SERVER },
|
|
{ "names", "List users on channel",
|
|
"[<channel>[,<channel>...]]",
|
|
handle_command_names, HANDLER_SERVER },
|
|
{ "who", "List users",
|
|
"[<mask> [o]]",
|
|
handle_command_who, HANDLER_SERVER },
|
|
{ "whois", "Get user information",
|
|
"[<target>] <mask>",
|
|
handle_command_whois, HANDLER_SERVER },
|
|
{ "whowas", "Get user information",
|
|
"<user> [<count> [<target>]]",
|
|
handle_command_whowas, HANDLER_SERVER },
|
|
|
|
{ "motd", "Get the Message of The Day",
|
|
"[<target>]",
|
|
handle_command_motd, HANDLER_SERVER },
|
|
{ "stats", "Query server statistics",
|
|
"[<query> [<target>]]",
|
|
handle_command_stats, HANDLER_SERVER },
|
|
{ "away", "Set away status",
|
|
"[<text>]",
|
|
handle_command_away, HANDLER_SERVER },
|
|
{ "nick", "Change current nick",
|
|
"<nickname>",
|
|
handle_command_nick, HANDLER_SERVER },
|
|
{ "quote", "Send a raw command to the server",
|
|
"<command>",
|
|
handle_command_quote, HANDLER_SERVER },
|
|
};
|
|
|
|
static bool
|
|
try_handle_command_help_option (struct app_context *ctx, const char *name)
|
|
{
|
|
struct config_item_ *item =
|
|
config_item_get (ctx->config.root, name, NULL);
|
|
if (!item)
|
|
return false;
|
|
|
|
struct config_schema *schema = item->schema;
|
|
if (!schema)
|
|
{
|
|
log_global_error (ctx, "#s: #s", "Option not recognized", name);
|
|
return true;
|
|
}
|
|
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "Option \"#s\":", name);
|
|
log_global_indent (ctx, " Description: #s", schema->comment);
|
|
log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type));
|
|
log_global_indent (ctx, " Default: #s",
|
|
schema->default_ ? schema->default_ : "null");
|
|
|
|
struct str tmp;
|
|
str_init (&tmp);
|
|
config_item_write (item, false, &tmp);
|
|
log_global_indent (ctx, " Current value: #s", tmp.str);
|
|
str_free (&tmp);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
show_command_list (struct app_context *ctx)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "Commands:");
|
|
|
|
int longest = 0;
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
int len = strlen (g_command_handlers[i].name);
|
|
longest = MAX (longest, len);
|
|
}
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
log_global_indent (ctx, " #&s", xstrdup_printf
|
|
("%-*s %s", longest, handler->name, handler->description));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
show_command_help (struct app_context *ctx, struct command_handler *handler)
|
|
{
|
|
log_global_indent (ctx, "");
|
|
log_global_indent (ctx, "/#s: #s", handler->name, handler->description);
|
|
log_global_indent (ctx, " Arguments: #s",
|
|
handler->usage ? handler->usage : "(none)");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_help (struct handler_args *a)
|
|
{
|
|
struct app_context *ctx = a->ctx;
|
|
if (!*a->arguments)
|
|
return show_command_list (ctx);
|
|
|
|
char *command = cut_word (&a->arguments);
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
if (!strcasecmp_ascii (command, handler->name))
|
|
return show_command_help (ctx, handler);
|
|
}
|
|
|
|
if (try_handle_command_help_option (ctx, command))
|
|
return true;
|
|
|
|
if (str_map_find (get_aliases_config (ctx), command))
|
|
log_global_status (ctx, "/#s is an alias", command);
|
|
else
|
|
log_global_error (ctx, "#s: #s", "No such command or option", command);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
init_user_command_map (struct str_map *map)
|
|
{
|
|
str_map_init (map);
|
|
map->key_xfrm = tolower_ascii_strxfrm;
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
str_map_set (map, handler->name, handler);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
process_user_command (struct app_context *ctx, struct buffer *buffer,
|
|
const char *command_name, char *input)
|
|
{
|
|
static bool initialized = false;
|
|
static struct str_map map;
|
|
if (!initialized)
|
|
{
|
|
init_user_command_map (&map);
|
|
initialized = true;
|
|
}
|
|
|
|
if (try_handle_buffer_goto (ctx, command_name))
|
|
return true;
|
|
|
|
struct handler_args args =
|
|
{
|
|
.ctx = ctx,
|
|
.buffer = buffer,
|
|
.arguments = input,
|
|
};
|
|
|
|
struct command_handler *handler;
|
|
if (!(handler = str_map_find (&map, command_name)))
|
|
return false;
|
|
hard_assert (handler->flags == 0 || (handler->flags & HANDLER_SERVER));
|
|
|
|
if ((handler->flags & HANDLER_SERVER)
|
|
&& args.buffer->type == BUFFER_GLOBAL)
|
|
log_global_error (ctx, "/#s: #s",
|
|
command_name, "can't do this from a global buffer");
|
|
else if ((handler->flags & HANDLER_SERVER)
|
|
&& !irc_is_connected ((args.s = args.buffer->server)))
|
|
log_server_error (args.s, args.s->buffer, "Not connected");
|
|
else if ((handler->flags & HANDLER_NEEDS_REG)
|
|
&& args.s->state != IRC_REGISTERED)
|
|
log_server_error (args.s, args.s->buffer, "Not registered");
|
|
else if (((handler->flags & HANDLER_CHANNEL_FIRST)
|
|
&& !(args.channel_name =
|
|
try_get_channel (&args, maybe_cut_word)))
|
|
|| ((handler->flags & HANDLER_CHANNEL_LAST)
|
|
&& !(args.channel_name =
|
|
try_get_channel (&args, maybe_cut_word_from_end))))
|
|
log_server_error (args.s, args.buffer, "/#s: #s", command_name,
|
|
"no channel name given and this buffer is not a channel");
|
|
else if (!handler->handler (&args))
|
|
log_global_error (ctx,
|
|
"#s: /#s #s", "Usage", handler->name, handler->usage);
|
|
return true;
|
|
}
|
|
|
|
static const char *
|
|
expand_alias_escape (const char *p, const char *arguments, struct str *output)
|
|
{
|
|
struct str_vector words;
|
|
str_vector_init (&words);
|
|
cstr_split_ignore_empty (arguments, ' ', &words);
|
|
|
|
// TODO: eventually also add support for argument ranges
|
|
int as_number = *p - '0';
|
|
if (as_number > 0 && as_number <= 9
|
|
&& (size_t) as_number <= words.len)
|
|
str_append (output, words.vector[as_number - 1]);
|
|
else if (*p == '*')
|
|
str_append (output, arguments);
|
|
else if (strchr ("$;", *p))
|
|
str_append_c (output, *p);
|
|
else
|
|
str_append_printf (output, "$%c", *p);
|
|
|
|
str_vector_free (&words);
|
|
return ++p;
|
|
}
|
|
|
|
static void
|
|
expand_alias_definition (const struct str *definition, const char *arguments,
|
|
struct str_vector *commands)
|
|
{
|
|
struct str expanded;
|
|
str_init (&expanded);
|
|
|
|
bool escape = false;
|
|
for (const char *p = definition->str; *p; p++)
|
|
{
|
|
if (escape)
|
|
{
|
|
p = expand_alias_escape (p, arguments, &expanded);
|
|
escape = false;
|
|
}
|
|
else if (*p == ';')
|
|
{
|
|
str_vector_add_owned (commands, str_steal (&expanded));
|
|
str_init (&expanded);
|
|
}
|
|
else if (*p == '$' && p[1])
|
|
escape = true;
|
|
else
|
|
str_append_c (&expanded, *p);
|
|
}
|
|
str_vector_add_owned (commands, str_steal (&expanded));
|
|
}
|
|
|
|
static bool
|
|
expand_alias (struct app_context *ctx,
|
|
const char *alias_name, char *input, struct str_vector *commands)
|
|
{
|
|
struct config_item_ *entry =
|
|
str_map_find (get_aliases_config (ctx), alias_name);
|
|
if (!entry)
|
|
return false;
|
|
|
|
if (!config_item_type_is_string (entry->type))
|
|
{
|
|
log_global_error (ctx, "Error executing `/%s': %s",
|
|
alias_name, "alias definition is not a string");
|
|
return false;
|
|
}
|
|
|
|
expand_alias_definition (&entry->value.string, input, commands);
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
send_message_to_target (struct server *s,
|
|
const char *target, char *message, struct buffer *buffer)
|
|
{
|
|
if (!irc_is_connected (s))
|
|
log_server_error (s, buffer, "Not connected");
|
|
else
|
|
SEND_AUTOSPLIT_PRIVMSG (s, target, message);
|
|
}
|
|
|
|
static void
|
|
send_message_to_buffer (struct app_context *ctx, struct buffer *buffer,
|
|
char *message)
|
|
{
|
|
hard_assert (buffer != NULL);
|
|
|
|
switch (buffer->type)
|
|
{
|
|
case BUFFER_CHANNEL:
|
|
send_message_to_target (buffer->server,
|
|
buffer->channel->name, message, buffer);
|
|
break;
|
|
case BUFFER_PM:
|
|
send_message_to_target (buffer->server,
|
|
buffer->user->nickname, message, buffer);
|
|
break;
|
|
default:
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR,
|
|
"This buffer is not a channel");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
process_alias (struct app_context *ctx, struct buffer *buffer,
|
|
struct str_vector *commands, int level)
|
|
{
|
|
for (size_t i = 0; i < commands->len; i++)
|
|
log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
|
|
(int) i, commands->vector[i]);
|
|
for (size_t i = 0; i < commands->len; i++)
|
|
if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
process_input_utf8 (struct app_context *ctx, struct buffer *buffer,
|
|
char *input, int alias_level)
|
|
{
|
|
if (*input != '/' || *++input == '/')
|
|
{
|
|
send_message_to_buffer (ctx, buffer, input);
|
|
return true;
|
|
}
|
|
|
|
char *name = cut_word (&input);
|
|
if (process_user_command (ctx, buffer, name, input))
|
|
return true;
|
|
|
|
struct str_vector commands;
|
|
str_vector_init (&commands);
|
|
|
|
bool result = false;
|
|
if (!expand_alias (ctx, name, input, &commands))
|
|
log_global_error (ctx, "#s: /#s", "No such command or alias", name);
|
|
else if (alias_level != 0)
|
|
log_global_error (ctx, "#s: /#s", "Aliases can't nest", name);
|
|
else
|
|
result = process_alias (ctx, buffer, &commands, alias_level);
|
|
|
|
str_vector_free (&commands);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
process_input (struct app_context *ctx, char *user_input)
|
|
{
|
|
char *input;
|
|
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
|
|
print_error ("character conversion failed for `%s'", "user input");
|
|
else
|
|
(void) process_input_utf8 (ctx, ctx->current_buffer, input, 0);
|
|
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 || !len)
|
|
return 0;
|
|
|
|
struct utf8_iter a[len];
|
|
for (size_t i = 0; i < len; i++)
|
|
utf8_iter_init (&a[i], vector[i]);
|
|
|
|
size_t ch_len;
|
|
int32_t ch;
|
|
while ((ch = utf8_iter_next (&a[0], &ch_len)) != -1)
|
|
{
|
|
for (size_t i = 1; i < len; i++)
|
|
{
|
|
int32_t other = utf8_iter_next (&a[i], NULL);
|
|
if (ch == other)
|
|
continue;
|
|
// Not bothering with lowercasing non-ASCII
|
|
if (ch >= 0x80 || other >= 0x80
|
|
|| tolower_ascii (ch) != tolower_ascii (other))
|
|
return prefix;
|
|
}
|
|
prefix += ch_len;
|
|
}
|
|
return prefix;
|
|
}
|
|
|
|
static void
|
|
complete_command (struct app_context *ctx, struct completion *data,
|
|
const char *word, struct str_vector *output)
|
|
{
|
|
(void) data;
|
|
|
|
const char *prefix = "";
|
|
if (*word == '/')
|
|
{
|
|
word++;
|
|
prefix = "/";
|
|
}
|
|
|
|
size_t word_len = strlen (word);
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
if (!strncasecmp_ascii (word, handler->name, word_len))
|
|
str_vector_add_owned (output,
|
|
xstrdup_printf ("%s%s", prefix, handler->name));
|
|
}
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, get_aliases_config (ctx));
|
|
struct config_item_ *alias;
|
|
while ((alias = str_map_iter_next (&iter)))
|
|
{
|
|
if (!strncasecmp_ascii (word, iter.link->key, word_len))
|
|
str_vector_add_owned (output,
|
|
xstrdup_printf ("%s%s", prefix, iter.link->key));
|
|
}
|
|
}
|
|
|
|
static void
|
|
complete_option (struct app_context *ctx, struct completion *data,
|
|
const char *word, struct str_vector *output)
|
|
{
|
|
(void) data;
|
|
|
|
struct str_vector options;
|
|
str_vector_init (&options);
|
|
|
|
config_dump (ctx->config.root, &options);
|
|
str_vector_sort (&options);
|
|
|
|
// Wildcard expansion is an interesting side-effect
|
|
char *mask = xstrdup_printf ("%s*", word);
|
|
for (size_t i = 0; i < options.len; i++)
|
|
{
|
|
char *key = cstr_cut_until (options.vector[i], " ");
|
|
if (!fnmatch (mask, key, 0))
|
|
str_vector_add_owned (output, key);
|
|
else
|
|
free (key);
|
|
}
|
|
free (mask);
|
|
str_vector_free (&options);
|
|
}
|
|
|
|
static void
|
|
complete_nicknames (struct app_context *ctx, struct completion *data,
|
|
const char *word, struct str_vector *output)
|
|
{
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
if (buffer->type != BUFFER_CHANNEL)
|
|
return;
|
|
|
|
size_t word_len = strlen (word);
|
|
LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
|
|
{
|
|
const char *nickname = iter->user->nickname;
|
|
if (irc_server_strncmp (buffer->server, word, nickname, word_len))
|
|
continue;
|
|
str_vector_add_owned (output, data->location == 0
|
|
? xstrdup_printf ("%s:", nickname)
|
|
: xstrdup (nickname));
|
|
}
|
|
}
|
|
|
|
static char **
|
|
complete_word (struct app_context *ctx, struct completion *data,
|
|
const char *word)
|
|
{
|
|
// First figure out what exactly 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);
|
|
|
|
// Normally, the text is deleted _afterwards_
|
|
rl_replace_line ("", false);
|
|
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 const char *g_first_time_help[] =
|
|
{
|
|
"",
|
|
"\x02Welcome to degesch!",
|
|
"",
|
|
"To get a list of all commands, type \x02/help\x0f. To obtain",
|
|
"more information on a command or option, simply add it as",
|
|
"a parameter, e.g. \x02/help set\x0f or \x02/help behaviour.logging\x0f.",
|
|
"",
|
|
"To switch between buffers, press \x02"
|
|
"F5/Ctrl-P\x0f or \x02" "F6/Ctrl-N\x0f.",
|
|
"",
|
|
"Finally, adding a network is as simple as:",
|
|
" - \x02/server add freenode\x0f",
|
|
" - \x02/set servers.freenode.addresses = \"chat.freenode.net\"\x0f",
|
|
" - \x02/connect freenode\x0f",
|
|
"",
|
|
"That should be enough to get you started. Have fun!",
|
|
""
|
|
};
|
|
|
|
static void
|
|
show_first_time_help (struct app_context *ctx)
|
|
{
|
|
for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++)
|
|
log_global_indent (ctx, "#m", g_first_time_help[i]);
|
|
}
|
|
|
|
const char *g_default_aliases[][2] =
|
|
{
|
|
{ "c", "/buffer clear" }, { "close", "/buffer close" },
|
|
{ "j", "/join $*" }, { "p", "/part $*" },
|
|
{ "k", "/kick $*" }, { "kb", "/kickban $*" },
|
|
{ "m", "/msg $*" }, { "q", "/query $*" },
|
|
{ "n", "/names $*" }, { "t", "/topic $*" },
|
|
{ "w", "/who $*" }, { "wi", "/whois $*" },
|
|
{ "ww", "/whowas $*" },
|
|
};
|
|
|
|
static void
|
|
load_default_aliases (struct app_context *ctx)
|
|
{
|
|
struct str_map *aliases = get_aliases_config (ctx);
|
|
for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
|
|
{
|
|
const char **pair = g_default_aliases[i];
|
|
str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1]));
|
|
}
|
|
}
|
|
|
|
static void
|
|
load_configuration (struct app_context *ctx)
|
|
{
|
|
struct config_item_ *root = NULL;
|
|
struct error *e = NULL;
|
|
|
|
char *filename = resolve_filename
|
|
(PROGRAM_NAME ".conf", resolve_relative_config_filename);
|
|
if (filename)
|
|
root = load_configuration_file (filename, &e);
|
|
else
|
|
log_global_error (ctx, "Configuration file not found");
|
|
free (filename);
|
|
|
|
if (e)
|
|
{
|
|
log_global_error (ctx, "#s", e->message);
|
|
error_free (e);
|
|
|
|
// That would be somewhat undesired
|
|
config_item_get (ctx->config.root, "behaviour.save_on_quit", NULL)
|
|
->value.boolean = false;
|
|
}
|
|
|
|
if (root)
|
|
{
|
|
config_item_destroy (ctx->config.root);
|
|
ctx->config.root = NULL;
|
|
config_load (&ctx->config, root);
|
|
log_global_status (ctx, "Configuration loaded");
|
|
}
|
|
else
|
|
{
|
|
show_first_time_help (ctx);
|
|
load_default_aliases (ctx);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
load_servers (struct app_context *ctx)
|
|
{
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, get_servers_config (ctx));
|
|
|
|
struct config_item_ *subtree;
|
|
while ((subtree = str_map_iter_next (&iter)))
|
|
{
|
|
const char *name = iter.link->key;
|
|
const char *err;
|
|
if (subtree->type != CONFIG_ITEM_OBJECT)
|
|
log_global_error (ctx, "Error in configuration: "
|
|
"ignoring server `#s' as it's not an object", name);
|
|
else if ((err = check_server_name_for_addition (ctx, name)))
|
|
log_global_error (ctx, "Cannot load server `#s': #s", name, err);
|
|
else
|
|
server_add (ctx, name, subtree);
|
|
}
|
|
}
|
|
|
|
// --- Signals -----------------------------------------------------------------
|
|
|
|
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
|
|
|
|
/// Program termination has been requested by a signal
|
|
static volatile sig_atomic_t g_termination_requested;
|
|
/// The window has changed in size
|
|
static volatile sig_atomic_t g_winch_received;
|
|
|
|
static void
|
|
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 ------------------------------------------------------
|
|
|
|
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)
|
|
// TODO: this way we don't send a QUIT message but just close the
|
|
// connection from our side and wait for a full close.
|
|
// Once we allow for custom quit messages, we will probably want to
|
|
// call irc_initiate_disconnect() for all servers.
|
|
initiate_quit (ctx);
|
|
|
|
if (g_winch_received)
|
|
{
|
|
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
|
|
rearm_flush_timer (struct app_context *ctx)
|
|
{
|
|
poller_timer_set (&ctx->flush_timer, 60 * 1000);
|
|
}
|
|
|
|
static void
|
|
on_flush_timer (struct app_context *ctx)
|
|
{
|
|
// I guess we don't need to do anything more complicated
|
|
fflush (NULL);
|
|
rearm_flush_timer (ctx);
|
|
}
|
|
|
|
static void
|
|
rearm_date_change_timer (struct app_context *ctx)
|
|
{
|
|
struct tm tm_;
|
|
const time_t now = time (NULL);
|
|
if (!soft_assert (localtime_r (&now, &tm_)))
|
|
return;
|
|
|
|
tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0;
|
|
tm_.tm_mday++;
|
|
tm_.tm_isdst = -1;
|
|
|
|
const time_t midnight = mktime (&tm_);
|
|
if (!soft_assert (midnight != (time_t) -1))
|
|
return;
|
|
poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000);
|
|
}
|
|
|
|
static void
|
|
on_date_change_timer (struct app_context *ctx)
|
|
{
|
|
buffer_update_time (ctx, time (NULL));
|
|
rearm_date_change_timer (ctx);
|
|
}
|
|
|
|
static void
|
|
init_poller_events (struct app_context *ctx)
|
|
{
|
|
poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]);
|
|
ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
|
|
ctx->signal_event.user_data = ctx;
|
|
poller_fd_set (&ctx->signal_event, POLLIN);
|
|
|
|
poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO);
|
|
ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
|
|
ctx->tty_event.user_data = ctx;
|
|
poller_fd_set (&ctx->tty_event, POLLIN);
|
|
|
|
poller_timer_init (&ctx->flush_timer, &ctx->poller);
|
|
ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
|
|
ctx->flush_timer.user_data = ctx;
|
|
rearm_flush_timer (ctx);
|
|
|
|
poller_timer_init (&ctx->date_chg_tmr, &ctx->poller);
|
|
ctx->date_chg_tmr.dispatcher = (poller_timer_fn) on_date_change_timer;
|
|
ctx->date_chg_tmr.user_data = ctx;
|
|
rearm_date_change_timer (ctx);
|
|
}
|
|
|
|
// --- Main program ------------------------------------------------------------
|
|
|
|
static const char *g_logo[] =
|
|
{
|
|
" __ __ ",
|
|
" __/ / ____ ____ ____ ____ ____ / /_ ",
|
|
" / / / , / / / / , / / __/ / __/ / __ \\ ",
|
|
" / / / / __/ / / / / __/ /_ / / /_ / / / / ",
|
|
"/___/ /___/ /_ / /___/ /___/ /___/ /_/ /_/ " PROGRAM_VERSION,
|
|
" /___/ ",
|
|
""
|
|
};
|
|
|
|
static void
|
|
show_logo (struct app_context *ctx)
|
|
{
|
|
for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
|
|
log_global_indent (ctx, "#m", g_logo[i]);
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
// We include a generated file from kike including this array we don't use;
|
|
// let's just keep it there and silence the compiler warning instead
|
|
(void) g_default_replies;
|
|
|
|
static const struct opt opts[] =
|
|
{
|
|
{ 'h', "help", NULL, 0, "display this help and exit" },
|
|
{ 'V', "version", NULL, 0, "output version information and exit" },
|
|
{ 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 '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);
|
|
|
|
// We only need to convert to and from the terminal encoding
|
|
setlocale (LC_CTYPE, "");
|
|
|
|
struct app_context ctx;
|
|
app_context_init (&ctx);
|
|
g_ctx = &ctx;
|
|
|
|
SSL_library_init ();
|
|
atexit (EVP_cleanup);
|
|
SSL_load_error_strings ();
|
|
atexit (ERR_free_strings);
|
|
|
|
// Bootstrap configuration, so that we can access schema items at all
|
|
register_config_modules (&ctx);
|
|
config_load (&ctx.config, config_item_object ());
|
|
|
|
// The following part is a bit brittle because of interdependencies
|
|
init_colors (&ctx);
|
|
init_global_buffer (&ctx);
|
|
show_logo (&ctx);
|
|
setup_signal_handlers ();
|
|
init_poller_events (&ctx);
|
|
load_configuration (&ctx);
|
|
|
|
// At this moment we can safely call any "on_change" callbacks
|
|
config_schema_call_changed (ctx.config.root);
|
|
|
|
// Finally, we juice the configuration for some servers to create
|
|
load_servers (&ctx);
|
|
|
|
refresh_prompt (&ctx);
|
|
input_start (&ctx.input, argv[0]);
|
|
|
|
ctx.polling = true;
|
|
while (ctx.polling)
|
|
poller_run (&ctx.poller);
|
|
|
|
if (get_config_boolean (ctx.config.root, "behaviour.save_on_quit"))
|
|
save_configuration (&ctx);
|
|
|
|
app_context_free (&ctx);
|
|
free_terminal ();
|
|
return EXIT_SUCCESS;
|
|
}
|