2015-04-12 04:52:39 +02:00
|
|
|
/*
|
2021-08-06 16:12:15 +02:00
|
|
|
* xC.c: a terminal-based IRC client
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
2022-08-17 18:27:37 +02:00
|
|
|
* Copyright (c) 2015 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
|
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
2018-06-21 22:42:05 +02:00
|
|
|
* purpose with or without fee is hereby granted.
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2015-04-29 21:25:57 +02:00
|
|
|
// A table of all attributes we use for output
|
2020-09-20 13:24:02 +02:00
|
|
|
#define ATTR_TABLE(XX) \
|
|
|
|
XX( PROMPT, prompt, Terminal attrs for the prompt ) \
|
|
|
|
XX( RESET, reset, String to reset terminal attributes ) \
|
|
|
|
XX( DATE_CHANGE, date_change, Terminal attrs for date change ) \
|
|
|
|
XX( READ_MARKER, read_marker, Terminal attrs for the read marker ) \
|
|
|
|
XX( WARNING, warning, Terminal attrs for warnings ) \
|
|
|
|
XX( ERROR, error, Terminal attrs for errors ) \
|
|
|
|
XX( EXTERNAL, external, Terminal attrs for external lines ) \
|
|
|
|
XX( TIMESTAMP, timestamp, Terminal attrs for timestamps ) \
|
|
|
|
XX( HIGHLIGHT, highlight, Terminal attrs for highlights ) \
|
|
|
|
XX( ACTION, action, Terminal attrs for user actions ) \
|
|
|
|
XX( USERHOST, userhost, Terminal attrs for user@host ) \
|
|
|
|
XX( JOIN, join, Terminal attrs for joins ) \
|
|
|
|
XX( PART, part, Terminal attrs for parts )
|
2015-04-29 20:56:26 +02:00
|
|
|
|
|
|
|
enum
|
|
|
|
{
|
|
|
|
#define XX(x, y, z) ATTR_ ## x,
|
|
|
|
ATTR_TABLE (XX)
|
|
|
|
#undef XX
|
|
|
|
ATTR_COUNT
|
|
|
|
};
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// User data for logger functions to enable formatted logging
|
2015-04-29 20:56:26 +02:00
|
|
|
#define print_fatal_data ((void *) ATTR_ERROR)
|
|
|
|
#define print_error_data ((void *) ATTR_ERROR)
|
|
|
|
#define print_warning_data ((void *) ATTR_WARNING)
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
#include "config.h"
|
2021-08-06 16:12:15 +02:00
|
|
|
#define PROGRAM_NAME "xC"
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
#include "common.c"
|
2021-08-06 16:12:15 +02:00
|
|
|
#include "xD-replies.c"
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2021-06-15 04:35:41 +02:00
|
|
|
#include <math.h>
|
2015-04-12 04:52:39 +02:00
|
|
|
#include <langinfo.h>
|
|
|
|
#include <locale.h>
|
|
|
|
#include <pwd.h>
|
2015-04-27 01:36:33 +02:00
|
|
|
#include <sys/utsname.h>
|
2015-05-07 05:19:13 +02:00
|
|
|
#include <wchar.h>
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-05 03:42:40 +02:00
|
|
|
#include <termios.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
#include <curses.h>
|
|
|
|
#include <term.h>
|
2015-04-13 00:06:08 +02:00
|
|
|
|
|
|
|
// Literally cancer
|
|
|
|
#undef lines
|
|
|
|
#undef columns
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
#include <ffi.h>
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
#ifdef HAVE_LUA
|
|
|
|
#include <lua.h>
|
|
|
|
#include <lualib.h>
|
|
|
|
#include <lauxlib.h>
|
|
|
|
#endif // HAVE_LUA
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// --- Terminal information ----------------------------------------------------
|
2015-05-06 22:20:02 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static struct
|
|
|
|
{
|
|
|
|
bool initialized; ///< Terminal is available
|
|
|
|
bool stdout_is_tty; ///< `stdout' is a terminal
|
|
|
|
bool stderr_is_tty; ///< `stderr' is a terminal
|
2015-05-06 22:20:02 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct termios termios; ///< Terminal attributes
|
|
|
|
char *color_set_fg[256]; ///< Codes to set the foreground colour
|
|
|
|
char *color_set_bg[256]; ///< Codes to set the background colour
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
int lines; ///< Number of lines
|
|
|
|
int columns; ///< Number of columns
|
|
|
|
}
|
|
|
|
g_terminal;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
update_screen_size (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
#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
|
|
|
|
}
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static bool
|
|
|
|
init_terminal (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
int err;
|
|
|
|
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Make sure all terminal features used by us are supported
|
|
|
|
if (!set_a_foreground || !set_a_background
|
|
|
|
|| !enter_bold_mode || !exit_attribute_mode
|
|
|
|
|| tcgetattr (tty_fd, &g_terminal.termios))
|
|
|
|
{
|
|
|
|
del_curterm (cur_term);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure newlines are output correctly
|
|
|
|
g_terminal.termios.c_oflag |= ONLCR;
|
|
|
|
(void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios);
|
|
|
|
|
|
|
|
g_terminal.lines = tigetnum ("lines");
|
|
|
|
g_terminal.columns = tigetnum ("cols");
|
|
|
|
update_screen_size ();
|
|
|
|
|
|
|
|
int max = MIN (256, max_colors);
|
|
|
|
for (int i = 0; i < max; i++)
|
|
|
|
{
|
|
|
|
g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
|
|
|
|
i, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
|
|
g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
|
|
|
|
i, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
|
|
}
|
|
|
|
return g_terminal.initialized = true;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
free_terminal (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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);
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// --- User interface ----------------------------------------------------------
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// I'm not sure which one of these backends is worse: whether it's GNU Readline
|
|
|
|
// or BSD Editline. They both have their own annoying problems. We use lots
|
|
|
|
// of hacks to get the results we want and need.
|
|
|
|
//
|
|
|
|
// The abstraction is a necessary evil. It's still not 100%, though.
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Some arbitrary limit for the history
|
|
|
|
#define HISTORY_LIMIT 10000
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Characters that separate words
|
|
|
|
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
struct input
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_vtable *vtable; ///< Virtual methods
|
2016-03-10 00:06:28 +01:00
|
|
|
void (*add_functions) (void *); ///< Define functions for binding
|
2016-03-07 01:08:52 +01:00
|
|
|
void *user_data; ///< User data for callbacks
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
typedef void *input_buffer_t; ///< Pointer alias for input buffers
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Named function that can be bound to a sequence of characters
|
|
|
|
typedef bool (*input_fn) (int count, int key, void *user_data);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// A little bit better than tons of forwarder functions in our case
|
|
|
|
#define CALL(self, name) ((self)->vtable->name ((self)))
|
|
|
|
#define CALL_(self, name, ...) ((self)->vtable->name ((self), __VA_ARGS__))
|
|
|
|
|
|
|
|
struct input_vtable
|
|
|
|
{
|
|
|
|
/// Start the interface under the given program name
|
|
|
|
void (*start) (void *input, const char *program_name);
|
|
|
|
/// Stop the interface
|
|
|
|
void (*stop) (void *input);
|
|
|
|
/// Prepare or unprepare terminal for our needs
|
|
|
|
void (*prepare) (void *input, bool enabled);
|
|
|
|
/// Destroy the object
|
|
|
|
void (*destroy) (void *input);
|
|
|
|
|
|
|
|
/// Hide the prompt if shown
|
|
|
|
void (*hide) (void *input);
|
|
|
|
/// Show the prompt if hidden
|
|
|
|
void (*show) (void *input);
|
2016-03-07 23:15:57 +01:00
|
|
|
/// Retrieve current prompt string
|
2016-03-07 01:08:52 +01:00
|
|
|
const char *(*get_prompt) (void *input);
|
|
|
|
/// Change the prompt string; takes ownership
|
|
|
|
void (*set_prompt) (void *input, char *prompt);
|
|
|
|
/// Ring the terminal bell
|
|
|
|
void (*ding) (void *input);
|
|
|
|
|
|
|
|
/// Create a new input buffer
|
|
|
|
input_buffer_t (*buffer_new) (void *input);
|
|
|
|
/// Destroy an input buffer
|
|
|
|
void (*buffer_destroy) (void *input, input_buffer_t buffer);
|
|
|
|
/// Switch to a different input buffer
|
|
|
|
void (*buffer_switch) (void *input, input_buffer_t buffer);
|
|
|
|
|
|
|
|
/// Register a function that can be bound to character sequences
|
|
|
|
void (*register_fn) (void *input,
|
|
|
|
const char *name, const char *help, input_fn fn, void *user_data);
|
|
|
|
/// Bind an arbitrary sequence of characters to the given named function
|
|
|
|
void (*bind) (void *input, const char *seq, const char *fn);
|
|
|
|
/// Bind Ctrl+key to the given named function
|
|
|
|
void (*bind_control) (void *input, char key, const char *fn);
|
|
|
|
/// Bind Alt+key to the given named function
|
|
|
|
void (*bind_meta) (void *input, char key, const char *fn);
|
|
|
|
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
/// Get the current line input and position within
|
|
|
|
char *(*get_line) (void *input, int *position);
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Clear the current line input
|
|
|
|
void (*clear_line) (void *input);
|
|
|
|
/// Insert text at current position
|
|
|
|
bool (*insert) (void *input, const char *text);
|
|
|
|
|
|
|
|
/// Handle terminal resize
|
|
|
|
void (*on_tty_resized) (void *input);
|
|
|
|
/// Handle terminal input
|
|
|
|
void (*on_tty_readable) (void *input);
|
2015-05-05 03:23:53 +02:00
|
|
|
};
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define INPUT_VTABLE(XX) \
|
|
|
|
XX (start) XX (stop) XX (prepare) XX (destroy) \
|
|
|
|
XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \
|
|
|
|
XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \
|
|
|
|
XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta) \
|
|
|
|
XX (get_line) XX (clear_line) XX (insert) \
|
|
|
|
XX (on_tty_resized) XX (on_tty_readable)
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ GNU Readline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#ifdef HAVE_READLINE
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#include <readline/readline.h>
|
|
|
|
#include <readline/history.h>
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE
|
|
|
|
#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn
|
|
|
|
{
|
|
|
|
ffi_closure closure; ///< Closure
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
LIST_HEADER (struct input_rl_fn)
|
|
|
|
input_fn callback; ///< Real callback
|
|
|
|
void *user_data; ///< Real callback user data
|
|
|
|
};
|
|
|
|
|
|
|
|
struct input_rl_buffer
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
HISTORY_STATE *history; ///< Saved history state
|
|
|
|
char *saved_line; ///< Saved line content
|
|
|
|
int saved_point; ///< Saved cursor position
|
|
|
|
int saved_mark; ///< Saved mark
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl
|
|
|
|
{
|
|
|
|
struct input super; ///< Parent class
|
|
|
|
|
|
|
|
bool active; ///< Interface has been started
|
|
|
|
char *prompt; ///< The prompt we use
|
|
|
|
int prompt_shown; ///< Whether the prompt is shown now
|
|
|
|
|
|
|
|
char *saved_line; ///< Saved line content
|
|
|
|
int saved_point; ///< Saved cursor position
|
|
|
|
int saved_mark; ///< Saved mark
|
|
|
|
|
|
|
|
struct input_rl_fn *fns; ///< Named functions
|
|
|
|
struct input_rl_buffer *current; ///< Current input buffer
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_ding (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
|
|
|
rl_ding ();
|
|
|
|
}
|
|
|
|
|
|
|
|
static const char *
|
|
|
|
input_rl_get_prompt (void *input)
|
|
|
|
{
|
|
|
|
struct input_rl *self = input;
|
|
|
|
return self->prompt;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_set_prompt (void *input, char *prompt)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->prompt, prompt);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2020-10-06 13:37:07 +02:00
|
|
|
if (!self->active || self->prompt_shown <= 0)
|
2015-05-12 07:07:02 +02:00
|
|
|
return;
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// First reset the prompt to work around a bug in readline
|
|
|
|
rl_set_prompt ("");
|
2020-10-06 13:37:07 +02:00
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
rl_set_prompt (self->prompt);
|
2020-10-06 13:37:07 +02:00
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_clear_line (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line ("", false);
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_redisplay ();
|
|
|
|
}
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__erase (struct input_rl *self)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
|
|
|
rl_set_prompt ("");
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_clear_line (self);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static bool
|
|
|
|
input_rl_insert (void *input, const char *s)
|
|
|
|
{
|
|
|
|
struct input_rl *self = input;
|
|
|
|
rl_insert_text (s);
|
|
|
|
if (self->prompt_shown > 0)
|
|
|
|
rl_redisplay ();
|
|
|
|
|
|
|
|
// GNU Readline, contrary to Editline, doesn't care about validity
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
input_rl_get_line (void *input, int *position)
|
2016-03-07 01:08:52 +01:00
|
|
|
{
|
|
|
|
(void) input;
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
if (position) *position = rl_point;
|
2016-03-07 01:08:52 +01:00
|
|
|
return rl_copy_text (0, rl_end);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (void *input, const char *seq, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
2015-05-08 06:23:38 +02:00
|
|
|
rl_bind_keyseq (seq, rl_named_function (function_name));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind_meta (void *input, char key, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
|
|
|
// This one seems to actually work
|
|
|
|
char keyseq[] = { '\\', 'e', key, 0 };
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (input, keyseq, function_name);
|
2015-05-08 06:23:38 +02:00
|
|
|
#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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind_control (void *input, char key, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
|
|
|
char keyseq[] = { '\\', 'C', '-', key, 0 };
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (input, keyseq, function_name);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
|
2016-03-06 17:59:45 +01:00
|
|
|
{
|
|
|
|
(void) cif;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn *data = user_data;
|
2016-03-06 17:59:45 +01:00
|
|
|
if (!data->callback
|
|
|
|
(*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data))
|
|
|
|
rl_ding ();
|
|
|
|
*(int *) ret = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_register_fn (void *input,
|
2016-03-06 17:59:45 +01:00
|
|
|
const char *name, const char *help, input_fn callback, void *user_data)
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2016-03-06 17:59:45 +01:00
|
|
|
(void) help;
|
|
|
|
|
|
|
|
void *bound_fn = NULL;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
|
2016-03-06 17:59:45 +01:00
|
|
|
hard_assert (data);
|
|
|
|
|
|
|
|
static ffi_cif cif;
|
|
|
|
static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint };
|
|
|
|
hard_assert (ffi_prep_cif
|
|
|
|
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK);
|
|
|
|
|
|
|
|
data->prev = data->next = NULL;
|
|
|
|
data->callback = callback;
|
|
|
|
data->user_data = user_data;
|
|
|
|
hard_assert (ffi_prep_closure_loc (&data->closure,
|
2016-03-07 01:08:52 +01:00
|
|
|
&cif, input_rl__forward, data, bound_fn) == FFI_OK);
|
2016-03-06 17:59:45 +01:00
|
|
|
|
|
|
|
rl_add_defun (name, (rl_command_func_t *) bound_fn, -1);
|
|
|
|
LIST_PREPEND (self->fns, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
static int app_readline_init (void);
|
|
|
|
static void on_readline_input (char *line);
|
2015-05-06 17:32:29 +02:00
|
|
|
static char **app_readline_completion (const char *text, int start, int end);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_start (void *input, const char *program_name)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
using_history ();
|
|
|
|
// This can cause memory leaks, or maybe even a segfault. Funny, eh?
|
|
|
|
stifle_history (HISTORY_LIMIT);
|
|
|
|
|
2015-05-06 22:01:48 +02:00
|
|
|
const char *slash = strrchr (program_name, '/');
|
|
|
|
rl_readline_name = slash ? ++slash : program_name;
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_startup_hook = app_readline_init;
|
|
|
|
rl_catch_sigwinch = false;
|
2020-03-21 22:02:02 +01:00
|
|
|
rl_change_environment = false;
|
2015-05-06 22:01:48 +02:00
|
|
|
|
2015-05-06 22:20:02 +02:00
|
|
|
rl_basic_word_break_characters = WORD_BREAKING_CHARS;
|
2015-05-06 22:01:48 +02:00
|
|
|
rl_completer_word_break_characters = NULL;
|
2015-05-06 17:32:29 +02:00
|
|
|
rl_attempted_completion_function = app_readline_completion;
|
2015-05-06 22:01:48 +02:00
|
|
|
|
2021-07-23 18:42:21 +02:00
|
|
|
// We shouldn't produce any duplicates that the library would help us
|
|
|
|
// autofilter, and we don't generally want alphabetic ordering at all
|
|
|
|
rl_sort_completion_matches = false;
|
|
|
|
|
2015-05-06 22:01:48 +02:00
|
|
|
hard_assert (self->prompt != NULL);
|
2016-03-10 00:06:28 +01:00
|
|
|
// The inputrc is read before any callbacks are called, so we need to
|
|
|
|
// register all functions that our user may want to map up front
|
|
|
|
self->super.add_functions (self->super.user_data);
|
2015-05-06 22:01:48 +02:00
|
|
|
rl_callback_handler_install (self->prompt, on_readline_input);
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
self->prompt_shown = 1;
|
|
|
|
self->active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_stop (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 03:23:53 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__erase (self);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
// This is okay as long as we're not called from within readline
|
|
|
|
rl_callback_handler_remove ();
|
|
|
|
self->active = false;
|
2015-05-12 07:07:02 +02:00
|
|
|
self->prompt_shown = false;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
input_rl_prepare (void *input, bool enabled)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
if (enabled)
|
|
|
|
rl_prep_terminal (true);
|
|
|
|
else
|
|
|
|
rl_deprep_terminal ();
|
|
|
|
}
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
// 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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
|
|
|
(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;
|
2015-11-24 03:04:14 +01:00
|
|
|
|
|
|
|
rl_replace_line ("", true);
|
|
|
|
if (self->prompt_shown > 0)
|
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
|
|
|
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;
|
2015-07-04 01:06:51 +02:00
|
|
|
state->entries = NULL;
|
2015-05-05 03:23:53 +02:00
|
|
|
history_set_history_state (state);
|
|
|
|
free (state);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buffer->saved_line)
|
|
|
|
{
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line (buffer->saved_line, true);
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_point = buffer->saved_point;
|
|
|
|
rl_mark = buffer->saved_mark;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&buffer->saved_line, NULL);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-07-05 17:29:44 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_redisplay ();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_buffer_switch (void *input, input_buffer_t input_buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
|
|
|
struct input_rl_buffer *buffer = input_buffer;
|
2015-05-05 03:23:53 +02:00
|
|
|
// 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)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save_buffer (self, self->current);
|
2015-05-05 03:23:53 +02:00
|
|
|
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
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore_buffer (self, buffer);
|
2015-05-05 03:23:53 +02:00
|
|
|
self->current = buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
free (self->history);
|
|
|
|
free (self->saved_line);
|
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_rl_buffer_destroy (void *input, input_buffer_t input_buffer)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
struct input_rl_buffer *buffer = input_buffer;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
// See input_rl_buffer_switch() for why we need to do this BS
|
2015-05-05 03:23:53 +02:00
|
|
|
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 ();
|
2015-06-28 23:00:46 +02:00
|
|
|
// rl_clear_history just removes history entries,
|
|
|
|
// we have to reclaim memory for their actual container ourselves
|
|
|
|
free (buffer->history->entries);
|
2015-06-20 19:50:45 +02:00
|
|
|
free (buffer->history);
|
|
|
|
buffer->history = NULL;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
history_set_history_state (state);
|
|
|
|
free (state);
|
|
|
|
}
|
|
|
|
#endif // RL_READLINE_VERSION
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__buffer_destroy_wo_history (buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static input_buffer_t
|
|
|
|
input_rl_buffer_new (void *input)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
struct input_rl_buffer *self = xcalloc (1, sizeof *self);
|
|
|
|
return self;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// Since {save,restore}_buffer() store history, we can't use them here like we
|
|
|
|
// do with libedit, because then buffer_destroy() can free memory that's still
|
|
|
|
// being used by readline. This situation is bound to happen on quit.
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save (struct input_rl *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore (struct input_rl *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
hard_assert (self->saved_line);
|
|
|
|
|
|
|
|
rl_set_prompt (self->prompt);
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line (self->saved_line, false);
|
2015-05-05 08:46:59 +02:00
|
|
|
rl_point = self->saved_point;
|
|
|
|
rl_mark = self->saved_mark;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->saved_line, NULL);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_hide (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || self->prompt_shown-- < 1)
|
|
|
|
return;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save (self);
|
|
|
|
input_rl__erase (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_rl_show (void *input)
|
|
|
|
{
|
|
|
|
struct input_rl *self = input;
|
|
|
|
if (!self->active || ++self->prompt_shown < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
input_rl__restore (self);
|
|
|
|
rl_redisplay ();
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_rl_on_tty_resized (void *input)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
// This fucks up big time on terminals with automatic wrapping such as
|
|
|
|
// rxvt-unicode or newer VTE when the current line overflows, however we
|
|
|
|
// can't do much about that
|
|
|
|
rl_resize_terminal ();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_rl_on_tty_readable (void *input)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
rl_callback_read_char ();
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_rl_destroy (void *input)
|
|
|
|
{
|
|
|
|
struct input_rl *self = input;
|
|
|
|
free (self->saved_line);
|
|
|
|
LIST_FOR_EACH (struct input_rl_fn, iter, self->fns)
|
|
|
|
ffi_closure_free (iter);
|
|
|
|
free (self->prompt);
|
|
|
|
free (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define XX(a) .a = input_rl_ ## a,
|
|
|
|
static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
|
|
|
|
#undef XX
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static struct input *
|
|
|
|
input_rl_new (void)
|
|
|
|
{
|
|
|
|
struct input_rl *self = xcalloc (1, sizeof *self);
|
|
|
|
self->super.vtable = &input_rl_vtable;
|
|
|
|
return &self->super;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define input_new input_rl_new
|
2015-05-05 08:46:59 +02:00
|
|
|
#endif // HAVE_READLINE
|
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ BSD Editline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
#ifdef HAVE_EDITLINE
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#include <histedit.h>
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#define INPUT_START_IGNORE '\x01'
|
|
|
|
#define INPUT_END_IGNORE '\x01'
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
ffi_closure closure; ///< Closure
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
LIST_HEADER (struct input_el_fn)
|
|
|
|
input_fn callback; ///< Real callback
|
|
|
|
void *user_data; ///< Real callback user data
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
wchar_t *name; ///< Function name
|
|
|
|
wchar_t *help; ///< Function help
|
|
|
|
};
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_buffer
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
HistoryW *history; ///< The history object
|
|
|
|
wchar_t *saved_line; ///< Saved line content
|
|
|
|
int saved_len; ///< Length of the saved line
|
|
|
|
int saved_point; ///< Saved cursor position
|
|
|
|
};
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input super; ///< Parent class
|
|
|
|
EditLine *editline; ///< The EditLine object
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
bool active; ///< Are we a thing?
|
|
|
|
char *prompt; ///< The prompt we use
|
|
|
|
int prompt_shown; ///< Whether the prompt is shown now
|
|
|
|
|
|
|
|
struct input_el_fn *fns; ///< Named functions
|
|
|
|
struct input_el_buffer *current; ///< Current input buffer
|
|
|
|
};
|
|
|
|
|
|
|
|
static void app_editline_init (struct input_el *self);
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2021-10-28 08:18:15 +02:00
|
|
|
// See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT
|
|
|
|
// so we bind redisplay somewhere else in app_editline_init()
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2021-10-28 08:18:15 +02:00
|
|
|
char x[] = { 'q' & 31, 0 };
|
2015-05-05 08:46:59 +02:00
|
|
|
el_push (self->editline, x);
|
|
|
|
|
|
|
|
// We have to do this or it gets stuck and nothing is done
|
2020-09-02 19:11:10 +02:00
|
|
|
int count = 0;
|
|
|
|
(void) el_wgets (self->editline, &count);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_prompt (EditLine *editline)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self;
|
2015-05-05 08:46:59 +02:00
|
|
|
el_get (editline, EL_CLIENTDATA, &self);
|
2015-05-05 22:35:51 +02:00
|
|
|
if (!self->prompt)
|
|
|
|
return "";
|
2015-05-05 08:46:59 +02:00
|
|
|
return self->prompt;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_empty_prompt (EditLine *editline)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
(void) editline;
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_ding (void *input)
|
|
|
|
{
|
|
|
|
// XXX: this isn't probably very portable;
|
|
|
|
// we could use "bell" from terminfo but that creates a dependency
|
|
|
|
(void) input;
|
|
|
|
write (STDOUT_FILENO, "\a", 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const char *
|
|
|
|
input_el_get_prompt (void *input)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
|
|
|
return self->prompt;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_set_prompt (void *input, char *prompt)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->prompt, prompt);
|
2016-03-07 01:08:52 +01:00
|
|
|
|
|
|
|
if (self->prompt_shown > 0)
|
|
|
|
input_el__redisplay (self);
|
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_clear_line (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
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);
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (self);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__erase (struct input_el *self)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
|
|
|
|
input_el_clear_line (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
2015-11-15 00:59:11 +01:00
|
|
|
static bool
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_insert (void *input, const char *s)
|
2015-05-12 03:48:52 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-12-25 04:21:18 +01:00
|
|
|
bool success = !*s || !el_insertstr (self->editline, s);
|
2015-07-06 01:54:02 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (self);
|
2015-11-15 00:59:11 +01:00
|
|
|
return success;
|
2015-05-12 03:48:52 +02:00
|
|
|
}
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static char *
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
input_el_get_line (void *input, int *position)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-12-25 04:21:18 +01:00
|
|
|
const LineInfo *info = el_line (self->editline);
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
int point = info->cursor - info->buffer;
|
|
|
|
if (position) *position = point;
|
2015-12-25 04:21:18 +01:00
|
|
|
return xstrndup (info->buffer, info->lastchar - info->buffer);
|
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_bind (void *input, const char *seq, const char *function_name)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
|
|
|
el_set (self->editline, EL_BIND, seq, function_name, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_bind_meta (void *input, char key, const char *function_name)
|
|
|
|
{
|
|
|
|
char keyseq[] = { 'M', '-', key, 0 };
|
|
|
|
input_el_bind (input, keyseq, function_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_bind_control (void *input, char key, const char *function_name)
|
|
|
|
{
|
|
|
|
char keyseq[] = { '^', key, 0 };
|
|
|
|
input_el_bind (input, keyseq, function_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
|
2016-03-06 17:59:45 +01:00
|
|
|
{
|
|
|
|
(void) cif;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn *data = user_data;
|
2016-03-06 17:59:45 +01:00
|
|
|
*(unsigned char *) ret = data->callback
|
|
|
|
(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
static wchar_t *
|
|
|
|
ascii_to_wide (const char *ascii)
|
|
|
|
{
|
|
|
|
size_t len = strlen (ascii) + 1;
|
|
|
|
wchar_t *wide = xcalloc (sizeof *wide, len);
|
|
|
|
while (len--)
|
|
|
|
hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
|
|
|
|
return wide;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_register_fn (void *input,
|
2016-03-06 17:59:45 +01:00
|
|
|
const char *name, const char *help, input_fn callback, void *user_data)
|
|
|
|
{
|
|
|
|
void *bound_fn = NULL;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
|
2016-03-06 17:59:45 +01:00
|
|
|
hard_assert (data);
|
|
|
|
|
|
|
|
static ffi_cif cif;
|
|
|
|
static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
|
|
|
|
hard_assert (ffi_prep_cif
|
|
|
|
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
|
|
|
|
|
|
|
|
data->user_data = user_data;
|
|
|
|
data->callback = callback;
|
|
|
|
data->name = ascii_to_wide (name);
|
|
|
|
data->help = ascii_to_wide (help);
|
|
|
|
hard_assert (ffi_prep_closure_loc (&data->closure,
|
2016-03-07 01:08:52 +01:00
|
|
|
&cif, input_el__forward, data, bound_fn) == FFI_OK);
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2016-03-06 17:59:45 +01:00
|
|
|
el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
|
|
|
|
LIST_PREPEND (self->fns, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_start (void *input, const char *program_name)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
self->editline = el_init (program_name, stdin, stdout, stderr);
|
|
|
|
el_set (self->editline, EL_CLIENTDATA, self);
|
|
|
|
el_set (self->editline, EL_PROMPT_ESC,
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_prompt, INPUT_START_IGNORE);
|
2015-05-05 08:46:59 +02:00
|
|
|
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);
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
self->prompt_shown = 1;
|
|
|
|
self->active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_stop (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__erase (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
el_end (self->editline);
|
|
|
|
self->editline = NULL;
|
|
|
|
self->active = false;
|
2015-05-12 07:07:02 +02:00
|
|
|
self->prompt_shown = false;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
input_el_prepare (void *input, bool enabled)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
|
|
|
el_set (self->editline, EL_PREP_TERM, enabled);
|
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save (struct input_el *self)
|
|
|
|
{
|
|
|
|
if (self->current)
|
|
|
|
input_el__save_buffer (self, self->current);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el__restore_buffer (struct input_el *self, struct input_el_buffer *buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
if (buffer->saved_line)
|
|
|
|
{
|
|
|
|
el_winsertstr (self->editline, buffer->saved_line);
|
|
|
|
el_cursor (self->editline,
|
|
|
|
-(buffer->saved_len - buffer->saved_point));
|
2020-09-02 19:01:31 +02:00
|
|
|
free (buffer->saved_line);
|
|
|
|
buffer->saved_line = NULL;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore (struct input_el *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
if (self->current)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore_buffer (self, self->current);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_buffer_switch (void *input, input_buffer_t input_buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
|
|
|
struct input_el_buffer *buffer = input_buffer;
|
|
|
|
|
2015-05-05 22:35:51 +02:00
|
|
|
if (self->current)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save_buffer (self, self->current);
|
|
|
|
|
|
|
|
input_el__restore_buffer (self, buffer);
|
|
|
|
el_wset (self->editline, EL_HIST, history, buffer->history);
|
|
|
|
self->current = buffer;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
|
|
|
struct input_el_buffer *buffer = input_buffer;
|
|
|
|
|
|
|
|
history_wend (buffer->history);
|
|
|
|
free (buffer->saved_line);
|
|
|
|
free (buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static input_buffer_t
|
|
|
|
input_el_buffer_new (void *input)
|
|
|
|
{
|
|
|
|
(void) input;
|
|
|
|
struct input_el_buffer *self = xcalloc (1, sizeof *self);
|
|
|
|
self->history = history_winit ();
|
|
|
|
|
|
|
|
HistEventW ev;
|
|
|
|
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
|
|
|
|
return self;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_hide (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || self->prompt_shown-- < 1)
|
|
|
|
return;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save (self);
|
|
|
|
input_el__erase (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_show (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || ++self->prompt_shown < 1)
|
|
|
|
return;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
el_set (self->editline,
|
2016-03-07 01:08:52 +01:00
|
|
|
EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE);
|
|
|
|
input_el__redisplay (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_on_tty_resized (void *input)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
|
|
|
el_resize (self->editline);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_on_tty_readable (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
// We bind the return key to process it how we need to
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */)
|
2015-05-05 20:18:41 +02:00
|
|
|
{
|
|
|
|
el_deletestr (self->editline, 1);
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (self);
|
|
|
|
input_el_ding (self);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_el_destroy (void *input)
|
|
|
|
{
|
|
|
|
struct input_el *self = input;
|
|
|
|
LIST_FOR_EACH (struct input_el_fn, iter, self->fns)
|
|
|
|
{
|
|
|
|
free (iter->name);
|
|
|
|
free (iter->help);
|
|
|
|
ffi_closure_free (iter);
|
2015-05-05 20:18:41 +02:00
|
|
|
}
|
2016-03-07 01:08:52 +01:00
|
|
|
free (self->prompt);
|
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
#define XX(a) .a = input_el_ ## a,
|
|
|
|
static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) };
|
|
|
|
#undef XX
|
|
|
|
|
|
|
|
static struct input *
|
|
|
|
input_el_new (void)
|
|
|
|
{
|
|
|
|
struct input_el *self = xcalloc (1, sizeof *self);
|
|
|
|
self->super.vtable = &input_el_vtable;
|
|
|
|
return &self->super;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define input_new input_el_new
|
2015-05-05 08:46:59 +02:00
|
|
|
#endif // HAVE_EDITLINE
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// --- Application data --------------------------------------------------------
|
|
|
|
|
2020-10-02 07:09:58 +02:00
|
|
|
// All text stored in our data structures is encoded in UTF-8. Or at least
|
|
|
|
// should be--our only ways of retrieving strings are: via the command line
|
|
|
|
// (converted from locale, no room for errors), via the configuration file
|
|
|
|
// (restrictive ASCII grammar for bare words and an internal check for strings),
|
|
|
|
// and via plugins (meticulously validated).
|
|
|
|
//
|
|
|
|
// The only exception is IRC identifiers.
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Scripting support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-04-21 00:29:07 +02:00
|
|
|
|
2015-11-18 23:03:21 +01:00
|
|
|
// We need a few reference countable objects with support for both strong
|
|
|
|
// and weak references (mainly used for scripted plugins).
|
|
|
|
//
|
|
|
|
// Beware that if you don't own the object, you will most probably want
|
|
|
|
// to keep the weak reference link so that you can get rid of it later.
|
|
|
|
// Also note that you have to make sure the user_data don't leak resources.
|
|
|
|
//
|
|
|
|
// Having a callback is more versatile than just nulling out a pointer.
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2015-04-21 00:04:34 +02:00
|
|
|
/// Callback just before a reference counted object is destroyed
|
|
|
|
typedef void (*destroy_cb_fn) (void *object, void *user_data);
|
|
|
|
|
2015-11-18 23:03:21 +01:00
|
|
|
struct weak_ref_link
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct weak_ref_link)
|
|
|
|
|
|
|
|
destroy_cb_fn on_destroy; ///< Called when object is destroyed
|
|
|
|
void *user_data; ///< User data
|
|
|
|
};
|
|
|
|
|
2015-11-21 15:15:49 +01:00
|
|
|
static struct weak_ref_link *
|
|
|
|
weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data)
|
|
|
|
{
|
|
|
|
struct weak_ref_link *link = xcalloc (1, sizeof *link);
|
|
|
|
link->on_destroy = cb;
|
|
|
|
link->user_data = user_data;
|
|
|
|
LIST_PREPEND (*list, link);
|
|
|
|
return link;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
weak_unref (struct weak_ref_link **list, struct weak_ref_link **link)
|
|
|
|
{
|
|
|
|
if (*link)
|
|
|
|
LIST_UNLINK (*list, *link);
|
|
|
|
free (*link);
|
|
|
|
*link = NULL;
|
|
|
|
}
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
#define REF_COUNTABLE_HEADER \
|
|
|
|
size_t ref_count; /**< Reference count */ \
|
2015-11-18 23:03:21 +01:00
|
|
|
struct weak_ref_link *weak_refs; /**< To remove any weak references */
|
2015-04-21 00:29:07 +02:00
|
|
|
|
|
|
|
#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; \
|
2015-11-18 23:03:21 +01:00
|
|
|
LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \
|
|
|
|
{ \
|
|
|
|
iter->on_destroy (self, iter->user_data); \
|
|
|
|
free (iter); \
|
|
|
|
} \
|
2015-04-21 00:29:07 +02:00
|
|
|
name ## _destroy (self); \
|
2015-11-18 23:03:21 +01:00
|
|
|
} \
|
|
|
|
\
|
|
|
|
static struct weak_ref_link * \
|
|
|
|
name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \
|
2016-10-27 11:48:48 +02:00
|
|
|
{ return weak_ref (&self->weak_refs, cb, user_data); } \
|
2015-11-18 23:03:21 +01:00
|
|
|
\
|
|
|
|
static void \
|
|
|
|
name ## _weak_unref (struct name *self, struct weak_ref_link **link) \
|
2015-11-21 15:15:49 +01:00
|
|
|
{ weak_unref (&self->weak_refs, link); }
|
2015-04-21 00:29:07 +02:00
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
// Simple introspection framework to simplify exporting stuff to Lua, since
|
|
|
|
// there is a lot of it. While not fully automated, at least we have control
|
|
|
|
// over which fields are exported.
|
|
|
|
|
|
|
|
enum ispect_type
|
|
|
|
{
|
2016-10-27 19:10:35 +02:00
|
|
|
ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING,
|
|
|
|
|
|
|
|
ISPECT_STR, ///< "struct str"
|
|
|
|
ISPECT_STR_MAP, ///< "struct str_map"
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_REF ///< Weakly referenced object
|
2016-10-27 17:03:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
struct ispect_field
|
|
|
|
{
|
|
|
|
const char *name; ///< Name of the field
|
|
|
|
size_t offset; ///< Offset in the structure
|
|
|
|
enum ispect_type type; ///< Type of the field
|
2016-10-28 00:07:49 +02:00
|
|
|
|
|
|
|
enum ispect_type subtype; ///< STR_MAP subtype
|
|
|
|
struct ispect_field *fields; ///< REF target fields
|
|
|
|
bool is_list; ///< REF target is a list
|
2016-10-27 17:03:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
#define ISPECT(object, field, type) \
|
2016-10-28 00:07:49 +02:00
|
|
|
{ #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false },
|
|
|
|
#define ISPECT_REF(object, field, is_list, ref_type) \
|
|
|
|
{ #field, offsetof (struct object, field), ISPECT_REF, 0, \
|
|
|
|
g_##ref_type##_ispect, is_list },
|
|
|
|
#define ISPECT_MAP(object, field, subtype) \
|
|
|
|
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \
|
|
|
|
ISPECT_##subtype, NULL, false },
|
|
|
|
#define ISPECT_MAP_REF(object, field, is_list, ref_type) \
|
|
|
|
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \
|
|
|
|
ISPECT_REF, g_##ref_type##_ispect, is_list },
|
2016-10-27 17:03:53 +02:00
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Chat ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2016-10-27 17:03:53 +02:00
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct user_channel
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct user_channel)
|
|
|
|
|
|
|
|
struct channel *channel; ///< Reference to channel
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct user_channel *
|
2020-10-04 08:32:15 +02:00
|
|
|
user_channel_new (struct channel *channel)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
struct user_channel *self = xcalloc (1, sizeof *self);
|
2020-10-04 08:32:15 +02:00
|
|
|
self->channel = channel;
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:29:07 +02:00
|
|
|
user_channel_destroy (struct user_channel *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
2015-04-25 00:36:02 +02:00
|
|
|
// The "channel" reference is weak and this object should get
|
|
|
|
// destroyed whenever the user stops being in the channel.
|
2015-04-19 02:12:59 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// We keep references to user information in channels and buffers,
|
2015-04-21 00:04:34 +02:00
|
|
|
// and weak references in the name lookup table.
|
2015-04-19 02:12:59 +02:00
|
|
|
|
|
|
|
struct user
|
2015-04-18 22:09:05 +02:00
|
|
|
{
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2015-04-18 22:09:05 +02:00
|
|
|
char *nickname; ///< Literal nickname
|
|
|
|
bool away; ///< User is away
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
struct user_channel *channels; ///< Channels the user is on (with us)
|
2015-04-18 22:09:05 +02:00
|
|
|
};
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_user_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
|
|
|
ISPECT( user, nickname, STRING )
|
|
|
|
ISPECT( user, away, BOOL )
|
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
static struct user *
|
2020-10-04 08:32:15 +02:00
|
|
|
user_new (char *nickname)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
struct user *self = xcalloc (1, sizeof *self);
|
|
|
|
self->ref_count = 1;
|
2020-10-04 08:32:15 +02:00
|
|
|
self->nickname = nickname;
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2015-04-18 22:09:05 +02:00
|
|
|
static void
|
2015-04-19 02:12:59 +02:00
|
|
|
user_destroy (struct user *self)
|
2015-04-18 22:09:05 +02:00
|
|
|
{
|
|
|
|
free (self->nickname);
|
2015-04-19 02:12:59 +02:00
|
|
|
LIST_FOR_EACH (struct user_channel, iter, self->channels)
|
|
|
|
user_channel_destroy (iter);
|
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_METHODS (user)
|
2015-04-19 02:12:59 +02:00
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct channel_user
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct channel_user)
|
|
|
|
|
|
|
|
struct user *user; ///< Reference to user
|
2020-10-04 08:22:20 +02:00
|
|
|
char *prefixes; ///< Ordered @+... characters
|
2015-04-19 02:12:59 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
static struct channel_user *
|
2020-10-04 08:22:20 +02:00
|
|
|
channel_user_new (struct user *user, const char *prefixes)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
struct channel_user *self = xcalloc (1, sizeof *self);
|
2020-10-04 08:22:20 +02:00
|
|
|
self->user = user;
|
|
|
|
self->prefixes = xstrdup (prefixes);
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:29:07 +02:00
|
|
|
channel_user_destroy (struct channel_user *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
user_unref (self->user);
|
2020-10-04 08:22:20 +02:00
|
|
|
free (self->prefixes);
|
2015-04-19 02:12:59 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-25 00:36:02 +02:00
|
|
|
// We keep references to channels in their buffers,
|
|
|
|
// and weak references in their users and the name lookup table.
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel
|
|
|
|
{
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
struct server *s; ///< Server
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
char *name; ///< Channel name
|
|
|
|
char *topic; ///< Channel topic
|
|
|
|
|
2015-06-04 22:19:32 +02:00
|
|
|
// 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
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel_user *users; ///< Channel users
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv names_buf; ///< Buffer for RPL_NAMREPLY
|
2016-10-23 16:29:55 +02:00
|
|
|
size_t users_len; ///< User count
|
2015-07-12 00:30:10 +02:00
|
|
|
|
|
|
|
bool left_manually; ///< Don't rejoin on reconnect
|
2020-10-16 16:45:40 +02:00
|
|
|
bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES
|
2015-04-19 02:12:59 +02:00
|
|
|
};
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_channel_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT( channel, name, STRING )
|
|
|
|
ISPECT( channel, topic, STRING )
|
|
|
|
ISPECT( channel, no_param_modes, STR )
|
|
|
|
ISPECT_MAP( channel, param_modes, STRING )
|
|
|
|
ISPECT( channel, users_len, SIZE )
|
|
|
|
ISPECT( channel, left_manually, BOOL )
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
static struct channel *
|
2020-10-16 16:45:40 +02:00
|
|
|
channel_new (struct server *s, char *name)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
struct channel *self = xcalloc (1, sizeof *self);
|
|
|
|
self->ref_count = 1;
|
2020-10-16 16:45:40 +02:00
|
|
|
self->s = s;
|
2020-10-04 08:32:15 +02:00
|
|
|
self->name = name;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->no_param_modes = str_make ();
|
|
|
|
self->param_modes = str_map_make (free);
|
|
|
|
self->names_buf = strv_make ();
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:04:34 +02:00
|
|
|
channel_destroy (struct channel *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
free (self->name);
|
|
|
|
free (self->topic);
|
2015-06-04 22:19:32 +02:00
|
|
|
str_free (&self->no_param_modes);
|
|
|
|
str_map_free (&self->param_modes);
|
2015-04-25 00:36:02 +02:00
|
|
|
// Owner has to make sure we have no users by now
|
2016-10-23 16:29:55 +02:00
|
|
|
hard_assert (!self->users && !self->users_len);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&self->names_buf);
|
2015-04-18 22:09:05 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_METHODS (channel)
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-04-18 22:09:05 +02:00
|
|
|
|
2015-06-28 20:48:43 +02:00
|
|
|
enum formatter_item_type
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
FORMATTER_ITEM_END, ///< Sentinel value for arrays
|
2015-06-28 20:48:43 +02:00
|
|
|
FORMATTER_ITEM_TEXT, ///< Text
|
|
|
|
FORMATTER_ITEM_ATTR, ///< Formatting attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
FORMATTER_ITEM_FG_COLOR, ///< Foreground colour
|
|
|
|
FORMATTER_ITEM_BG_COLOR, ///< Background colour
|
2021-08-29 12:07:31 +02:00
|
|
|
FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting
|
2015-06-28 20:48:43 +02:00
|
|
|
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
|
|
|
|
};
|
|
|
|
|
|
|
|
struct formatter_item
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
enum formatter_item_type type : 16; ///< Type of this item
|
|
|
|
int attribute : 16; ///< Attribute ID
|
2020-10-11 17:36:21 +02:00
|
|
|
int color; ///< Colour
|
2016-01-31 20:07:20 +01:00
|
|
|
char *text; ///< String
|
2015-06-28 20:48:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
2016-01-31 20:07:20 +01:00
|
|
|
formatter_item_free (struct formatter_item *self)
|
2015-06-28 20:48:43 +02:00
|
|
|
{
|
|
|
|
free (self->text);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct formatter
|
|
|
|
{
|
|
|
|
struct app_context *ctx; ///< Application context
|
|
|
|
struct server *s; ///< Server
|
|
|
|
|
|
|
|
struct formatter_item *items; ///< Items
|
2016-01-31 20:07:20 +01:00
|
|
|
size_t items_len; ///< Items used
|
|
|
|
size_t items_alloc; ///< Items allocated
|
2015-06-28 20:48:43 +02:00
|
|
|
};
|
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
static struct formatter
|
|
|
|
formatter_make (struct app_context *ctx, struct server *s)
|
2015-06-28 20:48:43 +02:00
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter self = { .ctx = ctx, .s = s };
|
|
|
|
self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16));
|
|
|
|
return self;
|
2015-06-28 20:48:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
formatter_free (struct formatter *self)
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
for (size_t i = 0; i < self->items_len; i++)
|
|
|
|
formatter_item_free (&self->items[i]);
|
|
|
|
free (self->items);
|
2015-06-28 20:48:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
enum buffer_line_flags
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
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
|
2015-06-28 19:56:05 +02:00
|
|
|
BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file
|
2015-08-08 21:17:32 +02:00
|
|
|
BUFFER_LINE_INDENT = 1 << 4, ///< Just indent the line
|
|
|
|
BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam
|
2015-04-25 13:41:10 +02:00
|
|
|
};
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
struct buffer_line
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct buffer_line)
|
|
|
|
|
|
|
|
int flags; ///< Flags
|
|
|
|
time_t when; ///< Time of the event
|
2016-01-31 20:07:20 +01:00
|
|
|
struct formatter_item items[]; ///< Line data
|
2015-04-12 04:52:39 +02:00
|
|
|
};
|
|
|
|
|
2016-01-31 20:07:20 +01:00
|
|
|
/// Create a new buffer line stealing all data from the provided formatter
|
2015-04-13 00:06:08 +02:00
|
|
|
struct buffer_line *
|
2016-01-17 22:15:48 +01:00
|
|
|
buffer_line_new (struct formatter *f)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
// We make space for one more item that gets initialized to all zeros,
|
|
|
|
// meaning FORMATTER_ITEM_END (because it's the first value in the enum)
|
|
|
|
size_t items_size = f->items_len * sizeof *f->items;
|
|
|
|
struct buffer_line *self =
|
|
|
|
xcalloc (1, sizeof *self + items_size + sizeof *self->items);
|
|
|
|
memcpy (self->items, f->items, items_size);
|
|
|
|
|
|
|
|
// We've stolen pointers from the formatter, let's destroy it altogether
|
|
|
|
free (f->items);
|
|
|
|
memset (f, 0, sizeof *f);
|
2015-04-13 00:06:08 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_line_destroy (struct buffer_line *self)
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
for (struct formatter_item *iter = self->items; iter->type; iter++)
|
|
|
|
formatter_item_free (iter);
|
2015-04-13 00:06:08 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
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)
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
enum buffer_type type; ///< Type of the buffer
|
|
|
|
char *name; ///< The name of the buffer
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *input; ///< API for "input_data"
|
|
|
|
input_buffer_t input_data; ///< User interface data
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
// Buffer contents:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
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
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-26 03:09:29 +01:00
|
|
|
unsigned new_messages_count; ///< # messages since last left
|
|
|
|
unsigned new_unimportant_count; ///< How much of that is unimportant
|
2015-06-21 04:00:20 +02:00
|
|
|
bool highlighted; ///< We've been highlighted
|
2016-10-23 16:53:31 +02:00
|
|
|
bool hide_unimportant; ///< Hide unimportant messages
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
FILE *log_file; ///< Log file
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// Origin information:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server *server; ///< Reference to server
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel *channel; ///< Reference to channel
|
2015-04-30 00:02:14 +02:00
|
|
|
struct user *user; ///< Reference to user
|
2015-04-12 04:52:39 +02:00
|
|
|
};
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_server_ispect[];
|
|
|
|
static struct ispect_field g_buffer_ispect[] =
|
|
|
|
{
|
|
|
|
ISPECT( buffer, name, STRING )
|
|
|
|
ISPECT( buffer, new_messages_count, UINT )
|
|
|
|
ISPECT( buffer, new_unimportant_count, UINT )
|
|
|
|
ISPECT( buffer, highlighted, BOOL )
|
|
|
|
ISPECT( buffer, hide_unimportant, BOOL )
|
|
|
|
ISPECT_REF( buffer, server, false, server )
|
|
|
|
ISPECT_REF( buffer, channel, false, channel )
|
|
|
|
ISPECT_REF( buffer, user, false, user )
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static struct buffer *
|
2020-10-31 23:42:49 +01:00
|
|
|
buffer_new (struct input *input, enum buffer_type type, char *name)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
struct buffer *self = xcalloc (1, sizeof *self);
|
2015-11-18 23:49:09 +01:00
|
|
|
self->ref_count = 1;
|
2016-03-07 01:08:52 +01:00
|
|
|
self->input = input;
|
|
|
|
self->input_data = CALL (input, buffer_new);
|
2020-10-31 23:42:49 +01:00
|
|
|
self->type = type;
|
|
|
|
self->name = name;
|
2015-04-12 04:52:39 +02:00
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_destroy (struct buffer *self)
|
|
|
|
{
|
|
|
|
free (self->name);
|
2015-05-05 03:23:53 +02:00
|
|
|
if (self->input_data)
|
2016-03-07 01:08:52 +01:00
|
|
|
{
|
|
|
|
#ifdef HAVE_READLINE
|
|
|
|
// FIXME: can't really free "history" contents from here, as we cannot
|
|
|
|
// be sure that the user interface pointer is valid and usable
|
|
|
|
input_rl__buffer_destroy_wo_history (self->input_data);
|
|
|
|
#else // ! HAVE_READLINE
|
|
|
|
CALL_ (self->input, buffer_destroy, self->input_data);
|
|
|
|
#endif // ! HAVE_READLINE
|
|
|
|
}
|
2015-04-19 02:12:59 +02:00
|
|
|
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
|
|
|
|
buffer_line_destroy (iter);
|
2015-07-04 15:24:08 +02:00
|
|
|
if (self->log_file)
|
|
|
|
(void) fclose (self->log_file);
|
2015-04-19 02:12:59 +02:00
|
|
|
if (self->user)
|
|
|
|
user_unref (self->user);
|
|
|
|
if (self->channel)
|
|
|
|
channel_unref (self->channel);
|
2015-04-12 04:52:39 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_METHODS (buffer)
|
|
|
|
#define buffer_ref do_not_use_dangerous
|
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-15 23:34:36 +02:00
|
|
|
// The only real purpose of this is to abstract away TLS
|
2015-07-03 20:32:31 +02:00
|
|
|
struct transport
|
|
|
|
{
|
|
|
|
/// Initialize the transport
|
2015-12-09 00:53:56 +01:00
|
|
|
bool (*init) (struct server *s, const char *hostname, struct error **e);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// Destroy the user data pointer
|
|
|
|
void (*cleanup) (struct server *s);
|
|
|
|
|
|
|
|
/// The underlying socket may have become readable, update `read_buffer'
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result (*try_read) (struct server *s);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// The underlying socket may have become writeable, flush `write_buffer'
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result (*try_write) (struct server *s);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// 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);
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
enum server_state
|
|
|
|
{
|
|
|
|
IRC_DISCONNECTED, ///< Not connected
|
2015-05-09 22:10:58 +02:00
|
|
|
IRC_CONNECTING, ///< Connecting to the server
|
2015-05-08 17:39:26 +02:00
|
|
|
IRC_CONNECTED, ///< Trying to register
|
2015-07-03 20:32:31 +02:00
|
|
|
IRC_REGISTERED, ///< We can chat now
|
|
|
|
IRC_CLOSING, ///< Flushing output before shutdown
|
|
|
|
IRC_HALF_CLOSED ///< Connection shutdown from our side
|
2015-05-08 17:39:26 +02:00
|
|
|
};
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
/// 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);
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-30 00:02:14 +02:00
|
|
|
struct app_context *ctx; ///< Application context
|
2015-05-09 23:30:04 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
char *name; ///< Server identifier
|
|
|
|
struct buffer *buffer; ///< The buffer for this server
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *config; ///< Configuration root
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
// Connection:
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
enum server_state state; ///< Connection state
|
2015-05-09 22:10:58 +02:00
|
|
|
struct connector *connector; ///< Connection establisher
|
2015-07-20 23:31:26 +02:00
|
|
|
struct socks_connector *socks_conn; ///< SOCKS connection establisher
|
2015-07-17 21:18:05 +02:00
|
|
|
unsigned reconnect_attempt; ///< Number of reconnect attempt
|
2015-05-16 12:45:39 +02:00
|
|
|
bool manual_disconnect; ///< Don't reconnect after disconnect
|
2015-05-08 17:39:26 +02:00
|
|
|
|
|
|
|
int socket; ///< Socket FD of the server
|
2015-04-12 04:52:39 +02:00
|
|
|
struct str read_buffer; ///< Input yet to be processed
|
2015-07-03 20:32:31 +02:00
|
|
|
struct str write_buffer; ///< Outut yet to be be sent out
|
|
|
|
struct poller_fd socket_event; ///< We can read from the socket
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
struct transport *transport; ///< Transport method
|
|
|
|
void *transport_data; ///< Transport data
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
// 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
|
2015-07-18 14:12:34 +02:00
|
|
|
struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
// IRC:
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
// TODO: an output queue to prevent excess floods (this will be needed
|
|
|
|
// especially for away status polling)
|
|
|
|
|
2015-06-23 22:10:16 +02:00
|
|
|
bool rehashing; ///< Rehashing IRC identifiers
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
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
|
2015-04-18 22:09:05 +02:00
|
|
|
|
2015-04-20 22:09:56 +02:00
|
|
|
struct user *irc_user; ///< Our own user
|
2015-07-09 02:46:31 +02:00
|
|
|
int nick_counter; ///< Iterates "nicks" when registering
|
2015-06-07 04:20:39 +02:00
|
|
|
struct str irc_user_mode; ///< Our current user modes
|
2015-04-20 22:09:56 +02:00
|
|
|
char *irc_user_host; ///< Our current user@host
|
2015-11-22 23:04:51 +01:00
|
|
|
bool autoaway_active; ///< Autoaway is currently active
|
2015-04-15 16:54:05 +02:00
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS
|
2020-10-16 16:45:40 +02:00
|
|
|
bool cap_echo_message; ///< Whether the server echoes messages
|
|
|
|
bool cap_away_notify; ///< Whether we get AWAY notifications
|
2021-05-28 02:11:23 +02:00
|
|
|
bool cap_sasl; ///< Whether SASL is available
|
2015-06-20 21:38:04 +02:00
|
|
|
|
2015-06-01 21:45:04 +02:00
|
|
|
// Server-specific information (from RPL_ISUPPORT):
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
irc_tolower_fn irc_tolower; ///< Server tolower()
|
|
|
|
irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm()
|
2015-06-01 21:45:04 +02:00
|
|
|
|
|
|
|
char *irc_chantypes; ///< Channel types (name prefixes)
|
|
|
|
char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
|
|
|
|
char *irc_statusmsg; ///< Prefixes for channel targets
|
|
|
|
|
2021-06-17 12:08:08 +02:00
|
|
|
char irc_extban_prefix; ///< EXTBAN prefix or \0
|
|
|
|
char *irc_extban_types; ///< EXTBAN types
|
|
|
|
|
2015-06-03 23:17:10 +02:00
|
|
|
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
|
|
|
|
|
2015-06-01 21:45:04 +02:00
|
|
|
char *irc_chanuser_prefixes; ///< Channel user prefixes
|
|
|
|
char *irc_chanuser_modes; ///< Channel user modes
|
|
|
|
|
2015-06-17 21:19:09 +02:00
|
|
|
unsigned irc_max_modes; ///< Max parametrized modes per command
|
2015-04-30 00:02:14 +02:00
|
|
|
};
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_server_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT( server, name, STRING )
|
|
|
|
ISPECT( server, state, INT )
|
|
|
|
ISPECT( server, reconnect_attempt, UINT )
|
|
|
|
ISPECT( server, manual_disconnect, BOOL )
|
|
|
|
ISPECT( server, irc_user_host, STRING )
|
|
|
|
ISPECT( server, autoaway_active, BOOL )
|
|
|
|
ISPECT( server, cap_echo_message, BOOL )
|
|
|
|
ISPECT_REF( server, buffer, false, buffer )
|
2016-10-27 18:46:27 +02:00
|
|
|
|
|
|
|
// TODO: either rename the underlying field or fix the plugins
|
2016-10-27 20:58:14 +02:00
|
|
|
{ "user", offsetof (struct server, irc_user),
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_REF, 0, g_user_ispect, false },
|
2016-10-27 20:58:14 +02:00
|
|
|
{ "user_mode", offsetof (struct server, irc_user_mode),
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_STR, 0, NULL, false },
|
2016-10-27 18:46:27 +02:00
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
static void on_irc_timeout (void *user_data);
|
2015-05-09 22:10:58 +02:00
|
|
|
static void on_irc_ping_timeout (void *user_data);
|
2015-07-18 14:12:34 +02:00
|
|
|
static void on_irc_autojoin_timeout (void *user_data);
|
2015-05-09 22:10:58 +02:00
|
|
|
static void irc_initiate_connect (struct server *s);
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
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 ("");
|
|
|
|
|
2021-06-17 12:08:08 +02:00
|
|
|
self->irc_extban_prefix = 0;
|
|
|
|
self->irc_extban_types = xstrdup ("");
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
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);
|
|
|
|
|
2021-06-17 12:08:08 +02:00
|
|
|
free (self->irc_extban_types);
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
static struct server *
|
|
|
|
server_new (struct poller *poller)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-11-18 23:49:09 +01:00
|
|
|
struct server *self = xcalloc (1, sizeof *self);
|
|
|
|
self->ref_count = 1;
|
2015-05-16 12:33:59 +02:00
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
self->socket = -1;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->read_buffer = str_make ();
|
|
|
|
self->write_buffer = str_make ();
|
2015-05-08 17:39:26 +02:00
|
|
|
self->state = IRC_DISCONNECTED;
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->timeout_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->timeout_tmr.dispatcher = on_irc_timeout;
|
|
|
|
self->timeout_tmr.user_data = self;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->ping_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->ping_tmr.dispatcher = on_irc_ping_timeout;
|
|
|
|
self->ping_tmr.user_data = self;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->reconnect_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
|
|
|
|
self->reconnect_tmr.user_data = self;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->autojoin_tmr = poller_timer_make (poller);
|
2015-07-18 14:12:34 +02:00
|
|
|
self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
|
|
|
|
self->autojoin_tmr.user_data = self;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_users = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_users.key_xfrm = irc_strxfrm;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_channels = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_channels.key_xfrm = irc_strxfrm;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_buffer_map = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_buffer_map.key_xfrm = irc_strxfrm;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_user_mode = str_make ();
|
2015-06-07 04:20:39 +02:00
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
self->cap_ls_buf = strv_make ();
|
2015-06-20 22:42:38 +02:00
|
|
|
server_init_specifics (self);
|
2015-11-18 23:49:09 +01:00
|
|
|
return self;
|
2015-04-30 00:02:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-11-18 23:49:09 +01:00
|
|
|
server_destroy (struct server *self)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-06-20 21:10:50 +02:00
|
|
|
free (self->name);
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
if (self->connector)
|
|
|
|
{
|
|
|
|
connector_free (self->connector);
|
|
|
|
free (self->connector);
|
|
|
|
}
|
2015-07-20 23:31:26 +02:00
|
|
|
if (self->socks_conn)
|
|
|
|
{
|
|
|
|
socks_connector_free (self->socks_conn);
|
|
|
|
free (self->socks_conn);
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
|
|
|
if (self->transport
|
|
|
|
&& self->transport->cleanup)
|
|
|
|
self->transport->cleanup (self);
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
if (self->socket != -1)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
poller_fd_reset (&self->socket_event);
|
2017-05-06 21:35:44 +02:00
|
|
|
xclose (self->socket);
|
2015-04-30 00:02:14 +02:00
|
|
|
}
|
|
|
|
str_free (&self->read_buffer);
|
2015-07-03 20:32:31 +02:00
|
|
|
str_free (&self->write_buffer);
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-07-14 06:50:39 +02:00
|
|
|
poller_timer_reset (&self->ping_tmr);
|
|
|
|
poller_timer_reset (&self->timeout_tmr);
|
|
|
|
poller_timer_reset (&self->reconnect_tmr);
|
2015-07-18 14:12:34 +02:00
|
|
|
poller_timer_reset (&self->autojoin_tmr);
|
2015-07-14 06:50:39 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
str_map_free (&self->irc_users);
|
|
|
|
str_map_free (&self->irc_channels);
|
|
|
|
str_map_free (&self->irc_buffer_map);
|
2015-05-15 20:05:27 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
if (self->irc_user)
|
|
|
|
user_unref (self->irc_user);
|
2015-06-07 04:20:39 +02:00
|
|
|
str_free (&self->irc_user_mode);
|
2015-04-30 00:02:14 +02:00
|
|
|
free (self->irc_user_host);
|
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
strv_free (&self->cap_ls_buf);
|
2015-06-20 22:42:38 +02:00
|
|
|
server_free_specifics (self);
|
2015-05-16 12:33:59 +02:00
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_METHODS (server)
|
|
|
|
#define server_ref do_not_use_dangerous
|
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Scripting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
struct plugin
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct plugin)
|
|
|
|
|
|
|
|
char *name; ///< Name of the plugin
|
|
|
|
struct plugin_vtable *vtable; ///< Methods
|
|
|
|
};
|
|
|
|
|
|
|
|
struct plugin_vtable
|
|
|
|
{
|
2020-10-20 02:02:09 +02:00
|
|
|
/// Collect garbage
|
|
|
|
void (*gc) (struct plugin *self);
|
2015-11-19 14:23:10 +01:00
|
|
|
/// Unregister and free the plugin including all relevant resources
|
|
|
|
void (*free) (struct plugin *self);
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
plugin_destroy (struct plugin *self)
|
|
|
|
{
|
|
|
|
self->vtable->free (self);
|
|
|
|
free (self->name);
|
|
|
|
free (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-21 18:40:07 +01:00
|
|
|
// This is a bit ugly since insertion is O(n) and the need to get rid of the
|
|
|
|
// specific type because of list macros, however I don't currently posses any
|
|
|
|
// strictly better, ordered data structure
|
|
|
|
|
|
|
|
struct hook
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
LIST_HEADER (struct hook)
|
|
|
|
int priority; ///< The lesser the sooner
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct hook *
|
|
|
|
hook_insert (struct hook *list, struct hook *item)
|
|
|
|
{
|
|
|
|
// Corner cases: list is empty or we precede everything
|
|
|
|
if (!list || item->priority < list->priority)
|
|
|
|
{
|
|
|
|
LIST_PREPEND (list, item);
|
|
|
|
return list;
|
|
|
|
}
|
2015-11-19 19:09:05 +01:00
|
|
|
|
2015-11-21 18:40:07 +01:00
|
|
|
// Otherwise fast-forward to the last entry that precedes us
|
|
|
|
struct hook *before = list;
|
2015-11-24 02:32:11 +01:00
|
|
|
while (before->next && before->next->priority < item->priority)
|
2015-11-21 18:40:07 +01:00
|
|
|
before = before->next;
|
|
|
|
|
|
|
|
// And link ourselves in between it and its successor
|
|
|
|
if ((item->next = before->next))
|
|
|
|
item->next->prev = item;
|
|
|
|
before->next = item;
|
|
|
|
item->prev = before;
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct input_hook
|
|
|
|
{
|
|
|
|
struct hook super; ///< Common hook fields
|
2015-11-19 19:09:05 +01:00
|
|
|
|
|
|
|
/// Takes over the ownership of "input", returns either NULL if input
|
|
|
|
/// was thrown away, or a possibly modified version of it
|
|
|
|
char *(*filter) (struct input_hook *self,
|
|
|
|
struct buffer *buffer, char *input);
|
|
|
|
};
|
|
|
|
|
|
|
|
struct irc_hook
|
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
struct hook super; ///< Common hook fields
|
2015-11-19 19:09:05 +01:00
|
|
|
|
|
|
|
/// Takes over the ownership of "message", returns either NULL if message
|
|
|
|
/// was thrown away, or a possibly modified version of it
|
|
|
|
char *(*filter) (struct irc_hook *self,
|
|
|
|
struct server *server, char *message);
|
|
|
|
};
|
|
|
|
|
2016-10-28 01:01:27 +02:00
|
|
|
struct prompt_hook
|
|
|
|
{
|
|
|
|
struct hook super; ///< Common hook fields
|
|
|
|
|
|
|
|
/// Returns what the prompt should look like right now based on other state
|
|
|
|
char *(*make) (struct prompt_hook *self);
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-01-15 01:44:35 +01:00
|
|
|
struct completion_word
|
|
|
|
{
|
|
|
|
size_t start; ///< Offset to start of word
|
|
|
|
size_t end; ///< Offset to end of word
|
|
|
|
};
|
|
|
|
|
|
|
|
struct completion
|
|
|
|
{
|
|
|
|
char *line; ///< The line which is being completed
|
|
|
|
|
|
|
|
struct completion_word *words; ///< Word locations
|
|
|
|
size_t words_len; ///< Number of words
|
|
|
|
size_t words_alloc; ///< Number of words allocated
|
|
|
|
|
|
|
|
size_t location; ///< Which word is being completed
|
|
|
|
};
|
|
|
|
|
|
|
|
struct completion_hook
|
|
|
|
{
|
|
|
|
struct hook super; ///< Common hook fields
|
|
|
|
|
|
|
|
/// Tries to add possible completions of "word" to "output"
|
|
|
|
void (*complete) (struct completion_hook *self,
|
2017-01-23 23:50:27 +01:00
|
|
|
struct completion *data, const char *word, struct strv *output);
|
2016-01-15 01:44:35 +01:00
|
|
|
};
|
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Main context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2016-01-15 01:44:35 +01:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct app_context
|
|
|
|
{
|
2015-07-05 15:57:53 +02:00
|
|
|
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
// Configuration:
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
struct config config; ///< Program configuration
|
2015-04-30 00:02:14 +02:00
|
|
|
char *attrs[ATTR_COUNT]; ///< Terminal attributes
|
|
|
|
bool isolate_buffers; ///< Isolate global/server buffers
|
2015-07-05 16:28:27 +02:00
|
|
|
bool beep_on_highlight; ///< Beep on highlight
|
2015-07-05 17:02:11 +02:00
|
|
|
bool logging; ///< Logging to file enabled
|
2015-07-26 22:21:08 +02:00
|
|
|
bool show_all_prefixes; ///< Show all prefixes before nicks
|
2016-03-21 00:30:59 +01:00
|
|
|
bool word_wrapping; ///< Enable simple word wrapping
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
struct str_map servers; ///< Our servers
|
2015-04-30 00:02:14 +02:00
|
|
|
|
|
|
|
// Events:
|
|
|
|
|
|
|
|
struct poller_fd tty_event; ///< Terminal input event
|
|
|
|
struct poller_fd signal_event; ///< Signal FD event
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-04 15:43:45 +02:00
|
|
|
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
|
2015-07-11 06:10:46 +02:00
|
|
|
struct poller_timer date_chg_tmr; ///< Print a date change
|
2015-11-22 23:04:51 +01:00
|
|
|
struct poller_timer autoaway_tmr; ///< Autoaway timer
|
2015-07-04 15:43:45 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
struct poller poller; ///< Manages polled descriptors
|
|
|
|
bool quitting; ///< User requested quitting
|
|
|
|
bool polling; ///< The event loop is running
|
|
|
|
|
|
|
|
// Buffers:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
struct buffer *buffers; ///< All our buffers in order
|
|
|
|
struct buffer *buffers_tail; ///< The tail of our buffers
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
struct buffer *global_buffer; ///< The global buffer
|
|
|
|
struct buffer *current_buffer; ///< The current buffer
|
2015-04-27 22:51:40 +02:00
|
|
|
struct buffer *last_buffer; ///< Last used buffer
|
|
|
|
|
2015-05-15 20:05:27 +02:00
|
|
|
struct str_map buffers_by_name; ///< Buffers by name
|
2015-04-18 22:09:05 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
unsigned backlog_limit; ///< Limit for buffer lines
|
2015-04-12 04:52:39 +02:00
|
|
|
time_t last_displayed_msg_time; ///< Time of last displayed message
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
// Terminal:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
|
|
|
|
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *input; ///< User interface
|
2015-05-12 03:48:52 +02:00
|
|
|
|
2016-10-23 17:34:52 +02:00
|
|
|
struct poller_idle prompt_event; ///< Deferred prompt refresh
|
2016-01-07 22:12:29 +01:00
|
|
|
struct poller_idle input_event; ///< Pending input event
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv pending_input; ///< Pending input lines
|
2016-01-07 22:12:29 +01:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
int *nick_palette; ///< A 256-colour palette for nicknames
|
2015-08-10 07:51:03 +02:00
|
|
|
size_t nick_palette_len; ///< Number of entries in nick_palette
|
|
|
|
|
2021-08-29 12:07:31 +02:00
|
|
|
bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape
|
2015-11-15 01:03:29 +01:00
|
|
|
bool in_bracketed_paste; ///< User is pasting some content
|
2015-11-15 01:23:32 +01:00
|
|
|
struct str input_buffer; ///< Buffered pasted content
|
2015-11-15 01:03:29 +01:00
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
bool running_pager; ///< Running a pager for buffer history
|
2015-12-25 04:21:18 +01:00
|
|
|
bool running_editor; ///< Running editor for the input
|
|
|
|
char *editor_filename; ///< The file being edited by user
|
2015-11-15 15:36:03 +01:00
|
|
|
int terminal_suspended; ///< Terminal suspension level
|
2015-11-19 14:23:10 +01:00
|
|
|
|
|
|
|
struct plugin *plugins; ///< Loaded plugins
|
2015-11-21 18:40:07 +01:00
|
|
|
struct hook *input_hooks; ///< Input hooks
|
|
|
|
struct hook *irc_hooks; ///< IRC hooks
|
2016-10-28 01:01:27 +02:00
|
|
|
struct hook *prompt_hooks; ///< Prompt hooks
|
2016-01-15 01:44:35 +01:00
|
|
|
struct hook *completion_hooks; ///< Autocomplete hooks
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
*g_ctx;
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_ctx_ispect[] =
|
2016-10-27 20:02:25 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_MAP_REF( app_context, servers, false, server )
|
|
|
|
ISPECT_REF( app_context, buffers, true, buffer )
|
|
|
|
ISPECT_REF( app_context, global_buffer, false, buffer )
|
|
|
|
ISPECT_REF( app_context, current_buffer, false, buffer )
|
2016-10-27 20:02:25 +02:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-08-10 07:51:03 +02:00
|
|
|
static int *
|
|
|
|
filter_color_cube_for_acceptable_nick_colors (size_t *len)
|
|
|
|
{
|
|
|
|
// This is a pure function and we don't use threads, static storage is fine
|
|
|
|
static int table[6 * 6 * 6];
|
|
|
|
size_t len_counter = 0;
|
2021-06-15 07:11:35 +02:00
|
|
|
for (int x = 0; x < (int) N_ELEMENTS (table); x++)
|
2015-08-10 07:51:03 +02:00
|
|
|
{
|
|
|
|
int r = x / 36;
|
|
|
|
int g = (x / 6) % 6;
|
|
|
|
int b = (x % 6);
|
|
|
|
|
2021-06-15 04:35:41 +02:00
|
|
|
// The first step is 95/255, the rest are 40/255,
|
|
|
|
// as an approximation we can double the first step
|
|
|
|
double linear_R = pow ((r + !!r) / 6., 2.2);
|
|
|
|
double linear_G = pow ((g + !!g) / 6., 2.2);
|
|
|
|
double linear_B = pow ((b + !!b) / 6., 2.2);
|
|
|
|
|
|
|
|
// Use the relative luminance of colours within the cube to filter
|
|
|
|
// colours that look okay-ish on terminals with both black and white
|
|
|
|
// backgrounds (use the test-nick-colors script to calibrate)
|
|
|
|
double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
|
|
|
|
if (Y >= .25 && Y <= .4)
|
2015-08-10 07:51:03 +02:00
|
|
|
table[len_counter++] = 16 + x;
|
|
|
|
}
|
|
|
|
*len = len_counter;
|
|
|
|
return table;
|
|
|
|
}
|
|
|
|
|
2016-03-26 14:07:09 +01:00
|
|
|
static bool
|
|
|
|
app_iconv_open (iconv_t *target, const char *to, const char *from)
|
|
|
|
{
|
|
|
|
if (ICONV_ACCEPTS_TRANSLIT)
|
|
|
|
{
|
|
|
|
char *to_real = xstrdup_printf ("%s//TRANSLIT", to);
|
|
|
|
*target = iconv_open (to_real, from);
|
|
|
|
free (to_real);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
*target = iconv_open (to, from);
|
|
|
|
return *target != (iconv_t) -1;
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
|
|
|
app_context_init (struct app_context *self)
|
|
|
|
{
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->config = config_make ();
|
2015-04-19 02:12:59 +02:00
|
|
|
poller_init (&self->poller);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->servers = str_map_make ((str_map_free_fn) server_unref);
|
2015-06-01 21:45:36 +02:00
|
|
|
self->servers.key_xfrm = tolower_ascii_strxfrm;
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->buffers_by_name = str_map_make (NULL);
|
2015-06-01 21:45:36 +02:00
|
|
|
self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
// So that we don't lose the logo shortly after startup
|
|
|
|
self->backlog_limit = 1000;
|
2015-04-12 04:52:39 +02:00
|
|
|
self->last_displayed_msg_time = time (NULL);
|
|
|
|
|
2016-03-26 14:07:09 +01:00
|
|
|
char *native = nl_langinfo (CODESET);
|
|
|
|
if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8")
|
2016-04-03 04:05:04 +02:00
|
|
|
|| !app_iconv_open (&self->term_to_utf8, "UTF-8", native))
|
2015-04-12 04:52:39 +02:00
|
|
|
exit_fatal ("creating the UTF-8 conversion object failed: %s",
|
|
|
|
strerror (errno));
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
self->input = input_new ();
|
2016-03-10 00:06:28 +01:00
|
|
|
self->input->user_data = self;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->pending_input = strv_make ();
|
|
|
|
self->input_buffer = str_make ();
|
2015-08-10 07:51:03 +02:00
|
|
|
|
|
|
|
self->nick_palette =
|
|
|
|
filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
app_context_free (struct app_context *self)
|
|
|
|
{
|
2015-11-19 14:23:10 +01:00
|
|
|
// Plugins can try to use of the other fields when destroyed
|
|
|
|
LIST_FOR_EACH (struct plugin, iter, self->plugins)
|
|
|
|
plugin_destroy (iter);
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
config_free (&self->config);
|
2015-04-29 21:25:57 +02:00
|
|
|
for (size_t i = 0; i < ATTR_COUNT; i++)
|
2015-07-05 15:57:53 +02:00
|
|
|
{
|
|
|
|
free (self->attrs_defaults[i]);
|
2015-04-29 21:25:57 +02:00
|
|
|
free (self->attrs[i]);
|
2015-07-05 15:57:53 +02:00
|
|
|
}
|
2015-04-15 16:54:05 +02:00
|
|
|
|
2015-04-24 23:30:48 +02:00
|
|
|
LIST_FOR_EACH (struct buffer, iter, self->buffers)
|
2015-06-28 23:00:46 +02:00
|
|
|
{
|
|
|
|
#ifdef HAVE_READLINE
|
2016-03-07 01:08:52 +01:00
|
|
|
// We can use the user interface here; see buffer_destroy()
|
|
|
|
CALL_ (self->input, buffer_destroy, iter->input_data);
|
2015-06-28 23:00:46 +02:00
|
|
|
iter->input_data = NULL;
|
|
|
|
#endif // HAVE_READLINE
|
2015-11-18 23:49:09 +01:00
|
|
|
buffer_unref (iter);
|
2015-06-28 23:00:46 +02:00
|
|
|
}
|
2015-04-24 23:30:48 +02:00
|
|
|
str_map_free (&self->buffers_by_name);
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
str_map_free (&self->servers);
|
2015-04-19 02:12:59 +02:00
|
|
|
poller_free (&self->poller);
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
iconv_close (self->term_from_utf8);
|
|
|
|
iconv_close (self->term_to_utf8);
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (self->input, destroy);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&self->pending_input);
|
2015-11-15 01:23:32 +01:00
|
|
|
str_free (&self->input_buffer);
|
2015-12-25 04:21:18 +01:00
|
|
|
|
|
|
|
free (self->editor_filename);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2016-10-23 17:34:52 +02:00
|
|
|
static void
|
|
|
|
refresh_prompt (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
// XXX: the need for this conditional could probably be resolved
|
|
|
|
// by some clever reordering
|
|
|
|
if (ctx->prompt_event.poller)
|
|
|
|
poller_idle_set (&ctx->prompt_event);
|
|
|
|
}
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
// --- Configuration -----------------------------------------------------------
|
|
|
|
|
2015-07-11 04:28:34 +02:00
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_debug_mode_change (struct config_item *item)
|
2015-07-11 04:28:34 +02:00
|
|
|
{
|
|
|
|
g_debug_mode = item->value.boolean;
|
|
|
|
}
|
|
|
|
|
2015-07-26 22:21:08 +02:00
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_show_all_prefixes_change (struct config_item *item)
|
2015-07-26 22:21:08 +02:00
|
|
|
{
|
|
|
|
struct app_context *ctx = item->user_data;
|
|
|
|
ctx->show_all_prefixes = item->value.boolean;
|
|
|
|
refresh_prompt (ctx);
|
|
|
|
}
|
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
static void on_config_backlog_limit_change (struct config_item *item);
|
2015-08-17 00:08:19 +02:00
|
|
|
static void on_config_attribute_change (struct config_item *item);
|
|
|
|
static void on_config_logging_change (struct config_item *item);
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-05 16:28:27 +02:00
|
|
|
#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \
|
|
|
|
static void \
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_ ## name ## _change (struct config_item *item) \
|
2015-07-05 16:28:27 +02:00
|
|
|
{ \
|
|
|
|
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)
|
2016-03-21 00:30:59 +01:00
|
|
|
TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping)
|
2015-07-05 16:28:27 +02:00
|
|
|
|
2015-07-11 04:28:34 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
static bool
|
|
|
|
config_validate_nonjunk_string
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
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];
|
2020-10-02 06:52:11 +02:00
|
|
|
if (iscntrl_ascii (c))
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
error_set (e, "control characters are not allowed");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
static bool
|
|
|
|
config_validate_addresses
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-14 06:22:58 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
static bool
|
|
|
|
config_validate_nonnegative
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-05-08 23:27:21 +02:00
|
|
|
static struct config_schema g_config_server[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-07-09 02:46:31 +02:00
|
|
|
{ .name = "nicks",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "IRC nickname",
|
2015-07-09 02:46:31 +02:00
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
2015-05-02 23:00:34 +02:00
|
|
|
.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 },
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
{ .name = "addresses",
|
|
|
|
.comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
|
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
.validate = config_validate_addresses },
|
2015-05-20 21:18:15 +02:00
|
|
|
{ .name = "password",
|
|
|
|
.comment = "Password to connect to the server, if any",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2016-02-01 21:57:43 +01:00
|
|
|
// XXX: if we add support for new capabilities, the value stays unchanged
|
|
|
|
{ .name = "capabilities",
|
|
|
|
.comment = "Capabilities to use if supported by server",
|
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
.validate = config_validate_nonjunk_string,
|
2020-10-16 16:45:40 +02:00
|
|
|
.default_ = "\"multi-prefix,invite-notify,server-time,echo-message,"
|
2021-05-30 08:06:38 +02:00
|
|
|
"message-tags,away-notify,cap-notify,chghost\"" },
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls",
|
2015-07-15 23:34:36 +02:00
|
|
|
.comment = "Whether to use TLS",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "off" },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_cert",
|
2015-07-15 23:34:36 +02:00
|
|
|
.comment = "Client TLS certificate (PEM)",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_verify",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "Whether to verify certificates",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "on" },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ca_file",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "OpenSSL CA bundle file",
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ca_path",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "OpenSSL CA bundle path",
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ciphers",
|
2015-07-12 17:15:33 +02:00
|
|
|
.comment = "OpenSSL cipher preference list",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.default_ = "\"DEFAULT:!MEDIUM:!LOW\"" },
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-11 06:16:53 +02:00
|
|
|
{ .name = "autoconnect",
|
|
|
|
.comment = "Connect automatically on startup",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "on" },
|
2015-05-02 23:00:34 +02:00
|
|
|
{ .name = "autojoin",
|
2016-12-30 07:49:10 +01:00
|
|
|
.comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2015-07-18 13:23:22 +02:00
|
|
|
{ .name = "command",
|
|
|
|
.comment = "Command to execute after a successful connect",
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-18 14:12:34 +02:00
|
|
|
{ .name = "command_delay",
|
|
|
|
.comment = "Delay between executing \"command\" and joining channels",
|
|
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
|
|
.validate = config_validate_nonnegative,
|
|
|
|
.default_ = "0" },
|
2015-05-02 23:00:34 +02:00
|
|
|
{ .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,
|
2015-05-16 12:45:39 +02:00
|
|
|
.validate = config_validate_nonnegative,
|
2015-05-02 23:00:34 +02:00
|
|
|
.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 },
|
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
static struct config_schema g_config_general[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2022-08-26 03:43:32 +02:00
|
|
|
{ .name = "save_on_quit",
|
|
|
|
.comment = "Save configuration before quitting",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "on" },
|
|
|
|
{ .name = "debug_mode",
|
|
|
|
.comment = "Produce some debugging output",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2015-07-05 16:28:27 +02:00
|
|
|
.default_ = "off",
|
2022-08-26 03:43:32 +02:00
|
|
|
.on_change = on_config_debug_mode_change },
|
|
|
|
{ .name = "logging",
|
|
|
|
.comment = "Log buffer contents to file",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "off",
|
|
|
|
.on_change = on_config_logging_change },
|
|
|
|
{ .name = "plugin_autoload",
|
|
|
|
.comment = "Plugins to automatically load on start",
|
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
.validate = config_validate_nonjunk_string },
|
|
|
|
|
|
|
|
// Buffer history:
|
|
|
|
{ .name = "backlog_limit",
|
|
|
|
.comment = "Maximum number of lines stored in the backlog",
|
|
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
|
|
.validate = config_validate_nonnegative,
|
|
|
|
.default_ = "1000",
|
|
|
|
.on_change = on_config_backlog_limit_change },
|
|
|
|
{ .name = "pager",
|
|
|
|
.comment = "Shell command to page buffer history (args: name [path])",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.default_ = "`name=$(echo \"$1\" | sed 's/[%?:.]/\\\\&/g'); "
|
|
|
|
"prompt='?f%F:'$name'. ?db- page %db?L of %D. .(?eEND:?PB%PB\\%..)'; "
|
|
|
|
"LESSSECURE=1 less +Gb -Ps\"$prompt\" \"${2:--R}\"`" },
|
|
|
|
{ .name = "pager_strip_formatting",
|
|
|
|
.comment = "Strip terminal formatting from pager input",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "off" },
|
|
|
|
|
|
|
|
// Output adjustments:
|
2015-06-28 19:41:31 +02:00
|
|
|
{ .name = "beep_on_highlight",
|
2022-08-26 03:43:32 +02:00
|
|
|
.comment = "Ring the bell when highlighted or on a new invisible PM",
|
2015-06-28 19:41:31 +02:00
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2015-07-05 16:28:27 +02:00
|
|
|
.default_ = "on",
|
|
|
|
.on_change = on_config_beep_on_highlight_change },
|
2022-08-26 03:43:32 +02:00
|
|
|
{ .name = "date_change_line",
|
|
|
|
.comment = "Input to strftime(3) for the date change line",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.default_ = "\"%F\"" },
|
|
|
|
{ .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 = "read_marker_char",
|
|
|
|
.comment = "The character to use for the read marker line",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.default_ = "\"-\"",
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2015-07-26 22:21:08 +02:00
|
|
|
{ .name = "show_all_prefixes",
|
|
|
|
.comment = "Show all prefixes in front of nicknames",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "off",
|
|
|
|
.on_change = on_config_show_all_prefixes_change },
|
2016-03-21 00:30:59 +01:00
|
|
|
{ .name = "word_wrapping",
|
|
|
|
.comment = "Enable simple word wrapping in buffers",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "on",
|
|
|
|
.on_change = on_config_word_wrapping_change },
|
2022-08-26 03:43:32 +02:00
|
|
|
|
|
|
|
// User input:
|
|
|
|
{ .name = "editor",
|
2022-08-14 20:26:38 +02:00
|
|
|
.comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\", "
|
|
|
|
"nano/micro/kakoune: \"nano/micro/kak +%L:%C %F\"",
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
.type = CONFIG_ITEM_STRING },
|
2021-10-30 04:18:52 +02:00
|
|
|
{ .name = "process_pasted_text",
|
|
|
|
.comment = "Normalize newlines and quote the command prefix in pastes",
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
.default_ = "on" },
|
2015-07-17 21:18:05 +02:00
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
// Pan-server configuration:
|
|
|
|
{ .name = "autoaway_message",
|
|
|
|
.comment = "Automated away message",
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
.default_ = "\"I'm not here right now\"" },
|
|
|
|
{ .name = "autoaway_delay",
|
|
|
|
.comment = "Delay from the last keypress in seconds",
|
2016-01-15 03:49:24 +01:00
|
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
|
|
.validate = config_validate_nonnegative,
|
2022-08-26 03:43:32 +02:00
|
|
|
.default_ = "1800" },
|
2015-07-17 21:18:05 +02:00
|
|
|
{ .name = "reconnect_delay_growing",
|
2022-08-26 03:43:32 +02:00
|
|
|
.comment = "Growth factor for the reconnect delay",
|
2015-07-17 21:18:05 +02:00
|
|
|
.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" },
|
2015-05-02 23:00:34 +02:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
2015-05-08 23:27:21 +02:00
|
|
|
static struct config_schema g_config_attributes[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2020-09-20 13:24:02 +02:00
|
|
|
#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \
|
2015-07-05 15:57:53 +02:00
|
|
|
.on_change = on_config_attribute_change },
|
2015-05-02 23:00:34 +02:00
|
|
|
ATTR_TABLE (XX)
|
|
|
|
#undef XX
|
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2022-08-26 03:43:32 +02:00
|
|
|
load_config_general (struct config_item *subtree, void *user_data)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2022-08-26 03:43:32 +02:00
|
|
|
config_schema_apply_to_object (g_config_general, subtree, user_data);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
load_config_attributes (struct config_item *subtree, void *user_data)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-07-05 01:56:00 +02:00
|
|
|
config_schema_apply_to_object (g_config_attributes, subtree, user_data);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
register_config_modules (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
struct config *config = &ctx->config;
|
2015-07-05 01:11:20 +02:00
|
|
|
// The servers are loaded later when we can create buffers for them
|
|
|
|
config_register_module (config, "servers", NULL, NULL);
|
2015-07-09 22:32:14 +02:00
|
|
|
config_register_module (config, "aliases", NULL, NULL);
|
2015-12-28 02:03:26 +01:00
|
|
|
config_register_module (config, "plugins", NULL, NULL);
|
2022-08-26 03:43:32 +02:00
|
|
|
config_register_module (config, "general", load_config_general, ctx);
|
2015-07-05 01:11:20 +02:00
|
|
|
config_register_module (config, "attributes", load_config_attributes, ctx);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static const char *
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_string (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
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
|
2015-05-16 12:33:59 +02:00
|
|
|
set_config_string
|
2015-08-17 00:08:19 +02:00
|
|
|
(struct config_item *root, const char *key, const char *value)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item);
|
|
|
|
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *new_ = config_item_string_from_cstr (value);
|
2015-05-02 23:00:34 +02:00
|
|
|
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
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_integer (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
|
|
|
|
return item->value.integer;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_boolean (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
|
|
|
|
return item->value.boolean;
|
|
|
|
}
|
|
|
|
|
2015-05-03 19:34:01 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-07-10 01:44:02 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-12-28 02:03:26 +01:00
|
|
|
static struct str_map *
|
|
|
|
get_plugins_config (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
|
|
|
|
}
|
|
|
|
|
2015-07-10 01:44:02 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-03 19:34:01 +02:00
|
|
|
static void
|
2015-12-10 21:21:21 +01:00
|
|
|
serialize_configuration (struct config_item *root, struct str *output)
|
2015-05-03 19:34:01 +02:00
|
|
|
{
|
|
|
|
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");
|
|
|
|
|
2015-12-10 21:21:21 +01:00
|
|
|
config_item_write (root, true, output);
|
2015-05-03 19:34:01 +02:00
|
|
|
}
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
// --- Terminal output ---------------------------------------------------------
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Default colour pair
|
2015-05-10 09:55:44 +02:00
|
|
|
#define COLOR_DEFAULT -1
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Bright versions of the basic colour set
|
2015-05-10 09:55:44 +02:00
|
|
|
#define COLOR_BRIGHT(x) (COLOR_ ## x + 8)
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Builds a colour pair for 256-colour terminals with a 16-colour backup value
|
2015-05-10 23:06:19 +02:00
|
|
|
#define COLOR_256(name, c256) \
|
2015-11-13 09:05:25 +01:00
|
|
|
(((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16))
|
2015-05-10 23:06:19 +02:00
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
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,
|
2015-04-29 20:56:26 +02:00
|
|
|
FILE *stream, intptr_t attribute, const char *fmt, va_list ap)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
terminal_printer_fn printer = get_attribute_printer (stream);
|
|
|
|
if (!attribute)
|
|
|
|
printer = NULL;
|
|
|
|
|
|
|
|
if (printer)
|
2015-04-29 21:25:57 +02:00
|
|
|
tputs (ctx->attrs[attribute], 1, printer);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
vfprintf (stream, fmt, ap);
|
|
|
|
|
|
|
|
if (printer)
|
2015-04-29 21:25:57 +02:00
|
|
|
tputs (ctx->attrs[ATTR_RESET], 1, printer);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
print_attributed (struct app_context *ctx,
|
2015-04-29 20:56:26 +02:00
|
|
|
FILE *stream, intptr_t attribute, const char *fmt, ...)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
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;
|
2015-05-07 18:39:43 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-07 18:39:43 +02:00
|
|
|
print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
|
|
|
|
vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
|
2015-04-12 04:52:39 +02:00
|
|
|
fputs ("\n", stream);
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-12-24 16:02:26 +01:00
|
|
|
static ssize_t
|
|
|
|
attr_by_name (const char *name)
|
2015-04-29 21:25:57 +02:00
|
|
|
{
|
|
|
|
static const char *table[ATTR_COUNT] =
|
|
|
|
{
|
2020-09-20 13:24:02 +02:00
|
|
|
#define XX(x, y, z) [ATTR_ ## x] = #y,
|
2015-04-29 21:25:57 +02:00
|
|
|
ATTR_TABLE (XX)
|
|
|
|
#undef XX
|
|
|
|
};
|
|
|
|
|
2015-07-05 16:15:30 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (table); i++)
|
2015-12-24 16:02:26 +01:00
|
|
|
if (!strcmp (name, table[i]))
|
|
|
|
return i;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_config_attribute_change (struct config_item *item)
|
|
|
|
{
|
|
|
|
struct app_context *ctx = item->user_data;
|
|
|
|
ssize_t id = attr_by_name (item->schema->name);
|
|
|
|
if (id != -1)
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL
|
2015-12-24 16:02:26 +01:00
|
|
|
? ctx->attrs_defaults[id]
|
2018-01-08 22:15:29 +01:00
|
|
|
: item->value.string.str));
|
2015-12-24 16:02:26 +01:00
|
|
|
}
|
2015-04-29 21:25:57 +02:00
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
|
|
|
init_colors (struct app_context *ctx)
|
|
|
|
{
|
2015-04-21 20:52:41 +02:00
|
|
|
bool have_ti = init_terminal ();
|
2015-07-05 15:57:53 +02:00
|
|
|
char **defaults = ctx->attrs_defaults;
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2015-07-05 15:57:53 +02:00
|
|
|
#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "")
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2015-07-11 05:39:00 +02:00
|
|
|
INIT_ATTR (PROMPT, enter_bold_mode);
|
|
|
|
INIT_ATTR (RESET, exit_attribute_mode);
|
2016-04-10 22:13:53 +02:00
|
|
|
INIT_ATTR (DATE_CHANGE, enter_bold_mode);
|
2015-07-11 05:39:00 +02:00
|
|
|
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]);
|
2015-04-26 18:23:43 +02:00
|
|
|
|
2015-07-11 05:39:00 +02:00
|
|
|
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]);
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2016-03-13 05:35:23 +01:00
|
|
|
char *highlight = have_ti ? xstrdup_printf ("%s%s%s",
|
2015-05-02 20:23:02 +02:00
|
|
|
g_terminal.color_set_fg[COLOR_YELLOW],
|
|
|
|
g_terminal.color_set_bg[COLOR_MAGENTA],
|
2016-03-13 05:35:23 +01:00
|
|
|
enter_bold_mode) : NULL;
|
2015-04-29 21:25:57 +02:00
|
|
|
INIT_ATTR (HIGHLIGHT, highlight);
|
2015-04-27 22:40:33 +02:00
|
|
|
free (highlight);
|
|
|
|
|
2015-04-21 20:52:41 +02:00
|
|
|
#undef INIT_ATTR
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-13 05:35:23 +01:00
|
|
|
// This prevents formatters from obtaining an attribute printer function
|
|
|
|
if (!have_ti)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-04-29 20:56:26 +02:00
|
|
|
g_terminal.stdout_is_tty = false;
|
|
|
|
g_terminal.stderr_is_tty = false;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
g_log_message_real = log_message_attributed;
|
2015-07-05 15:57:53 +02:00
|
|
|
|
|
|
|
// 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));
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2022-08-25 12:28:17 +02:00
|
|
|
// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-05-10 23:06:19 +02:00
|
|
|
|
|
|
|
// 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
|
2020-10-11 17:36:21 +02:00
|
|
|
// with colour pairs.
|
2015-05-10 23:06:19 +02:00
|
|
|
|
|
|
|
enum
|
|
|
|
{
|
2020-10-11 17:58:35 +02:00
|
|
|
TEXT_BOLD = 1 << 0,
|
|
|
|
TEXT_ITALIC = 1 << 1,
|
|
|
|
TEXT_UNDERLINE = 1 << 2,
|
|
|
|
TEXT_INVERSE = 1 << 3,
|
|
|
|
TEXT_BLINK = 1 << 4,
|
2021-08-28 14:44:38 +02:00
|
|
|
TEXT_CROSSED_OUT = 1 << 5,
|
|
|
|
TEXT_MONOSPACE = 1 << 6
|
2015-05-10 23:06:19 +02:00
|
|
|
};
|
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
struct attr_printer
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2016-10-29 05:33:22 +02:00
|
|
|
char **attrs; ///< Named attributes
|
2015-08-08 20:29:31 +02:00
|
|
|
FILE *stream; ///< Output stream
|
2015-05-11 19:09:42 +02:00
|
|
|
bool dirty; ///< Attributes are set
|
|
|
|
};
|
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
#define ATTR_PRINTER_INIT(ctx, stream) { ctx->attrs, stream, true }
|
2016-10-29 05:33:22 +02:00
|
|
|
|
2020-10-04 12:04:24 +02:00
|
|
|
static void
|
|
|
|
attr_printer_filtered_puts (FILE *stream, const char *attr)
|
|
|
|
{
|
|
|
|
for (; *attr; attr++)
|
|
|
|
{
|
|
|
|
// sgr/set_attributes and sgr0/exit_attribute_mode like to enable or
|
|
|
|
// disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R`
|
|
|
|
// does not skip over these characters and it screws up word wrapping
|
|
|
|
if (*attr == 14 /* SO */ || *attr == 15 /* SI */)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Trivially skip delay sequences intended to be processed by tputs()
|
|
|
|
const char *end = NULL;
|
|
|
|
if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>')))
|
|
|
|
attr = end;
|
|
|
|
else
|
|
|
|
fputc (*attr, stream);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-08 20:29:31 +02:00
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (struct attr_printer *self, const char *attr)
|
2015-08-08 20:29:31 +02:00
|
|
|
{
|
|
|
|
terminal_printer_fn printer = get_attribute_printer (self->stream);
|
|
|
|
if (printer)
|
|
|
|
tputs (attr, 1, printer);
|
|
|
|
else
|
2020-10-04 12:04:24 +02:00
|
|
|
// We shouldn't really do this but we need it to output formatting
|
|
|
|
// to the backlog helper--it should be SGR-only
|
|
|
|
attr_printer_filtered_puts (self->stream, attr);
|
2015-08-08 20:29:31 +02:00
|
|
|
}
|
|
|
|
|
2015-05-11 19:09:42 +02:00
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (struct attr_printer *self)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
|
|
|
if (self->dirty)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, self->attrs[ATTR_RESET]);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
|
|
|
self->dirty = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply_named (struct attr_printer *self, int attribute)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (self);
|
2015-05-11 19:09:42 +02:00
|
|
|
if (attribute != ATTR_RESET)
|
|
|
|
{
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, self->attrs[attribute]);
|
2015-05-11 19:09:42 +02:00
|
|
|
self->dirty = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-12 02:43:53 +02:00
|
|
|
// NOTE: commonly terminals have:
|
2020-10-11 17:36:21 +02:00
|
|
|
// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
|
|
|
|
// 16 colours (okayish, we have the full basic range guaranteed)
|
|
|
|
// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey)
|
|
|
|
// 256 colours (best, like above but with a larger cube and more grey)
|
2015-05-12 02:43:53 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Interpolate from the 256-colour palette to the 88-colour one
|
2015-05-12 02:43:53 +02:00
|
|
|
static int
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_256_to_88 (int color)
|
2015-05-12 02:43:53 +02:00
|
|
|
{
|
|
|
|
// These colours are the same everywhere
|
|
|
|
if (color < 16)
|
|
|
|
return color;
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// 24 -> 8 extra shades of grey
|
2015-05-12 02:43:53 +02:00
|
|
|
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
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_decode_color (int color, bool *is_bright)
|
2015-05-12 02:43:53 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
2018-01-07 05:45:34 +01:00
|
|
|
// Fall-through
|
2015-05-12 02:43:53 +02:00
|
|
|
case 16:
|
|
|
|
return c16;
|
|
|
|
|
|
|
|
case 88:
|
2016-10-29 18:02:03 +02:00
|
|
|
return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256);
|
2015-05-12 02:43:53 +02:00
|
|
|
case 256:
|
|
|
|
return c256 <= 0 ? c16 : c256;
|
|
|
|
|
|
|
|
default:
|
|
|
|
// Unsupported palette
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
2015-05-11 19:09:42 +02:00
|
|
|
|
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply (struct attr_printer *self,
|
|
|
|
int text_attrs, int wanted_fg, int wanted_bg)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2015-05-12 02:43:53 +02:00
|
|
|
bool fg_is_bright;
|
2016-10-29 18:02:03 +02:00
|
|
|
int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright);
|
2015-05-12 02:43:53 +02:00
|
|
|
bool bg_is_bright;
|
2016-10-29 18:02:03 +02:00
|
|
|
int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
bool have_inverse = !!(text_attrs & TEXT_INVERSE);
|
2015-08-10 22:42:53 +02:00
|
|
|
if (have_inverse)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2015-05-12 02:43:53 +02:00
|
|
|
bool tmp = fg_is_bright;
|
|
|
|
fg_is_bright = bg_is_bright;
|
|
|
|
bg_is_bright = tmp;
|
2015-05-11 19:09:42 +02:00
|
|
|
}
|
|
|
|
|
2015-08-10 22:42:53 +02:00
|
|
|
// In 8 colour mode, some terminals don't support bright backgrounds.
|
|
|
|
// However, we can make use of the fact that the brightness change caused
|
|
|
|
// by the bold attribute is retained when inverting the colours.
|
|
|
|
// This has the downside of making the text bold when it's not supposed
|
|
|
|
// to be, and we still can't make both colours bright, so it's more of
|
|
|
|
// an interesting hack rather than anything else.
|
|
|
|
if (!fg_is_bright && bg_is_bright && have_inverse)
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs |= TEXT_BOLD;
|
2015-08-10 22:42:53 +02:00
|
|
|
else if (!fg_is_bright && bg_is_bright
|
|
|
|
&& !have_inverse && fg >= 0 && bg >= 0)
|
|
|
|
{
|
|
|
|
// As long as none of the colours is the default, we can swap them
|
|
|
|
int tmp = fg; fg = bg; bg = tmp;
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs |= TEXT_BOLD | TEXT_INVERSE;
|
2015-08-10 22:42:53 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-09-16 07:42:05 +02:00
|
|
|
// This often works, however...
|
2016-10-29 18:02:03 +02:00
|
|
|
if (fg_is_bright) text_attrs |= TEXT_BOLD;
|
2020-09-16 07:42:05 +02:00
|
|
|
// this turns out to be annoying if implemented "correctly"
|
2016-10-29 18:02:03 +02:00
|
|
|
if (bg_is_bright) text_attrs |= TEXT_BLINK;
|
2015-08-10 22:42:53 +02:00
|
|
|
}
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (self);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2021-08-29 12:06:53 +02:00
|
|
|
// TEXT_MONOSPACE is unimplemented, for obvious reasons
|
2016-10-29 18:02:03 +02:00
|
|
|
if (text_attrs)
|
|
|
|
attr_printer_tputs (self, tparm (set_attributes,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // standout
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs & TEXT_UNDERLINE,
|
|
|
|
text_attrs & TEXT_INVERSE,
|
|
|
|
text_attrs & TEXT_BLINK,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // dim
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs & TEXT_BOLD,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // blank
|
|
|
|
0, // protect
|
|
|
|
0)); // acs
|
2020-10-11 17:58:35 +02:00
|
|
|
if ((text_attrs & TEXT_ITALIC) && enter_italics_mode)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, enter_italics_mode);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2020-10-11 17:58:35 +02:00
|
|
|
char *smxx = NULL;
|
|
|
|
if ((text_attrs & TEXT_CROSSED_OUT)
|
|
|
|
&& (smxx = tigetstr ("smxx")) && smxx != (char *) -1)
|
|
|
|
attr_printer_tputs (self, smxx);
|
|
|
|
|
2015-05-12 02:43:53 +02:00
|
|
|
if (fg >= 0)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, g_terminal.color_set_fg[fg]);
|
2015-05-12 02:43:53 +02:00
|
|
|
if (bg >= 0)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, g_terminal.color_set_bg[bg]);
|
2015-05-12 02:43:53 +02:00
|
|
|
|
2015-05-11 19:09:42 +02:00
|
|
|
self->dirty = true;
|
|
|
|
}
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// --- 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)
|
|
|
|
{
|
2015-07-11 17:54:38 +02:00
|
|
|
return cstr_cut_until (prefix, "!@");
|
2015-07-06 01:54:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2015-09-24 01:39:49 +02:00
|
|
|
// Message targets can be prefixed by a character filtering their targets
|
|
|
|
static const char *
|
|
|
|
irc_skip_statusmsg (struct server *s, const char *target)
|
|
|
|
{
|
|
|
|
return target + (*target && strchr (s->irc_statusmsg, *target));
|
|
|
|
}
|
|
|
|
|
2021-06-17 12:08:08 +02:00
|
|
|
static bool
|
|
|
|
irc_is_extban (struct server *s, const char *target)
|
|
|
|
{
|
|
|
|
// Some servers have a prefix, and some support negation
|
|
|
|
if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix)
|
|
|
|
return false;
|
|
|
|
if (*target == '~')
|
|
|
|
target++;
|
|
|
|
|
|
|
|
// XXX: we don't know if it's supposed to have an argument, or not
|
|
|
|
return *target && strchr (s->irc_extban_types, *target++)
|
|
|
|
&& strchr (":\0", *target);
|
|
|
|
}
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2020-10-12 23:32:58 +02:00
|
|
|
// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it
|
2015-07-06 01:54:02 +02:00
|
|
|
// as ISO Latin 1. This function should not be called on the whole message.
|
|
|
|
static char *
|
2016-04-03 04:05:04 +02:00
|
|
|
irc_to_utf8 (const char *text)
|
2015-07-06 01:54:02 +02:00
|
|
|
{
|
|
|
|
if (!text)
|
|
|
|
return NULL;
|
2020-10-21 05:39:05 +02:00
|
|
|
|
|
|
|
// XXX: the validation may be unnecessarily harsh, could do with a lenient
|
|
|
|
// first pass, then replace any errors with the replacement character
|
2015-07-06 01:54:02 +02:00
|
|
|
size_t len = strlen (text) + 1;
|
|
|
|
if (utf8_validate (text, len))
|
|
|
|
return xstrdup (text);
|
2016-04-03 04:05:04 +02:00
|
|
|
|
|
|
|
// Windows 1252 redefines several silly C1 control characters as glyphs
|
2016-10-28 18:16:21 +02:00
|
|
|
static const char c1[32][4] =
|
2016-04-03 04:05:04 +02:00
|
|
|
{
|
|
|
|
"\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92",
|
|
|
|
"\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1",
|
|
|
|
"\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9",
|
|
|
|
"\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f",
|
|
|
|
"\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c",
|
|
|
|
"\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94",
|
|
|
|
"\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba",
|
|
|
|
"\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8",
|
|
|
|
};
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str s = str_make ();
|
2016-04-03 04:05:04 +02:00
|
|
|
for (const char *p = text; *p; p++)
|
|
|
|
{
|
|
|
|
int c = *(unsigned char *) p;
|
|
|
|
if (c < 0x80)
|
|
|
|
str_append_c (&s, c);
|
|
|
|
else if (c < 0xA0)
|
|
|
|
str_append (&s, c1[c & 0x1f]);
|
|
|
|
else
|
|
|
|
str_append_data (&s,
|
|
|
|
(char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2);
|
|
|
|
}
|
|
|
|
return str_steal (&s);
|
2015-07-06 01:54:02 +02:00
|
|
|
}
|
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
// --- 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:
|
2015-06-28 02:49:28 +02:00
|
|
|
// #s inserts a string (expected to be in UTF-8)
|
|
|
|
// #d inserts a signed integer
|
2015-12-25 21:22:59 +01:00
|
|
|
// #l inserts a locale-encoded string
|
2015-06-28 02:49:28 +02:00
|
|
|
//
|
|
|
|
// #S inserts a string from the server with unknown encoding
|
2021-08-29 12:07:31 +02:00
|
|
|
// #m inserts an IRC-formatted string (auto-resets at boundaries)
|
2015-06-28 02:49:28 +02:00
|
|
|
// #n cuts the nickname from a string and automatically colours it
|
2015-06-29 08:54:00 +02:00
|
|
|
// #N is like #n but also appends userhost, if present
|
2015-04-26 18:23:43 +02:00
|
|
|
//
|
|
|
|
// #a inserts named attributes (auto-resets)
|
|
|
|
// #r resets terminal attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
// #c sets foreground colour
|
|
|
|
// #C sets background colour
|
2015-06-28 02:49:28 +02:00
|
|
|
//
|
|
|
|
// Modifiers:
|
|
|
|
// & free() the string argument after using it
|
2015-04-26 18:23:43 +02:00
|
|
|
|
|
|
|
static void
|
2015-05-10 23:06:19 +02:00
|
|
|
formatter_add_item (struct formatter *self, struct formatter_item template_)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
if (template_.text)
|
|
|
|
template_.text = xstrdup (template_.text);
|
2015-04-26 18:39:38 +02:00
|
|
|
|
2016-01-31 20:07:20 +01:00
|
|
|
if (self->items_len == self->items_alloc)
|
|
|
|
self->items = xreallocarray
|
|
|
|
(self->items, sizeof *self->items, (self->items_alloc <<= 1));
|
|
|
|
self->items[self->items_len++] = template_;
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
|
|
|
|
(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
|
2015-04-26 18:39:38 +02:00
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
#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_))
|
2015-04-26 18:23:43 +02:00
|
|
|
|
2015-05-10 05:28:12 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// We use estimates from the 16 colour terminal palette, or the 256 colour cube,
|
2015-05-10 05:28:12 +02:00
|
|
|
// which is not always available. The mIRC orange colour is only in the cube.
|
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
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),
|
2015-05-10 05:28:12 +02:00
|
|
|
};
|
|
|
|
|
2020-10-11 17:41:57 +02:00
|
|
|
// https://modern.ircdocs.horse/formatting.html
|
|
|
|
// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html
|
2021-08-28 17:51:58 +02:00
|
|
|
static const int16_t g_extra_to_256[100 - 16] =
|
2020-10-11 17:41:57 +02:00
|
|
|
{
|
|
|
|
52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
|
|
|
|
88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
|
|
|
|
124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
|
|
|
|
196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198,
|
|
|
|
203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
|
|
|
|
217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
|
|
|
|
16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1
|
|
|
|
};
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
static const char *
|
2021-08-28 17:51:58 +02:00
|
|
|
irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg)
|
2015-05-10 05:28:12 +02:00
|
|
|
{
|
2015-05-10 09:55:44 +02:00
|
|
|
if (!isdigit_ascii (*s))
|
2016-03-08 01:59:51 +01:00
|
|
|
{
|
2021-08-28 17:51:58 +02:00
|
|
|
*fg = *bg = 99;
|
2015-05-10 09:55:44 +02:00
|
|
|
return s;
|
2016-03-08 01:59:51 +01:00
|
|
|
}
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2021-08-28 17:51:58 +02:00
|
|
|
*fg = *s++ - '0';
|
2015-05-10 09:55:44 +02:00
|
|
|
if (isdigit_ascii (*s))
|
2021-08-28 17:51:58 +02:00
|
|
|
*fg = *fg * 10 + (*s++ - '0');
|
2015-05-10 09:55:44 +02:00
|
|
|
|
|
|
|
if (*s != ',' || !isdigit_ascii (s[1]))
|
|
|
|
return s;
|
2015-05-12 02:48:12 +02:00
|
|
|
s++;
|
2015-05-10 09:55:44 +02:00
|
|
|
|
2021-08-28 17:51:58 +02:00
|
|
|
*bg = *s++ - '0';
|
2015-05-12 02:48:12 +02:00
|
|
|
if (isdigit_ascii (*s))
|
2021-08-28 17:51:58 +02:00
|
|
|
*bg = *bg * 10 + (*s++ - '0');
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2021-08-29 12:06:53 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct irc_char_attrs
|
|
|
|
{
|
|
|
|
uint8_t fg, bg; ///< {Fore,back}ground colour or 99
|
|
|
|
uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK
|
|
|
|
uint8_t starts_at_boundary; ///< Possible to split here?
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out)
|
|
|
|
{
|
|
|
|
soft_assert (attrs->fg < 100 && attrs->bg < 100);
|
|
|
|
|
|
|
|
if (attrs->fg != 99 || attrs->bg != 99)
|
|
|
|
{
|
|
|
|
str_append_printf (out, "\x03%u", attrs->fg);
|
|
|
|
if (attrs->bg != 99)
|
|
|
|
str_append_printf (out, ",%02u", attrs->bg);
|
|
|
|
}
|
|
|
|
if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02');
|
|
|
|
if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d');
|
|
|
|
if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f');
|
|
|
|
if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16');
|
|
|
|
if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e');
|
|
|
|
if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11');
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
irc_parse_attribute (char c)
|
|
|
|
{
|
|
|
|
switch (c)
|
|
|
|
{
|
|
|
|
case '\x02' /* ^B */: return TEXT_BOLD;
|
|
|
|
case '\x11' /* ^Q */: return TEXT_MONOSPACE;
|
|
|
|
case '\x16' /* ^V */: return TEXT_INVERSE;
|
|
|
|
case '\x1d' /* ^] */: return TEXT_ITALIC;
|
|
|
|
case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT;
|
|
|
|
case '\x1f' /* ^_ */: return TEXT_UNDERLINE;
|
|
|
|
case '\x0f' /* ^O */: return -1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The text needs to be NUL-terminated, and a valid UTF-8 string
|
|
|
|
static struct irc_char_attrs *
|
|
|
|
irc_analyze_text (const char *text, size_t len)
|
|
|
|
{
|
|
|
|
struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs),
|
|
|
|
blank = { .fg = 99, .bg = 99, .starts_at_boundary = true },
|
|
|
|
next = blank, cur = next;
|
|
|
|
|
|
|
|
for (size_t i = 0; i != len; cur = next)
|
|
|
|
{
|
|
|
|
const char *start = text;
|
|
|
|
hard_assert (utf8_decode (&text, len - i) >= 0);
|
|
|
|
|
|
|
|
int attribute = irc_parse_attribute (*start);
|
|
|
|
if (*start == '\x03')
|
|
|
|
text = irc_parse_mirc_color (text, &next.fg, &next.bg);
|
|
|
|
else if (attribute > 0)
|
|
|
|
next.attributes ^= attribute;
|
|
|
|
else if (attribute < 0)
|
|
|
|
next = blank;
|
|
|
|
|
|
|
|
while (start++ != text)
|
|
|
|
{
|
|
|
|
attrs[i++] = cur;
|
|
|
|
cur.starts_at_boundary = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2021-08-28 17:51:58 +02:00
|
|
|
static const char *
|
|
|
|
formatter_parse_mirc_color (struct formatter *self, const char *s)
|
|
|
|
{
|
|
|
|
uint8_t fg = 255, bg = 255;
|
|
|
|
s = irc_parse_mirc_color (s, &fg, &bg);
|
|
|
|
|
|
|
|
if (fg < 16)
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
|
|
|
|
else if (fg < 100)
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR,
|
|
|
|
.color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16]));
|
|
|
|
|
2020-10-11 17:41:57 +02:00
|
|
|
if (bg < 16)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
|
2021-08-28 17:51:58 +02:00
|
|
|
else if (bg < 100)
|
2020-10-11 17:41:57 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR,
|
2021-08-28 17:51:58 +02:00
|
|
|
.color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16]));
|
2015-05-10 09:55:44 +02:00
|
|
|
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-08-29 12:07:31 +02:00
|
|
|
formatter_parse_message (struct formatter *self, const char *s)
|
2015-05-10 09:55:44 +02:00
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str buf = str_make ();
|
2015-05-10 23:11:43 +02:00
|
|
|
unsigned char c;
|
2015-05-10 05:28:12 +02:00
|
|
|
while ((c = *s++))
|
|
|
|
{
|
2015-05-10 09:55:44 +02:00
|
|
|
if (buf.len && c < 0x20)
|
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-05-10 09:55:44 +02:00
|
|
|
str_reset (&buf);
|
|
|
|
}
|
|
|
|
|
2021-08-29 12:06:53 +02:00
|
|
|
int attribute = irc_parse_attribute (c);
|
|
|
|
if (c == '\x03')
|
2015-05-10 09:55:44 +02:00
|
|
|
s = formatter_parse_mirc_color (self, s);
|
2021-08-29 12:06:53 +02:00
|
|
|
else if (attribute > 0)
|
|
|
|
FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute);
|
|
|
|
else if (attribute < 0)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2021-08-29 12:06:53 +02:00
|
|
|
else
|
2015-05-10 09:55:44 +02:00
|
|
|
str_append_c (&buf, c);
|
2015-05-10 05:28:12 +02:00
|
|
|
}
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
if (buf.len)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
str_free (&buf);
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-05-10 05:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
static void
|
2016-10-23 13:38:46 +02:00
|
|
|
formatter_parse_nick (struct formatter *self, const char *s)
|
2015-06-28 02:49:28 +02:00
|
|
|
{
|
2015-09-24 02:03:10 +02:00
|
|
|
// For outgoing messages; maybe we should add a special #t for them
|
|
|
|
// which would also make us not cut off the userhost part, ever
|
|
|
|
if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s)))
|
|
|
|
{
|
2016-04-03 04:05:04 +02:00
|
|
|
char *tmp = irc_to_utf8 (s);
|
2015-09-24 02:03:10 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, tmp);
|
|
|
|
free (tmp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
char *nick = irc_cut_nickname (s);
|
2015-07-27 01:18:32 +02:00
|
|
|
int color = siphash_wrapper (nick, strlen (nick)) % 7;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
2015-07-27 01:18:32 +02:00
|
|
|
// Never use the black colour, could become transparent on black terminals;
|
|
|
|
// white is similarly excluded from the range
|
|
|
|
if (color == COLOR_BLACK)
|
2015-08-11 21:22:44 +02:00
|
|
|
color = (uint16_t) -1;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// Use a colour from the 256-colour cube if available
|
2015-08-10 07:51:03 +02:00
|
|
|
color |= self->ctx->nick_palette[siphash_wrapper (nick,
|
|
|
|
strlen (nick)) % self->ctx->nick_palette_len] << 16;
|
2015-07-27 01:18:32 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// We always use the default colour for ourselves
|
2015-07-27 01:18:32 +02:00
|
|
|
if (self->s && irc_is_this_us (self->s, nick))
|
2015-06-28 02:49:28 +02:00
|
|
|
color = -1;
|
|
|
|
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
|
|
|
|
|
2016-04-03 04:05:04 +02:00
|
|
|
char *x = irc_to_utf8 (nick);
|
2015-06-28 02:49:28 +02:00
|
|
|
free (nick);
|
|
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
|
|
free (x);
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// Need to reset the colour afterwards
|
2015-06-28 02:49:28 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
|
|
|
|
}
|
|
|
|
|
2015-06-29 08:54:00 +02:00
|
|
|
static void
|
2016-10-23 13:38:46 +02:00
|
|
|
formatter_parse_nick_full (struct formatter *self, const char *s)
|
2015-06-29 08:54:00 +02:00
|
|
|
{
|
|
|
|
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);
|
|
|
|
|
2016-04-03 04:05:04 +02:00
|
|
|
char *x = irc_to_utf8 (userhost);
|
2015-06-29 08:54:00 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
|
|
free (x);
|
|
|
|
|
|
|
|
FORMATTER_ADD_RESET (self);
|
|
|
|
FORMATTER_ADD_TEXT (self, ")");
|
|
|
|
}
|
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
static const char *
|
|
|
|
formatter_parse_field (struct formatter *self,
|
|
|
|
const char *field, struct str *buf, va_list *ap)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
bool free_string = false;
|
|
|
|
char *s = NULL;
|
|
|
|
char *tmp = NULL;
|
2015-04-26 18:23:43 +02:00
|
|
|
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
|
2015-06-28 02:49:28 +02:00
|
|
|
case 'd':
|
|
|
|
tmp = xstrdup_printf ("%d", va_arg (*ap, int));
|
|
|
|
str_append (buf, tmp);
|
|
|
|
free (tmp);
|
|
|
|
break;
|
2015-04-26 18:23:43 +02:00
|
|
|
case 's':
|
2015-06-28 02:49:28 +02:00
|
|
|
str_append (buf, (s = va_arg (*ap, char *)));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
2015-12-25 21:22:59 +01:00
|
|
|
case 'l':
|
|
|
|
if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
|
|
|
|
(s = va_arg (*ap, char *)), -1, NULL)))
|
|
|
|
print_error ("character conversion failed for: %s", "output");
|
|
|
|
else
|
|
|
|
str_append (buf, tmp);
|
|
|
|
free (tmp);
|
|
|
|
break;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
|
|
|
case 'S':
|
2016-04-03 04:05:04 +02:00
|
|
|
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
|
2015-06-28 02:49:28 +02:00
|
|
|
str_append (buf, tmp);
|
|
|
|
free (tmp);
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
2015-05-10 09:55:44 +02:00
|
|
|
case 'm':
|
2016-04-03 04:05:04 +02:00
|
|
|
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
|
2021-08-29 12:07:31 +02:00
|
|
|
formatter_parse_message (self, tmp);
|
2015-06-28 02:49:28 +02:00
|
|
|
free (tmp);
|
|
|
|
break;
|
|
|
|
case 'n':
|
|
|
|
formatter_parse_nick (self, (s = va_arg (*ap, char *)));
|
2015-05-10 09:55:44 +02:00
|
|
|
break;
|
2015-06-29 08:54:00 +02:00
|
|
|
case 'N':
|
|
|
|
formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
|
|
|
|
break;
|
2015-05-10 09:55:44 +02:00
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
case 'a':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
case 'c':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
case 'C':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
case 'r':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2015-06-28 02:49:28 +02:00
|
|
|
if (c == '&' && !free_string)
|
|
|
|
free_string = true;
|
2015-04-26 18:23:43 +02:00
|
|
|
else if (c)
|
|
|
|
hard_assert (!"unexpected format specifier");
|
|
|
|
else
|
|
|
|
hard_assert (!"unexpected end of format string");
|
|
|
|
goto restart;
|
|
|
|
}
|
2015-06-28 02:49:28 +02:00
|
|
|
|
|
|
|
if (free_string)
|
|
|
|
free (s);
|
2015-04-26 18:23:43 +02:00
|
|
|
return field;
|
|
|
|
}
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
// 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
|
2015-04-26 18:23:43 +02:00
|
|
|
static void
|
2015-06-28 02:49:28 +02:00
|
|
|
formatter_addv (struct formatter *self, const char *format, va_list *ap)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str buf = str_make ();
|
2015-04-26 18:23:43 +02:00
|
|
|
while (*format)
|
|
|
|
{
|
|
|
|
if (*format != '#' || *++format == '#')
|
|
|
|
{
|
|
|
|
str_append_c (&buf, *format++);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (buf.len)
|
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-04-26 18:23:43 +02:00
|
|
|
str_reset (&buf);
|
|
|
|
}
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
format = formatter_parse_field (self, format, &buf, ap);
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (buf.len)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-04-26 18:23:43 +02:00
|
|
|
|
|
|
|
str_free (&buf);
|
2015-06-28 02:49:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
formatter_add (struct formatter *self, const char *format, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
va_start (ap, format);
|
|
|
|
formatter_addv (self, format, &ap);
|
2015-04-26 18:23:43 +02:00
|
|
|
va_end (ap);
|
|
|
|
}
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct line_char_attrs
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
short named; ///< Named attribute or -1
|
|
|
|
short text; ///< Text attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
int fg; ///< Foreground colour (-1 for default)
|
|
|
|
int bg; ///< Background colour (-1 for default)
|
2016-03-21 00:30:59 +01:00
|
|
|
};
|
|
|
|
|
2020-10-19 23:37:19 +02:00
|
|
|
// We can get rid of the linked list and do this in one allocation (use strlen()
|
|
|
|
// for the upper bound)--since we only prepend and/or replace characters, add
|
|
|
|
// a member to specify the prepended character and how many times to repeat it.
|
|
|
|
// Tabs may nullify the wide character but it's not necessary.
|
|
|
|
//
|
|
|
|
// This would be slighly more optimal but it would also set the algorithm in
|
|
|
|
// stone and complicate flushing.
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
struct line_char
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct line_char)
|
|
|
|
|
|
|
|
wchar_t wide; ///< The character as a wchar_t
|
|
|
|
int width; ///< Width of the character in cells
|
|
|
|
struct line_char_attrs attrs; ///< Attributes
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct line_char *
|
2020-10-19 05:19:28 +02:00
|
|
|
line_char_new (wchar_t wc)
|
2016-03-21 00:30:59 +01:00
|
|
|
{
|
|
|
|
struct line_char *self = xcalloc (1, sizeof *self);
|
|
|
|
self->width = wcwidth ((self->wide = wc));
|
|
|
|
|
2016-03-26 20:59:37 +01:00
|
|
|
// Typically various control characters
|
|
|
|
if (self->width < 0)
|
|
|
|
self->width = 0;
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.bg = self->attrs.fg = -1;
|
|
|
|
self->attrs.named = ATTR_RESET;
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct line_wrap_mark
|
|
|
|
{
|
|
|
|
struct line_char *start; ///< First character
|
|
|
|
int used; ///< Display cells used
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c)
|
|
|
|
{
|
|
|
|
if (!mark->start)
|
|
|
|
mark->start = c;
|
|
|
|
mark->used += c->width;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct line_wrap_state
|
|
|
|
{
|
|
|
|
struct line_char *result; ///< Head of result
|
|
|
|
struct line_char *result_tail; ///< Tail of result
|
|
|
|
|
|
|
|
int line_used; ///< Line length before marks
|
|
|
|
int line_max; ///< Maximum line length
|
|
|
|
struct line_wrap_mark chunk; ///< All buffered text
|
|
|
|
struct line_wrap_mark overflow; ///< Overflowing text
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before)
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *nl = line_char_new (L'\n');
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start);
|
|
|
|
s->line_used = before->used;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_flush (struct line_wrap_state *s, bool force_split)
|
|
|
|
{
|
|
|
|
if (!s->overflow.start)
|
|
|
|
s->line_used += s->chunk.used;
|
|
|
|
else if (force_split || s->chunk.used > s->line_max)
|
|
|
|
{
|
|
|
|
#ifdef WRAP_UNNECESSARILY
|
2016-04-21 22:34:11 +02:00
|
|
|
// When the line wraps at the end of the screen and a background colour
|
|
|
|
// is set, the terminal paints the entire new line with that colour.
|
|
|
|
// Explicitly inserting a newline with the default attributes fixes it.
|
2016-03-21 00:30:59 +01:00
|
|
|
line_wrap_flush_split (s, &s->overflow);
|
|
|
|
#else
|
2016-04-21 22:34:11 +02:00
|
|
|
// Splitting here breaks link searching mechanisms in some terminals,
|
|
|
|
// though, so we make a trade-off and let the chunk wrap naturally.
|
|
|
|
// Fuck terminals, really.
|
2016-03-21 00:30:59 +01:00
|
|
|
s->line_used = s->overflow.used;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
else
|
|
|
|
// Print the chunk in its entirety on a new line
|
|
|
|
line_wrap_flush_split (s, &s->chunk);
|
|
|
|
|
|
|
|
memset (&s->chunk, 0, sizeof s->chunk);
|
|
|
|
memset (&s->overflow, 0, sizeof s->overflow);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_nl (struct line_wrap_state *s)
|
|
|
|
{
|
|
|
|
line_wrap_flush (s, true);
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *nl = line_char_new (L'\n');
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl);
|
|
|
|
s->line_used = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_tab (struct line_wrap_state *s, struct line_char *c)
|
|
|
|
{
|
|
|
|
line_wrap_flush (s, true);
|
|
|
|
if (s->line_used >= s->line_max)
|
|
|
|
line_wrap_nl (s);
|
|
|
|
|
|
|
|
// Compute the number of characters needed to get to the next tab stop
|
|
|
|
int tab_width = ((s->line_used + 8) & ~7) - s->line_used;
|
|
|
|
// On overflow just fill the rest of the line with spaces
|
|
|
|
if (s->line_used + tab_width > s->line_max)
|
|
|
|
tab_width = s->line_max - s->line_used;
|
|
|
|
|
|
|
|
s->line_used += tab_width;
|
|
|
|
while (tab_width--)
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *space = line_char_new (L' ');
|
2016-03-21 00:30:59 +01:00
|
|
|
space->attrs = c->attrs;
|
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
line_wrap_push_char (struct line_wrap_state *s, struct line_char *c)
|
|
|
|
{
|
|
|
|
// Note that when processing whitespace here, any non-WS chunk has already
|
|
|
|
// been flushed, and thus it matters little if we flush with force split
|
|
|
|
if (wcschr (L"\r\f\v", c->wide))
|
|
|
|
/* Skip problematic characters */;
|
|
|
|
else if (c->wide == L'\n')
|
|
|
|
line_wrap_nl (s);
|
|
|
|
else if (c->wide == L'\t')
|
|
|
|
line_wrap_tab (s, c);
|
|
|
|
else
|
|
|
|
goto use_as_is;
|
|
|
|
free (c);
|
|
|
|
return;
|
|
|
|
|
|
|
|
use_as_is:
|
|
|
|
if (s->overflow.start
|
|
|
|
|| s->line_used + s->chunk.used + c->width > s->line_max)
|
|
|
|
{
|
|
|
|
if (s->overflow.used + c->width > s->line_max)
|
|
|
|
{
|
|
|
|
#ifdef WRAP_UNNECESSARILY
|
|
|
|
// If the overflow overflows, restart on a new line
|
|
|
|
line_wrap_nl (s);
|
|
|
|
#else
|
|
|
|
// See line_wrap_flush(), we would end up on a new line anyway
|
|
|
|
line_wrap_flush (s, true);
|
|
|
|
s->line_used = 0;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
else
|
|
|
|
line_wrap_mark_push (&s->overflow, c);
|
|
|
|
}
|
|
|
|
line_wrap_mark_push (&s->chunk, c);
|
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Basic word wrapping that respects wcwidth(3) and expands tabs.
|
|
|
|
/// Besides making text easier to read, it also fixes the problem with
|
|
|
|
/// formatting spilling over the entire new line on line wrap.
|
|
|
|
static struct line_char *
|
|
|
|
line_wrap (struct line_char *line, int max_width)
|
|
|
|
{
|
|
|
|
struct line_wrap_state s = { .line_max = max_width };
|
|
|
|
bool last_was_word_char = false;
|
|
|
|
LIST_FOR_EACH (struct line_char, c, line)
|
|
|
|
{
|
|
|
|
// Act on the right boundary of (\s*\S+) chunks
|
|
|
|
bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide);
|
|
|
|
if (last_was_word_char && !this_is_word_char)
|
|
|
|
line_wrap_flush (&s, false);
|
|
|
|
last_was_word_char = this_is_word_char;
|
|
|
|
|
|
|
|
LIST_UNLINK (line, c);
|
|
|
|
line_wrap_push_char (&s, c);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure to process the last word and return the modified list
|
|
|
|
line_wrap_flush (&s, false);
|
|
|
|
return s.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct exploder
|
|
|
|
{
|
|
|
|
struct app_context *ctx; ///< Application context
|
|
|
|
struct line_char *result; ///< Result
|
|
|
|
struct line_char *result_tail; ///< Tail of result
|
|
|
|
struct line_char_attrs attrs; ///< Current attributes
|
|
|
|
};
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
static bool
|
2016-03-21 00:30:59 +01:00
|
|
|
explode_formatter_attr (struct exploder *self, struct formatter_item *item)
|
2015-06-28 02:49:28 +02:00
|
|
|
{
|
|
|
|
switch (item->type)
|
|
|
|
{
|
|
|
|
case FORMATTER_ITEM_ATTR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = item->attribute;
|
|
|
|
self->attrs.text = 0;
|
|
|
|
self->attrs.fg = -1;
|
|
|
|
self->attrs.bg = -1;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
case FORMATTER_ITEM_SIMPLE:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
self->attrs.text ^= item->attribute;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
case FORMATTER_ITEM_FG_COLOR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
self->attrs.fg = item->color;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
case FORMATTER_ITEM_BG_COLOR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
self->attrs.bg = item->color;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-11 04:40:48 +02:00
|
|
|
static void
|
2016-03-21 00:30:59 +01:00
|
|
|
explode_text (struct exploder *self, const char *text)
|
2015-07-11 04:40:48 +02:00
|
|
|
{
|
2017-07-07 20:45:24 +02:00
|
|
|
// Throw away any potentially harmful control characters first
|
|
|
|
struct str filtered = str_make ();
|
|
|
|
for (const char *p = text; *p; p++)
|
2020-10-04 12:04:24 +02:00
|
|
|
if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p))
|
2017-07-07 20:45:24 +02:00
|
|
|
str_append_c (&filtered, *p);
|
|
|
|
|
2022-08-27 08:55:44 +02:00
|
|
|
size_t term_len = 0, processed = 0, len;
|
2016-03-21 00:30:59 +01:00
|
|
|
char *term = iconv_xstrdup (self->ctx->term_from_utf8,
|
2017-07-07 20:45:24 +02:00
|
|
|
filtered.str, filtered.len + 1, &term_len);
|
|
|
|
str_free (&filtered);
|
2016-03-21 00:30:59 +01:00
|
|
|
|
|
|
|
mbstate_t ps;
|
|
|
|
memset (&ps, 0, sizeof ps);
|
|
|
|
|
|
|
|
wchar_t wch;
|
|
|
|
while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
|
|
|
|
{
|
|
|
|
hard_assert (len != (size_t) -2 && len != (size_t) -1);
|
2022-08-27 08:55:44 +02:00
|
|
|
hard_assert ((processed += len) <= term_len);
|
2015-07-11 04:40:48 +02:00
|
|
|
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *c = line_char_new (wch);
|
2016-03-21 00:30:59 +01:00
|
|
|
c->attrs = self->attrs;
|
|
|
|
LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c);
|
|
|
|
}
|
2015-07-11 04:40:48 +02:00
|
|
|
free (term);
|
2016-03-21 00:30:59 +01:00
|
|
|
}
|
2015-07-11 04:40:48 +02:00
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
static struct line_char *
|
|
|
|
formatter_to_chars (struct formatter *formatter)
|
|
|
|
{
|
|
|
|
struct exploder self = { .ctx = formatter->ctx };
|
|
|
|
self.attrs.fg = self.attrs.bg = self.attrs.named = -1;
|
|
|
|
|
|
|
|
int attribute_ignore = 0;
|
|
|
|
for (size_t i = 0; i < formatter->items_len; i++)
|
|
|
|
{
|
|
|
|
struct formatter_item *iter = &formatter->items[i];
|
|
|
|
if (iter->type == FORMATTER_ITEM_TEXT)
|
|
|
|
explode_text (&self, iter->text);
|
|
|
|
else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR)
|
|
|
|
attribute_ignore += iter->attribute;
|
|
|
|
else if (attribute_ignore <= 0
|
|
|
|
&& !explode_formatter_attr (&self, iter))
|
|
|
|
hard_assert (!"unhandled formatter item type");
|
|
|
|
}
|
|
|
|
return self.result;
|
2015-07-11 04:40:48 +02:00
|
|
|
}
|
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
enum
|
|
|
|
{
|
|
|
|
FLUSH_OPT_RAW = (1 << 0), ///< Print raw attributes
|
|
|
|
FLUSH_OPT_NOWRAP = (1 << 1) ///< Do not wrap
|
|
|
|
};
|
|
|
|
|
2020-10-19 05:19:28 +02:00
|
|
|
/// The input is a bunch of wide characters--respect shift state encodings
|
|
|
|
static void
|
|
|
|
formatter_putc (struct line_char *c, FILE *stream)
|
|
|
|
{
|
|
|
|
static mbstate_t mb;
|
|
|
|
char buf[MB_LEN_MAX] = {};
|
|
|
|
size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb);
|
|
|
|
if (len != (size_t) -1 && len)
|
|
|
|
fwrite (buf, len - !c, 1, stream);
|
|
|
|
free (c);
|
|
|
|
}
|
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
static void
|
2016-04-10 21:37:36 +02:00
|
|
|
formatter_flush (struct formatter *self, FILE *stream, int flush_opts)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2016-03-21 00:30:59 +01:00
|
|
|
struct line_char *line = formatter_to_chars (self);
|
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
bool is_tty = !!get_attribute_printer (stream);
|
|
|
|
if (!is_tty && !(flush_opts & FLUSH_OPT_RAW))
|
2015-04-28 17:25:50 +02:00
|
|
|
{
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_FOR_EACH (struct line_char, c, line)
|
2020-10-19 05:19:28 +02:00
|
|
|
formatter_putc (c, stream);
|
|
|
|
formatter_putc (NULL, stream);
|
2015-04-28 17:25:50 +02:00
|
|
|
return;
|
|
|
|
}
|
2015-04-26 18:23:43 +02:00
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP))
|
2016-03-21 00:30:59 +01:00
|
|
|
line = line_wrap (line, g_terminal.columns);
|
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
struct attr_printer state = ATTR_PRINTER_INIT (self->ctx, stream);
|
2016-10-29 05:33:22 +02:00
|
|
|
struct line_char_attrs attrs = {}; // Won't compare equal to anything
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_FOR_EACH (struct line_char, c, line)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2016-03-21 00:30:59 +01:00
|
|
|
if (attrs.fg != c->attrs.fg
|
|
|
|
|| attrs.bg != c->attrs.bg
|
|
|
|
|| attrs.named != c->attrs.named
|
|
|
|
|| attrs.text != c->attrs.text)
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
formatter_putc (NULL, stream);
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
attrs = c->attrs;
|
2016-10-29 05:33:22 +02:00
|
|
|
if (attrs.named != -1)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply_named (&state, attrs.named);
|
2016-10-29 05:33:22 +02:00
|
|
|
else
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg);
|
2016-03-21 00:30:59 +01:00
|
|
|
}
|
|
|
|
|
2020-10-19 05:19:28 +02:00
|
|
|
formatter_putc (c, stream);
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
2020-10-19 05:19:28 +02:00
|
|
|
formatter_putc (NULL, stream);
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (&state);
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// --- Buffers -----------------------------------------------------------------
|
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
static void
|
|
|
|
buffer_pop_excess_lines (struct app_context *ctx, struct buffer *self)
|
|
|
|
{
|
|
|
|
int to_delete = (int) self->lines_count - (int) ctx->backlog_limit;
|
|
|
|
while (to_delete-- > 0 && self->lines)
|
|
|
|
{
|
|
|
|
struct buffer_line *excess = self->lines;
|
|
|
|
LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, excess);
|
|
|
|
buffer_line_destroy (excess);
|
|
|
|
self->lines_count--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_config_backlog_limit_change (struct config_item *item)
|
|
|
|
{
|
|
|
|
struct app_context *ctx = item->user_data;
|
2016-01-15 22:11:05 +01:00
|
|
|
ctx->backlog_limit = MIN (item->value.integer, INT_MAX);
|
2016-01-15 03:49:24 +01:00
|
|
|
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
|
|
buffer_pop_excess_lines (ctx, iter);
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_update_time (struct app_context *ctx, time_t now, FILE *stream,
|
|
|
|
int flush_opts)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
|
2016-04-10 22:13:53 +02:00
|
|
|
char buf[64] = "";
|
|
|
|
const char *format =
|
2022-08-26 03:43:32 +02:00
|
|
|
get_config_string (ctx->config.root, "general.date_change_line");
|
2016-04-10 22:13:53 +02:00
|
|
|
if (!strftime (buf, sizeof buf, format, ¤t))
|
|
|
|
{
|
|
|
|
print_error ("%s: %s", "strftime", strerror (errno));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2016-04-10 22:13:53 +02:00
|
|
|
formatter_add (&f, "#a#s\n", ATTR_DATE_CHANGE, buf);
|
|
|
|
formatter_flush (&f, stream, flush_opts);
|
|
|
|
// Flush the trailing formatting reset item
|
|
|
|
fflush (stream);
|
|
|
|
formatter_free (&f);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
static void
|
2015-08-08 20:29:31 +02:00
|
|
|
buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output,
|
2016-04-10 21:37:36 +02:00
|
|
|
int flush_opts)
|
2015-07-04 15:24:08 +02:00
|
|
|
{
|
|
|
|
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);
|
|
|
|
|
2016-01-31 20:07:20 +01:00
|
|
|
for (struct formatter_item *iter = line->items; iter->type; iter++)
|
|
|
|
formatter_add_item (f, *iter);
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
formatter_add (f, "\n");
|
2016-04-10 21:37:36 +02:00
|
|
|
formatter_flush (f, output, flush_opts);
|
2015-07-04 15:24:08 +02:00
|
|
|
formatter_free (f);
|
|
|
|
}
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
static void
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_line_write_time (struct formatter *f, struct buffer_line *line,
|
|
|
|
FILE *stream, int flush_opts)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// Normal timestamps don't include the date, make sure the user won't be
|
2015-04-13 00:06:08 +02:00
|
|
|
// confused as to when an event has happened
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_update_time (f->ctx, line->when, stream, flush_opts);
|
2015-04-13 00:06:08 +02:00
|
|
|
|
|
|
|
struct tm current;
|
2015-07-04 15:24:08 +02:00
|
|
|
char buf[9];
|
2015-04-13 00:06:08 +02:00
|
|
|
if (!localtime_r (&line->when, ¤t))
|
|
|
|
print_error ("%s: %s", "localtime_r", strerror (errno));
|
2015-07-04 15:24:08 +02:00
|
|
|
else if (!strftime (buf, sizeof buf, "%T", ¤t))
|
|
|
|
print_error ("%s: %s", "strftime", "buffer too small");
|
2015-04-13 00:06:08 +02:00
|
|
|
else
|
2016-04-10 22:13:53 +02:00
|
|
|
formatter_add (f, "#a#s#r ", ATTR_TIMESTAMP, buf);
|
|
|
|
}
|
|
|
|
|
2016-10-23 16:53:31 +02:00
|
|
|
#define buffer_line_will_show_up(buffer, line) \
|
|
|
|
(!(buffer)->hide_unimportant || !((line)->flags & BUFFER_LINE_UNIMPORTANT))
|
|
|
|
|
2016-04-10 22:13:53 +02:00
|
|
|
static void
|
|
|
|
buffer_line_display (struct app_context *ctx,
|
2016-10-23 16:53:31 +02:00
|
|
|
struct buffer *buffer, struct buffer_line *line, bool is_external)
|
2016-04-10 22:13:53 +02:00
|
|
|
{
|
2016-10-23 16:53:31 +02:00
|
|
|
if (!buffer_line_will_show_up (buffer, line))
|
|
|
|
return;
|
|
|
|
|
2016-04-10 22:13:53 +02:00
|
|
|
CALL (ctx->input, hide);
|
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_line_write_time (&f, line, stdout, 0);
|
2015-04-25 13:41:10 +02:00
|
|
|
|
2015-04-26 18:39:38 +02:00
|
|
|
// 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);
|
2015-06-28 02:49:28 +02:00
|
|
|
FORMATTER_ADD_ITEM (&f, IGNORE_ATTR, .attribute = 1);
|
2015-04-26 18:39:38 +02:00
|
|
|
}
|
2016-04-10 21:37:36 +02:00
|
|
|
buffer_line_flush (line, &f, stdout, 0);
|
2015-08-06 23:58:42 +02:00
|
|
|
// Flush the trailing formatting reset item
|
|
|
|
fflush (stdout);
|
2016-04-10 22:13:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-07-04 15:24:08 +02:00
|
|
|
}
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-08-08 20:29:31 +02:00
|
|
|
static void
|
|
|
|
buffer_line_write_to_backlog (struct app_context *ctx,
|
2016-04-10 21:37:36 +02:00
|
|
|
struct buffer_line *line, FILE *log_file, int flush_opts)
|
2015-08-08 20:29:31 +02:00
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_line_write_time (&f, line, log_file, flush_opts);
|
2016-04-10 21:37:36 +02:00
|
|
|
buffer_line_flush (line, &f, log_file, flush_opts);
|
2015-08-08 20:29:31 +02:00
|
|
|
}
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
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;
|
2015-04-15 12:38:21 +02:00
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2015-07-04 15:24:08 +02:00
|
|
|
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);
|
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
// The target is not a terminal, thus it won't wrap in spite of the 0
|
|
|
|
buffer_line_flush (line, &f, log_file, 0);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-06-28 02:49:28 +02:00
|
|
|
log_formatter (struct app_context *ctx,
|
|
|
|
struct buffer *buffer, int flags, struct formatter *f)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2015-06-20 23:30:05 +02:00
|
|
|
if (!buffer)
|
|
|
|
buffer = ctx->global_buffer;
|
|
|
|
|
2016-01-17 22:15:48 +01:00
|
|
|
struct buffer_line *line = buffer_line_new (f);
|
2015-04-13 00:06:08 +02:00
|
|
|
line->flags = flags;
|
2016-01-31 20:06:45 +01:00
|
|
|
// TODO: allow providing custom time (IRCv3.2 server-time)
|
2015-04-13 00:06:08 +02:00
|
|
|
line->when = time (NULL);
|
2015-06-28 02:49:28 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
buffer_pop_excess_lines (ctx, buffer);
|
2015-04-13 00:06:08 +02:00
|
|
|
LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
|
|
|
|
buffer->lines_count++;
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
if (buffer->log_file)
|
|
|
|
buffer_line_write_to_log (ctx, line, buffer->log_file);
|
|
|
|
|
2015-10-30 23:49:43 +01:00
|
|
|
bool unseen_pm = buffer->type == BUFFER_PM
|
|
|
|
&& buffer != ctx->current_buffer
|
|
|
|
&& !(flags & BUFFER_LINE_UNIMPORTANT);
|
2015-11-15 01:43:00 +01:00
|
|
|
bool important = (flags & BUFFER_LINE_HIGHLIGHT) || unseen_pm;
|
|
|
|
if (ctx->beep_on_highlight && important)
|
2016-10-23 16:30:44 +02:00
|
|
|
// XXX: this may disturb any other foreground process
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, ding);
|
2015-06-21 04:00:20 +02:00
|
|
|
|
2015-05-03 06:20:36 +02:00
|
|
|
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;
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
bool displayed = true;
|
2015-12-24 16:02:40 +01:00
|
|
|
if (ctx->terminal_suspended > 0)
|
2015-08-08 19:36:34 +02:00
|
|
|
// Another process is using the terminal
|
|
|
|
displayed = false;
|
|
|
|
else if (buffer == ctx->current_buffer)
|
2016-10-23 16:53:31 +02:00
|
|
|
buffer_line_display (ctx, buffer, line, false);
|
2015-06-28 20:56:45 +02:00
|
|
|
else if (!ctx->isolate_buffers && can_leak)
|
2016-10-23 16:53:31 +02:00
|
|
|
buffer_line_display (ctx, buffer, line, true);
|
2015-04-18 21:50:42 +02:00
|
|
|
else
|
2015-08-08 19:36:34 +02:00
|
|
|
displayed = false;
|
|
|
|
|
2016-03-26 20:59:37 +01:00
|
|
|
// Advance the unread marker in active buffers but don't create a new one
|
|
|
|
if (!displayed
|
|
|
|
|| (buffer == ctx->current_buffer && buffer->new_messages_count))
|
2015-04-18 21:50:42 +02:00
|
|
|
{
|
2016-03-26 03:09:29 +01:00
|
|
|
buffer->new_messages_count++;
|
2015-08-08 21:17:32 +02:00
|
|
|
if (flags & BUFFER_LINE_UNIMPORTANT)
|
2016-03-26 03:09:29 +01:00
|
|
|
buffer->new_unimportant_count++;
|
2015-11-17 00:06:48 +01:00
|
|
|
buffer->highlighted |= important;
|
2015-04-18 21:50:42 +02:00
|
|
|
}
|
2016-03-26 03:09:29 +01:00
|
|
|
if (!displayed)
|
|
|
|
refresh_prompt (ctx);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
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);
|
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, s);
|
2015-06-28 02:49:28 +02:00
|
|
|
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__)
|
2015-06-28 19:56:05 +02:00
|
|
|
#define log_global_indent(ctx, ...) \
|
|
|
|
log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__)
|
2015-04-25 13:41:10 +02:00
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
#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__)
|
|
|
|
|
2015-07-09 23:09:10 +02:00
|
|
|
#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
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-04-17 22:10:43 +02:00
|
|
|
|
2015-06-28 16:16:19 +02:00
|
|
|
// Lines that are used in more than one place
|
|
|
|
|
|
|
|
#define log_nick_self(s, buffer, new_) \
|
2015-08-08 21:17:32 +02:00
|
|
|
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
|
|
|
"You are now known as #n", (new_))
|
2015-06-28 16:16:19 +02:00
|
|
|
#define log_nick(s, buffer, old, new_) \
|
2015-08-08 21:17:32 +02:00
|
|
|
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
|
|
|
"#n is now known as #n", (old), (new_))
|
2015-06-28 16:16:19 +02:00
|
|
|
|
2021-05-30 08:06:38 +02:00
|
|
|
#define log_chghost_self(s, buffer, new_) \
|
|
|
|
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
|
|
|
"You are now #N", (new_))
|
|
|
|
#define log_chghost(s, buffer, old, new_) \
|
|
|
|
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
|
|
|
"#N is now #N", (old), (new_))
|
|
|
|
|
2015-06-28 16:16:19 +02:00
|
|
|
#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))
|
|
|
|
|
2015-06-29 21:39:40 +02:00
|
|
|
#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))
|
2021-09-05 02:51:05 +02:00
|
|
|
#define log_outcoming_orphan_action(s, target, text) \
|
|
|
|
log_server_status ((s), (s)->buffer, "MSG(#n): #a*#r #m", (target), \
|
|
|
|
ATTR_ACTION, (text))
|
2015-06-29 21:39:40 +02:00
|
|
|
|
2015-06-28 16:57:08 +02:00
|
|
|
#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)
|
|
|
|
|
2015-06-28 16:16:19 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2015-09-24 16:12:07 +02:00
|
|
|
static char *
|
|
|
|
buffer_get_log_path (struct buffer *buffer)
|
2015-07-04 15:24:08 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str path = str_make ();
|
2015-07-04 15:24:08 +02:00
|
|
|
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);
|
|
|
|
|
|
|
|
str_append_c (&path, '/');
|
|
|
|
make_log_filename (buffer->name, &path);
|
|
|
|
str_append (&path, ".log");
|
2015-09-24 16:12:07 +02:00
|
|
|
return str_steal (&path);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
|
|
|
|
{
|
|
|
|
if (!ctx->logging || buffer->log_file)
|
|
|
|
return;
|
2015-07-04 15:24:08 +02:00
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// TODO: should we try to reopen files wrt. case mapping?
|
2021-06-16 21:17:34 +02:00
|
|
|
// - Need to read the whole directory and look for matches:
|
|
|
|
// irc_server_strcmp(buffer->s, d_name, make_log_filename())
|
|
|
|
// remember to strip the ".log" suffix from d_name, case-sensitively.
|
|
|
|
// - The tolower_ascii() in make_log_filename() is a perfect overlap,
|
|
|
|
// it may stay as-is.
|
|
|
|
// - buffer_get_log_path() will need to return a FILE *,
|
|
|
|
// or an error that includes the below message.
|
2015-09-24 16:12:07 +02:00
|
|
|
char *path = buffer_get_log_path (buffer);
|
|
|
|
if (!(buffer->log_file = fopen (path, "ab")))
|
2015-12-25 21:22:59 +01:00
|
|
|
log_global_error (ctx, "Couldn't open log file `#s': #l",
|
2015-09-24 16:12:07 +02:00
|
|
|
path, strerror (errno));
|
2015-08-08 19:36:34 +02:00
|
|
|
else
|
|
|
|
set_cloexec (fileno (buffer->log_file));
|
2015-09-24 16:12:07 +02:00
|
|
|
free (path);
|
2015-07-04 15:24:08 +02:00
|
|
|
}
|
|
|
|
|
2015-07-05 17:02:11 +02:00
|
|
|
static void
|
|
|
|
buffer_close_log_file (struct buffer *buffer)
|
|
|
|
{
|
|
|
|
if (buffer->log_file)
|
|
|
|
(void) fclose (buffer->log_file);
|
|
|
|
buffer->log_file = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_logging_change (struct config_item *item)
|
2015-07-05 17:02:11 +02:00
|
|
|
{
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
static struct buffer *
|
|
|
|
buffer_by_name (struct app_context *ctx, const char *name)
|
|
|
|
{
|
|
|
|
return str_map_find (&ctx->buffers_by_name, name);
|
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
static void
|
2015-04-13 00:06:08 +02:00
|
|
|
buffer_add (struct app_context *ctx, struct buffer *buffer)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-04-13 00:06:08 +02:00
|
|
|
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);
|
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
buffer_open_log_file (ctx, buffer);
|
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// Normally this doesn't cause changes in the prompt but a prompt hook
|
|
|
|
// could decide to show some information for all buffers nonetheless
|
2015-04-16 00:56:05 +02:00
|
|
|
refresh_prompt (ctx);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_remove (struct app_context *ctx, struct buffer *buffer)
|
|
|
|
{
|
|
|
|
hard_assert (buffer != ctx->current_buffer);
|
2015-07-06 01:54:02 +02:00
|
|
|
hard_assert (buffer != ctx->global_buffer);
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (ctx->input, buffer_destroy, buffer->input_data);
|
2015-05-05 03:23:53 +02:00
|
|
|
buffer->input_data = NULL;
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-04-20 23:37:34 +02:00
|
|
|
// And make sure to unlink the buffer from "irc_buffer_map"
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server *s = buffer->server;
|
2015-04-20 23:37:34 +02:00
|
|
|
if (buffer->channel)
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL);
|
2015-04-20 23:37:34 +02:00
|
|
|
if (buffer->user)
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);
|
2015-04-20 23:37:34 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
if (buffer == ctx->last_buffer)
|
|
|
|
ctx->last_buffer = NULL;
|
2015-07-14 06:50:39 +02:00
|
|
|
if (buffer->type == BUFFER_SERVER)
|
|
|
|
buffer->server->buffer = NULL;
|
2015-05-16 12:33:59 +02:00
|
|
|
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
|
|
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
2015-11-18 23:49:09 +01:00
|
|
|
buffer_unref (buffer);
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-04-16 00:56:05 +02:00
|
|
|
refresh_prompt (ctx);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
2015-07-11 05:39:00 +02:00
|
|
|
static void
|
2016-04-10 21:37:36 +02:00
|
|
|
buffer_print_read_marker (struct app_context *ctx, FILE *stream, int flush_opts)
|
2015-07-11 05:39:00 +02:00
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2016-04-21 23:58:44 +02:00
|
|
|
const int timestamp_width = 8; // hardcoded to %T right now, simple
|
2022-08-26 03:43:32 +02:00
|
|
|
const char *marker_char =
|
|
|
|
get_config_string (ctx->config.root, "general.read_marker_char");
|
2016-04-21 23:58:44 +02:00
|
|
|
|
|
|
|
// We could turn this off on FLUSH_OPT_NOWRAP, however our default pager
|
|
|
|
// wraps lines for us even if we don't do it ourselves, and thus there's
|
|
|
|
// no need to worry about inconsistency.
|
|
|
|
if (*marker_char)
|
|
|
|
{
|
|
|
|
struct str s = str_make ();
|
|
|
|
for (int i = 0; i < timestamp_width; i++)
|
|
|
|
str_append (&s, marker_char);
|
|
|
|
formatter_add (&f, "#a#s#r", ATTR_TIMESTAMP, s.str);
|
|
|
|
str_reset (&s);
|
|
|
|
for (int i = timestamp_width; i < g_terminal.columns; i++)
|
|
|
|
str_append (&s, marker_char);
|
|
|
|
formatter_add (&f, "#a#s#r\n", ATTR_READ_MARKER, s.str);
|
|
|
|
str_free (&s);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
|
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
formatter_flush (&f, stream, flush_opts);
|
2015-08-10 00:09:43 +02:00
|
|
|
// Flush the trailing formatting reset item
|
2016-03-26 03:09:29 +01:00
|
|
|
fflush (stream);
|
2015-07-11 05:39:00 +02:00
|
|
|
formatter_free (&f);
|
|
|
|
}
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
static void
|
2015-05-08 05:03:36 +02:00
|
|
|
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2015-07-04 16:56:06 +02:00
|
|
|
// The prompt can take considerable time to redraw
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
2015-07-15 22:59:55 +02:00
|
|
|
|
2016-01-15 05:08:54 +01:00
|
|
|
// Simulate curses-like fullscreen buffers if the terminal allows it
|
2016-03-13 05:35:23 +01:00
|
|
|
if (g_terminal.initialized && clear_screen)
|
2016-01-15 04:58:51 +01:00
|
|
|
{
|
2016-01-15 05:08:54 +01:00
|
|
|
terminal_printer_fn printer = get_attribute_printer (stdout);
|
|
|
|
tputs (clear_screen, 1, printer);
|
2016-01-15 04:58:51 +01:00
|
|
|
if (cursor_to_ll)
|
|
|
|
tputs (cursor_to_ll, 1, printer);
|
|
|
|
else if (row_address)
|
|
|
|
tputs (tparm (row_address, g_terminal.lines - 1,
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
|
|
|
|
else if (cursor_address)
|
|
|
|
tputs (tparm (cursor_address, g_terminal.lines - 1,
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
|
2016-01-15 05:36:58 +01:00
|
|
|
fflush (stdout);
|
2016-04-10 22:13:53 +02:00
|
|
|
|
|
|
|
// We should update "last_displayed_msg_time" here just to be sure
|
|
|
|
// that the first date marker, if necessary, is shown, but in practice
|
|
|
|
// the value should always be from today when this function is called
|
2016-01-15 04:58:51 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
char *buffer_name_localized =
|
|
|
|
iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL);
|
|
|
|
print_status ("%s", buffer_name_localized);
|
|
|
|
free (buffer_name_localized);
|
|
|
|
}
|
2015-07-11 05:39:00 +02:00
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// That is, minus the readline prompt (taking at least one line)
|
2016-10-23 16:53:31 +02:00
|
|
|
int display_limit = MAX (10, g_terminal.lines - 1);
|
|
|
|
int to_display = 0;
|
|
|
|
|
|
|
|
struct buffer_line *line;
|
|
|
|
for (line = buffer->lines_tail; line; line = line->prev)
|
|
|
|
{
|
2015-07-11 05:39:00 +02:00
|
|
|
to_display++;
|
2016-10-23 16:53:31 +02:00
|
|
|
if (buffer_line_will_show_up (buffer, line))
|
|
|
|
display_limit--;
|
|
|
|
if (!line->prev || display_limit <= 0)
|
|
|
|
break;
|
|
|
|
}
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// Once we've found where we want to start with the backlog, print it
|
2016-03-26 03:09:29 +01:00
|
|
|
int until_marker = to_display - (int) buffer->new_messages_count;
|
2015-05-05 03:23:53 +02:00
|
|
|
for (; line; line = line->next)
|
2015-07-11 05:39:00 +02:00
|
|
|
{
|
2016-03-26 03:09:29 +01:00
|
|
|
if (until_marker-- == 0
|
|
|
|
&& buffer->new_messages_count != buffer->lines_count)
|
2016-04-10 21:37:36 +02:00
|
|
|
buffer_print_read_marker (ctx, stdout, 0);
|
2016-10-23 16:53:31 +02:00
|
|
|
buffer_line_display (ctx, buffer, line, 0);
|
2015-07-11 05:39:00 +02:00
|
|
|
}
|
|
|
|
|
2015-07-11 06:10:46 +02:00
|
|
|
// So that it is obvious if the last line in the buffer is not from today
|
2016-04-10 22:13:53 +02:00
|
|
|
buffer_update_time (ctx, time (NULL), stdout, 0);
|
2015-07-11 06:10:46 +02:00
|
|
|
|
2015-05-08 05:03:36 +02:00
|
|
|
refresh_prompt (ctx);
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-05-08 05:03:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_activate (struct app_context *ctx, struct buffer *buffer)
|
|
|
|
{
|
|
|
|
if (ctx->current_buffer == buffer)
|
|
|
|
return;
|
2018-01-08 21:53:42 +01:00
|
|
|
|
|
|
|
// This is the only place where the unread messages marker
|
|
|
|
// and highlight indicator are reset
|
2016-03-26 03:09:29 +01:00
|
|
|
if (ctx->current_buffer)
|
|
|
|
{
|
|
|
|
ctx->current_buffer->new_messages_count = 0;
|
|
|
|
ctx->current_buffer->new_unimportant_count = 0;
|
|
|
|
ctx->current_buffer->highlighted = false;
|
|
|
|
}
|
2015-05-08 05:03:36 +02:00
|
|
|
|
|
|
|
buffer_print_backlog (ctx, buffer);
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (ctx->input, buffer_switch, buffer->input_data);
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-04-18 03:25:10 +02:00
|
|
|
// Now at last we can switch the pointers
|
2015-04-27 22:51:40 +02:00
|
|
|
ctx->last_buffer = ctx->current_buffer;
|
2015-04-18 03:25:10 +02:00
|
|
|
ctx->current_buffer = buffer;
|
|
|
|
|
2015-04-16 00:56:05 +02:00
|
|
|
refresh_prompt (ctx);
|
2015-04-13 00:06:08 +02:00
|
|
|
}
|
|
|
|
|
2015-04-20 22:51:30 +02:00
|
|
|
static void
|
|
|
|
buffer_merge (struct app_context *ctx,
|
|
|
|
struct buffer *buffer, struct buffer *merged)
|
|
|
|
{
|
2015-06-21 21:50:47 +02:00
|
|
|
// XXX: anything better to do? This situation is arguably rare and I'm
|
|
|
|
// not entirely sure what action to take.
|
2015-06-28 02:49:28 +02:00
|
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS,
|
|
|
|
"Buffer #s was merged into this buffer", merged->name);
|
2015-06-21 21:50:47 +02:00
|
|
|
|
|
|
|
// 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++;
|
2015-06-22 21:29:42 +02:00
|
|
|
struct buffer_line *tail = merged->lines_tail;
|
2015-06-21 21:50:47 +02:00
|
|
|
|
2015-06-22 21:29:42 +02:00
|
|
|
// Cut them from the original buffer
|
|
|
|
if (start == merged->lines)
|
|
|
|
merged->lines = NULL;
|
|
|
|
else if (start->prev)
|
|
|
|
start->prev->next = NULL;
|
2018-01-08 21:45:53 +01:00
|
|
|
merged->lines_tail = start->prev;
|
2015-06-22 21:29:42 +02:00
|
|
|
merged->lines_count -= n;
|
|
|
|
|
|
|
|
// And append them to current lines in the buffer
|
2015-06-21 21:50:47 +02:00
|
|
|
buffer->lines_tail->next = start;
|
|
|
|
start->prev = buffer->lines_tail;
|
2015-06-22 21:29:42 +02:00
|
|
|
buffer->lines_tail = tail;
|
2015-06-21 21:50:47 +02:00
|
|
|
buffer->lines_count += n;
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE,
|
|
|
|
"End of merged content");
|
2015-04-20 22:51:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_rename (struct app_context *ctx,
|
|
|
|
struct buffer *buffer, const char *new_name)
|
|
|
|
{
|
2015-06-22 21:29:42 +02:00
|
|
|
struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name);
|
|
|
|
if (collision == buffer)
|
|
|
|
return;
|
2015-04-20 22:51:30 +02:00
|
|
|
|
2015-06-22 21:29:42 +02:00
|
|
|
hard_assert (!collision);
|
2015-04-20 22:51:30 +02:00
|
|
|
|
2015-06-22 21:29:42 +02:00
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
|
|
str_map_set (&ctx->buffers_by_name, new_name, buffer);
|
2015-04-20 22:51:30 +02:00
|
|
|
|
2020-03-23 00:41:08 +01:00
|
|
|
cstr_set (&buffer->name, xstrdup (new_name));
|
|
|
|
|
2015-07-05 17:02:11 +02:00
|
|
|
buffer_close_log_file (buffer);
|
2015-07-04 15:24:08 +02:00
|
|
|
buffer_open_log_file (ctx, buffer);
|
|
|
|
|
2015-06-22 21:29:42 +02:00
|
|
|
// We might have renamed the current buffer
|
|
|
|
refresh_prompt (ctx);
|
2015-04-20 22:51:30 +02:00
|
|
|
}
|
|
|
|
|
2015-06-21 03:35:35 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-04-17 21:21:30 +02:00
|
|
|
static struct buffer *
|
|
|
|
buffer_at_index (struct app_context *ctx, int n)
|
2015-04-16 21:16:10 +02:00
|
|
|
{
|
|
|
|
int i = 0;
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
|
|
if (++i == n)
|
2015-04-17 21:21:30 +02:00
|
|
|
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;
|
2015-04-16 21:16:10 +02:00
|
|
|
}
|
|
|
|
|
2016-03-10 00:07:59 +01:00
|
|
|
static int
|
|
|
|
buffer_count (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
int total = 0;
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
|
|
total++;
|
|
|
|
return total;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
buffer_move (struct app_context *ctx, struct buffer *buffer, int n)
|
|
|
|
{
|
|
|
|
hard_assert (n >= 1 && n <= buffer_count (ctx));
|
|
|
|
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
|
|
|
|
|
|
struct buffer *following = ctx->buffers;
|
|
|
|
while (--n && following)
|
|
|
|
following = following->next;
|
|
|
|
|
|
|
|
LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
|
|
|
|
refresh_prompt (ctx);
|
|
|
|
}
|
|
|
|
|
2015-04-17 20:45:54 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-07-14 06:11:14 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
static void
|
2015-07-05 01:11:20 +02:00
|
|
|
init_global_buffer (struct app_context *ctx)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2020-10-31 23:42:49 +01:00
|
|
|
struct buffer *global = ctx->global_buffer =
|
|
|
|
buffer_new (ctx->input, BUFFER_GLOBAL, xstrdup (PROGRAM_NAME));
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-15 19:49:25 +02:00
|
|
|
buffer_add (ctx, global);
|
2015-07-05 01:11:20 +02:00
|
|
|
buffer_activate (ctx, global);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-04-19 21:58:46 +02:00
|
|
|
// --- Users, channels ---------------------------------------------------------
|
|
|
|
|
2020-10-31 23:42:49 +01:00
|
|
|
static char *
|
|
|
|
irc_make_buffer_name (struct server *s, const char *target)
|
|
|
|
{
|
|
|
|
if (!target)
|
|
|
|
return xstrdup (s->name);
|
|
|
|
|
|
|
|
// XXX: this may be able to trigger the uniqueness assertion with non-UTF-8
|
|
|
|
char *target_utf8 = irc_to_utf8 (target);
|
|
|
|
char *result = xstrdup_printf ("%s.%s", s->name, target_utf8);
|
|
|
|
free (target_utf8);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-04-21 00:04:34 +02:00
|
|
|
static void
|
|
|
|
irc_user_on_destroy (void *object, void *user_data)
|
|
|
|
{
|
|
|
|
struct user *user = object;
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server *s = user_data;
|
2015-06-23 22:10:16 +02:00
|
|
|
if (!s->rehashing)
|
|
|
|
str_map_set (&s->irc_users, user->nickname, NULL);
|
2015-04-21 00:04:34 +02:00
|
|
|
}
|
|
|
|
|
2015-04-23 02:59:58 +02:00
|
|
|
static struct user *
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_make_user (struct server *s, char *nickname)
|
2015-04-21 00:04:34 +02:00
|
|
|
{
|
2015-04-30 00:02:14 +02:00
|
|
|
hard_assert (!str_map_find (&s->irc_users, nickname));
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2020-10-04 08:32:15 +02:00
|
|
|
struct user *user = user_new (nickname);
|
2015-11-18 23:03:21 +01:00
|
|
|
(void) user_weak_ref (user, irc_user_on_destroy, s);
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_set (&s->irc_users, user->nickname, user);
|
2015-04-21 00:04:34 +02:00
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
2015-05-15 05:43:59 +02:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2015-04-23 03:00:19 +02:00
|
|
|
static struct buffer *
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_get_or_make_user_buffer (struct server *s, const char *nickname)
|
2015-04-23 03:00:19 +02:00
|
|
|
{
|
2015-04-30 00:02:14 +02:00
|
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, nickname);
|
2015-04-23 03:00:19 +02:00
|
|
|
if (buffer)
|
|
|
|
return buffer;
|
|
|
|
|
2015-05-15 05:43:59 +02:00
|
|
|
struct user *user = irc_get_or_make_user (s, nickname);
|
2015-04-23 03:00:19 +02:00
|
|
|
|
|
|
|
// Open a new buffer for the user
|
2020-10-31 23:42:49 +01:00
|
|
|
buffer = buffer_new (s->ctx->input,
|
|
|
|
BUFFER_PM, irc_make_buffer_name (s, nickname));
|
2015-04-30 00:02:14 +02:00
|
|
|
buffer->server = s;
|
2015-04-24 23:30:48 +02:00
|
|
|
buffer->user = user;
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_set (&s->irc_buffer_map, user->nickname, buffer);
|
2015-05-15 19:49:25 +02:00
|
|
|
|
|
|
|
buffer_add (s->ctx, buffer);
|
2015-04-23 03:00:19 +02:00
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
|
2015-07-26 22:21:08 +02:00
|
|
|
static void
|
|
|
|
irc_get_channel_user_prefix (struct server *s,
|
|
|
|
struct channel_user *channel_user, struct str *output)
|
|
|
|
{
|
|
|
|
if (s->ctx->show_all_prefixes)
|
2020-10-04 08:22:20 +02:00
|
|
|
str_append (output, channel_user->prefixes);
|
|
|
|
else if (channel_user->prefixes[0])
|
|
|
|
str_append_c (output, channel_user->prefixes[0]);
|
2015-07-26 22:21:08 +02:00
|
|
|
}
|
|
|
|
|
2015-08-06 21:58:34 +02:00
|
|
|
static bool
|
|
|
|
irc_channel_is_joined (struct channel *channel)
|
|
|
|
{
|
|
|
|
// TODO: find a better way of checking if we're on a channel
|
2016-10-23 16:29:55 +02:00
|
|
|
return !!channel->users_len;
|
2015-08-06 21:58:34 +02:00
|
|
|
}
|
|
|
|
|
2015-06-06 00:27:29 +02:00
|
|
|
// Note that this eats the user reference
|
|
|
|
static void
|
|
|
|
irc_channel_link_user (struct channel *channel, struct user *user,
|
|
|
|
const char *prefixes)
|
|
|
|
{
|
2020-10-04 08:32:15 +02:00
|
|
|
struct user_channel *user_channel = user_channel_new (channel);
|
2015-06-06 00:27:29 +02:00
|
|
|
LIST_PREPEND (user->channels, user_channel);
|
|
|
|
|
2020-10-04 08:22:20 +02:00
|
|
|
struct channel_user *channel_user = channel_user_new (user, prefixes);
|
2015-06-06 00:27:29 +02:00
|
|
|
LIST_PREPEND (channel->users, channel_user);
|
2016-10-23 16:29:55 +02:00
|
|
|
channel->users_len++;
|
2015-06-06 00:27:29 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 00:36:02 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
// TODO: poll the away status for users we don't share a channel with.
|
|
|
|
// It might or might not be worth to auto-set this on with RPL_AWAY.
|
|
|
|
if (!user->channels && user != channel->s->irc_user)
|
|
|
|
user->away = false;
|
|
|
|
|
2015-04-25 00:36:02 +02:00
|
|
|
// Then just unlink the user from the channel
|
|
|
|
LIST_UNLINK (channel->users, channel_user);
|
|
|
|
channel_user_destroy (channel_user);
|
2016-10-23 16:29:55 +02:00
|
|
|
channel->users_len--;
|
2015-04-25 00:36:02 +02:00
|
|
|
}
|
|
|
|
|
2015-04-21 00:04:34 +02:00
|
|
|
static void
|
|
|
|
irc_channel_on_destroy (void *object, void *user_data)
|
|
|
|
{
|
|
|
|
struct channel *channel = object;
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server *s = user_data;
|
2015-04-25 00:36:02 +02:00
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
|
|
irc_channel_unlink_user (channel, iter);
|
2015-06-23 22:10:16 +02:00
|
|
|
if (!s->rehashing)
|
|
|
|
str_map_set (&s->irc_channels, channel->name, NULL);
|
2015-04-21 00:04:34 +02:00
|
|
|
}
|
|
|
|
|
2015-04-23 02:59:58 +02:00
|
|
|
static struct channel *
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_make_channel (struct server *s, char *name)
|
2015-04-21 00:04:34 +02:00
|
|
|
{
|
2015-04-30 00:02:14 +02:00
|
|
|
hard_assert (!str_map_find (&s->irc_channels, name));
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
struct channel *channel = channel_new (s, name);
|
2015-11-18 23:03:21 +01:00
|
|
|
(void) channel_weak_ref (channel, irc_channel_on_destroy, s);
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_set (&s->irc_channels, channel->name, channel);
|
2015-04-21 00:04:34 +02:00
|
|
|
return channel;
|
|
|
|
}
|
|
|
|
|
2015-06-05 01:17:20 +02:00
|
|
|
static struct channel_user *
|
|
|
|
irc_channel_get_user (struct channel *channel, struct user *user)
|
2015-04-21 21:36:16 +02:00
|
|
|
{
|
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
|
|
if (iter->user == user)
|
2015-06-05 01:17:20 +02:00
|
|
|
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);
|
2015-04-21 21:36:16 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 23:50:44 +02:00
|
|
|
static void
|
|
|
|
irc_left_channel (struct channel *channel)
|
|
|
|
{
|
2020-10-16 16:45:40 +02:00
|
|
|
strv_reset (&channel->names_buf);
|
|
|
|
channel->show_names_after_who = false;
|
|
|
|
|
2015-05-09 23:50:44 +02:00
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
|
|
irc_channel_unlink_user (channel, iter);
|
|
|
|
}
|
|
|
|
|
2015-06-23 21:04:38 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-06-23 23:13:15 +02:00
|
|
|
static void
|
|
|
|
remove_conflicting_buffer (struct server *s, struct buffer *buffer)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"Removed buffer #s because of casemapping conflict", buffer->name);
|
2015-06-23 23:13:15 +02:00
|
|
|
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
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_try_readd_channel (struct channel *channel, struct buffer *buffer)
|
2015-06-23 23:13:15 +02:00
|
|
|
{
|
2020-10-16 20:19:52 +02:00
|
|
|
struct server *s = channel->s;
|
2015-06-23 23:13:15 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-23 21:04:38 +02:00
|
|
|
static void
|
|
|
|
irc_rehash_and_fix_conflicts (struct server *s)
|
|
|
|
{
|
2015-06-23 22:10:16 +02:00
|
|
|
// 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;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
s->irc_users = str_map_make (NULL);
|
|
|
|
s->irc_channels = str_map_make (NULL);
|
|
|
|
s->irc_buffer_map = str_map_make (NULL);
|
2015-06-23 22:10:16 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2015-06-23 23:13:15 +02:00
|
|
|
// 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));
|
|
|
|
|
2015-06-23 22:10:16 +02:00
|
|
|
struct str_map_iter iter;
|
|
|
|
struct user *user;
|
2015-06-23 23:13:15 +02:00
|
|
|
struct channel *channel;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
iter = str_map_iter_make (&old_users);
|
2015-06-23 22:10:16 +02:00
|
|
|
while ((user = str_map_iter_next (&iter)))
|
2015-06-23 23:13:15 +02:00
|
|
|
irc_try_readd_user (s, user,
|
|
|
|
str_map_find (&old_buffer_map, user->nickname));
|
2015-06-23 22:10:16 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
iter = str_map_iter_make (&old_channels);
|
2015-06-23 22:10:16 +02:00
|
|
|
while ((channel = str_map_iter_next (&iter)))
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_try_readd_channel (channel,
|
2015-06-23 23:13:15 +02:00
|
|
|
str_map_find (&old_buffer_map, channel->name));
|
2015-06-23 22:10:16 +02:00
|
|
|
|
|
|
|
// 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);
|
2015-06-23 21:04:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// --- Core functionality ------------------------------------------------------
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
static bool
|
|
|
|
irc_is_connected (struct server *s)
|
|
|
|
{
|
2015-05-09 23:15:24 +02:00
|
|
|
return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
|
2015-05-08 17:39:26 +02:00
|
|
|
}
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_cancel_timers (struct server *s)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
poller_timer_reset (&s->timeout_tmr);
|
|
|
|
poller_timer_reset (&s->ping_tmr);
|
|
|
|
poller_timer_reset (&s->reconnect_tmr);
|
2015-07-18 14:12:34 +02:00
|
|
|
poller_timer_reset (&s->autojoin_tmr);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_reset_connection_timeouts (struct server *s)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
|
|
|
|
poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
|
2015-07-18 14:12:34 +02:00
|
|
|
poller_timer_reset (&s->reconnect_tmr);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-07-18 13:39:30 +02:00
|
|
|
static int64_t
|
|
|
|
irc_get_reconnect_delay (struct server *s)
|
2015-04-25 14:53:29 +02:00
|
|
|
{
|
2015-05-16 12:45:39 +02:00
|
|
|
int64_t delay = get_config_integer (s->config, "reconnect_delay");
|
2022-08-26 03:43:32 +02:00
|
|
|
int64_t delay_factor = get_config_integer
|
|
|
|
(s->ctx->config.root, "general.reconnect_delay_growing");
|
2015-07-17 21:18:05 +02:00
|
|
|
for (unsigned i = 0; i < s->reconnect_attempt; i++)
|
2015-10-28 03:46:41 +01:00
|
|
|
{
|
|
|
|
if (delay_factor && delay > INT64_MAX / delay_factor)
|
|
|
|
break;
|
2015-07-17 21:18:05 +02:00
|
|
|
delay *= delay_factor;
|
2015-10-28 03:46:41 +01:00
|
|
|
}
|
2015-07-17 21:18:05 +02:00
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
int64_t delay_max =
|
|
|
|
get_config_integer (s->ctx->config.root, "general.reconnect_delay_max");
|
2015-10-28 03:46:41 +01:00
|
|
|
return MIN (delay, delay_max);
|
2015-07-18 13:39:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2015-07-17 21:18:05 +02:00
|
|
|
|
2015-06-02 22:34:03 +02:00
|
|
|
// XXX: maybe add a state for when a connect is queued?
|
2015-05-16 12:45:39 +02:00
|
|
|
hard_assert (s->state == IRC_DISCONNECTED);
|
2015-07-18 13:39:30 +02:00
|
|
|
|
|
|
|
int64_t delay = irc_get_reconnect_delay (s);
|
|
|
|
s->reconnect_attempt++;
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"Trying to reconnect in #&s seconds...",
|
|
|
|
xstrdup_printf ("%" PRId64, delay));
|
2015-05-16 12:45:39 +02:00
|
|
|
poller_timer_set (&s->reconnect_tmr, delay * 1000);
|
2015-04-25 14:53:29 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-02-09 05:10:41 +01:00
|
|
|
static void irc_process_sent_message
|
|
|
|
(const struct irc_message *msg, struct server *s);
|
2015-05-09 22:10:58 +02:00
|
|
|
static void irc_send (struct server *s,
|
|
|
|
const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_send (struct server *s, const char *format, ...)
|
|
|
|
{
|
|
|
|
if (!soft_assert (irc_is_connected (s)))
|
|
|
|
{
|
2015-07-09 23:09:10 +02:00
|
|
|
log_server_debug (s, "sending a message to a dead server connection");
|
2015-05-09 22:10:58 +02:00
|
|
|
return;
|
|
|
|
}
|
2015-07-03 23:30:54 +02:00
|
|
|
|
|
|
|
if (s->state == IRC_CLOSING
|
|
|
|
|| s->state == IRC_HALF_CLOSED)
|
2015-07-03 20:32:31 +02:00
|
|
|
return;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
va_list ap;
|
|
|
|
va_start (ap, format);
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str str = str_make ();
|
2015-05-09 22:10:58 +02:00
|
|
|
str_append_vprintf (&str, format, ap);
|
|
|
|
va_end (ap);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-09 23:09:10 +02:00
|
|
|
log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-02-09 05:10:41 +01:00
|
|
|
struct irc_message msg;
|
|
|
|
irc_parse_message (&msg, str.str);
|
|
|
|
irc_process_sent_message (&msg, s);
|
|
|
|
irc_free_message (&msg);
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
str_append_str (&s->write_buffer, &str);
|
2015-05-09 22:10:58 +02:00
|
|
|
str_free (&str);
|
2015-07-03 20:32:31 +02:00
|
|
|
str_append (&s->write_buffer, "\r\n");
|
|
|
|
irc_update_poller (s, NULL);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
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)
|
2018-08-03 22:49:51 +02:00
|
|
|
// XXX: we get ENOTCONN with OpenSSL (not plain) when a localhost
|
|
|
|
// server is aborted, why? strace says read 0, write 31, shutdown -1.
|
2015-07-03 20:32:31 +02:00
|
|
|
if (!soft_assert (errno == EINTR))
|
|
|
|
break;
|
|
|
|
|
|
|
|
s->state = IRC_HALF_CLOSED;
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_shutdown (struct server *s)
|
2015-04-15 16:18:43 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
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;
|
2015-06-02 22:34:03 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
// 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);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-15 16:18:43 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_destroy_connector (struct server *s)
|
|
|
|
{
|
2015-07-20 23:31:26 +02:00
|
|
|
if (s->connector)
|
|
|
|
connector_free (s->connector);
|
2015-05-09 22:10:58 +02:00
|
|
|
free (s->connector);
|
|
|
|
s->connector = NULL;
|
2015-04-15 16:18:43 +02:00
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
if (s->socks_conn)
|
|
|
|
socks_connector_free (s->socks_conn);
|
|
|
|
free (s->socks_conn);
|
|
|
|
s->socks_conn = NULL;
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// Not connecting anymore
|
|
|
|
s->state = IRC_DISCONNECTED;
|
|
|
|
}
|
2015-04-15 16:18:43 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
try_finish_quit (struct app_context *ctx)
|
|
|
|
{
|
2015-05-16 12:33:59 +02:00
|
|
|
if (!ctx->quitting)
|
|
|
|
return;
|
|
|
|
|
|
|
|
bool disconnected_all = true;
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
2015-05-16 12:33:59 +02:00
|
|
|
struct server *s;
|
|
|
|
while ((s = str_map_iter_next (&iter)))
|
|
|
|
if (irc_is_connected (s))
|
|
|
|
disconnected_all = false;
|
|
|
|
|
|
|
|
if (disconnected_all)
|
2015-05-09 22:10:58 +02:00
|
|
|
ctx->polling = false;
|
2015-04-15 16:18:43 +02:00
|
|
|
}
|
|
|
|
|
2015-04-17 21:40:08 +02:00
|
|
|
static void
|
2015-07-11 04:54:07 +02:00
|
|
|
irc_destroy_transport (struct server *s)
|
2015-04-17 21:40:08 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
if (s->transport
|
|
|
|
&& s->transport->cleanup)
|
|
|
|
s->transport->cleanup (s);
|
2015-07-03 23:30:54 +02:00
|
|
|
s->transport = NULL;
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2017-05-06 21:35:44 +02:00
|
|
|
poller_fd_reset (&s->socket_event);
|
2015-05-09 22:10:58 +02:00
|
|
|
xclose (s->socket);
|
|
|
|
s->socket = -1;
|
|
|
|
s->state = IRC_DISCONNECTED;
|
|
|
|
|
2015-07-04 16:37:31 +02:00
|
|
|
str_reset (&s->read_buffer);
|
|
|
|
str_reset (&s->write_buffer);
|
2015-07-11 04:54:07 +02:00
|
|
|
}
|
2015-07-04 16:37:31 +02:00
|
|
|
|
2015-07-11 04:54:07 +02:00
|
|
|
static void
|
|
|
|
irc_destroy_state (struct server *s)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
|
2015-05-09 23:50:44 +02:00
|
|
|
struct channel *channel;
|
|
|
|
while ((channel = str_map_iter_next (&iter)))
|
|
|
|
irc_left_channel (channel);
|
|
|
|
|
2015-05-15 05:54:03 +02:00
|
|
|
if (s->irc_user)
|
|
|
|
{
|
|
|
|
user_unref (s->irc_user);
|
|
|
|
s->irc_user = NULL;
|
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-06-07 04:20:39 +02:00
|
|
|
str_reset (&s->irc_user_mode);
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_user_host, NULL);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
strv_reset (&s->cap_ls_buf);
|
2021-05-28 02:19:31 +02:00
|
|
|
s->cap_away_notify = false;
|
2015-06-20 21:38:04 +02:00
|
|
|
s->cap_echo_message = false;
|
2021-05-28 02:11:23 +02:00
|
|
|
s->cap_sasl = false;
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-06-23 21:04:38 +02:00
|
|
|
// Need to call this before server_init_specifics()
|
|
|
|
irc_set_casemapping (s, irc_tolower, irc_strxfrm);
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
server_free_specifics (s);
|
|
|
|
server_init_specifics (s);
|
2015-07-11 04:54:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_disconnect (struct server *s)
|
|
|
|
{
|
|
|
|
hard_assert (irc_is_connected (s));
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
|
2015-07-11 04:54:07 +02:00
|
|
|
struct buffer *buffer;
|
|
|
|
while ((buffer = str_map_iter_next (&iter)))
|
2015-08-08 21:17:32 +02:00
|
|
|
log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT,
|
|
|
|
"Disconnected from server");
|
2015-07-11 04:54:07 +02:00
|
|
|
|
|
|
|
irc_cancel_timers (s);
|
|
|
|
irc_destroy_transport (s);
|
|
|
|
irc_destroy_state (s);
|
2015-06-20 22:42:38 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
// Take any relevant actions
|
2015-05-09 22:10:58 +02:00
|
|
|
if (s->ctx->quitting)
|
|
|
|
try_finish_quit (s->ctx);
|
2015-05-09 23:30:04 +02:00
|
|
|
else if (s->manual_disconnect)
|
|
|
|
s->manual_disconnect = false;
|
2015-05-05 08:46:59 +02:00
|
|
|
else
|
2015-07-17 21:18:05 +02:00
|
|
|
{
|
|
|
|
s->reconnect_attempt = 0;
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_queue_reconnect (s);
|
2015-07-17 21:18:05 +02:00
|
|
|
}
|
2015-05-12 07:02:14 +02:00
|
|
|
|
|
|
|
refresh_prompt (s->ctx);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 23:30:04 +02:00
|
|
|
static void
|
|
|
|
irc_initiate_disconnect (struct server *s, const char *reason)
|
|
|
|
{
|
|
|
|
hard_assert (irc_is_connected (s));
|
|
|
|
|
2017-04-20 20:25:21 +02:00
|
|
|
// It can take a very long time for sending QUIT to take effect
|
|
|
|
if (s->manual_disconnect)
|
|
|
|
{
|
2018-06-21 22:02:26 +02:00
|
|
|
log_server_error (s, s->buffer, "#s: #s", "Disconnected from server",
|
2017-04-20 20:25:21 +02:00
|
|
|
"connection torn down early per user request");
|
|
|
|
irc_disconnect (s);
|
2017-04-20 20:47:39 +02:00
|
|
|
return;
|
2017-04-20 20:25:21 +02:00
|
|
|
}
|
2017-04-20 20:47:39 +02:00
|
|
|
|
|
|
|
if (reason)
|
2015-05-09 23:30:04 +02:00
|
|
|
irc_send (s, "QUIT :%s", reason);
|
|
|
|
else
|
2017-04-20 20:47:39 +02:00
|
|
|
// TODO: make the default QUIT message customizable
|
|
|
|
// -> global/per server/both?
|
2020-10-16 23:28:54 +02:00
|
|
|
// -> implement it with an output hook in a plugin?
|
2015-05-09 23:30:04 +02:00
|
|
|
irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
|
2017-04-20 20:25:21 +02:00
|
|
|
|
|
|
|
s->manual_disconnect = true;
|
2017-04-20 20:47:39 +02:00
|
|
|
irc_shutdown (s);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2020-11-01 15:32:39 +01:00
|
|
|
request_quit (struct app_context *ctx, const char *message)
|
2017-04-20 20:47:39 +02:00
|
|
|
{
|
2020-11-01 15:32:39 +01:00
|
|
|
if (!ctx->quitting)
|
|
|
|
{
|
|
|
|
log_global_status (ctx, "Shutting down");
|
|
|
|
ctx->quitting = true;
|
2017-04-20 20:47:39 +02:00
|
|
|
|
2020-11-01 15:32:39 +01:00
|
|
|
// Disable the user interface
|
|
|
|
CALL (ctx->input, hide);
|
|
|
|
}
|
2017-04-20 20:47:39 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
2017-04-20 20:47:39 +02:00
|
|
|
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_initiate_disconnect (s, message);
|
|
|
|
else if (s->state == IRC_CONNECTING)
|
|
|
|
irc_destroy_connector (s);
|
|
|
|
}
|
|
|
|
|
|
|
|
try_finish_quit (ctx);
|
2015-05-09 23:30:04 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
on_irc_ping_timeout (void *user_data)
|
2015-05-09 06:11:36 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
struct server *s = user_data;
|
2017-04-20 20:25:55 +02:00
|
|
|
log_server_error (s, s->buffer,
|
2018-06-21 22:02:26 +02:00
|
|
|
"#s: #s", "Disconnected from server", "timeout");
|
2015-07-03 23:30:54 +02:00
|
|
|
irc_disconnect (s);
|
2015-05-09 06:11:36 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
on_irc_timeout (void *user_data)
|
2015-05-09 06:11:36 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
// Provoke a response from the server
|
|
|
|
struct server *s = user_data;
|
2015-05-15 05:45:07 +02:00
|
|
|
irc_send (s, "PING :%" PRIi64, (int64_t) time (NULL));
|
2015-05-09 06:11:36 +02:00
|
|
|
}
|
|
|
|
|
2015-07-18 14:12:34 +02:00
|
|
|
static void
|
|
|
|
on_irc_autojoin_timeout (void *user_data)
|
|
|
|
{
|
|
|
|
struct server *s = user_data;
|
|
|
|
|
2015-08-10 07:32:03 +02:00
|
|
|
// Since we may not have information from RPL_ISUPPORT yet,
|
|
|
|
// it's our safest bet to send the channels one at a time
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map joins_sent = str_map_make (NULL);
|
2015-08-10 07:32:03 +02:00
|
|
|
|
|
|
|
// We don't know the casemapping yet either, however ASCII should do
|
|
|
|
joins_sent.key_xfrm = tolower_ascii_strxfrm;
|
|
|
|
|
|
|
|
// First join autojoin channels in their given order
|
2015-07-18 14:12:34 +02:00
|
|
|
const char *autojoin = get_config_string (s->config, "autojoin");
|
|
|
|
if (autojoin)
|
2015-08-10 07:32:03 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (autojoin, ",", true, &v);
|
2015-08-10 07:32:03 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
|
|
|
{
|
2016-12-30 07:49:10 +01:00
|
|
|
irc_send (s, "JOIN %s", v.vector[i]);
|
2015-08-10 07:32:03 +02:00
|
|
|
str_map_set (&joins_sent, v.vector[i], (void *) 1);
|
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-08-10 07:32:03 +02:00
|
|
|
}
|
2015-07-18 14:12:34 +02:00
|
|
|
|
2015-08-10 07:32:03 +02:00
|
|
|
// Then also rejoin any channels from the last disconnect
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
|
2015-07-18 14:12:34 +02:00
|
|
|
struct channel *channel;
|
|
|
|
while ((channel = str_map_iter_next (&iter)))
|
2016-12-30 07:49:10 +01:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str target = str_make ();
|
2016-12-30 07:49:10 +01:00
|
|
|
str_append (&target, channel->name);
|
|
|
|
|
|
|
|
const char *key;
|
|
|
|
if ((key = str_map_find (&channel->param_modes, "k")))
|
|
|
|
str_append_printf (&target, " %s", key);
|
|
|
|
|
|
|
|
// When a channel is both autojoined and rejoined, both keys are tried
|
2015-08-10 07:32:03 +02:00
|
|
|
if (!channel->left_manually
|
2016-12-30 07:49:10 +01:00
|
|
|
&& !str_map_find (&joins_sent, target.str))
|
|
|
|
irc_send (s, "JOIN %s", target.str);
|
|
|
|
str_free (&target);
|
|
|
|
}
|
2015-08-10 07:32:03 +02:00
|
|
|
|
|
|
|
str_map_free (&joins_sent);
|
2015-07-18 14:12:34 +02:00
|
|
|
}
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
// --- Server I/O --------------------------------------------------------------
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-11-19 19:09:05 +01:00
|
|
|
static char *
|
|
|
|
irc_process_hooks (struct server *s, char *input)
|
|
|
|
{
|
|
|
|
log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, input);
|
2015-11-22 02:52:07 +01:00
|
|
|
uint64_t hash = siphash_wrapper (input, strlen (input));
|
2015-11-21 18:40:07 +01:00
|
|
|
LIST_FOR_EACH (struct hook, iter, s->ctx->irc_hooks)
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
struct irc_hook *hook = (struct irc_hook *) iter;
|
2016-10-29 19:35:48 +02:00
|
|
|
if (!(input = hook->filter (hook, s, input)))
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
|
|
|
log_server_debug (s, "#a>= #s#r", ATTR_JOIN, "thrown away by hook");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// The old input may get freed, so we compare against a hash of it
|
2015-11-22 02:52:07 +01:00
|
|
|
uint64_t new_hash = siphash_wrapper (input, strlen (input));
|
|
|
|
if (new_hash != hash)
|
|
|
|
log_server_debug (s, "#a>= \"#S\"#r", ATTR_JOIN, input);
|
|
|
|
hash = new_hash;
|
2015-11-19 19:09:05 +01:00
|
|
|
}
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void irc_process_message
|
2015-11-19 19:09:05 +01:00
|
|
|
(const struct irc_message *msg, struct server *s);
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_process_buffer_custom (struct server *s, struct str *buf)
|
|
|
|
{
|
|
|
|
const char *start = buf->str, *end = start + buf->len;
|
|
|
|
for (const char *p = start; p + 1 < end; p++)
|
|
|
|
{
|
|
|
|
// Split the input on newlines
|
|
|
|
if (p[0] != '\r' || p[1] != '\n')
|
|
|
|
continue;
|
|
|
|
|
|
|
|
char *processed = irc_process_hooks (s, xstrndup (start, p - start));
|
|
|
|
start = p + 2;
|
|
|
|
if (!processed)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
struct irc_message msg;
|
|
|
|
irc_parse_message (&msg, processed);
|
|
|
|
irc_process_message (&msg, s);
|
|
|
|
irc_free_message (&msg);
|
|
|
|
|
|
|
|
free (processed);
|
|
|
|
}
|
|
|
|
str_remove_slice (buf, 0, start - buf->str);
|
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
irc_try_read (struct server *s)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result result = s->transport->try_read (s);
|
2015-08-12 23:20:46 +02:00
|
|
|
if (s->read_buffer.len >= (1 << 20))
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// XXX: this is stupid; if anything, count it in dependence of time;
|
|
|
|
// we could make transport_tls_try_read() limit the immediate amount
|
|
|
|
// of data read like socket_io_try_read() does and remove this check
|
2015-08-12 23:20:46 +02:00
|
|
|
log_server_error (s, s->buffer,
|
|
|
|
"The IRC server seems to spew out data frantically");
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_ERROR;
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
2015-08-12 23:20:46 +02:00
|
|
|
if (s->read_buffer.len)
|
2015-11-19 19:09:05 +01:00
|
|
|
irc_process_buffer_custom (s, &s->read_buffer);
|
2015-07-03 23:30:54 +02:00
|
|
|
return result;
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
irc_try_write (struct server *s)
|
|
|
|
{
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result result = s->transport->try_write (s);
|
|
|
|
if (result == SOCKET_IO_OK)
|
2015-07-03 23:30:54 +02:00
|
|
|
{
|
|
|
|
// 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;
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2015-07-03 23:30:54 +02:00
|
|
|
static bool
|
|
|
|
irc_try_read_write (struct server *s)
|
|
|
|
{
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result read_result;
|
|
|
|
enum socket_io_result write_result;
|
|
|
|
if ((read_result = irc_try_read (s)) == SOCKET_IO_ERROR
|
|
|
|
|| (write_result = irc_try_write (s)) == SOCKET_IO_ERROR)
|
2015-07-03 23:30:54 +02:00
|
|
|
{
|
|
|
|
log_server_error (s, s->buffer, "Server connection failed");
|
|
|
|
return false;
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2015-07-03 23:30:54 +02:00
|
|
|
// FIXME: this may probably fire multiple times when we're flushing,
|
|
|
|
// we should probably store a flag next to the state
|
2016-01-06 23:37:30 +01:00
|
|
|
if (read_result == SOCKET_IO_EOF
|
|
|
|
|| write_result == SOCKET_IO_EOF)
|
2015-07-03 23:30:54 +02:00
|
|
|
log_server_error (s, s->buffer, "Server closed the connection");
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2015-07-03 23:30:54 +02:00
|
|
|
// If the write needs to read and we receive an EOF, we can't flush
|
2016-01-06 23:37:30 +01:00
|
|
|
if (write_result == SOCKET_IO_EOF)
|
2015-07-03 23:30:54 +02:00
|
|
|
return false;
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
if (read_result == SOCKET_IO_EOF)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
2015-07-03 23:30:54 +02:00
|
|
|
// Eventually initiate shutdown to flush the write buffer
|
2015-07-03 20:32:31 +02:00
|
|
|
irc_shutdown (s);
|
|
|
|
|
2015-07-03 23:30:54 +02:00
|
|
|
// If there's nothing to write, we can disconnect now
|
2015-07-03 20:32:31 +02:00
|
|
|
if (s->state == IRC_HALF_CLOSED)
|
2015-07-03 23:30:54 +02:00
|
|
|
return false;
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
2015-07-03 23:30:54 +02:00
|
|
|
return true;
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2015-07-03 23:30:54 +02:00
|
|
|
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);
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- Plain transport ---------------------------------------------------------
|
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
transport_plain_try_read (struct server *s)
|
2015-05-09 06:11:36 +02:00
|
|
|
{
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result result =
|
2016-10-11 10:52:49 +02:00
|
|
|
socket_io_try_read (s->socket, &s->read_buffer);
|
|
|
|
if (result == SOCKET_IO_ERROR)
|
|
|
|
print_debug ("%s: %s", __func__, strerror (errno));
|
2016-01-06 23:37:30 +01:00
|
|
|
return result;
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
transport_plain_try_write (struct server *s)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result result =
|
2016-10-11 10:52:49 +02:00
|
|
|
socket_io_try_write (s->socket, &s->write_buffer);
|
|
|
|
if (result == SOCKET_IO_ERROR)
|
|
|
|
print_debug ("%s: %s", __func__, strerror (errno));
|
2016-01-06 23:37:30 +01:00
|
|
|
return result;
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 =
|
|
|
|
{
|
2015-07-03 23:30:54 +02:00
|
|
|
.try_read = transport_plain_try_read,
|
|
|
|
.try_write = transport_plain_try_write,
|
2015-07-03 20:32:31 +02:00
|
|
|
.get_poll_events = transport_plain_get_poll_events,
|
|
|
|
};
|
|
|
|
|
2015-07-15 23:34:36 +02:00
|
|
|
// --- TLS transport -----------------------------------------------------------
|
2015-07-03 20:32:31 +02:00
|
|
|
|
|
|
|
struct transport_tls_data
|
|
|
|
{
|
|
|
|
SSL_CTX *ssl_ctx; ///< SSL context
|
2015-07-15 23:34:36 +02:00
|
|
|
SSL *ssl; ///< SSL connection
|
2015-07-03 20:32:31 +02:00
|
|
|
bool ssl_rx_want_tx; ///< SSL_read() wants to write
|
|
|
|
bool ssl_tx_want_rx; ///< SSL_write() wants to read
|
2015-05-09 22:10:58 +02:00
|
|
|
};
|
2015-05-09 06:11:36 +02:00
|
|
|
|
2015-07-12 01:58:38 +02:00
|
|
|
/// 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);
|
|
|
|
|
2015-07-12 06:03:28 +02:00
|
|
|
if (!preverify_ok)
|
|
|
|
{
|
|
|
|
log_server_error (s, s->buffer,
|
|
|
|
"Certificate verification failed: #s",
|
|
|
|
X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx)));
|
|
|
|
}
|
|
|
|
|
2015-07-12 01:58:38 +02:00
|
|
|
free (subject);
|
|
|
|
free (issuer);
|
|
|
|
return preverify_ok;
|
|
|
|
}
|
|
|
|
|
2016-02-19 23:46:44 +01:00
|
|
|
static bool
|
|
|
|
transport_tls_init_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
ERR_clear_error ();
|
|
|
|
|
|
|
|
if (file || path)
|
|
|
|
{
|
|
|
|
if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
|
|
|
|
return true;
|
|
|
|
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "%s: %s",
|
|
|
|
"Failed to set locations for the CA certificate bundle",
|
2020-10-20 01:55:46 +02:00
|
|
|
xerr_describe_error ());
|
2016-02-19 23:46:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "%s: %s",
|
|
|
|
"Couldn't load the default CA certificate bundle",
|
2020-10-20 01:55:46 +02:00
|
|
|
xerr_describe_error ());
|
2016-02-19 23:46:44 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
transport_tls_init_ca (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
|
|
|
|
{
|
|
|
|
const char *ca_file = get_config_string (s->config, "tls_ca_file");
|
|
|
|
const char *ca_path = get_config_string (s->config, "tls_ca_path");
|
|
|
|
|
|
|
|
char *full_ca_file = ca_file
|
|
|
|
? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
|
|
|
|
char *full_ca_path = ca_path
|
|
|
|
? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
|
|
|
|
|
|
|
|
bool ok = false;
|
|
|
|
if (ca_file && !full_ca_file)
|
|
|
|
error_set (e, "Couldn't find the CA bundle file");
|
|
|
|
else if (ca_path && !full_ca_path)
|
|
|
|
error_set (e, "Couldn't find the CA bundle path");
|
|
|
|
else
|
|
|
|
ok = transport_tls_init_ca_set (ssl_ctx, full_ca_file, full_ca_path, e);
|
|
|
|
|
|
|
|
free (full_ca_file);
|
|
|
|
free (full_ca_path);
|
|
|
|
return ok;
|
|
|
|
}
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
static bool
|
|
|
|
transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
|
2015-05-09 06:11:36 +02:00
|
|
|
{
|
2015-07-28 20:31:42 +02:00
|
|
|
bool verify = get_config_boolean (s->config, "tls_verify");
|
2015-07-12 01:58:38 +02:00
|
|
|
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);
|
2015-05-09 06:11:36 +02:00
|
|
|
|
2015-07-28 20:31:42 +02:00
|
|
|
const char *ciphers = get_config_string (s->config, "tls_ciphers");
|
2015-07-12 17:15:33 +02:00
|
|
|
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");
|
2015-07-12 00:43:52 +02:00
|
|
|
SSL_CTX_set_mode (ssl_ctx,
|
|
|
|
SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
|
|
|
|
|
2015-07-12 22:10:13 +02:00
|
|
|
// Disable deprecated protocols (see RFC 7568)
|
|
|
|
SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
|
|
|
|
|
2016-01-18 00:44:45 +01:00
|
|
|
// This seems to consume considerable amounts of memory while not giving
|
|
|
|
// that much in return; in addition to that, I'm not sure about security
|
|
|
|
// (see RFC 7525, section 3.3)
|
|
|
|
#ifdef SSL_OP_NO_COMPRESSION
|
|
|
|
SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_COMPRESSION);
|
|
|
|
#endif // SSL_OP_NO_COMPRESSION
|
2020-10-20 02:02:09 +02:00
|
|
|
#ifdef LOMEM
|
|
|
|
SSL_CTX_set_mode (ssl_ctx, SSL_MODE_RELEASE_BUFFERS);
|
|
|
|
#endif // LOMEM
|
2016-01-18 00:44:45 +01:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
struct error *error = NULL;
|
2016-02-19 23:46:44 +01:00
|
|
|
if (!transport_tls_init_ca (s, ssl_ctx, &error))
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
2016-02-19 23:46:44 +01:00
|
|
|
if (verify)
|
|
|
|
{
|
|
|
|
error_propagate (e, error);
|
|
|
|
return false;
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// Just inform the user if we're not actually verifying
|
2016-02-19 23:46:44 +01:00
|
|
|
log_server_error (s, s->buffer, "#s", error->message);
|
|
|
|
error_free (error);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
return true;
|
2015-05-09 06:11:36 +02:00
|
|
|
}
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
static bool
|
|
|
|
transport_tls_init_cert (struct server *s, SSL *ssl, struct error **e)
|
2015-04-27 21:52:16 +02:00
|
|
|
{
|
2015-07-28 20:31:42 +02:00
|
|
|
const char *tls_cert = get_config_string (s->config, "tls_cert");
|
|
|
|
if (!tls_cert)
|
2015-07-03 20:32:31 +02:00
|
|
|
return true;
|
|
|
|
|
2015-07-15 23:34:36 +02:00
|
|
|
ERR_clear_error ();
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
bool result = false;
|
2015-07-28 20:31:42 +02:00
|
|
|
char *path = resolve_filename (tls_cert, resolve_relative_config_filename);
|
2015-07-03 20:32:31 +02:00
|
|
|
if (!path)
|
2015-07-28 20:31:42 +02:00
|
|
|
error_set (e, "%s: %s", "Cannot open file", tls_cert);
|
2015-07-03 20:32:31 +02:00
|
|
|
// 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))
|
2015-07-28 20:31:42 +02:00
|
|
|
error_set (e, "%s: %s", "Setting the TLS client certificate failed",
|
2020-10-20 01:55:46 +02:00
|
|
|
xerr_describe_error ());
|
2015-07-03 20:32:31 +02:00
|
|
|
else
|
|
|
|
result = true;
|
|
|
|
free (path);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-12-09 00:53:56 +01:00
|
|
|
transport_tls_init (struct server *s, const char *hostname, struct error **e)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
2015-07-15 23:34:36 +02:00
|
|
|
ERR_clear_error ();
|
|
|
|
|
|
|
|
struct error *error = NULL;
|
2015-07-03 20:32:31 +02:00
|
|
|
SSL_CTX *ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
|
|
|
|
if (!ssl_ctx)
|
|
|
|
goto error_ssl_1;
|
2015-07-15 23:34:36 +02:00
|
|
|
if (!transport_tls_init_ctx (s, ssl_ctx, &error))
|
2015-07-03 20:32:31 +02:00
|
|
|
goto error_ssl_2;
|
|
|
|
|
|
|
|
SSL *ssl = SSL_new (ssl_ctx);
|
|
|
|
if (!ssl)
|
|
|
|
goto error_ssl_2;
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
if (!transport_tls_init_cert (s, ssl, &error))
|
2015-04-27 21:52:16 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
// XXX: is this a reason to abort the connection?
|
|
|
|
log_server_error (s, s->buffer, "#s", error->message);
|
|
|
|
error_free (error);
|
2015-11-13 09:05:25 +01:00
|
|
|
error = NULL;
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
SSL_set_connect_state (ssl);
|
|
|
|
if (!SSL_set_fd (ssl, s->socket))
|
|
|
|
goto error_ssl_3;
|
|
|
|
|
2015-12-09 00:53:56 +01:00
|
|
|
// Enable SNI, FWIW; literal IP addresses aren't allowed
|
|
|
|
struct in6_addr dummy;
|
2018-01-08 21:53:42 +01:00
|
|
|
if (!inet_pton (AF_INET, hostname, &dummy)
|
2015-12-09 00:53:56 +01:00
|
|
|
&& !inet_pton (AF_INET6, hostname, &dummy))
|
|
|
|
SSL_set_tlsext_host_name (ssl, hostname);
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
struct transport_tls_data *data = xcalloc (1, sizeof *data);
|
|
|
|
data->ssl_ctx = ssl_ctx;
|
|
|
|
data->ssl = ssl;
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2015-07-12 17:51:17 +02:00
|
|
|
// Forces a handshake even if neither side wants to transmit data
|
|
|
|
data->ssl_rx_want_tx = true;
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
s->transport_data = data;
|
|
|
|
return true;
|
|
|
|
|
|
|
|
error_ssl_3:
|
|
|
|
SSL_free (ssl);
|
|
|
|
error_ssl_2:
|
|
|
|
SSL_CTX_free (ssl_ctx);
|
|
|
|
error_ssl_1:
|
2015-07-15 23:34:36 +02:00
|
|
|
if (!error)
|
|
|
|
error_set (&error, "%s: %s", "Could not initialize TLS",
|
2020-10-20 01:55:46 +02:00
|
|
|
xerr_describe_error ());
|
2015-07-15 23:34:36 +02:00
|
|
|
|
|
|
|
error_propagate (e, error);
|
2015-07-03 20:32:31 +02:00
|
|
|
return false;
|
2015-04-27 21:52:16 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
2015-07-03 20:32:31 +02:00
|
|
|
transport_tls_cleanup (struct server *s)
|
2015-04-27 22:40:33 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
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);
|
|
|
|
}
|
2015-04-27 22:40:33 +02:00
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
transport_tls_try_read (struct server *s)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
|
|
|
struct transport_tls_data *data = s->transport_data;
|
|
|
|
if (data->ssl_tx_want_rx)
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-04-27 22:40:33 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
struct str *buf = &s->read_buffer;
|
2015-07-03 20:32:31 +02:00
|
|
|
data->ssl_rx_want_tx = false;
|
2015-05-09 22:10:58 +02:00
|
|
|
while (true)
|
|
|
|
{
|
2015-07-12 05:30:13 +02:00
|
|
|
ERR_clear_error ();
|
2017-01-23 23:50:27 +01:00
|
|
|
str_reserve (buf, 512);
|
2015-07-03 20:32:31 +02:00
|
|
|
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))
|
2015-04-27 22:40:33 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_NONE:
|
|
|
|
buf->str[buf->len += n_read] = '\0';
|
|
|
|
continue;
|
|
|
|
case SSL_ERROR_ZERO_RETURN:
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_EOF;
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_WANT_READ:
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_WANT_WRITE:
|
|
|
|
data->ssl_rx_want_tx = true;
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
case XSSL_ERROR_TRY_AGAIN:
|
|
|
|
continue;
|
|
|
|
default:
|
|
|
|
LOG_FUNC_FAILURE ("SSL_read", error_info);
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_ERROR;
|
2015-04-27 22:40:33 +02:00
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-06 23:37:30 +01:00
|
|
|
static enum socket_io_result
|
2015-07-03 23:30:54 +02:00
|
|
|
transport_tls_try_write (struct server *s)
|
2015-07-03 20:32:31 +02:00
|
|
|
{
|
|
|
|
struct transport_tls_data *data = s->transport_data;
|
|
|
|
if (data->ssl_rx_want_tx)
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
|
|
|
|
struct str *buf = &s->write_buffer;
|
|
|
|
data->ssl_tx_want_rx = false;
|
|
|
|
while (buf->len)
|
|
|
|
{
|
2015-07-12 05:30:13 +02:00
|
|
|
ERR_clear_error ();
|
2015-07-03 20:32:31 +02:00
|
|
|
int n_written = SSL_write (data->ssl, buf->str, buf->len);
|
2015-04-27 22:40:33 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
const char *error_info = NULL;
|
|
|
|
switch (xssl_get_error (data->ssl, n_written, &error_info))
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_NONE:
|
|
|
|
str_remove_slice (buf, 0, n_written);
|
|
|
|
continue;
|
|
|
|
case SSL_ERROR_ZERO_RETURN:
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_EOF;
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_WANT_WRITE:
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
case SSL_ERROR_WANT_READ:
|
|
|
|
data->ssl_tx_want_rx = true;
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
case XSSL_ERROR_TRY_AGAIN:
|
|
|
|
continue;
|
|
|
|
default:
|
|
|
|
LOG_FUNC_FAILURE ("SSL_write", error_info);
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_ERROR;
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
}
|
2016-01-06 23:37:30 +01:00
|
|
|
return SOCKET_IO_OK;
|
2015-07-03 20:32:31 +02:00
|
|
|
}
|
2015-04-27 22:40:33 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
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;
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-05-09 06:11:36 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
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,
|
2015-07-03 23:30:54 +02:00
|
|
|
.try_read = transport_tls_try_read,
|
|
|
|
.try_write = transport_tls_try_write,
|
2015-07-03 20:32:31 +02:00
|
|
|
.get_poll_events = transport_tls_get_poll_events,
|
|
|
|
.in_before_shutdown = transport_tls_in_before_shutdown,
|
|
|
|
};
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// --- Connection establishment ------------------------------------------------
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2015-05-16 12:39:30 +02:00
|
|
|
static bool
|
|
|
|
irc_autofill_user_info (struct server *s, struct error **e)
|
|
|
|
{
|
2015-07-09 02:46:31 +02:00
|
|
|
const char *nicks = get_config_string (s->config, "nicks");
|
2015-05-16 12:39:30 +02:00
|
|
|
const char *username = get_config_string (s->config, "username");
|
|
|
|
const char *realname = get_config_string (s->config, "realname");
|
|
|
|
|
2015-07-09 02:46:31 +02:00
|
|
|
if (nicks && *nicks && username && *username && realname)
|
2015-05-16 12:39:30 +02:00
|
|
|
return true;
|
|
|
|
|
|
|
|
// Read POSIX user info and fill the configuration if needed
|
2019-12-07 21:18:20 +01:00
|
|
|
errno = 0;
|
2015-05-16 12:39:30 +02:00
|
|
|
struct passwd *pwd = getpwuid (geteuid ());
|
|
|
|
if (!pwd)
|
2016-10-11 10:52:49 +02:00
|
|
|
{
|
|
|
|
return error_set (e,
|
|
|
|
"cannot retrieve user information: %s", strerror (errno));
|
|
|
|
}
|
2015-05-16 12:39:30 +02:00
|
|
|
|
|
|
|
// FIXME: set_config_strings() writes errors on its own
|
2015-07-09 02:46:31 +02:00
|
|
|
if (!nicks || !*nicks)
|
|
|
|
set_config_string (s->config, "nicks", pwd->pw_name);
|
|
|
|
if (!username || !*username)
|
2015-05-16 12:39:30 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-07-09 02:46:31 +02:00
|
|
|
static char *
|
|
|
|
irc_fetch_next_nickname (struct server *s)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (get_config_string (s->config, "nicks"), ",", true, &v);
|
2015-07-09 02:46:31 +02:00
|
|
|
|
|
|
|
char *result = NULL;
|
2015-07-11 01:53:04 +02:00
|
|
|
if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len)
|
2015-07-11 18:04:07 +02:00
|
|
|
result = xstrdup (v.vector[s->nick_counter++]);
|
2015-07-11 01:53:04 +02:00
|
|
|
if ((size_t) s->nick_counter >= v.len)
|
2015-07-09 02:46:31 +02:00
|
|
|
// Exhausted all nicknames
|
|
|
|
s->nick_counter = -1;
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-07-09 02:46:31 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_register (struct server *s)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-05-16 12:39:30 +02:00
|
|
|
// Fill in user information automatically if needed
|
|
|
|
irc_autofill_user_info (s, NULL);
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
const char *username = get_config_string (s->config, "username");
|
|
|
|
const char *realname = get_config_string (s->config, "realname");
|
2015-07-09 02:46:31 +02:00
|
|
|
hard_assert (username && realname);
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
// Start IRCv3 capability negotiation, with up to 3.2 features;
|
2015-06-14 18:33:55 +02:00
|
|
|
// at worst the server will ignore this or send a harmless error message
|
2021-05-28 01:25:10 +02:00
|
|
|
irc_send (s, "CAP LS 302");
|
2015-06-14 18:33:55 +02:00
|
|
|
|
2015-05-20 21:18:15 +02:00
|
|
|
const char *password = get_config_string (s->config, "password");
|
|
|
|
if (password)
|
|
|
|
irc_send (s, "PASS :%s", password);
|
|
|
|
|
2015-07-09 02:46:31 +02:00
|
|
|
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);
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// IRC servers may ignore the last argument if it's empty
|
|
|
|
irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " ");
|
|
|
|
}
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
2015-12-09 00:53:56 +01:00
|
|
|
irc_finish_connection (struct server *s, int socket, const char *hostname)
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
|
|
|
struct app_context *ctx = s->ctx;
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2015-11-22 02:12:52 +01:00
|
|
|
// Most of our output comes from the user one full command at a time and we
|
|
|
|
// use output buffering, so it makes a lot of sense to avoid these delays
|
|
|
|
int yes = 1;
|
|
|
|
soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY,
|
|
|
|
&yes, sizeof yes) != -1);
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
set_blocking (socket, false);
|
2015-05-09 22:10:58 +02:00
|
|
|
s->socket = socket;
|
2015-07-28 20:31:42 +02:00
|
|
|
s->transport = get_config_boolean (s->config, "tls")
|
2015-07-03 20:32:31 +02:00
|
|
|
? &g_transport_tls
|
|
|
|
: &g_transport_plain;
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
struct error *e = NULL;
|
2015-12-09 00:53:56 +01:00
|
|
|
if (s->transport->init && !s->transport->init (s, hostname, &e))
|
2015-04-24 23:30:48 +02:00
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer, "Connection failed: #s", e->message);
|
2015-05-09 22:10:58 +02:00
|
|
|
error_free (e);
|
|
|
|
|
|
|
|
xclose (s->socket);
|
|
|
|
s->socket = -1;
|
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
s->transport = NULL;
|
2015-05-09 22:10:58 +02:00
|
|
|
return;
|
2015-04-24 23:30:48 +02:00
|
|
|
}
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer, "Connection established");
|
2015-05-09 22:10:58 +02:00
|
|
|
s->state = IRC_CONNECTED;
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
s->socket_event = poller_fd_make (&ctx->poller, s->socket);
|
2015-07-03 20:32:31 +02:00
|
|
|
s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready;
|
|
|
|
s->socket_event.user_data = s;
|
2015-04-19 21:58:46 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
irc_update_poller (s, NULL);
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_reset_connection_timeouts (s);
|
|
|
|
irc_register (s);
|
2015-07-03 20:32:31 +02:00
|
|
|
|
2015-05-12 07:02:14 +02:00
|
|
|
refresh_prompt (s->ctx);
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2018-01-08 21:46:35 +01:00
|
|
|
/// Unwrap IPv6 addresses in format_host_port_pair() format
|
2015-07-20 23:31:26 +02:00
|
|
|
static void
|
|
|
|
irc_split_host_port (char *s, char **host, char **port)
|
|
|
|
{
|
2018-01-08 21:46:35 +01:00
|
|
|
*host = s;
|
|
|
|
*port = "6667";
|
|
|
|
|
|
|
|
char *right_bracket = strchr (s, ']');
|
|
|
|
if (s[0] == '[' && right_bracket)
|
|
|
|
{
|
|
|
|
*right_bracket = '\0';
|
|
|
|
*host = s + 1;
|
|
|
|
s = right_bracket + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
char *colon = strchr (s, ':');
|
2015-07-20 23:31:26 +02:00
|
|
|
if (colon)
|
|
|
|
{
|
|
|
|
*colon = '\0';
|
2018-01-08 21:46:35 +01:00
|
|
|
*port = colon + 1;
|
2015-07-20 23:31:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_on_connector_connecting (void *user_data, const char *address)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
struct server *s = user_data;
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer, "Connecting to #s...", address);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-21 21:36:16 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_on_connector_error (void *user_data, const char *error)
|
|
|
|
{
|
|
|
|
struct server *s = user_data;
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer, "Connection failed: #s", error);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-21 21:36:16 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_on_connector_failure (void *user_data)
|
|
|
|
{
|
|
|
|
struct server *s = user_data;
|
|
|
|
irc_destroy_connector (s);
|
|
|
|
irc_queue_reconnect (s);
|
|
|
|
}
|
2015-04-21 21:36:16 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
2015-12-09 00:53:56 +01:00
|
|
|
irc_on_connector_connected (void *user_data, int socket, const char *hostname)
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
|
|
|
struct server *s = user_data;
|
2015-12-09 00:53:56 +01:00
|
|
|
char *hostname_copy = xstrdup (hostname);
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_destroy_connector (s);
|
2015-12-09 00:53:56 +01:00
|
|
|
irc_finish_connection (s, socket, hostname_copy);
|
|
|
|
free (hostname_copy);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-21 21:36:16 +02:00
|
|
|
|
2015-12-09 00:53:56 +01:00
|
|
|
static void
|
2017-01-23 23:50:27 +01:00
|
|
|
irc_setup_connector (struct server *s, const struct strv *addresses)
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
|
|
|
struct connector *connector = xmalloc (sizeof *connector);
|
|
|
|
connector_init (connector, &s->ctx->poller);
|
2015-07-20 23:31:26 +02:00
|
|
|
s->connector = connector;
|
2015-04-21 21:36:16 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
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;
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
for (size_t i = 0; i < addresses->len; i++)
|
2015-04-21 21:36:16 +02:00
|
|
|
{
|
2015-05-14 06:22:58 +02:00
|
|
|
char *host, *port;
|
|
|
|
irc_split_host_port (addresses->vector[i], &host, &port);
|
2015-12-09 00:53:56 +01:00
|
|
|
connector_add_target (connector, host, port);
|
2015-04-21 21:36:16 +02:00
|
|
|
}
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
static bool
|
2017-01-23 23:50:27 +01:00
|
|
|
irc_setup_connector_socks (struct server *s, const struct strv *addresses,
|
|
|
|
struct error **e)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-05-16 12:33:59 +02:00
|
|
|
const char *socks_host = get_config_string (s->config, "socks_host");
|
|
|
|
int64_t socks_port_int = get_config_integer (s->config, "socks_port");
|
2015-04-20 22:51:30 +02:00
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
if (!socks_host)
|
|
|
|
return false;
|
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
struct socks_connector *connector = xmalloc (sizeof *connector);
|
|
|
|
socks_connector_init (connector, &s->ctx->poller);
|
|
|
|
s->socks_conn = connector;
|
2015-05-14 06:22:58 +02:00
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
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;
|
2015-05-14 06:22:58 +02:00
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
for (size_t i = 0; i < addresses->len; i++)
|
2015-04-20 00:08:18 +02:00
|
|
|
{
|
2015-07-20 23:31:26 +02:00
|
|
|
char *host, *port;
|
|
|
|
irc_split_host_port (addresses->vector[i], &host, &port);
|
|
|
|
|
|
|
|
if (!socks_connector_add_target (connector, host, port, e))
|
|
|
|
return false;
|
2015-04-20 00:08:18 +02:00
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-07-23 04:07:30 +02:00
|
|
|
char *service = xstrdup_printf ("%" PRIi64, socks_port_int);
|
|
|
|
socks_connector_run (connector, socks_host, service,
|
|
|
|
get_config_string (s->config, "socks_username"),
|
|
|
|
get_config_string (s->config, "socks_password"));
|
|
|
|
free (service);
|
2015-07-23 04:56:40 +02:00
|
|
|
|
|
|
|
// The SOCKS connector can have already failed; we mustn't return true then
|
|
|
|
if (!s->socks_conn)
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "SOCKS connection failed");
|
2015-07-20 23:31:26 +02:00
|
|
|
return true;
|
2015-05-14 06:22:58 +02:00
|
|
|
}
|
|
|
|
|
2015-07-20 23:31:26 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
static void
|
|
|
|
irc_initiate_connect (struct server *s)
|
|
|
|
{
|
|
|
|
hard_assert (s->state == IRC_DISCONNECTED);
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
const char *addresses = get_config_string (s->config, "addresses");
|
2015-05-14 06:22:58 +02:00
|
|
|
if (!addresses || !addresses[strspn (addresses, ",")])
|
|
|
|
{
|
|
|
|
// No sense in trying to reconnect
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer,
|
2015-05-14 06:22:58 +02:00
|
|
|
"No addresses specified in configuration");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv servers = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (addresses, ",", true, &servers);
|
2015-05-14 06:22:58 +02:00
|
|
|
|
|
|
|
struct error *e = NULL;
|
2015-07-20 23:31:26 +02:00
|
|
|
if (!irc_setup_connector_socks (s, &servers, &e) && !e)
|
2015-12-09 00:53:56 +01:00
|
|
|
irc_setup_connector (s, &servers);
|
2015-05-14 06:22:58 +02:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&servers);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-07-23 10:12:22 +02:00
|
|
|
if (e)
|
2015-04-20 00:08:18 +02:00
|
|
|
{
|
2015-07-20 23:31:26 +02:00
|
|
|
irc_destroy_connector (s);
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer, "#s", e->message);
|
2015-05-09 22:10:58 +02:00
|
|
|
error_free (e);
|
|
|
|
irc_queue_reconnect (s);
|
2015-04-20 00:08:18 +02:00
|
|
|
}
|
2015-07-23 10:12:22 +02:00
|
|
|
else if (s->state != IRC_CONNECTED)
|
|
|
|
s->state = IRC_CONNECTING;
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-20 00:08:18 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// --- Input prompt ------------------------------------------------------------
|
2015-04-20 22:51:30 +02:00
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
static void
|
|
|
|
make_unseen_prefix (struct app_context *ctx, struct str *active_buffers)
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
2015-08-08 21:17:32 +02:00
|
|
|
size_t buffer_no = 0;
|
2015-05-09 22:10:58 +02:00
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
|
|
{
|
2015-08-08 21:17:32 +02:00
|
|
|
buffer_no++;
|
2016-03-26 03:09:29 +01:00
|
|
|
if (!(iter->new_messages_count - iter->new_unimportant_count)
|
|
|
|
|| iter == ctx->current_buffer)
|
2015-05-09 22:10:58 +02:00
|
|
|
continue;
|
2015-04-17 22:45:58 +02:00
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
if (active_buffers->len)
|
|
|
|
str_append_c (active_buffers, ',');
|
2015-06-21 04:00:20 +02:00
|
|
|
if (iter->highlighted)
|
2015-07-06 01:54:02 +02:00
|
|
|
str_append_c (active_buffers, '!');
|
2015-08-08 21:17:32 +02:00
|
|
|
str_append_printf (active_buffers, "%zu", buffer_no);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-27 21:52:16 +02:00
|
|
|
}
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
static void
|
|
|
|
make_chanmode_postfix (struct channel *channel, struct str *modes)
|
2015-06-04 22:19:32 +02:00
|
|
|
{
|
|
|
|
if (channel->no_param_modes.len)
|
2015-07-06 01:54:02 +02:00
|
|
|
str_append (modes, channel->no_param_modes.str);
|
2015-06-04 22:19:32 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
|
2015-06-04 22:19:32 +02:00
|
|
|
char *param;
|
|
|
|
while ((param = str_map_iter_next (&iter)))
|
2015-07-06 01:54:02 +02:00
|
|
|
str_append_c (modes, iter.link->key[0]);
|
2015-06-04 22:19:32 +02:00
|
|
|
}
|
|
|
|
|
2015-07-26 22:21:08 +02:00
|
|
|
static void
|
|
|
|
make_server_postfix_registered (struct buffer *buffer, struct str *output)
|
|
|
|
{
|
|
|
|
struct server *s = buffer->server;
|
|
|
|
if (buffer->type == BUFFER_CHANNEL)
|
|
|
|
{
|
|
|
|
struct channel_user *channel_user =
|
|
|
|
irc_channel_get_user (buffer->channel, s->irc_user);
|
|
|
|
if (channel_user)
|
|
|
|
irc_get_channel_user_prefix (s, channel_user, output);
|
|
|
|
}
|
|
|
|
str_append (output, s->irc_user->nickname);
|
|
|
|
if (s->irc_user_mode.len)
|
|
|
|
str_append_printf (output, "(%s)", s->irc_user_mode.str);
|
|
|
|
}
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
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
|
2015-07-26 22:21:08 +02:00
|
|
|
make_server_postfix_registered (buffer, output);
|
2015-07-06 01:54:02 +02:00
|
|
|
}
|
2016-10-27 11:48:48 +02:00
|
|
|
|
2015-04-27 21:52:16 +02:00
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
make_prompt (struct app_context *ctx, struct str *output)
|
2015-04-27 21:52:16 +02:00
|
|
|
{
|
2016-10-28 01:01:27 +02:00
|
|
|
LIST_FOR_EACH (struct hook, iter, ctx->prompt_hooks)
|
|
|
|
{
|
|
|
|
struct prompt_hook *hook = (struct prompt_hook *) iter;
|
2016-10-29 19:35:48 +02:00
|
|
|
char *made = hook->make (hook);
|
2016-10-28 01:01:27 +02:00
|
|
|
if (made)
|
|
|
|
{
|
|
|
|
str_append (output, made);
|
|
|
|
free (made);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
|
|
if (!buffer)
|
|
|
|
return;
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
str_append_c (output, '[');
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str active_buffers = str_make ();
|
2015-07-06 01:54:02 +02:00
|
|
|
make_unseen_prefix (ctx, &active_buffers);
|
|
|
|
if (active_buffers.len)
|
|
|
|
str_append_printf (output, "(%s) ", active_buffers.str);
|
|
|
|
str_free (&active_buffers);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
str_append_printf (output, "%d:%s",
|
|
|
|
buffer_get_index (ctx, buffer), buffer->name);
|
2016-12-30 07:49:10 +01:00
|
|
|
// We remember old modes, don't show them while we're not on the channel
|
|
|
|
if (buffer->type == BUFFER_CHANNEL
|
|
|
|
&& buffer->channel->users_len)
|
2015-06-04 22:19:32 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str modes = str_make ();
|
2015-07-06 01:54:02 +02:00
|
|
|
make_chanmode_postfix (buffer->channel, &modes);
|
|
|
|
if (modes.len)
|
|
|
|
str_append_printf (output, "(+%s)", modes.str);
|
|
|
|
str_free (&modes);
|
2016-10-23 16:29:55 +02:00
|
|
|
|
2016-12-30 07:49:10 +01:00
|
|
|
str_append_printf (output, "{%zu}", buffer->channel->users_len);
|
2015-06-04 22:19:32 +02:00
|
|
|
}
|
2016-10-23 16:53:31 +02:00
|
|
|
if (buffer->hide_unimportant)
|
|
|
|
str_append (output, "<H>");
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
if (buffer != ctx->global_buffer)
|
2015-07-06 01:54:02 +02:00
|
|
|
make_server_postfix (buffer, output);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
str_append_c (output, ']');
|
2016-10-28 01:01:27 +02:00
|
|
|
str_append_c (output, ' ');
|
2015-04-27 21:52:16 +02:00
|
|
|
}
|
|
|
|
|
2015-11-22 17:49:27 +01:00
|
|
|
static void
|
|
|
|
input_maybe_set_prompt (struct input *self, char *new_prompt)
|
|
|
|
{
|
2020-10-31 15:15:08 +01:00
|
|
|
// Fix libedit's expectations to see a non-control character following
|
|
|
|
// the end mark (see prompt.c and literal.c) by cleaning this up
|
|
|
|
for (char *p = new_prompt; *p; )
|
|
|
|
if (p[0] == INPUT_END_IGNORE && p[1] == INPUT_START_IGNORE)
|
|
|
|
memmove (p, p + 2, strlen (p + 2) + 1);
|
|
|
|
else
|
|
|
|
p++;
|
|
|
|
|
2015-11-22 17:49:27 +01:00
|
|
|
// Redisplay can be an expensive operation
|
2016-03-07 01:08:52 +01:00
|
|
|
const char *prompt = CALL (self, get_prompt);
|
|
|
|
if (prompt && !strcmp (new_prompt, prompt))
|
2015-11-22 17:49:27 +01:00
|
|
|
free (new_prompt);
|
|
|
|
else
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, set_prompt, new_prompt);
|
2015-11-22 17:49:27 +01:00
|
|
|
}
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2016-10-23 17:34:52 +02:00
|
|
|
on_refresh_prompt (struct app_context *ctx)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2016-10-23 17:34:52 +02:00
|
|
|
poller_idle_reset (&ctx->prompt_event);
|
2015-05-09 22:10:58 +02:00
|
|
|
bool have_attributes = !!get_attribute_printer (stdout);
|
2015-04-27 21:52:16 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str prompt = str_make ();
|
2015-05-09 22:10:58 +02:00
|
|
|
make_prompt (ctx, &prompt);
|
2020-09-02 19:35:38 +02:00
|
|
|
|
|
|
|
// libedit has a weird bug where it misapplies ignores when they're not
|
|
|
|
// followed by anything else, so let's try to move a trailing space,
|
|
|
|
// which will at least fix the default prompt.
|
|
|
|
const char *attributed_suffix = "";
|
|
|
|
#ifdef HAVE_EDITLINE
|
|
|
|
if (have_attributes && prompt.len && prompt.str[prompt.len - 1] == ' ')
|
|
|
|
{
|
|
|
|
prompt.str[--prompt.len] = 0;
|
|
|
|
attributed_suffix = " ";
|
|
|
|
}
|
2020-10-31 15:15:08 +01:00
|
|
|
|
|
|
|
// Also enable a uniform interface for prompt hooks by assuming it uses
|
|
|
|
// GNU Readline escapes: turn this into libedit's almost-flip-flop
|
|
|
|
for (size_t i = 0; i < prompt.len; i++)
|
|
|
|
if (prompt.str[i] == '\x01' || prompt.str[i] == '\x02')
|
|
|
|
prompt.str[i] = INPUT_START_IGNORE /* == INPUT_END_IGNORE */;
|
2020-09-02 19:35:38 +02:00
|
|
|
#endif // HAVE_EDITLINE
|
|
|
|
|
2015-07-15 22:59:55 +02:00
|
|
|
char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
|
|
|
|
str_free (&prompt);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
if (have_attributes)
|
|
|
|
{
|
|
|
|
// XXX: to be completely correct, we should use tputs, but we cannot
|
2020-09-02 19:35:38 +02:00
|
|
|
input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s",
|
2015-05-09 22:10:58 +02:00
|
|
|
INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
|
|
|
|
INPUT_END_IGNORE,
|
2015-07-15 22:59:55 +02:00
|
|
|
localized,
|
2015-05-09 22:10:58 +02:00
|
|
|
INPUT_START_IGNORE, ctx->attrs[ATTR_RESET],
|
2020-09-02 19:35:38 +02:00
|
|
|
INPUT_END_IGNORE,
|
|
|
|
attributed_suffix));
|
2015-07-15 22:59:55 +02:00
|
|
|
free (localized);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
else
|
2016-03-07 01:08:52 +01:00
|
|
|
input_maybe_set_prompt (ctx->input, localized);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- Helpers -----------------------------------------------------------------
|
|
|
|
|
|
|
|
static struct buffer *
|
|
|
|
irc_get_buffer_for_message (struct server *s,
|
|
|
|
const struct irc_message *msg, const char *target)
|
|
|
|
{
|
2015-09-24 01:39:49 +02:00
|
|
|
// TODO: display such messages differently
|
|
|
|
target = irc_skip_statusmsg (s, target);
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
// This is weird
|
|
|
|
if (!channel)
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
else if (!buffer)
|
|
|
|
{
|
2016-02-12 04:04:35 +01:00
|
|
|
// Outgoing messages needn't have a prefix, no buffer associated
|
|
|
|
if (!msg->prefix)
|
|
|
|
return NULL;
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// 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);
|
2015-06-20 21:38:04 +02:00
|
|
|
if (irc_is_this_us (s, target))
|
|
|
|
buffer = irc_get_or_make_user_buffer (s, nickname);
|
2015-05-09 22:10:58 +02:00
|
|
|
free (nickname);
|
2015-06-29 21:39:40 +02:00
|
|
|
|
|
|
|
// 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
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
irc_is_highlight (struct server *s, const char *message)
|
|
|
|
{
|
2015-05-15 05:54:03 +02:00
|
|
|
// This may be called by notices before even successfully registering
|
|
|
|
if (!s->irc_user)
|
|
|
|
return false;
|
|
|
|
|
2016-09-23 23:46:26 +02:00
|
|
|
// Strip formatting from the message so that it doesn't interfere
|
2020-10-11 17:36:21 +02:00
|
|
|
// with nickname detection (colour sequences in particular)
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (s->ctx, NULL);
|
2021-08-29 12:07:31 +02:00
|
|
|
formatter_parse_message (&f, message);
|
2016-09-23 23:46:26 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str stripped = str_make ();
|
2016-09-23 23:46:26 +02:00
|
|
|
for (size_t i = 0; i < f.items_len; i++)
|
|
|
|
{
|
|
|
|
if (f.items[i].type == FORMATTER_ITEM_TEXT)
|
|
|
|
str_append (&stripped, f.items[i].text);
|
|
|
|
}
|
|
|
|
formatter_free (&f);
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// Well, this is rather crude but it should make most users happy.
|
2016-09-25 14:06:24 +02:00
|
|
|
// We could do this in proper Unicode but that's two more conversions per
|
|
|
|
// message when both the nickname and the message are likely valid UTF-8.
|
|
|
|
char *copy = str_steal (&stripped);
|
2015-07-11 17:54:38 +02:00
|
|
|
cstr_transform (copy, s->irc_tolower);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
char *nick = xstrdup (s->irc_user->nickname);
|
2015-07-11 17:54:38 +02:00
|
|
|
cstr_transform (nick, s->irc_tolower);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
|
|
|
|
// Also excluded from the ASCII: common user channel prefixes: +%@&~
|
2018-01-08 21:53:42 +01:00
|
|
|
// XXX: why did I exclude those? It won't match when IRC newbies use them.
|
2015-05-09 22:10:58 +02:00
|
|
|
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;
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2015-07-26 22:21:08 +02:00
|
|
|
static char *
|
2015-06-28 16:26:51 +02:00
|
|
|
irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str prefix = str_make ();
|
|
|
|
if (user && irc_is_channel (s, (target = irc_skip_statusmsg (s, target))))
|
2015-06-28 16:26:51 +02:00
|
|
|
{
|
|
|
|
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)))
|
2015-07-26 22:21:08 +02:00
|
|
|
irc_get_channel_user_prefix (s, channel_user, &prefix);
|
2015-06-28 16:26:51 +02:00
|
|
|
}
|
2015-07-26 22:21:08 +02:00
|
|
|
return str_steal (&prefix);
|
2015-06-28 16:26:51 +02:00
|
|
|
}
|
|
|
|
|
2015-06-07 05:28:57 +02:00
|
|
|
// --- Mode processor ----------------------------------------------------------
|
2015-06-04 23:55:10 +02:00
|
|
|
|
|
|
|
struct mode_processor
|
|
|
|
{
|
|
|
|
char **params; ///< Mode string parameters
|
2015-06-07 04:41:46 +02:00
|
|
|
bool adding; ///< Currently adding modes
|
|
|
|
char mode_char; ///< Currently processed mode char
|
|
|
|
|
|
|
|
// User data:
|
2015-06-04 23:55:10 +02:00
|
|
|
|
2015-06-05 00:57:39 +02:00
|
|
|
struct server *s; ///< Server
|
|
|
|
struct channel *channel; ///< The channel being modified
|
2016-10-28 13:14:51 +02:00
|
|
|
unsigned changes; ///< Count of all changes
|
|
|
|
unsigned usermode_changes; ///< Count of all usermode changes
|
2015-06-04 23:55:10 +02:00
|
|
|
};
|
|
|
|
|
2015-06-07 04:41:46 +02:00
|
|
|
/// Process a single mode character
|
2015-06-07 05:16:25 +02:00
|
|
|
typedef bool (*mode_processor_apply_fn) (struct mode_processor *);
|
2015-06-04 23:55:10 +02:00
|
|
|
|
|
|
|
static const char *
|
|
|
|
mode_processor_next_param (struct mode_processor *self)
|
|
|
|
{
|
|
|
|
if (!*self->params)
|
|
|
|
return NULL;
|
|
|
|
return *self->params++;
|
|
|
|
}
|
|
|
|
|
2015-06-07 04:41:46 +02:00
|
|
|
static void
|
|
|
|
mode_processor_run (struct mode_processor *self,
|
2015-06-07 05:16:25 +02:00
|
|
|
char **params, mode_processor_apply_fn apply_cb)
|
2015-06-07 04:41:46 +02:00
|
|
|
{
|
|
|
|
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;
|
2015-06-07 05:16:25 +02:00
|
|
|
else if (!apply_cb (self))
|
2015-06-07 04:41:46 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-07 05:23:31 +02:00
|
|
|
static int
|
|
|
|
mode_char_cmp (const void *a, const void *b)
|
|
|
|
{
|
|
|
|
return *(const char *) a - *(const char *) b;
|
|
|
|
}
|
|
|
|
|
2015-06-07 05:28:57 +02:00
|
|
|
/// Add/remove the current mode character to/from the given ordered list
|
2015-06-07 05:16:25 +02:00
|
|
|
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);
|
2015-06-07 05:23:31 +02:00
|
|
|
qsort (modes->str, modes->len, 1, mode_char_cmp);
|
2015-06-07 05:16:25 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
str_remove_slice (modes, pos - modes->str, 1);
|
|
|
|
}
|
|
|
|
|
2015-06-07 04:20:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-06-04 23:55:10 +02:00
|
|
|
static void
|
|
|
|
mode_processor_do_user (struct mode_processor *self)
|
|
|
|
{
|
|
|
|
const char *nickname;
|
|
|
|
struct user *user;
|
2015-06-05 01:17:20 +02:00
|
|
|
struct channel_user *channel_user;
|
2015-06-04 23:55:10 +02:00
|
|
|
if (!(nickname = mode_processor_next_param (self))
|
2015-06-05 01:17:20 +02:00
|
|
|
|| !(user = str_map_find (&self->s->irc_users, nickname))
|
|
|
|
|| !(channel_user = irc_channel_get_user (self->channel, user)))
|
2015-06-04 23:55:10 +02:00
|
|
|
return;
|
|
|
|
|
2016-01-18 00:59:43 +01:00
|
|
|
// Translate mode character to user prefix character
|
2015-06-05 00:57:39 +02:00
|
|
|
const char *all_prefixes = self->s->irc_chanuser_prefixes;
|
|
|
|
const char *all_modes = self->s->irc_chanuser_modes;
|
2016-01-18 00:59:43 +01:00
|
|
|
|
|
|
|
const char *mode = strchr (all_modes, self->mode_char);
|
|
|
|
hard_assert (mode && (size_t) (mode - all_modes) < strlen (all_prefixes));
|
|
|
|
char prefix = all_prefixes[mode - all_modes];
|
2015-06-04 23:55:10 +02:00
|
|
|
|
2020-10-04 08:22:20 +02:00
|
|
|
char **prefixes = &channel_user->prefixes;
|
|
|
|
char *pos = strchr (*prefixes, prefix);
|
2015-06-04 23:55:10 +02:00
|
|
|
if (self->adding == !!pos)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (self->adding)
|
|
|
|
{
|
2015-06-05 00:57:39 +02:00
|
|
|
// Add the new mode prefix while retaining the right order
|
2020-10-04 08:22:20 +02:00
|
|
|
struct str buf = str_make ();
|
2015-06-05 00:57:39 +02:00
|
|
|
for (const char *p = all_prefixes; *p; p++)
|
2020-10-04 08:22:20 +02:00
|
|
|
if (*p == prefix || strchr (*prefixes, *p))
|
|
|
|
str_append_c (&buf, *p);
|
|
|
|
cstr_set (prefixes, str_steal (&buf));
|
2015-06-04 23:55:10 +02:00
|
|
|
}
|
|
|
|
else
|
2020-10-04 08:22:20 +02:00
|
|
|
memmove (pos, pos + 1, strlen (pos));
|
2015-06-04 23:55:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 };
|
2015-06-07 05:16:25 +02:00
|
|
|
str_map_set (&self->channel->param_modes, key,
|
|
|
|
self->adding ? xstrdup (param) : NULL);
|
2015-06-04 23:55:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 };
|
2015-06-07 05:16:25 +02:00
|
|
|
str_map_set (&self->channel->param_modes, key,
|
|
|
|
self->adding ? xstrdup (param) : NULL);
|
2015-06-04 23:55:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-07 05:16:25 +02:00
|
|
|
mode_processor_apply_channel (struct mode_processor *self)
|
2015-06-04 23:55:10 +02:00
|
|
|
{
|
2016-10-28 13:14:51 +02:00
|
|
|
self->changes++;
|
2015-06-07 05:16:25 +02:00
|
|
|
if (strchr (self->s->irc_chanuser_modes, self->mode_char))
|
2016-10-28 13:14:51 +02:00
|
|
|
{
|
|
|
|
self->usermode_changes++;
|
2015-06-07 05:16:25 +02:00
|
|
|
mode_processor_do_user (self);
|
2016-10-28 13:14:51 +02:00
|
|
|
}
|
2015-06-07 05:16:25 +02:00
|
|
|
else if (strchr (self->s->irc_chanmodes_list, self->mode_char))
|
|
|
|
// Nothing to do here, just skip the next argument if there's any
|
2015-06-04 23:55:10 +02:00
|
|
|
(void) mode_processor_next_param (self);
|
2015-06-07 05:16:25 +02:00
|
|
|
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))
|
2015-06-04 23:55:10 +02:00
|
|
|
mode_processor_do_param_when_set (self);
|
2015-06-07 05:16:25 +02:00
|
|
|
else if (strchr (self->s->irc_chanmodes_param_never, self->mode_char))
|
|
|
|
mode_processor_toggle (self, &self->channel->no_param_modes);
|
2015-06-04 23:55:10 +02:00
|
|
|
else
|
|
|
|
// It's not safe to continue, results could be undesired
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-10-28 13:14:51 +02:00
|
|
|
/// Returns whether the change has only affected channel user modes
|
|
|
|
static bool
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_handle_mode_channel (struct channel *channel, char **params)
|
2015-06-04 23:55:10 +02:00
|
|
|
{
|
2020-10-16 20:19:52 +02:00
|
|
|
struct mode_processor p = { .s = channel->s, .channel = channel };
|
2015-06-07 04:41:46 +02:00
|
|
|
mode_processor_run (&p, params, mode_processor_apply_channel);
|
2016-10-28 13:14:51 +02:00
|
|
|
return p.changes == p.usermode_changes;
|
2015-06-04 23:55:10 +02:00
|
|
|
}
|
|
|
|
|
2015-06-07 04:20:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static bool
|
2015-06-07 05:16:25 +02:00
|
|
|
mode_processor_apply_user (struct mode_processor *self)
|
2015-06-07 04:20:39 +02:00
|
|
|
{
|
2015-06-07 05:16:25 +02:00
|
|
|
mode_processor_toggle (self, &self->s->irc_user_mode);
|
2015-06-07 04:20:39 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_mode_user (struct server *s, char **params)
|
|
|
|
{
|
2015-06-07 04:41:46 +02:00
|
|
|
struct mode_processor p = { .s = s };
|
|
|
|
mode_processor_run (&p, params, mode_processor_apply_user);
|
2015-06-07 04:20:39 +02:00
|
|
|
}
|
|
|
|
|
2016-02-09 05:10:41 +01:00
|
|
|
// --- Output processing -------------------------------------------------------
|
|
|
|
|
|
|
|
// Both user and plugins can send whatever the heck they want to,
|
|
|
|
// we need to parse it back so that it's evident what's happening
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_sent_cap (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
2021-05-28 02:54:07 +02:00
|
|
|
if (msg->params.len < 1)
|
2016-02-09 05:10:41 +01:00
|
|
|
return;
|
|
|
|
|
2021-05-28 02:54:07 +02:00
|
|
|
const char *subcommand = msg->params.vector[0];
|
|
|
|
const char *args = (msg->params.len > 1) ? msg->params.vector[1] : "";
|
2016-02-09 05:10:41 +01:00
|
|
|
if (!strcasecmp_ascii (subcommand, "REQ"))
|
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"#s: #S", "Capabilities requested", args);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_sent_notice_text (struct server *s,
|
|
|
|
const struct irc_message *msg, struct str *text)
|
|
|
|
{
|
|
|
|
const char *target = msg->params.vector[0];
|
|
|
|
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
|
|
|
|
if (buffer && soft_assert (s->irc_user))
|
|
|
|
log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str);
|
|
|
|
else
|
|
|
|
log_outcoming_orphan_notice (s, target, text->str);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_sent_notice (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 2 || s->cap_echo_message)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// This ignores empty messages which we should not normally send
|
|
|
|
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
|
|
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
|
|
|
|
{
|
|
|
|
if (iter->is_extended)
|
|
|
|
log_ctcp_reply (s, msg->params.vector[0],
|
|
|
|
xstrdup_printf ("%s %s", iter->tag.str, iter->text.str));
|
|
|
|
else
|
|
|
|
irc_handle_sent_notice_text (s, msg, &iter->text);
|
|
|
|
}
|
|
|
|
ctcp_destroy (chunks);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_sent_privmsg_text (struct server *s,
|
|
|
|
const struct irc_message *msg, struct str *text, bool is_action)
|
|
|
|
{
|
|
|
|
const char *target = msg->params.vector[0];
|
|
|
|
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
|
|
|
|
if (buffer && soft_assert (s->irc_user))
|
|
|
|
{
|
|
|
|
char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target);
|
|
|
|
if (is_action)
|
|
|
|
log_outcoming_action (s, buffer, s->irc_user->nickname, text->str);
|
|
|
|
else
|
|
|
|
log_outcoming_privmsg (s, buffer,
|
|
|
|
prefixes, s->irc_user->nickname, text->str);
|
|
|
|
free (prefixes);
|
|
|
|
}
|
2021-09-05 02:51:05 +02:00
|
|
|
else if (is_action)
|
|
|
|
log_outcoming_orphan_action (s, target, text->str);
|
2016-02-09 05:10:41 +01:00
|
|
|
else
|
|
|
|
log_outcoming_orphan_privmsg (s, target, text->str);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 2 || s->cap_echo_message)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// This ignores empty messages which we should not normally send
|
2018-01-08 21:53:42 +01:00
|
|
|
// and the server is likely going to reject with an error reply anyway
|
2016-02-09 05:10:41 +01:00
|
|
|
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
|
|
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
|
|
|
|
{
|
|
|
|
if (!iter->is_extended)
|
|
|
|
irc_handle_sent_privmsg_text (s, msg, &iter->text, false);
|
|
|
|
else if (!strcmp (iter->tag.str, "ACTION"))
|
|
|
|
irc_handle_sent_privmsg_text (s, msg, &iter->text, true);
|
|
|
|
else
|
|
|
|
log_ctcp_query (s, msg->params.vector[0], iter->tag.str);
|
|
|
|
}
|
|
|
|
ctcp_destroy (chunks);
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct irc_handler
|
|
|
|
{
|
|
|
|
const char *name;
|
|
|
|
void (*handler) (struct server *s, const struct irc_message *msg);
|
|
|
|
}
|
|
|
|
g_irc_sent_handlers[] =
|
|
|
|
{
|
|
|
|
// This list needs to stay sorted
|
|
|
|
{ "CAP", irc_handle_sent_cap },
|
|
|
|
{ "NOTICE", irc_handle_sent_notice },
|
|
|
|
{ "PRIVMSG", irc_handle_sent_privmsg },
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
irc_handler_cmp_by_name (const void *a, const void *b)
|
|
|
|
{
|
|
|
|
const struct irc_handler *first = a;
|
|
|
|
const struct irc_handler *second = b;
|
|
|
|
return strcasecmp_ascii (first->name, second->name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_process_sent_message (const struct irc_message *msg, struct server *s)
|
|
|
|
{
|
|
|
|
// The server is free to reject even a matching prefix
|
2018-01-08 21:53:42 +01:00
|
|
|
// XXX: even though no prefix should normally be present, this is racy
|
2016-02-09 05:10:41 +01:00
|
|
|
if (msg->prefix && !irc_is_this_us (s, msg->prefix))
|
|
|
|
return;
|
|
|
|
|
|
|
|
struct irc_handler key = { .name = msg->command };
|
|
|
|
struct irc_handler *handler = bsearch (&key, g_irc_sent_handlers,
|
|
|
|
N_ELEMENTS (g_irc_sent_handlers), sizeof key, irc_handler_cmp_by_name);
|
|
|
|
if (handler)
|
|
|
|
handler->handler (s, msg);
|
|
|
|
}
|
|
|
|
|
2015-06-07 05:28:57 +02:00
|
|
|
// --- Input handling ----------------------------------------------------------
|
|
|
|
|
2021-05-28 02:11:23 +02:00
|
|
|
static void
|
|
|
|
irc_handle_authenticate (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Empty challenge -> empty response for e.g. SASL EXTERNAL,
|
|
|
|
// abort anything else as it doesn't make much sense to let the user do it
|
|
|
|
if (!strcmp (msg->params.vector[0], "+"))
|
|
|
|
irc_send (s, "AUTHENTICATE +");
|
|
|
|
else
|
|
|
|
irc_send (s, "AUTHENTICATE *");
|
|
|
|
}
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
static void
|
|
|
|
irc_handle_away (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (!msg->prefix)
|
|
|
|
return;
|
|
|
|
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
|
|
free (nickname);
|
|
|
|
|
|
|
|
// Let's allow the server to make us away
|
|
|
|
if (user)
|
|
|
|
user->away = !!msg->params.len;
|
|
|
|
}
|
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
static void
|
|
|
|
irc_process_cap_ls (struct server *s)
|
|
|
|
{
|
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"#s: #&S", "Capabilities supported", strv_join (&s->cap_ls_buf, " "));
|
|
|
|
|
|
|
|
struct strv chosen = strv_make ();
|
|
|
|
struct strv use = strv_make ();
|
|
|
|
|
|
|
|
cstr_split (get_config_string (s->config, "capabilities"), ",", true, &use);
|
|
|
|
|
|
|
|
// Filter server capabilities for ones we can make use of
|
|
|
|
for (size_t i = 0; i < s->cap_ls_buf.len; i++)
|
|
|
|
{
|
|
|
|
const char *cap = s->cap_ls_buf.vector[i];
|
|
|
|
size_t cap_name_len = strcspn (cap, "=");
|
|
|
|
for (size_t k = 0; k < use.len; k++)
|
|
|
|
if (!strncasecmp_ascii (use.vector[k], cap, cap_name_len))
|
|
|
|
strv_append_owned (&chosen, xstrndup (cap, cap_name_len));
|
|
|
|
}
|
|
|
|
strv_reset (&s->cap_ls_buf);
|
|
|
|
|
|
|
|
char *chosen_str = strv_join (&chosen, " ");
|
|
|
|
strv_free (&chosen);
|
|
|
|
strv_free (&use);
|
|
|
|
|
|
|
|
// XXX: with IRCv3.2, this may end up being too long for one message,
|
|
|
|
// and we need to be careful with CAP END. One probably has to count
|
|
|
|
// the number of sent CAP REQ vs the number of received CAP ACK/NAK.
|
2021-05-28 01:32:15 +02:00
|
|
|
if (s->state == IRC_CONNECTED)
|
|
|
|
irc_send (s, "CAP REQ :%s", chosen_str);
|
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
free (chosen_str);
|
|
|
|
}
|
|
|
|
|
2021-05-28 01:44:46 +02:00
|
|
|
static void
|
|
|
|
irc_toggle_cap (struct server *s, const char *cap, bool active)
|
|
|
|
{
|
2021-05-28 02:11:23 +02:00
|
|
|
if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active;
|
|
|
|
if (!strcasecmp_ascii (cap, "away-notify")) s->cap_away_notify = active;
|
|
|
|
if (!strcasecmp_ascii (cap, "sasl")) s->cap_sasl = active;
|
2021-05-28 01:44:46 +02:00
|
|
|
}
|
|
|
|
|
2021-05-28 04:25:38 +02:00
|
|
|
static void
|
|
|
|
irc_try_finish_cap_negotiation (struct server *s)
|
|
|
|
{
|
|
|
|
// It does not make sense to do this post-registration, although it would
|
|
|
|
// not hurt either, as the server must ignore it in that case
|
|
|
|
if (s->state == IRC_CONNECTED)
|
|
|
|
irc_send (s, "CAP END");
|
|
|
|
}
|
|
|
|
|
2015-06-14 18:33:55 +02:00
|
|
|
static void
|
|
|
|
irc_handle_cap (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 2)
|
|
|
|
return;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2015-07-11 05:04:17 +02:00
|
|
|
const char *args = "";
|
2015-06-14 18:33:55 +02:00
|
|
|
if (msg->params.len > 2)
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split ((args = msg->params.vector[2]), " ", true, &v);
|
2015-06-14 18:33:55 +02:00
|
|
|
|
|
|
|
const char *subcommand = msg->params.vector[1];
|
2015-06-20 21:38:04 +02:00
|
|
|
if (!strcasecmp_ascii (subcommand, "ACK"))
|
2015-06-14 18:33:55 +02:00
|
|
|
{
|
2015-07-11 05:04:17 +02:00
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"#s: #S", "Capabilities acknowledged", args);
|
2015-06-20 21:38:04 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
|
|
|
{
|
|
|
|
const char *cap = v.vector[i];
|
|
|
|
bool active = true;
|
|
|
|
if (*cap == '-')
|
|
|
|
{
|
|
|
|
active = false;
|
|
|
|
cap++;
|
|
|
|
}
|
2021-05-28 01:44:46 +02:00
|
|
|
irc_toggle_cap (s, cap, active);
|
2015-06-20 21:38:04 +02:00
|
|
|
}
|
2021-05-28 02:11:23 +02:00
|
|
|
if (s->cap_sasl && s->transport == &g_transport_tls)
|
|
|
|
irc_send (s, "AUTHENTICATE EXTERNAL");
|
2021-05-28 04:25:38 +02:00
|
|
|
else
|
|
|
|
irc_try_finish_cap_negotiation (s);
|
2015-06-14 18:33:55 +02:00
|
|
|
}
|
2015-06-20 21:38:04 +02:00
|
|
|
else if (!strcasecmp_ascii (subcommand, "NAK"))
|
2015-07-11 05:04:17 +02:00
|
|
|
{
|
|
|
|
log_server_error (s, s->buffer,
|
|
|
|
"#s: #S", "Capabilities not acknowledged", args);
|
2021-05-28 04:25:38 +02:00
|
|
|
irc_try_finish_cap_negotiation (s);
|
2015-07-11 05:04:17 +02:00
|
|
|
}
|
2021-05-28 01:44:46 +02:00
|
|
|
else if (!strcasecmp_ascii (subcommand, "DEL"))
|
|
|
|
{
|
|
|
|
log_server_error (s, s->buffer,
|
|
|
|
"#s: #S", "Capabilities deleted", args);
|
|
|
|
for (size_t i = 0; i < v.len; i++)
|
|
|
|
irc_toggle_cap (s, v.vector[i], false);
|
|
|
|
}
|
2015-06-14 18:33:55 +02:00
|
|
|
else if (!strcasecmp_ascii (subcommand, "LS"))
|
|
|
|
{
|
|
|
|
|
2021-05-28 01:25:10 +02:00
|
|
|
if (msg->params.len > 3 && !strcmp (args, "*"))
|
|
|
|
cstr_split (msg->params.vector[3], " ", true, &s->cap_ls_buf);
|
|
|
|
else
|
2015-06-14 18:33:55 +02:00
|
|
|
{
|
2021-05-28 01:25:10 +02:00
|
|
|
strv_append_vector (&s->cap_ls_buf, v.vector);
|
|
|
|
irc_process_cap_ls (s);
|
2015-06-14 18:33:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-06-14 18:33:55 +02:00
|
|
|
}
|
|
|
|
|
2021-05-30 08:06:38 +02:00
|
|
|
static void
|
|
|
|
irc_handle_chghost (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
|
|
return;
|
|
|
|
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
|
|
free (nickname);
|
|
|
|
if (!user)
|
|
|
|
return;
|
|
|
|
|
|
|
|
char *new_prefix = xstrdup_printf ("%s!%s@%s", user->nickname,
|
|
|
|
msg->params.vector[0], msg->params.vector[1]);
|
|
|
|
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
{
|
2021-05-30 08:23:23 +02:00
|
|
|
cstr_set (&s->irc_user_host, xstrdup_printf ("%s@%s",
|
|
|
|
msg->params.vector[0], msg->params.vector[1]));
|
|
|
|
|
2021-05-30 08:06:38 +02:00
|
|
|
log_chghost_self (s, s->buffer, new_prefix);
|
|
|
|
|
|
|
|
// Log a message in all open buffers on this server
|
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
|
|
|
|
struct buffer *buffer;
|
|
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
|
|
log_chghost_self (s, buffer, new_prefix);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Log a message in any PM buffer
|
|
|
|
struct buffer *buffer =
|
|
|
|
str_map_find (&s->irc_buffer_map, user->nickname);
|
|
|
|
if (buffer)
|
|
|
|
log_chghost (s, buffer, msg->prefix, new_prefix);
|
|
|
|
|
|
|
|
// Log a message in all channels the user is in
|
|
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
|
|
{
|
|
|
|
buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
|
|
|
|
hard_assert (buffer != NULL);
|
|
|
|
log_chghost (s, buffer, msg->prefix, new_prefix);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
free (new_prefix);
|
|
|
|
}
|
|
|
|
|
2015-08-12 23:21:11 +02:00
|
|
|
static void
|
|
|
|
irc_handle_error (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
log_server_error (s, s->buffer, "#m", msg->params.vector[0]);
|
|
|
|
}
|
|
|
|
|
2015-06-10 22:37:29 +02:00
|
|
|
static void
|
|
|
|
irc_handle_invite (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
|
|
return;
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
const char *target = msg->params.vector[0];
|
2015-06-10 22:37:29 +02:00
|
|
|
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))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, buffer,
|
|
|
|
"#n has invited you to #S", msg->prefix, channel_name);
|
2015-06-10 22:37:29 +02:00
|
|
|
else
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, buffer,
|
|
|
|
"#n has invited #n to #S", msg->prefix, target, channel_name);
|
2015-06-10 22:37:29 +02:00
|
|
|
}
|
|
|
|
|
2015-06-07 05:28:57 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-07 05:28:57 +02:00
|
|
|
|
|
|
|
// We've joined a new channel
|
2020-10-16 17:38:49 +02:00
|
|
|
if (!channel)
|
2015-06-07 05:28:57 +02:00
|
|
|
{
|
2020-10-16 17:38:49 +02:00
|
|
|
// This is weird, ignoring
|
|
|
|
if (!irc_is_this_us (s, msg->prefix))
|
|
|
|
return;
|
|
|
|
|
2020-10-31 23:42:49 +01:00
|
|
|
buffer = buffer_new (s->ctx->input,
|
|
|
|
BUFFER_CHANNEL, irc_make_buffer_name (s, channel_name));
|
2015-06-07 05:28:57 +02:00
|
|
|
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);
|
2021-04-10 05:11:46 +02:00
|
|
|
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
char *input = CALL_ (s->ctx->input, get_line, NULL);
|
2021-04-10 05:11:46 +02:00
|
|
|
if (!*input)
|
|
|
|
buffer_activate (s->ctx, buffer);
|
|
|
|
else
|
|
|
|
buffer->highlighted = true;
|
|
|
|
free (input);
|
2016-12-30 07:49:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
{
|
|
|
|
// Reset the field so that we rejoin the channel after reconnecting
|
|
|
|
channel->left_manually = false;
|
2015-06-07 05:28:57 +02:00
|
|
|
|
|
|
|
// Request the channel mode as we don't get it automatically
|
2016-12-30 07:49:10 +01:00
|
|
|
str_reset (&channel->no_param_modes);
|
|
|
|
str_map_clear (&channel->param_modes);
|
2015-06-07 05:28:57 +02:00
|
|
|
irc_send (s, "MODE %s", channel_name);
|
2020-10-16 16:45:40 +02:00
|
|
|
|
|
|
|
if ((channel->show_names_after_who = s->cap_away_notify))
|
|
|
|
irc_send (s, "WHO %s", channel_name);
|
2015-06-07 05:28:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
{
|
2015-08-08 21:17:32 +02:00
|
|
|
log_server (s, buffer, BUFFER_LINE_UNIMPORTANT, "#a-->#r #N #a#s#r #S",
|
2015-06-29 08:54:00 +02:00
|
|
|
ATTR_JOIN, msg->prefix, ATTR_JOIN, "has joined", channel_name);
|
2015-06-07 05:28:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
const char *message = NULL;
|
2015-06-07 05:28:57 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-07 05:28:57 +02:00
|
|
|
|
2015-12-31 03:45:44 +01:00
|
|
|
// It would be weird for this to be false
|
2015-06-07 05:28:57 +02:00
|
|
|
if (user && channel)
|
|
|
|
{
|
|
|
|
if (irc_is_this_us (s, target))
|
|
|
|
irc_left_channel (channel);
|
|
|
|
else
|
|
|
|
irc_remove_user_from_channel (user, channel);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buffer)
|
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (s->ctx, s);
|
2015-06-29 08:54:00 +02:00
|
|
|
formatter_add (&f, "#a<--#r #N #a#s#r #n",
|
|
|
|
ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target);
|
2015-06-28 02:49:28 +02:00
|
|
|
if (message)
|
|
|
|
formatter_add (&f, " (#m)", message);
|
|
|
|
log_formatter (s->ctx, buffer, 0, &f);
|
2015-06-07 05:28:57 +02:00
|
|
|
}
|
|
|
|
}
|
2015-06-07 04:20:39 +02:00
|
|
|
|
2016-09-23 22:50:30 +02:00
|
|
|
static void
|
|
|
|
irc_handle_kill (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 *comment = msg->params.vector[1];
|
|
|
|
|
|
|
|
if (irc_is_this_us (s, target))
|
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"You've been killed by #n (#m)", msg->prefix, comment);
|
|
|
|
}
|
|
|
|
|
2015-04-16 00:56:05 +02:00
|
|
|
static void
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_handle_mode (struct server *s, const struct irc_message *msg)
|
2015-04-16 00:56:05 +02:00
|
|
|
{
|
2015-05-21 19:57:12 +02:00
|
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const char *context = msg->params.vector[0];
|
|
|
|
|
|
|
|
// Join the modes back to a single string
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv copy = strv_make ();
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_vector (©, msg->params.vector + 1);
|
|
|
|
char *modes = strv_join (©, " ");
|
|
|
|
strv_free (©);
|
2015-05-21 19:57:12 +02:00
|
|
|
|
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-05-21 19:57:12 +02:00
|
|
|
|
2016-10-28 13:14:51 +02:00
|
|
|
int flags = 0;
|
|
|
|
if (channel
|
2020-10-16 20:19:52 +02:00
|
|
|
&& irc_handle_mode_channel (channel, msg->params.vector + 1))
|
2016-10-28 13:14:51 +02:00
|
|
|
// This is 90% automode spam, let's not let it steal attention,
|
|
|
|
// maybe this behaviour should be configurable though
|
|
|
|
flags = BUFFER_LINE_UNIMPORTANT;
|
2015-06-04 23:55:10 +02:00
|
|
|
|
2015-05-21 19:57:12 +02:00
|
|
|
if (buffer)
|
|
|
|
{
|
2016-10-28 13:14:51 +02:00
|
|
|
log_server (s, buffer, BUFFER_LINE_STATUS | flags,
|
2015-06-28 02:49:28 +02:00
|
|
|
"Mode #S [#S] by #n", context, modes, msg->prefix);
|
2015-05-21 19:57:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (irc_is_this_us (s, context))
|
|
|
|
{
|
2015-06-07 04:20:39 +02:00
|
|
|
irc_handle_mode_user (s, msg->params.vector + 1);
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer,
|
|
|
|
"User mode [#S] by #n", modes, msg->prefix);
|
2015-05-21 19:57:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
free (modes);
|
2015-04-16 00:56:05 +02:00
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
irc_handle_nick (struct server *s, const struct irc_message *msg)
|
2015-04-27 01:36:33 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
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;
|
|
|
|
|
2015-06-23 00:14:18 +02:00
|
|
|
bool lexicographically_different =
|
|
|
|
!!irc_server_strcmp (s, user->nickname, new_nickname);
|
2015-06-21 22:30:59 +02:00
|
|
|
|
|
|
|
// What the fuck, someone renamed themselves to ourselves
|
2015-05-09 22:10:58 +02:00
|
|
|
// TODO: probably log a message and force a reconnect
|
2015-06-23 00:14:18 +02:00
|
|
|
if (lexicographically_different
|
2015-06-21 22:30:59 +02:00
|
|
|
&& !irc_server_strcmp (s, new_nickname, s->irc_user->nickname))
|
2015-05-09 22:10:58 +02:00
|
|
|
return;
|
|
|
|
|
2015-06-23 00:14:18 +02:00
|
|
|
// Log a message in any PM buffer (we may even have one for ourselves)
|
2015-05-09 22:10:58 +02:00
|
|
|
struct buffer *pm_buffer =
|
|
|
|
str_map_find (&s->irc_buffer_map, user->nickname);
|
|
|
|
if (pm_buffer)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
2015-06-28 16:16:19 +02:00
|
|
|
log_nick_self (s, pm_buffer, new_nickname);
|
2015-06-28 02:49:28 +02:00
|
|
|
else
|
2015-06-28 16:16:19 +02:00
|
|
|
log_nick (s, pm_buffer, msg->prefix, new_nickname);
|
2015-06-23 00:14:18 +02:00
|
|
|
}
|
2015-05-15 20:05:27 +02:00
|
|
|
|
2015-06-23 00:14:18 +02:00
|
|
|
// 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);
|
2015-07-11 14:47:45 +02:00
|
|
|
}
|
2015-06-23 00:14:18 +02:00
|
|
|
|
2015-07-11 14:47:45 +02:00
|
|
|
if (pm_buffer && buffer_collision)
|
|
|
|
{
|
2015-06-23 00:14:18 +02:00
|
|
|
// 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;
|
|
|
|
}
|
2015-06-22 21:29:42 +02:00
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// The colliding user should be completely gone by now
|
2015-06-23 00:14:18 +02:00
|
|
|
if (lexicographically_different)
|
|
|
|
hard_assert (!str_map_find (&s->irc_users, new_nickname));
|
2015-06-22 21:29:42 +02:00
|
|
|
|
2015-06-23 00:14:18 +02:00
|
|
|
// 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);
|
2015-06-22 21:29:42 +02:00
|
|
|
|
2015-05-15 20:05:27 +02:00
|
|
|
char *x = xstrdup_printf ("%s.%s", s->name, new_nickname);
|
|
|
|
buffer_rename (s->ctx, pm_buffer, x);
|
|
|
|
free (x);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
{
|
2015-06-28 16:36:51 +02:00
|
|
|
log_nick_self (s, s->buffer, new_nickname);
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// Log a message in all open buffers on this server
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
|
2015-05-09 22:10:58 +02:00
|
|
|
struct buffer *buffer;
|
|
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
// We've already done that
|
2015-06-28 16:16:19 +02:00
|
|
|
if (buffer != pm_buffer)
|
|
|
|
log_nick_self (s, buffer, new_nickname);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
2015-06-28 16:16:19 +02:00
|
|
|
log_nick (s, buffer, msg->prefix, new_nickname);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-23 00:14:18 +02:00
|
|
|
// 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);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&user->nickname, xstrdup (new_nickname));
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_ctcp_reply (struct server *s,
|
|
|
|
const struct irc_message *msg, struct ctcp_chunk *chunk)
|
|
|
|
{
|
2015-06-28 16:57:08 +02:00
|
|
|
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);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2015-06-28 16:26:51 +02:00
|
|
|
if (!buffer)
|
2015-06-29 21:39:40 +02:00
|
|
|
{
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
log_outcoming_orphan_notice (s, target, text->str);
|
2015-06-28 16:26:51 +02:00
|
|
|
return;
|
2015-06-29 21:39:40 +02:00
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2015-06-28 16:26:51 +02:00
|
|
|
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);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2015-06-06 23:56:43 +02:00
|
|
|
const char *message = NULL;
|
2015-05-09 22:10:58 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-05-09 22:10:58 +02:00
|
|
|
|
2016-10-23 16:30:44 +02:00
|
|
|
// It would be weird for this to be false
|
2015-05-09 22:10:58 +02:00
|
|
|
if (user && channel)
|
2015-05-09 23:50:44 +02:00
|
|
|
{
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
irc_left_channel (channel);
|
|
|
|
else
|
|
|
|
irc_remove_user_from_channel (user, channel);
|
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
|
|
|
|
if (buffer)
|
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (s->ctx, s);
|
2015-06-29 08:54:00 +02:00
|
|
|
formatter_add (&f, "#a<--#r #N #a#s#r #S",
|
|
|
|
ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name);
|
2015-06-28 02:49:28 +02:00
|
|
|
if (message)
|
|
|
|
formatter_add (&f, " (#m)", message);
|
2015-08-08 21:17:32 +02:00
|
|
|
log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &f);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2015-04-27 01:36:33 +02:00
|
|
|
|
|
|
|
// Annoying thing
|
|
|
|
*strchr (buf, '\n') = '\0';
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
static void irc_send_ctcp_reply (struct server *s, const char *recipient,
|
2015-04-27 01:47:21 +02:00
|
|
|
const char *format, ...) ATTRIBUTE_PRINTF (3, 4);
|
|
|
|
|
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_send_ctcp_reply (struct server *s,
|
2015-04-27 01:47:21 +02:00
|
|
|
const char *recipient, const char *format, ...)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str m = str_make ();
|
2015-04-27 01:47:21 +02:00
|
|
|
|
|
|
|
va_list ap;
|
|
|
|
va_start (ap, format);
|
|
|
|
str_append_vprintf (&m, format, ap);
|
|
|
|
va_end (ap);
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_send (s, "NOTICE %s :\x01%s\x01", recipient, m.str);
|
2016-02-09 05:10:41 +01:00
|
|
|
str_free (&m);
|
2015-04-27 01:47:21 +02:00
|
|
|
}
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_ctcp_request (struct server *s,
|
2015-04-26 22:31:07 +02:00
|
|
|
const struct irc_message *msg, struct ctcp_chunk *chunk)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-06-28 16:57:08 +02:00
|
|
|
const char *target = msg->params.vector[0];
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
2015-08-06 23:00:16 +02:00
|
|
|
{
|
|
|
|
if (s->cap_echo_message)
|
|
|
|
log_ctcp_query (s, target, chunk->tag.str);
|
|
|
|
if (!irc_is_this_us (s, target))
|
|
|
|
return;
|
|
|
|
}
|
2015-04-19 22:19:52 +02:00
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (s->ctx, s);
|
2015-08-10 07:39:43 +02:00
|
|
|
formatter_add (&f, "CTCP requested by #n", msg->prefix);
|
2015-09-24 01:39:49 +02:00
|
|
|
if (irc_is_channel (s, irc_skip_statusmsg (s, target)))
|
2015-08-10 07:39:43 +02:00
|
|
|
formatter_add (&f, " (to #S)", target);
|
|
|
|
formatter_add (&f, ": #S", chunk->tag.str);
|
|
|
|
log_formatter (s->ctx, s->buffer, BUFFER_LINE_STATUS, &f);
|
2015-04-19 22:19:52 +02:00
|
|
|
|
2015-08-06 23:00:16 +02:00
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
2015-04-27 01:36:33 +02:00
|
|
|
|
|
|
|
if (!strcmp (chunk->tag.str, "CLIENTINFO"))
|
2015-08-06 23:00:16 +02:00
|
|
|
irc_send_ctcp_reply (s, nickname, "CLIENTINFO %s %s %s %s",
|
2015-04-27 01:47:21 +02:00
|
|
|
"PING", "VERSION", "TIME", "CLIENTINFO");
|
2015-04-27 01:36:33 +02:00
|
|
|
else if (!strcmp (chunk->tag.str, "PING"))
|
2015-08-06 23:00:16 +02:00
|
|
|
irc_send_ctcp_reply (s, nickname, "PING %s", chunk->text.str);
|
2015-04-27 01:36:33 +02:00
|
|
|
else if (!strcmp (chunk->tag.str, "VERSION"))
|
|
|
|
{
|
|
|
|
struct utsname info;
|
|
|
|
if (uname (&info))
|
|
|
|
LOG_LIBC_FAILURE ("uname");
|
|
|
|
else
|
2015-08-06 23:00:16 +02:00
|
|
|
irc_send_ctcp_reply (s, nickname, "VERSION %s %s on %s %s",
|
2015-04-27 01:47:21 +02:00
|
|
|
PROGRAM_NAME, PROGRAM_VERSION, info.sysname, info.machine);
|
2015-04-27 01:36:33 +02:00
|
|
|
}
|
|
|
|
else if (!strcmp (chunk->tag.str, "TIME"))
|
|
|
|
{
|
|
|
|
char buf[26];
|
|
|
|
if (!ctime_now (buf))
|
|
|
|
LOG_LIBC_FAILURE ("asctime_r");
|
|
|
|
else
|
2015-08-06 23:00:16 +02:00
|
|
|
irc_send_ctcp_reply (s, nickname, "TIME %s", buf);
|
2015-04-27 01:36:33 +02:00
|
|
|
}
|
2015-04-26 22:31:07 +02:00
|
|
|
|
2015-08-06 23:00:16 +02:00
|
|
|
free (nickname);
|
2015-04-26 22:31:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_privmsg_text (struct server *s,
|
2015-04-26 22:31:07 +02:00
|
|
|
const struct irc_message *msg, struct str *text, bool is_action)
|
|
|
|
{
|
|
|
|
const char *target = msg->params.vector[0];
|
2015-04-30 00:02:14 +02:00
|
|
|
struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);
|
2015-06-28 16:26:51 +02:00
|
|
|
if (!buffer)
|
2015-06-29 21:39:40 +02:00
|
|
|
{
|
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
|
|
|
log_outcoming_orphan_privmsg (s, target, text->str);
|
2015-06-28 16:26:51 +02:00
|
|
|
return;
|
2015-06-29 21:39:40 +02:00
|
|
|
}
|
2015-04-19 22:19:52 +02:00
|
|
|
|
2015-06-28 16:26:51 +02:00
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
2015-07-27 00:08:28 +02:00
|
|
|
char *prefixes = irc_get_privmsg_prefix
|
2015-06-28 16:26:51 +02:00
|
|
|
(s, str_map_find (&s->irc_users, nickname), target);
|
2015-06-07 06:15:31 +02:00
|
|
|
|
2021-07-24 09:22:01 +02:00
|
|
|
// Make autocomplete offer recent speakers first on partial matches
|
|
|
|
// (note that freshly joined users also move to the front)
|
2021-07-23 18:42:21 +02:00
|
|
|
struct user *user;
|
|
|
|
struct channel_user *channel_user;
|
2021-07-24 09:22:01 +02:00
|
|
|
if (!irc_is_this_us (s, msg->prefix) && buffer->channel
|
2021-07-23 18:42:21 +02:00
|
|
|
&& (user = str_map_find (&s->irc_users, nickname))
|
|
|
|
&& (channel_user = irc_channel_get_user (buffer->channel, user)))
|
|
|
|
{
|
|
|
|
LIST_UNLINK (buffer->channel->users, channel_user);
|
|
|
|
LIST_PREPEND (buffer->channel->users, channel_user);
|
|
|
|
}
|
|
|
|
|
2015-06-28 16:26:51 +02:00
|
|
|
// 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))
|
2015-04-19 22:19:52 +02:00
|
|
|
{
|
2015-06-28 16:26:51 +02:00
|
|
|
if (is_action)
|
|
|
|
log_outcoming_action (s, buffer, nickname, text->str);
|
2015-06-28 02:49:28 +02:00
|
|
|
else
|
2015-06-28 16:26:51 +02:00
|
|
|
log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str);
|
2015-04-19 22:19:52 +02:00
|
|
|
}
|
2015-06-28 16:26:51 +02:00
|
|
|
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,
|
2015-06-29 21:39:40 +02:00
|
|
|
"#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);
|
2015-06-28 16:16:19 +02:00
|
|
|
|
2015-06-28 16:26:51 +02:00
|
|
|
free (nickname);
|
2015-07-27 00:08:28 +02:00
|
|
|
free (prefixes);
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2015-04-26 22:31:07 +02:00
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_privmsg (struct server *s, const struct irc_message *msg)
|
2015-04-26 22:31:07 +02:00
|
|
|
{
|
|
|
|
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)
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_privmsg_text (s, msg, &iter->text, false);
|
2015-04-26 22:31:07 +02:00
|
|
|
else if (!strcmp (iter->tag.str, "ACTION"))
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_privmsg_text (s, msg, &iter->text, true);
|
2015-04-26 22:31:07 +02:00
|
|
|
else
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_ctcp_request (s, msg, iter);
|
2015-04-26 22:31:07 +02:00
|
|
|
ctcp_destroy (chunks);
|
|
|
|
}
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
static void
|
|
|
|
log_quit (struct server *s,
|
|
|
|
struct buffer *buffer, const char *prefix, const char *reason)
|
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (s->ctx, s);
|
2015-06-29 08:54:00 +02:00
|
|
|
formatter_add (&f, "#a<--#r #N #a#s#r",
|
|
|
|
ATTR_PART, prefix, ATTR_PART, "has quit");
|
2015-06-28 02:49:28 +02:00
|
|
|
if (reason)
|
|
|
|
formatter_add (&f, " (#m)", reason);
|
2015-08-08 21:17:32 +02:00
|
|
|
log_formatter (s->ctx, buffer, BUFFER_LINE_UNIMPORTANT, &f);
|
2015-06-28 02:49:28 +02:00
|
|
|
}
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_quit (struct server *s, const struct irc_message *msg)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-04-20 00:08:18 +02:00
|
|
|
if (!msg->prefix)
|
|
|
|
return;
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// What the fuck, the server never sends this back
|
2015-04-30 00:02:14 +02:00
|
|
|
if (irc_is_this_us (s, msg->prefix))
|
2015-04-20 00:08:18 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
2015-04-30 00:02:14 +02:00
|
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
2015-04-20 00:08:18 +02:00
|
|
|
free (nickname);
|
|
|
|
if (!user)
|
|
|
|
return;
|
|
|
|
|
2015-06-06 23:56:43 +02:00
|
|
|
const char *message = NULL;
|
2015-04-20 00:08:18 +02:00
|
|
|
if (msg->params.len > 0)
|
|
|
|
message = msg->params.vector[0];
|
|
|
|
|
|
|
|
// Log a message in any PM buffer
|
|
|
|
struct buffer *buffer =
|
2015-04-30 00:02:14 +02:00
|
|
|
str_map_find (&s->irc_buffer_map, user->nickname);
|
2015-04-20 00:08:18 +02:00
|
|
|
if (buffer)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_quit (s, buffer, msg->prefix, message);
|
2015-04-20 21:49:46 +02:00
|
|
|
|
|
|
|
// TODO: set some kind of a flag in the buffer and when the user
|
2015-06-01 21:46:44 +02:00
|
|
|
// reappears on a channel (JOIN), log a "is back online" message.
|
2015-04-20 21:49:46 +02:00
|
|
|
// Also set this flag when we receive a "no such nick" numeric
|
|
|
|
// and reset it when we send something to the buffer.
|
2015-04-20 00:08:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Log a message in all channels the user is in
|
|
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name)))
|
|
|
|
log_quit (s, buffer, msg->prefix, message);
|
2015-04-20 21:49:46 +02:00
|
|
|
|
2015-04-25 00:36:02 +02:00
|
|
|
// This destroys "iter" which doesn't matter to us
|
|
|
|
irc_remove_user_from_channel (user, iter->channel);
|
2015-04-20 00:08:18 +02:00
|
|
|
}
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2020-10-16 16:56:24 +02:00
|
|
|
static void
|
|
|
|
irc_handle_tagmsg (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
// TODO: here we can process "typing" tags, once we find this useful
|
|
|
|
(void) s;
|
|
|
|
(void) msg;
|
|
|
|
}
|
|
|
|
|
2015-04-17 22:45:58 +02:00
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_handle_topic (struct server *s, const struct irc_message *msg)
|
2015-04-17 22:45:58 +02:00
|
|
|
{
|
2015-04-21 21:47:34 +02:00
|
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
|
|
const char *topic = msg->params.vector[1];
|
2015-04-30 00:02:14 +02:00
|
|
|
if (!irc_is_channel (s, channel_name))
|
2015-04-21 21:47:34 +02:00
|
|
|
return;
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-04-21 21:47:34 +02:00
|
|
|
|
2020-10-04 08:44:16 +02:00
|
|
|
// It would be weird for this to be false
|
2015-04-21 21:47:34 +02:00
|
|
|
if (channel)
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&channel->topic, xstrdup (topic));
|
2015-04-21 21:47:34 +02:00
|
|
|
|
|
|
|
if (buffer)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"",
|
|
|
|
msg->prefix, "has changed the topic to", topic);
|
2015-04-21 21:47:34 +02:00
|
|
|
}
|
2015-04-17 22:45:58 +02:00
|
|
|
}
|
|
|
|
|
2022-02-04 22:45:04 +01:00
|
|
|
static void
|
|
|
|
irc_handle_wallops (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const char *message = msg->params.vector[0];
|
|
|
|
log_server (s, s->buffer, 0, "<#n> #m", msg->prefix, message);
|
|
|
|
}
|
|
|
|
|
2015-05-09 06:11:36 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-02-09 05:10:41 +01:00
|
|
|
static struct irc_handler g_irc_handlers[] =
|
2015-04-16 00:56:05 +02:00
|
|
|
{
|
|
|
|
// This list needs to stay sorted
|
2021-05-28 02:11:23 +02:00
|
|
|
{ "AUTHENTICATE", irc_handle_authenticate },
|
|
|
|
{ "AWAY", irc_handle_away },
|
|
|
|
{ "CAP", irc_handle_cap },
|
2021-05-30 08:06:38 +02:00
|
|
|
{ "CHGHOST", irc_handle_chghost },
|
2021-05-28 02:11:23 +02:00
|
|
|
{ "ERROR", irc_handle_error },
|
|
|
|
{ "INVITE", irc_handle_invite },
|
|
|
|
{ "JOIN", irc_handle_join },
|
|
|
|
{ "KICK", irc_handle_kick },
|
|
|
|
{ "KILL", irc_handle_kill },
|
|
|
|
{ "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 },
|
|
|
|
{ "TAGMSG", irc_handle_tagmsg },
|
|
|
|
{ "TOPIC", irc_handle_topic },
|
2022-02-04 22:45:04 +01:00
|
|
|
{ "WALLOPS", irc_handle_wallops },
|
2015-04-16 00:56:05 +02:00
|
|
|
};
|
|
|
|
|
2015-04-22 21:59:50 +02:00
|
|
|
static bool
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_try_parse_word_for_userhost (struct server *s, const char *word)
|
2015-04-22 21:59:50 +02:00
|
|
|
{
|
|
|
|
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))
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_user_host, xstrndup (word + matches[1].rm_so,
|
|
|
|
matches[1].rm_eo - matches[1].rm_so));
|
2015-04-22 21:59:50 +02:00
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
regfree (&re);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
|
2015-04-22 21:59:50 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (m, " ", true, &v);
|
2015-04-22 21:59:50 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
2015-04-30 00:02:14 +02:00
|
|
|
if (irc_try_parse_word_for_userhost (s, v.vector[i]))
|
2015-04-22 21:59:50 +02:00
|
|
|
break;
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-04-22 21:59:50 +02:00
|
|
|
}
|
|
|
|
|
2015-07-18 13:23:22 +02:00
|
|
|
static bool process_input_utf8
|
2015-11-19 19:09:05 +01:00
|
|
|
(struct app_context *, struct buffer *, const char *, int);
|
2015-11-22 23:04:51 +01:00
|
|
|
static void on_autoaway_timer (struct app_context *ctx);
|
2015-07-18 13:23:22 +02:00
|
|
|
|
2015-05-15 05:54:03 +02:00
|
|
|
static void
|
|
|
|
irc_on_registered (struct server *s, const char *nickname)
|
|
|
|
{
|
|
|
|
s->irc_user = irc_get_or_make_user (s, nickname);
|
2015-06-07 04:20:39 +02:00
|
|
|
str_reset (&s->irc_user_mode);
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_user_host, NULL);
|
2015-05-15 05:54:03 +02:00
|
|
|
|
|
|
|
s->state = IRC_REGISTERED;
|
|
|
|
refresh_prompt (s->ctx);
|
|
|
|
|
2015-05-22 22:13:28 +02:00
|
|
|
// XXX: we can also use WHOIS if it's not supported (optional by RFC 2812)
|
2018-01-08 21:53:42 +01:00
|
|
|
// TODO: maybe rather always use RPL_ISUPPORT NICKLEN & USERLEN & HOSTLEN
|
|
|
|
// since we don't seem to follow any subsequent changes in userhost;
|
|
|
|
// unrealircd sends RPL_HOSTHIDDEN (396), which has an optional user part,
|
|
|
|
// and there is also CAP CHGHOST which /may/ send it to ourselves
|
2015-05-15 05:54:03 +02:00
|
|
|
irc_send (s, "USERHOST %s", s->irc_user->nickname);
|
|
|
|
|
2015-11-22 23:04:51 +01:00
|
|
|
// A little hack that reinstates auto-away status when we get disconnected
|
|
|
|
if (s->autoaway_active)
|
|
|
|
on_autoaway_timer (s->ctx);
|
|
|
|
|
2015-07-18 13:23:22 +02:00
|
|
|
const char *command = get_config_string (s->config, "command");
|
|
|
|
if (command)
|
|
|
|
{
|
|
|
|
log_server_debug (s, "Executing \"#s\"", command);
|
2015-11-19 19:09:05 +01:00
|
|
|
process_input_utf8 (s->ctx, s->buffer, command, 0);
|
2015-07-18 13:23:22 +02:00
|
|
|
}
|
|
|
|
|
2015-07-18 14:12:34 +02:00
|
|
|
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);
|
2015-05-15 05:54:03 +02:00
|
|
|
}
|
|
|
|
|
2015-05-22 22:13:28 +02:00
|
|
|
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];
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (response, " ", true, &v);
|
2015-05-22 22:13:28 +02:00
|
|
|
|
|
|
|
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))
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_user_host, xstrdup (userhost));
|
2015-05-22 22:13:28 +02:00
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-05-22 22:13:28 +02:00
|
|
|
}
|
|
|
|
|
2015-06-10 22:15:58 +02:00
|
|
|
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?
|
|
|
|
}
|
|
|
|
|
2015-05-24 21:56:46 +02:00
|
|
|
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];
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// Just push the nicknames to a string vector to process later
|
2015-05-24 21:56:46 +02:00
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
|
|
if (channel)
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (nicks, " ", true, &channel->names_buf);
|
2020-10-16 17:57:32 +02:00
|
|
|
else
|
|
|
|
log_server_status (s, s->buffer, "Users on #S: #S",
|
|
|
|
channel_name, nicks);
|
2015-05-24 21:56:46 +02:00
|
|
|
}
|
|
|
|
|
2015-07-26 23:12:36 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct channel_user_sort_entry
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
struct server *s; ///< Server (because of the qsort API)
|
2015-07-26 23:12:36 +02:00
|
|
|
struct channel_user *channel_user; ///< Channel user
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
channel_user_sort_entry_cmp (const void *entry_a, const void *entry_b)
|
|
|
|
{
|
|
|
|
const struct channel_user_sort_entry *a = entry_a;
|
|
|
|
const struct channel_user_sort_entry *b = entry_b;
|
|
|
|
struct server *s = a->s;
|
|
|
|
|
|
|
|
// First order by the most significant channel user prefix
|
|
|
|
const char *prio_a = strchr (s->irc_chanuser_prefixes,
|
2020-10-04 08:22:20 +02:00
|
|
|
a->channel_user->prefixes[0]);
|
2015-07-26 23:12:36 +02:00
|
|
|
const char *prio_b = strchr (s->irc_chanuser_prefixes,
|
2020-10-04 08:22:20 +02:00
|
|
|
b->channel_user->prefixes[0]);
|
2015-07-26 23:12:36 +02:00
|
|
|
|
|
|
|
// Put unrecognized prefixes at the end of the list
|
|
|
|
if (prio_a || prio_b)
|
|
|
|
{
|
|
|
|
if (!prio_a) return 1;
|
|
|
|
if (!prio_b) return -1;
|
|
|
|
|
|
|
|
if (prio_a != prio_b)
|
|
|
|
return prio_a - prio_b;
|
|
|
|
}
|
|
|
|
|
|
|
|
return irc_server_strcmp (s,
|
|
|
|
a->channel_user->user->nickname,
|
|
|
|
b->channel_user->user->nickname);
|
|
|
|
}
|
|
|
|
|
2021-07-23 19:00:25 +02:00
|
|
|
static void
|
|
|
|
irc_sort_channel_users (struct channel *channel)
|
|
|
|
{
|
|
|
|
size_t n_users = channel->users_len;
|
|
|
|
struct channel_user_sort_entry entries[n_users], *p = entries;
|
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
|
|
{
|
|
|
|
p->s = channel->s;
|
|
|
|
p->channel_user = iter;
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
|
|
|
|
qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
|
|
|
|
|
|
|
|
channel->users = NULL;
|
|
|
|
while (p-- != entries)
|
|
|
|
LIST_PREPEND (channel->users, p->channel_user);
|
|
|
|
}
|
|
|
|
|
2015-07-26 23:12:36 +02:00
|
|
|
static char *
|
2020-10-16 20:19:52 +02:00
|
|
|
make_channel_users_list (struct channel *channel)
|
2015-07-26 23:12:36 +02:00
|
|
|
{
|
2021-07-23 18:30:13 +02:00
|
|
|
size_t n_users = channel->users_len;
|
|
|
|
struct channel_user_sort_entry entries[n_users], *p = entries;
|
2015-07-26 23:12:36 +02:00
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
|
|
{
|
2021-07-23 18:30:13 +02:00
|
|
|
p->s = channel->s;
|
|
|
|
p->channel_user = iter;
|
|
|
|
p++;
|
2015-07-26 23:12:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
// Make names of users that are away italicised, constructing a formatter
|
|
|
|
// and adding a new attribute seems like unnecessary work
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str list = str_make ();
|
2021-07-23 18:30:13 +02:00
|
|
|
for (size_t i = 0; i < n_users; i++)
|
2015-07-26 23:12:36 +02:00
|
|
|
{
|
2020-10-16 16:45:40 +02:00
|
|
|
struct channel_user *channel_user = entries[i].channel_user;
|
|
|
|
if (channel_user->user->away) str_append_c (&list, '\x1d');
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_get_channel_user_prefix (channel->s, channel_user, &list);
|
2020-10-16 16:45:40 +02:00
|
|
|
str_append (&list, channel_user->user->nickname);
|
|
|
|
if (channel_user->user->away) str_append_c (&list, '\x1d');
|
2015-07-26 23:12:36 +02:00
|
|
|
str_append_c (&list, ' ');
|
|
|
|
}
|
|
|
|
if (list.len)
|
|
|
|
list.str[--list.len] = '\0';
|
|
|
|
return str_steal (&list);
|
|
|
|
}
|
|
|
|
|
2015-06-06 00:38:58 +02:00
|
|
|
static void
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_sync_channel_user (struct channel *channel, const char *nickname,
|
|
|
|
const char *prefixes)
|
2015-06-06 00:38:58 +02:00
|
|
|
{
|
2020-10-16 20:19:52 +02:00
|
|
|
struct user *user = irc_get_or_make_user (channel->s, nickname);
|
2015-06-06 00:38:58 +02:00
|
|
|
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
|
2020-10-04 08:22:20 +02:00
|
|
|
if (channel_user->prefixes[0] != prefixes[0])
|
|
|
|
cstr_set (&channel_user->prefixes, xstrdup (prefixes));
|
2015-06-06 00:38:58 +02:00
|
|
|
}
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
static void
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names_finish (struct channel *channel)
|
2020-10-16 16:45:40 +02:00
|
|
|
{
|
2020-10-16 20:19:52 +02:00
|
|
|
struct server *s = channel->s;
|
2020-10-16 16:45:40 +02:00
|
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
|
|
|
|
if (buffer)
|
|
|
|
{
|
2020-10-16 20:19:52 +02:00
|
|
|
log_server_status (channel->s, buffer, "Users on #S: #&m",
|
|
|
|
channel->name, make_channel_users_list (channel));
|
2020-10-16 16:45:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-24 21:56:46 +02:00
|
|
|
static void
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names (struct channel *channel)
|
2015-05-24 21:56:46 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map present = str_map_make (NULL);
|
2020-10-16 20:19:52 +02:00
|
|
|
present.key_xfrm = channel->s->irc_strxfrm;
|
2015-05-24 21:56:46 +02:00
|
|
|
|
2021-07-23 19:00:25 +02:00
|
|
|
// Either that, or there is no other inhabitant, and sorting does nothing
|
|
|
|
bool we_have_just_joined = channel->users_len == 1;
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv *updates = &channel->names_buf;
|
2015-05-25 08:23:08 +02:00
|
|
|
for (size_t i = 0; i < updates->len; i++)
|
|
|
|
{
|
|
|
|
const char *item = updates->vector[i];
|
2020-10-16 20:19:52 +02:00
|
|
|
size_t n_prefixes = strspn (item, channel->s->irc_chanuser_prefixes);
|
2015-06-06 00:10:20 +02:00
|
|
|
const char *nickname = item + n_prefixes;
|
|
|
|
|
|
|
|
// Store the nickname in a hashset
|
2015-06-06 00:38:58 +02:00
|
|
|
str_map_set (&present, nickname, (void *) 1);
|
2015-06-05 00:57:39 +02:00
|
|
|
|
2015-06-06 00:38:58 +02:00
|
|
|
char *prefixes = xstrndup (item, n_prefixes);
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_sync_channel_user (channel, nickname, prefixes);
|
2015-06-06 00:38:58 +02:00
|
|
|
free (prefixes);
|
2015-05-25 08:23:08 +02:00
|
|
|
}
|
|
|
|
|
2015-06-06 00:10:20 +02:00
|
|
|
// Get rid of channel users missing from "updates"
|
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
2015-06-06 00:38:58 +02:00
|
|
|
if (!str_map_find (&present, iter->user->nickname))
|
2015-06-06 00:10:20 +02:00
|
|
|
irc_channel_unlink_user (channel, iter);
|
2015-05-25 08:23:08 +02:00
|
|
|
|
2015-06-06 00:38:58 +02:00
|
|
|
str_map_free (&present);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_reset (&channel->names_buf);
|
2015-06-03 21:51:35 +02:00
|
|
|
|
2021-07-23 19:00:25 +02:00
|
|
|
if (we_have_just_joined)
|
|
|
|
irc_sort_channel_users (channel);
|
2020-10-16 16:45:40 +02:00
|
|
|
if (!channel->show_names_after_who)
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names_finish (channel);
|
2015-05-24 21:56:46 +02:00
|
|
|
}
|
|
|
|
|
2015-07-26 23:12:36 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-24 21:56:46 +02:00
|
|
|
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, "*"))
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
|
2015-05-24 21:56:46 +02:00
|
|
|
struct channel *channel;
|
|
|
|
while ((channel = str_map_iter_next (&iter)))
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names (channel);
|
2015-05-24 21:56:46 +02:00
|
|
|
}
|
|
|
|
else if (channel)
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names (channel);
|
2015-05-24 21:56:46 +02:00
|
|
|
}
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
static bool
|
|
|
|
irc_handle_rpl_whoreply (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 7)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Sequence: channel, user, host, server, nick, chars
|
|
|
|
const char *channel_name = msg->params.vector[1];
|
|
|
|
const char *nickname = msg->params.vector[5];
|
|
|
|
const char *chars = msg->params.vector[6];
|
|
|
|
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, channel_name);
|
|
|
|
struct user *user = str_map_find (&s->irc_users, nickname);
|
|
|
|
|
|
|
|
// This makes sense to set only with the away-notify capability so far.
|
2020-10-19 04:21:52 +02:00
|
|
|
if (!channel || !channel->show_names_after_who)
|
2020-10-16 16:45:40 +02:00
|
|
|
return false;
|
|
|
|
|
2020-10-19 04:21:52 +02:00
|
|
|
// We track ourselves by other means and we can't track PM-only users yet.
|
|
|
|
if (user && user != s->irc_user && user->channels)
|
|
|
|
user->away = *chars == 'G';
|
2020-10-16 16:45:40 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
irc_handle_rpl_endofwho (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 2)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const char *target = msg->params.vector[1];
|
|
|
|
|
|
|
|
struct channel *channel = str_map_find (&s->irc_channels, target);
|
|
|
|
if (!channel || !channel->show_names_after_who)
|
|
|
|
return false;
|
|
|
|
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_process_names_finish (channel);
|
2020-10-16 16:45:40 +02:00
|
|
|
channel->show_names_after_who = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-02 23:42:04 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-02 23:42:04 +02:00
|
|
|
|
2015-06-06 00:54:41 +02:00
|
|
|
if (channel)
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&channel->topic, xstrdup (topic));
|
2015-06-06 00:54:41 +02:00
|
|
|
|
2015-06-02 23:42:04 +02:00
|
|
|
if (buffer)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, buffer, "The topic is: #m", topic);
|
2015-06-02 23:42:04 +02:00
|
|
|
}
|
|
|
|
|
2015-06-07 00:10:36 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-07 00:10:36 +02:00
|
|
|
|
|
|
|
if (channel)
|
|
|
|
{
|
|
|
|
str_reset (&channel->no_param_modes);
|
|
|
|
str_map_clear (&channel->param_modes);
|
|
|
|
|
2020-10-16 20:19:52 +02:00
|
|
|
irc_handle_mode_channel (channel, msg->params.vector + 1);
|
2015-06-07 00:10:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// XXX: do we want to log a message?
|
|
|
|
}
|
|
|
|
|
2015-06-07 00:39:35 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-07 00:39:35 +02:00
|
|
|
|
|
|
|
if (buffer)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, buffer, "Channel created on #&s",
|
|
|
|
make_time_string (created));
|
2015-06-07 00:39:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-07 00:41:59 +02:00
|
|
|
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);
|
2018-01-08 21:43:18 +01:00
|
|
|
hard_assert (channel || !buffer);
|
2015-06-07 00:41:59 +02:00
|
|
|
|
|
|
|
if (buffer)
|
|
|
|
{
|
2015-06-29 08:54:00 +02:00
|
|
|
log_server_status (s, buffer, "Topic set by #N on #&s",
|
|
|
|
who, make_time_string (changed));
|
2015-06-07 00:41:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-20 22:50:28 +02:00
|
|
|
static void
|
|
|
|
irc_handle_rpl_inviting (struct server *s, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
if (msg->params.len < 3)
|
|
|
|
return;
|
|
|
|
|
2015-09-24 00:35:30 +02:00
|
|
|
const char *nickname = msg->params.vector[1];
|
|
|
|
const char *channel_name = msg->params.vector[2];
|
2015-06-20 22:50:28 +02:00
|
|
|
|
2015-07-09 22:32:31 +02:00
|
|
|
struct buffer *buffer;
|
2015-06-20 22:50:28 +02:00
|
|
|
if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
|
|
|
|
buffer = s->buffer;
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, buffer,
|
|
|
|
"You have invited #n to #S", nickname, channel_name);
|
2015-06-20 22:50:28 +02:00
|
|
|
}
|
|
|
|
|
2015-07-09 02:46:31 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-20 22:50:28 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-25 22:54:04 +02:00
|
|
|
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;
|
|
|
|
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_chanuser_modes, xstrndup (modes, n_prefixes));
|
|
|
|
cstr_set (&s->irc_chanuser_prefixes, xstrndup (prefixes, n_prefixes));
|
2015-06-01 21:45:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_isupport_casemapping (struct server *s, char *value)
|
|
|
|
{
|
|
|
|
if (!strcmp (value, "ascii"))
|
2015-07-09 01:48:34 +02:00
|
|
|
irc_set_casemapping (s, tolower_ascii, tolower_ascii_strxfrm);
|
2015-06-01 21:45:04 +02:00
|
|
|
else if (!strcmp (value, "rfc1459"))
|
2015-07-09 01:48:34 +02:00
|
|
|
irc_set_casemapping (s, irc_tolower, irc_strxfrm);
|
2015-06-01 21:45:04 +02:00
|
|
|
else if (!strcmp (value, "rfc1459-strict"))
|
2015-07-09 01:48:34 +02:00
|
|
|
irc_set_casemapping (s, irc_tolower_strict, irc_strxfrm_strict);
|
2015-06-01 21:45:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_isupport_chantypes (struct server *s, char *value)
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_chantypes, xstrdup (value));
|
2015-06-01 21:45:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
irc_handle_isupport_idchan (struct server *s, char *value)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str prefixes = str_make ();
|
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (value, ",", true, &v);
|
2015-06-01 21:45:04 +02:00
|
|
|
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);
|
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_idchan_prefixes, str_steal (&prefixes));
|
2015-06-01 21:45:04 +02:00
|
|
|
}
|
2015-05-25 22:54:04 +02:00
|
|
|
|
2015-06-01 21:45:04 +02:00
|
|
|
static void
|
|
|
|
irc_handle_isupport_statusmsg (struct server *s, char *value)
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_statusmsg, xstrdup (value));
|
2015-05-25 22:54:04 +02:00
|
|
|
}
|
|
|
|
|
2021-06-17 12:08:08 +02:00
|
|
|
static void
|
|
|
|
irc_handle_isupport_extban (struct server *s, char *value)
|
|
|
|
{
|
|
|
|
s->irc_extban_prefix = 0;
|
|
|
|
if (*value && *value != ',')
|
|
|
|
s->irc_extban_prefix = *value++;
|
|
|
|
|
|
|
|
cstr_set (&s->irc_extban_types, xstrdup (*value == ',' ? ++value : ""));
|
|
|
|
}
|
|
|
|
|
2015-06-03 23:17:10 +02:00
|
|
|
static void
|
|
|
|
irc_handle_isupport_chanmodes (struct server *s, char *value)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (value, ",", true, &v);
|
2015-06-03 23:17:10 +02:00
|
|
|
if (v.len >= 4)
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->irc_chanmodes_list, xstrdup (v.vector[0]));
|
|
|
|
cstr_set (&s->irc_chanmodes_param_always, xstrdup (v.vector[1]));
|
|
|
|
cstr_set (&s->irc_chanmodes_param_when_set, xstrdup (v.vector[2]));
|
|
|
|
cstr_set (&s->irc_chanmodes_param_never, xstrdup (v.vector[3]));
|
2015-06-03 23:17:10 +02:00
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-06-03 23:17:10 +02:00
|
|
|
}
|
|
|
|
|
2015-06-17 21:19:09 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-05-30 22:34:09 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-09 01:28:52 +02:00
|
|
|
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);
|
2021-06-17 12:08:08 +02:00
|
|
|
MATCH ("EXTBAN", irc_handle_isupport_extban);
|
2015-07-09 01:28:52 +02:00
|
|
|
MATCH ("CHANMODES", irc_handle_isupport_chanmodes);
|
|
|
|
MATCH ("MODES", irc_handle_isupport_modes);
|
|
|
|
|
|
|
|
#undef MATCH
|
|
|
|
}
|
|
|
|
|
2015-05-25 22:54:04 +02:00
|
|
|
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';
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str value_unescaped = str_make ();
|
2015-05-30 22:34:09 +02:00
|
|
|
unescape_isupport_value (value, &value_unescaped);
|
2015-07-09 01:28:52 +02:00
|
|
|
dispatch_isupport (s, param, value_unescaped.str);
|
2015-05-30 22:34:09 +02:00
|
|
|
str_free (&value_unescaped);
|
2015-05-25 22:54:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-20 22:50:28 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-04-21 22:08:18 +02:00
|
|
|
static void
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_process_numeric (struct server *s,
|
2015-04-21 22:08:18 +02:00
|
|
|
const struct irc_message *msg, unsigned long numeric)
|
|
|
|
{
|
|
|
|
// Numerics typically have human-readable information
|
2015-05-15 05:54:03 +02:00
|
|
|
|
2015-04-21 22:08:18 +02:00
|
|
|
// 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
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv copy = strv_make ();
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_vector (©, msg->params.vector + !!msg->params.len);
|
2015-04-21 22:08:18 +02:00
|
|
|
|
2015-06-02 23:31:44 +02:00
|
|
|
struct buffer *buffer = s->buffer;
|
2015-11-23 08:33:58 +01:00
|
|
|
int flags = BUFFER_LINE_STATUS;
|
2015-04-21 22:08:18 +02:00
|
|
|
switch (numeric)
|
|
|
|
{
|
2015-04-22 21:59:50 +02:00
|
|
|
case IRC_RPL_WELCOME:
|
2015-05-15 05:54:03 +02:00
|
|
|
irc_on_registered (s, msg->params.vector[0]);
|
|
|
|
|
2015-04-22 21:59:50 +02:00
|
|
|
// We still issue a USERHOST anyway as this is in general unreliable
|
2015-04-24 22:33:23 +02:00
|
|
|
if (msg->params.len == 2)
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]);
|
2015-04-22 21:59:50 +02:00
|
|
|
break;
|
2015-05-24 21:56:46 +02:00
|
|
|
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_ISUPPORT:
|
2015-06-07 00:10:36 +02:00
|
|
|
irc_handle_rpl_isupport (s, msg); break;
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_USERHOST:
|
2015-06-07 00:10:36 +02:00
|
|
|
irc_handle_rpl_userhost (s, msg); break;
|
2015-06-10 22:15:58 +02:00
|
|
|
case IRC_RPL_UMODEIS:
|
|
|
|
irc_handle_rpl_umodeis (s, msg); buffer = NULL; break;
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_NAMREPLY:
|
2015-06-07 00:10:36 +02:00
|
|
|
irc_handle_rpl_namreply (s, msg); buffer = NULL; break;
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_ENDOFNAMES:
|
2015-06-07 00:10:36 +02:00
|
|
|
irc_handle_rpl_endofnames (s, msg); buffer = NULL; break;
|
2015-06-02 23:42:04 +02:00
|
|
|
case IRC_RPL_TOPIC:
|
2015-06-07 00:10:36 +02:00
|
|
|
irc_handle_rpl_topic (s, msg); buffer = NULL; break;
|
|
|
|
case IRC_RPL_CHANNELMODEIS:
|
|
|
|
irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break;
|
2015-06-07 00:39:35 +02:00
|
|
|
case IRC_RPL_CREATIONTIME:
|
|
|
|
irc_handle_rpl_creationtime (s, msg); buffer = NULL; break;
|
2015-06-07 00:41:59 +02:00
|
|
|
case IRC_RPL_TOPICWHOTIME:
|
|
|
|
irc_handle_rpl_topicwhotime (s, msg); buffer = NULL; break;
|
2015-06-20 22:50:28 +02:00
|
|
|
case IRC_RPL_INVITING:
|
|
|
|
irc_handle_rpl_inviting (s, msg); buffer = NULL; break;
|
2015-05-24 21:56:46 +02:00
|
|
|
|
2015-04-23 02:59:58 +02:00
|
|
|
case IRC_ERR_NICKNAMEINUSE:
|
2015-07-09 02:46:31 +02:00
|
|
|
irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break;
|
2015-06-02 23:31:44 +02:00
|
|
|
|
2015-11-23 08:33:58 +01:00
|
|
|
// Auto-away spams server buffers with activity
|
|
|
|
case IRC_RPL_NOWAWAY:
|
|
|
|
flags |= BUFFER_LINE_UNIMPORTANT;
|
2017-06-22 22:39:39 +02:00
|
|
|
if (s->irc_user) s->irc_user->away = true;
|
|
|
|
break;
|
2015-11-23 08:33:58 +01:00
|
|
|
case IRC_RPL_UNAWAY:
|
|
|
|
flags |= BUFFER_LINE_UNIMPORTANT;
|
2017-06-22 22:39:39 +02:00
|
|
|
if (s->irc_user) s->irc_user->away = false;
|
|
|
|
break;
|
2015-11-23 08:33:58 +01:00
|
|
|
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_WHOREPLY:
|
2020-10-19 05:17:41 +02:00
|
|
|
if (irc_handle_rpl_whoreply (s, msg)) buffer = NULL;
|
|
|
|
break;
|
2015-06-02 23:31:44 +02:00
|
|
|
case IRC_RPL_ENDOFWHO:
|
2020-10-19 05:17:41 +02:00
|
|
|
if (irc_handle_rpl_endofwho (s, msg)) buffer = NULL;
|
|
|
|
break;
|
2020-10-16 16:45:40 +02:00
|
|
|
|
2021-05-28 02:11:23 +02:00
|
|
|
case IRC_ERR_NICKLOCKED:
|
|
|
|
case IRC_RPL_SASLSUCCESS:
|
|
|
|
case IRC_ERR_SASLFAIL:
|
|
|
|
case IRC_ERR_SASLTOOLONG:
|
|
|
|
case IRC_ERR_SASLABORTED:
|
|
|
|
case IRC_ERR_SASLALREADY:
|
2021-05-28 04:25:38 +02:00
|
|
|
irc_try_finish_cap_negotiation (s);
|
2021-05-28 02:11:23 +02:00
|
|
|
break;
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
case IRC_RPL_LIST:
|
2015-06-02 23:31:44 +02:00
|
|
|
|
|
|
|
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
|
2015-07-06 01:54:02 +02:00
|
|
|
// (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
|
2015-06-02 23:31:44 +02:00
|
|
|
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)
|
|
|
|
{
|
2015-07-06 01:54:02 +02:00
|
|
|
// Join the parameter vector back and send it to the server buffer
|
2017-01-23 23:50:27 +01:00
|
|
|
log_server (s, buffer, flags, "#&m", strv_join (©, " "));
|
2015-04-21 22:08:18 +02:00
|
|
|
}
|
2015-06-07 00:44:31 +02:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (©);
|
2015-04-21 22:08:18 +02:00
|
|
|
}
|
|
|
|
|
2020-10-12 23:32:58 +02:00
|
|
|
static void
|
2020-10-16 16:58:11 +02:00
|
|
|
irc_sanitize_cut_off_utf8 (char **line)
|
2020-10-12 23:32:58 +02:00
|
|
|
{
|
|
|
|
// A variation on utf8_validate(), we need to detect the -2 return
|
|
|
|
const char *p = *line, *end = strchr (p, 0);
|
|
|
|
int32_t codepoint;
|
|
|
|
while ((codepoint = utf8_decode (&p, end - p)) >= 0
|
2020-10-21 05:39:05 +02:00
|
|
|
&& utf8_validate_cp (codepoint))
|
2020-10-12 23:32:58 +02:00
|
|
|
;
|
|
|
|
if (codepoint != -2)
|
|
|
|
return;
|
|
|
|
|
|
|
|
struct str fixed_up = str_make ();
|
|
|
|
str_append_data (&fixed_up, *line, p - *line);
|
|
|
|
str_append (&fixed_up, "\xEF\xBF\xBD" /* U+FFFD */);
|
|
|
|
cstr_set (line, str_steal (&fixed_up));
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
2015-11-19 19:09:05 +01:00
|
|
|
irc_process_message (const struct irc_message *msg, struct server *s)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2020-10-12 23:32:58 +02:00
|
|
|
if (msg->params.len)
|
2020-10-16 16:58:11 +02:00
|
|
|
irc_sanitize_cut_off_utf8 (&msg->params.vector[msg->params.len - 1]);
|
2020-10-12 23:32:58 +02:00
|
|
|
|
2016-01-31 20:06:45 +01:00
|
|
|
// TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec())
|
2021-06-16 21:17:34 +02:00
|
|
|
// -> change all calls to log_{server,nick,chghost,outcoming,ctcp}*()
|
|
|
|
// to take an extra numeric argument specifying time
|
2015-04-16 00:56:05 +02:00
|
|
|
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)
|
2015-04-30 00:02:14 +02:00
|
|
|
handler->handler (s, msg);
|
2015-04-16 00:56:05 +02:00
|
|
|
|
2015-04-21 22:08:18 +02:00
|
|
|
unsigned long numeric;
|
|
|
|
if (xstrtoul (&numeric, msg->command, 10))
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_process_numeric (s, msg, numeric);
|
2016-10-23 16:29:55 +02:00
|
|
|
|
|
|
|
// Better always make sure everything is in sync rather than care about
|
|
|
|
// each case explicitly whether anything might have changed
|
|
|
|
refresh_prompt (s->ctx);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-04-23 02:48:25 +02:00
|
|
|
// --- Message autosplitting magic ---------------------------------------------
|
|
|
|
|
2021-08-28 14:44:38 +02:00
|
|
|
// This is a rather basic algorithm; something like ICU with proper
|
2015-04-24 22:34:44 +02:00
|
|
|
// locale specification would be needed to make it work better.
|
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
static size_t
|
2021-08-28 14:44:38 +02:00
|
|
|
wrap_text_for_single_line (const char *text, struct irc_char_attrs *attrs,
|
|
|
|
size_t text_len, size_t target_len, struct str *output)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-12-08 22:39:16 +01:00
|
|
|
size_t eaten = 0;
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
// First try going word by word
|
|
|
|
const char *word_start;
|
|
|
|
const char *word_end = text + strcspn (text, " ");
|
|
|
|
size_t word_len = word_end - text;
|
2021-08-28 14:44:38 +02:00
|
|
|
while (target_len && word_len <= target_len)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-04-25 01:34:31 +02:00
|
|
|
if (word_len)
|
2015-04-24 22:34:44 +02:00
|
|
|
{
|
2015-04-25 01:34:31 +02:00
|
|
|
str_append_data (output, text, word_len);
|
|
|
|
|
|
|
|
text += word_len;
|
|
|
|
eaten += word_len;
|
2021-08-28 14:44:38 +02:00
|
|
|
target_len -= word_len;
|
2015-04-24 22:34:44 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
// Find the next word's end
|
|
|
|
word_start = text + strspn (text, " ");
|
|
|
|
word_end = word_start + strcspn (word_start, " ");
|
|
|
|
word_len = word_end - text;
|
|
|
|
}
|
2015-04-24 22:34:44 +02:00
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
if (eaten)
|
|
|
|
// Discard whitespace between words if split
|
|
|
|
return eaten + (word_start - text);
|
2015-04-24 22:34:44 +02:00
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
// And if that doesn't help, cut the longest valid block of characters
|
2021-08-28 14:44:38 +02:00
|
|
|
for (size_t i = 1; i <= text_len && i <= target_len; i++)
|
|
|
|
if (i == text_len || attrs[i].starts_at_boundary)
|
|
|
|
eaten = i;
|
|
|
|
|
2015-12-08 22:39:16 +01:00
|
|
|
str_append_data (output, text, eaten);
|
2015-04-25 01:34:31 +02:00
|
|
|
return eaten;
|
|
|
|
}
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2021-08-28 14:44:38 +02:00
|
|
|
// In practice, this should never fail at all, although it's not guaranteed
|
2015-04-25 01:34:31 +02:00
|
|
|
static bool
|
|
|
|
wrap_message (const char *message,
|
2017-01-23 23:50:27 +01:00
|
|
|
int line_max, struct strv *output, struct error **e)
|
2015-04-25 01:34:31 +02:00
|
|
|
{
|
2021-08-28 14:44:38 +02:00
|
|
|
size_t message_left = strlen (message), i = 0;
|
|
|
|
struct irc_char_attrs *attrs = irc_analyze_text (message, message_left);
|
|
|
|
struct str m = str_make ();
|
2015-04-25 01:34:31 +02:00
|
|
|
if (line_max <= 0)
|
|
|
|
goto error;
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2021-08-28 14:44:38 +02:00
|
|
|
while (m.len + message_left > (size_t) line_max)
|
2015-04-25 01:34:31 +02:00
|
|
|
{
|
2015-12-08 22:11:11 +01:00
|
|
|
size_t eaten = wrap_text_for_single_line
|
2021-08-28 14:44:38 +02:00
|
|
|
(message + i, attrs + i, message_left, line_max - m.len, &m);
|
2015-04-25 01:34:31 +02:00
|
|
|
if (!eaten)
|
|
|
|
goto error;
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output, str_steal (&m));
|
2021-08-28 14:44:38 +02:00
|
|
|
m = str_make ();
|
|
|
|
|
|
|
|
i += eaten;
|
|
|
|
if (!(message_left -= eaten))
|
|
|
|
break;
|
2015-12-08 22:11:11 +01:00
|
|
|
|
2021-08-28 14:44:38 +02:00
|
|
|
irc_serialize_char_attrs (attrs + i, &m);
|
|
|
|
if (m.len >= (size_t) line_max)
|
|
|
|
{
|
|
|
|
print_debug ("formatting continuation too long");
|
|
|
|
str_reset (&m);
|
|
|
|
}
|
|
|
|
}
|
2015-12-08 22:11:11 +01:00
|
|
|
if (message_left)
|
2021-08-28 14:44:38 +02:00
|
|
|
strv_append_owned (output,
|
|
|
|
xstrdup_printf ("%s%s", m.str, message + i));
|
2015-12-08 22:11:11 +01:00
|
|
|
|
2021-08-28 14:44:38 +02:00
|
|
|
free (attrs);
|
|
|
|
str_free (&m);
|
2015-04-23 02:48:25 +02:00
|
|
|
return true;
|
|
|
|
|
2015-04-25 01:34:31 +02:00
|
|
|
error:
|
2021-08-28 14:44:38 +02:00
|
|
|
free (attrs);
|
|
|
|
str_free (&m);
|
|
|
|
return error_set (e,
|
2015-04-25 01:34:31 +02:00
|
|
|
"Message splitting was unsuccessful as there was "
|
|
|
|
"too little room for UTF-8 characters");
|
|
|
|
}
|
2015-04-23 02:48:25 +02:00
|
|
|
|
|
|
|
/// Automatically splits messages that arrive at other clients with our prefix
|
|
|
|
/// so that they don't arrive cut off by the server
|
|
|
|
static bool
|
2015-04-30 00:02:14 +02:00
|
|
|
irc_autosplit_message (struct server *s, const char *message,
|
2017-01-23 23:50:27 +01:00
|
|
|
int fixed_part, struct strv *output, struct error **e)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
|
|
|
// :<nick>!<user>@<host> <fixed-part><message>
|
|
|
|
int space_in_one_message = 0;
|
2015-05-15 05:54:03 +02:00
|
|
|
if (s->irc_user && s->irc_user_host)
|
2015-04-23 02:48:25 +02:00
|
|
|
space_in_one_message = 510
|
2015-04-30 00:02:14 +02:00
|
|
|
- 1 - (int) strlen (s->irc_user->nickname)
|
|
|
|
- 1 - (int) strlen (s->irc_user_host)
|
2015-04-23 02:48:25 +02:00
|
|
|
- 1 - fixed_part;
|
|
|
|
|
|
|
|
// However we don't always have the full info for message splitting
|
|
|
|
if (!space_in_one_message)
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append (output, message);
|
2015-04-25 01:34:31 +02:00
|
|
|
else if (!wrap_message (message, space_in_one_message, output, e))
|
2015-04-23 02:48:25 +02:00
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-02-09 05:10:41 +01:00
|
|
|
send_autosplit_message (struct server *s,
|
|
|
|
const char *command, const char *target, const char *message,
|
|
|
|
const char *prefix, const char *suffix)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2016-02-09 05:10:41 +01:00
|
|
|
struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
|
|
|
|
int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
|
|
|
|
+ strlen (prefix) + strlen (suffix);
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-05-12 03:48:52 +02:00
|
|
|
// We might also want to preserve attributes across splits but
|
|
|
|
// that would make this code a lot more complicated
|
2015-05-10 02:12:39 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv lines = strv_make ();
|
2015-04-23 02:48:25 +02:00
|
|
|
struct error *e = NULL;
|
2016-02-09 05:10:41 +01:00
|
|
|
if (!irc_autosplit_message (s, message, fixed_part, &lines, &e))
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message);
|
2015-04-23 02:48:25 +02:00
|
|
|
error_free (e);
|
|
|
|
}
|
2016-02-09 05:10:41 +01:00
|
|
|
else
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2016-02-09 05:10:41 +01:00
|
|
|
for (size_t i = 0; i < lines.len; i++)
|
|
|
|
irc_send (s, "%s %s :%s%s%s", command, target,
|
|
|
|
prefix, lines.vector[i], suffix);
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&lines);
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
#define SEND_AUTOSPLIT_ACTION(s, target, message) \
|
2016-02-09 05:10:41 +01:00
|
|
|
send_autosplit_message ((s), "PRIVMSG", (target), (message), \
|
|
|
|
"\x01" "ACTION ", "\x01")
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
#define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \
|
2016-02-09 05:10:41 +01:00
|
|
|
send_autosplit_message ((s), "PRIVMSG", (target), (message), "", "")
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
#define SEND_AUTOSPLIT_NOTICE(s, target, message) \
|
2016-02-09 05:10:41 +01:00
|
|
|
send_autosplit_message ((s), "NOTICE", (target), (message), "", "")
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-05-03 16:47:16 +02:00
|
|
|
// --- Configuration dumper ----------------------------------------------------
|
|
|
|
|
|
|
|
struct config_dump_data
|
|
|
|
{
|
2018-01-08 21:38:13 +01:00
|
|
|
struct strv path; ///< Levels
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv *output; ///< Where to place new entries
|
2015-05-03 16:47:16 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
static void config_dump_item
|
2015-08-17 00:08:19 +02:00
|
|
|
(struct config_item *item, struct config_dump_data *data);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
|
|
|
static void
|
|
|
|
config_dump_children
|
2015-08-17 00:08:19 +02:00
|
|
|
(struct config_item *object, struct config_dump_data *data)
|
2015-05-03 16:47:16 +02:00
|
|
|
{
|
2015-07-11 14:47:45 +02:00
|
|
|
hard_assert (object->type == CONFIG_ITEM_OBJECT);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&object->value.object);
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *child;
|
2015-05-03 16:47:16 +02:00
|
|
|
while ((child = str_map_iter_next (&iter)))
|
|
|
|
{
|
2018-01-08 21:38:13 +01:00
|
|
|
strv_append_owned (&data->path, iter.link->key);
|
2015-05-03 16:47:16 +02:00
|
|
|
config_dump_item (child, data);
|
2018-01-08 21:38:13 +01:00
|
|
|
strv_steal (&data->path, data->path.len - 1);
|
2015-05-03 16:47:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
config_dump_item (struct config_item *item, struct config_dump_data *data)
|
2015-05-03 16:47:16 +02:00
|
|
|
{
|
2015-06-07 01:28:06 +02:00
|
|
|
// Empty objects will show as such
|
|
|
|
if (item->type == CONFIG_ITEM_OBJECT
|
|
|
|
&& item->value.object.len)
|
|
|
|
{
|
|
|
|
config_dump_children (item, data);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-07-16 20:40:10 +02:00
|
|
|
// Currently there's no reason for us to dump unknown items
|
|
|
|
struct config_schema *schema = item->schema;
|
|
|
|
if (!schema)
|
|
|
|
return;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str line = str_make ();
|
2018-01-08 21:38:13 +01:00
|
|
|
if (data->path.len)
|
|
|
|
str_append (&line, data->path.vector[0]);
|
|
|
|
for (size_t i = 1; i < data->path.len; i++)
|
|
|
|
str_append_printf (&line, ".%s", data->path.vector[i]);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str value = str_make ();
|
2015-07-05 18:26:46 +02:00
|
|
|
config_item_write (item, false, &value);
|
|
|
|
|
2015-05-03 16:47:16 +02:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2015-07-05 18:26:46 +02:00
|
|
|
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);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (data->output, str_steal (&line));
|
2015-05-03 16:47:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2017-01-23 23:50:27 +01:00
|
|
|
config_dump (struct config_item *root, struct strv *output)
|
2015-05-03 16:47:16 +02:00
|
|
|
{
|
|
|
|
struct config_dump_data data;
|
2018-01-08 21:38:13 +01:00
|
|
|
data.path = strv_make ();
|
2015-05-03 16:47:16 +02:00
|
|
|
data.output = output;
|
|
|
|
|
|
|
|
config_dump_item (root, &data);
|
2018-01-08 21:38:13 +01:00
|
|
|
|
|
|
|
hard_assert (!data.path.len);
|
|
|
|
strv_free (&data.path);
|
2015-05-03 16:47:16 +02:00
|
|
|
}
|
|
|
|
|
2015-05-03 18:40:51 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static int
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_sort_cb (const void *a, const void *b)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
|
|
|
return strcmp (*(const char **) a, *(const char **) b);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_sort (struct strv *self)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2017-01-23 23:50:27 +01:00
|
|
|
qsort (self->vector, self->len, sizeof *self->vector, strv_sort_cb);
|
2015-05-03 18:40:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
dump_matching_options
|
2017-01-23 23:50:27 +01:00
|
|
|
(struct config_item *root, const char *mask, struct strv *output)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2015-12-10 21:21:21 +01:00
|
|
|
config_dump (root, output);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_sort (output);
|
2015-05-03 18:40:51 +02:00
|
|
|
|
|
|
|
// Filter out results by wildcard matching
|
|
|
|
for (size_t i = 0; i < output->len; i++)
|
|
|
|
{
|
|
|
|
// Yeah, I know
|
2015-07-11 17:54:38 +02:00
|
|
|
char *key = cstr_cut_until (output->vector[i], " ");
|
2015-05-03 18:40:51 +02:00
|
|
|
if (fnmatch (mask, key, 0))
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_remove (output, i--);
|
2015-05-03 18:40:51 +02:00
|
|
|
free (key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-11 02:45:24 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
save_configuration (struct app_context *ctx)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str data = str_make ();
|
2015-12-10 21:21:21 +01:00
|
|
|
serialize_configuration (ctx->config.root, &data);
|
2015-07-11 02:45:24 +02:00
|
|
|
|
|
|
|
struct error *e = NULL;
|
2015-12-13 22:44:18 +01:00
|
|
|
char *filename = write_configuration_file (NULL, &data, &e);
|
2015-07-11 02:45:24 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-07-05 20:32:48 +02:00
|
|
|
// --- Server management -------------------------------------------------------
|
|
|
|
|
|
|
|
static bool
|
|
|
|
validate_server_name (const char *name)
|
|
|
|
{
|
|
|
|
for (const unsigned char *p = (const unsigned char *) name; *p; p++)
|
2020-10-02 06:52:11 +02:00
|
|
|
if (iscntrl_ascii (*p) || *p == '.')
|
2015-07-05 20:32:48 +02:00
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-14 07:29:30 +02:00
|
|
|
static const char *
|
2015-07-05 20:32:48 +02:00
|
|
|
check_server_name_for_addition (struct app_context *ctx, const char *name)
|
|
|
|
{
|
|
|
|
if (!strcasecmp_ascii (name, ctx->global_buffer->name))
|
2015-07-14 07:29:30 +02:00
|
|
|
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;
|
2015-07-05 20:32:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static struct server *
|
|
|
|
server_add (struct app_context *ctx,
|
2015-08-17 00:08:19 +02:00
|
|
|
const char *name, struct config_item *subtree)
|
2015-07-05 20:32:48 +02:00
|
|
|
{
|
|
|
|
hard_assert (!str_map_find (&ctx->servers, name));
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
struct server *s = server_new (&ctx->poller);
|
2015-07-05 20:32:48 +02:00
|
|
|
s->ctx = ctx;
|
|
|
|
s->name = xstrdup (name);
|
|
|
|
str_map_set (&ctx->servers, s->name, s);
|
|
|
|
s->config = subtree;
|
|
|
|
|
|
|
|
// Add a buffer and activate it
|
2020-10-31 23:42:49 +01:00
|
|
|
struct buffer *buffer = s->buffer = buffer_new (ctx->input,
|
|
|
|
BUFFER_SERVER, irc_make_buffer_name (s, NULL));
|
2015-07-05 20:32:48 +02:00
|
|
|
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);
|
|
|
|
|
2015-07-11 06:16:53 +02:00
|
|
|
if (get_config_boolean (s->config, "autoconnect"))
|
|
|
|
// Connect to the server ASAP
|
|
|
|
poller_timer_set (&s->reconnect_tmr, 0);
|
2015-07-05 20:32:48 +02:00
|
|
|
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);
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *subtree = config_item_object ();
|
2015-07-05 20:32:48 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-14 06:50:39 +02:00
|
|
|
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);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_unset_iter iter =
|
|
|
|
str_map_unset_iter_make (&s->irc_buffer_map);
|
2015-07-14 22:25:30 +02:00
|
|
|
struct buffer *buffer;
|
|
|
|
while ((buffer = str_map_unset_iter_next (&iter)))
|
2015-07-14 06:50:39 +02:00
|
|
|
buffer_remove_safe (ctx, buffer);
|
2015-07-14 22:25:30 +02:00
|
|
|
str_map_unset_iter_free (&iter);
|
2015-07-14 06:50:39 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-07-14 20:48:17 +02:00
|
|
|
static void
|
|
|
|
server_rename (struct app_context *ctx, struct server *s, const char *new_name)
|
|
|
|
{
|
2020-10-16 16:45:40 +02:00
|
|
|
hard_assert (!str_map_find (&ctx->servers, new_name));
|
2015-07-14 20:48:17 +02:00
|
|
|
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));
|
|
|
|
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&s->name, xstrdup (new_name));
|
2015-07-14 20:48:17 +02:00
|
|
|
buffer_rename (ctx, s->buffer, new_name);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
|
2015-07-14 20:48:17 +02:00
|
|
|
struct buffer *buffer;
|
|
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// TODO: creation of buffer names should be centralized -> replace
|
|
|
|
// calls to buffer_rename() and manual setting of buffer names
|
|
|
|
// with something like buffer_autorename() -- just mind the mess
|
|
|
|
// in irc_handle_nick(), which can hopefully be simplified
|
2015-07-14 20:48:17 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-28 02:03:26 +01:00
|
|
|
// --- Plugins -----------------------------------------------------------------
|
|
|
|
|
|
|
|
/// Returns the basename of the plugin's name without any extensions,
|
|
|
|
/// or NULL if the name isn't suitable (starts with a dot)
|
|
|
|
static char *
|
|
|
|
plugin_config_name (struct plugin *self)
|
|
|
|
{
|
|
|
|
const char *begin = self->name;
|
|
|
|
for (const char *p = begin; *p; )
|
|
|
|
if (*p++ == '/')
|
|
|
|
begin = p;
|
|
|
|
|
|
|
|
size_t len = strcspn (begin, ".");
|
2016-01-09 03:48:54 +01:00
|
|
|
if (!len)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
// XXX: we might also allow arbitrary strings as object keys (except dots)
|
|
|
|
char *copy = xstrndup (begin, len);
|
|
|
|
for (char *p = copy; *p; p++)
|
|
|
|
if (!config_tokenizer_is_word_char (*p))
|
|
|
|
*p = '_';
|
|
|
|
return copy;
|
2015-12-28 02:03:26 +01:00
|
|
|
}
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
// --- Lua ---------------------------------------------------------------------
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
// Each plugin has its own Lua state object, so that a/ they don't disturb each
|
|
|
|
// other and b/ unloading a plugin releases all resources.
|
|
|
|
//
|
|
|
|
// References to internal objects (buffers, servers) are all weak.
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
#ifdef HAVE_LUA
|
|
|
|
|
|
|
|
struct lua_plugin
|
|
|
|
{
|
|
|
|
struct plugin super; ///< The structure we're deriving
|
|
|
|
struct app_context *ctx; ///< Application context
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_State *L; ///< Lua state for the main thread
|
2015-12-28 02:03:26 +01:00
|
|
|
|
|
|
|
struct lua_schema_item *schemas; ///< Registered schema items
|
2015-11-19 15:45:32 +01:00
|
|
|
};
|
|
|
|
|
2020-10-20 02:02:09 +02:00
|
|
|
static void
|
|
|
|
lua_plugin_gc (struct plugin *self_)
|
|
|
|
{
|
|
|
|
struct lua_plugin *self = (struct lua_plugin *) self_;
|
2020-10-29 02:05:46 +01:00
|
|
|
lua_gc (self->L, LUA_GCCOLLECT, 0 /* Lua 5.3 required, 5.4 varargs */);
|
2020-10-20 02:02:09 +02:00
|
|
|
}
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
static void
|
|
|
|
lua_plugin_free (struct plugin *self_)
|
|
|
|
{
|
|
|
|
struct lua_plugin *self = (struct lua_plugin *) self_;
|
|
|
|
lua_close (self->L);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct plugin_vtable lua_plugin_vtable =
|
|
|
|
{
|
2020-10-20 02:02:09 +02:00
|
|
|
.gc = lua_plugin_gc,
|
2015-11-19 15:45:32 +01:00
|
|
|
.free = lua_plugin_free,
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
// The registry can be used as a cache for weakly referenced objects
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_cache_get (lua_State *L, void *object)
|
|
|
|
{
|
|
|
|
lua_rawgetp (L, LUA_REGISTRYINDEX, object);
|
|
|
|
if (lua_isnil (L, -1))
|
|
|
|
{
|
|
|
|
lua_pop (L, 1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_cache_store (lua_State *L, void *object, int index)
|
|
|
|
{
|
|
|
|
lua_pushvalue (L, index);
|
|
|
|
lua_rawsetp (L, LUA_REGISTRYINDEX, object);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_cache_invalidate (lua_State *L, void *object)
|
|
|
|
{
|
|
|
|
lua_pushnil (L);
|
|
|
|
lua_rawsetp (L, LUA_REGISTRYINDEX, object);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-01-14 03:26:02 +01:00
|
|
|
/// Append a traceback to all errors so that we can later extract it
|
|
|
|
static int
|
|
|
|
lua_plugin_error_handler (lua_State *L)
|
|
|
|
{
|
|
|
|
luaL_traceback (L, L, luaL_checkstring (L, 1), 1);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_plugin_process_error (struct lua_plugin *self, const char *message,
|
|
|
|
struct error **e)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (message, "\n", true, &v);
|
2016-01-14 03:26:02 +01:00
|
|
|
|
|
|
|
if (v.len < 2)
|
|
|
|
error_set (e, "%s", message);
|
|
|
|
else
|
|
|
|
{
|
|
|
|
error_set (e, "%s", v.vector[0]);
|
|
|
|
log_global_debug (self->ctx, "Lua: plugin \"#s\": #s",
|
|
|
|
self->super.name, v.vector[1]);
|
|
|
|
for (size_t i = 2; i < v.len; i++)
|
|
|
|
log_global_debug (self->ctx, " #s", v.vector[i]);
|
|
|
|
}
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2016-01-14 03:26:02 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Call a Lua function and process errors using our special error handler
|
|
|
|
static bool
|
|
|
|
lua_plugin_call (struct lua_plugin *self,
|
|
|
|
int n_params, int n_results, struct error **e)
|
|
|
|
{
|
2016-11-04 18:51:32 +01:00
|
|
|
// XXX: this may eventually be called from a thread, then this is wrong
|
2016-01-14 03:26:02 +01:00
|
|
|
lua_State *L = self->L;
|
|
|
|
|
|
|
|
// We need to pop the error handler at the end
|
|
|
|
lua_pushcfunction (L, lua_plugin_error_handler);
|
|
|
|
int error_handler_idx = -n_params - 2;
|
|
|
|
lua_insert (L, error_handler_idx);
|
|
|
|
|
|
|
|
if (!lua_pcall (L, n_params, n_results, error_handler_idx))
|
|
|
|
{
|
|
|
|
lua_remove (L, -n_results - 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
(void) lua_plugin_process_error (self, lua_tostring (L, -1), e);
|
|
|
|
lua_pop (L, 2);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convenience function; replaces the "original" string or produces an error
|
|
|
|
static bool
|
|
|
|
lua_plugin_handle_string_filter_result (struct lua_plugin *self,
|
|
|
|
char **original, bool utf8, struct error **e)
|
|
|
|
{
|
|
|
|
lua_State *L = self->L;
|
|
|
|
if (lua_isnil (L, -1))
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (original, NULL);
|
2016-01-14 03:26:02 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!lua_isstring (L, -1))
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "must return either a string or nil");
|
2016-01-14 03:26:02 +01:00
|
|
|
|
|
|
|
size_t len;
|
|
|
|
const char *processed = lua_tolstring (L, -1, &len);
|
|
|
|
if (utf8 && !utf8_validate (processed, len))
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "must return valid UTF-8");
|
2016-01-14 03:26:02 +01:00
|
|
|
|
|
|
|
// Only replace the string if it's different
|
|
|
|
if (strcmp (processed, *original))
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (original, xstrdup (processed));
|
2016-01-14 03:26:02 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-01-14 03:34:29 +01:00
|
|
|
static const char *
|
|
|
|
lua_plugin_check_utf8 (lua_State *L, int arg)
|
|
|
|
{
|
|
|
|
size_t len;
|
|
|
|
const char *s = luaL_checklstring (L, arg, &len);
|
2018-01-07 05:50:21 +01:00
|
|
|
luaL_argcheck (L, utf8_validate (s, len), arg, "must be valid UTF-8");
|
2016-01-14 03:34:29 +01:00
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2016-01-14 03:26:02 +01:00
|
|
|
static void
|
|
|
|
lua_plugin_log_error
|
|
|
|
(struct lua_plugin *self, const char *where, struct error *error)
|
|
|
|
{
|
|
|
|
log_global_error (self->ctx, "Lua: plugin \"#s\": #s: #s",
|
|
|
|
self->super.name, where, error->message);
|
|
|
|
error_free (error);
|
|
|
|
}
|
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
/// Pop "n" values from the stack into a table, using their indexes as keys
|
2016-11-01 04:07:53 +01:00
|
|
|
static void
|
|
|
|
lua_plugin_pack (lua_State *L, int n)
|
|
|
|
{
|
|
|
|
lua_createtable (L, n, 0);
|
|
|
|
lua_insert (L, -n - 1);
|
|
|
|
for (int i = n; i; i--)
|
|
|
|
lua_rawseti (L, -i - 1, i);
|
|
|
|
}
|
|
|
|
|
2016-01-14 03:26:02 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-04-20 23:23:53 +02:00
|
|
|
static void
|
|
|
|
lua_plugin_kv (lua_State *L, const char *key, const char *value)
|
|
|
|
{
|
|
|
|
lua_pushstring (L, value);
|
|
|
|
lua_setfield (L, -2, key);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_plugin_push_message (lua_State *L, const struct irc_message *msg)
|
|
|
|
{
|
|
|
|
lua_createtable (L, 0, 4);
|
|
|
|
|
|
|
|
lua_createtable (L, msg->tags.len, 0);
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&msg->tags);
|
2016-04-20 23:23:53 +02:00
|
|
|
const char *value;
|
|
|
|
while ((value = str_map_iter_next (&iter)))
|
|
|
|
lua_plugin_kv (L, iter.link->key, value);
|
|
|
|
lua_setfield (L, -2, "tags");
|
|
|
|
|
|
|
|
// TODO: parse the prefix further?
|
|
|
|
if (msg->prefix) lua_plugin_kv (L, "prefix", msg->prefix);
|
|
|
|
if (msg->command) lua_plugin_kv (L, "command", msg->command);
|
|
|
|
|
|
|
|
lua_createtable (L, msg->params.len, 0);
|
|
|
|
for (size_t i = 0; i < msg->params.len; i++)
|
|
|
|
{
|
|
|
|
lua_pushstring (L, msg->params.vector[i]);
|
|
|
|
lua_rawseti (L, -2, i + 1);
|
|
|
|
}
|
|
|
|
lua_setfield (L, -2, "params");
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_parse (lua_State *L)
|
|
|
|
{
|
|
|
|
struct irc_message msg;
|
|
|
|
irc_parse_message (&msg, luaL_checkstring (L, 1));
|
|
|
|
lua_plugin_push_message (L, &msg);
|
|
|
|
irc_free_message (&msg);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
// Lua code can use weakly referenced wrappers for internal objects.
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
typedef struct weak_ref_link *
|
|
|
|
(*lua_weak_ref_fn) (void *object, destroy_cb_fn cb, void *user_data);
|
|
|
|
typedef void (*lua_weak_unref_fn) (void *object, struct weak_ref_link **link);
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak_info
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
const char *name; ///< Metatable name
|
|
|
|
struct ispect_field *ispect; ///< Introspection data
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_weak_ref_fn ref; ///< Weak link invalidator
|
|
|
|
lua_weak_unref_fn unref; ///< Weak link generator
|
2016-11-01 04:07:53 +01:00
|
|
|
};
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak_info *info; ///< Introspection data
|
|
|
|
void *object; ///< The object
|
|
|
|
struct weak_ref_link *weak_ref; ///< A weak reference link
|
2016-11-01 04:07:53 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_weak_invalidate (void *object, void *user_data)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak *wrapper = user_data;
|
|
|
|
wrapper->object = NULL;
|
|
|
|
wrapper->weak_ref = NULL;
|
|
|
|
// This can in theory call the GC, order isn't arbitrary here
|
|
|
|
lua_cache_invalidate (wrapper->plugin->L, object);
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_weak_push (lua_State *L, struct lua_plugin *plugin, void *object,
|
|
|
|
struct lua_weak_info *info)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
if (!object)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_pushnil (L);
|
|
|
|
return;
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
2016-11-04 20:38:09 +01:00
|
|
|
if (lua_cache_get (L, object))
|
|
|
|
return;
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak *wrapper = lua_newuserdata (L, sizeof *wrapper);
|
|
|
|
luaL_setmetatable (L, info->name);
|
|
|
|
wrapper->plugin = plugin;
|
|
|
|
wrapper->info = info;
|
|
|
|
wrapper->object = object;
|
|
|
|
wrapper->weak_ref = NULL;
|
|
|
|
if (info->ref)
|
|
|
|
wrapper->weak_ref = info->ref (object, lua_weak_invalidate, wrapper);
|
|
|
|
lua_cache_store (L, object, -1);
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_weak_gc (lua_State *L, const struct lua_weak_info *info)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak *wrapper = luaL_checkudata (L, 1, info->name);
|
|
|
|
if (wrapper->object)
|
|
|
|
{
|
|
|
|
lua_cache_invalidate (L, wrapper->object);
|
|
|
|
if (info->unref)
|
|
|
|
info->unref (wrapper->object, &wrapper->weak_ref);
|
|
|
|
wrapper->object = NULL;
|
|
|
|
}
|
2016-11-01 04:07:53 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static struct lua_weak *
|
|
|
|
lua_weak_deref (lua_State *L, const struct lua_weak_info *info)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
|
|
|
|
luaL_argcheck (L, weak->object, 1, "dead reference used");
|
|
|
|
return weak;
|
|
|
|
}
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
#define LUA_WEAK_DECLARE(id, metatable_id) \
|
|
|
|
static struct lua_weak_info lua_ ## id ## _info = \
|
|
|
|
{ \
|
|
|
|
.name = metatable_id, \
|
|
|
|
.ispect = g_ ## id ## _ispect, \
|
|
|
|
.ref = (lua_weak_ref_fn) id ## _weak_ref, \
|
|
|
|
.unref = (lua_weak_unref_fn) id ## _weak_unref, \
|
|
|
|
};
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
#define XLUA_USER_METATABLE "user" ///< Identifier for Lua metatable
|
|
|
|
#define XLUA_CHANNEL_METATABLE "channel" ///< Identifier for Lua metatable
|
|
|
|
#define XLUA_BUFFER_METATABLE "buffer" ///< Identifier for Lua metatable
|
|
|
|
#define XLUA_SERVER_METATABLE "server" ///< Identifier for Lua metatable
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
LUA_WEAK_DECLARE (user, XLUA_USER_METATABLE)
|
|
|
|
LUA_WEAK_DECLARE (channel, XLUA_CHANNEL_METATABLE)
|
|
|
|
LUA_WEAK_DECLARE (buffer, XLUA_BUFFER_METATABLE)
|
|
|
|
LUA_WEAK_DECLARE (server, XLUA_SERVER_METATABLE)
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// The global context is kind of fake and doesn't have any ref-counting,
|
2016-11-04 20:38:09 +01:00
|
|
|
// however it's still very much an object
|
|
|
|
static struct lua_weak_info lua_ctx_info =
|
|
|
|
{
|
|
|
|
.name = PROGRAM_NAME,
|
|
|
|
.ispect = g_ctx_ispect,
|
|
|
|
};
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static int
|
|
|
|
lua_user_gc (lua_State *L)
|
|
|
|
{
|
|
|
|
return lua_weak_gc (L, &lua_user_info);
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static int
|
|
|
|
lua_user_get_channels (lua_State *L)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_user_info);
|
|
|
|
struct user *user = wrapper->object;
|
2016-11-01 04:07:53 +01:00
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
int i = 1;
|
|
|
|
lua_newtable (L);
|
|
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
lua_weak_push (L, wrapper->plugin, iter->channel, &lua_channel_info);
|
|
|
|
lua_rawseti (L, -2, i++);
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
2016-11-04 20:38:09 +01:00
|
|
|
return 1;
|
2016-11-01 04:07:53 +01:00
|
|
|
}
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static luaL_Reg lua_user_table[] =
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
{ "__gc", lua_user_gc },
|
|
|
|
{ "get_channels", lua_user_get_channels },
|
|
|
|
{ NULL, NULL }
|
2016-11-01 04:07:53 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static int
|
|
|
|
lua_channel_gc (lua_State *L)
|
2016-11-01 04:07:53 +01:00
|
|
|
{
|
2016-11-04 20:38:09 +01:00
|
|
|
return lua_weak_gc (L, &lua_channel_info);
|
|
|
|
}
|
2016-04-21 00:38:30 +02:00
|
|
|
|
|
|
|
static int
|
|
|
|
lua_channel_get_users (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_channel_info);
|
|
|
|
struct channel *channel = wrapper->object;
|
2016-04-21 00:38:30 +02:00
|
|
|
|
|
|
|
int i = 1;
|
|
|
|
lua_newtable (L);
|
2016-04-21 10:02:33 +02:00
|
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
2016-04-21 00:38:30 +02:00
|
|
|
{
|
|
|
|
lua_createtable (L, 0, 2);
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_weak_push (L, wrapper->plugin, iter->user, &lua_user_info);
|
2016-04-21 00:38:30 +02:00
|
|
|
lua_setfield (L, -2, "user");
|
2020-10-04 08:22:20 +02:00
|
|
|
lua_plugin_kv (L, "prefixes", iter->prefixes);
|
2016-04-21 00:38:30 +02:00
|
|
|
|
|
|
|
lua_rawseti (L, -2, i++);
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static luaL_Reg lua_channel_table[] =
|
|
|
|
{
|
|
|
|
{ "__gc", lua_channel_gc },
|
|
|
|
{ "get_users", lua_channel_get_users },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-24 21:52:16 +01:00
|
|
|
static int
|
|
|
|
lua_buffer_gc (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
return lua_weak_gc (L, &lua_buffer_info);
|
2015-11-24 21:52:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_buffer_log (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
|
|
|
|
struct buffer *buffer = wrapper->object;
|
2016-01-14 03:34:29 +01:00
|
|
|
const char *message = lua_plugin_check_utf8 (L, 2);
|
2015-11-24 21:52:16 +01:00
|
|
|
log_full (wrapper->plugin->ctx, buffer->server, buffer,
|
|
|
|
BUFFER_LINE_STATUS, "#s", message);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-01-14 03:34:29 +01:00
|
|
|
static int
|
|
|
|
lua_buffer_execute (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
|
|
|
|
struct buffer *buffer = wrapper->object;
|
2016-01-14 03:34:29 +01:00
|
|
|
const char *line = lua_plugin_check_utf8 (L, 2);
|
|
|
|
process_input_utf8 (wrapper->plugin->ctx, buffer, line, 0);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2015-11-24 21:52:16 +01:00
|
|
|
static luaL_Reg lua_buffer_table[] =
|
|
|
|
{
|
2016-04-21 00:38:30 +02:00
|
|
|
{ "__gc", lua_buffer_gc },
|
|
|
|
{ "log", lua_buffer_log },
|
|
|
|
{ "execute", lua_buffer_execute },
|
|
|
|
{ NULL, NULL }
|
2015-11-24 21:52:16 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
static int
|
|
|
|
lua_server_gc (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
return lua_weak_gc (L, &lua_server_info);
|
2015-11-20 02:43:43 +01:00
|
|
|
}
|
|
|
|
|
2016-10-28 12:45:38 +02:00
|
|
|
static int
|
|
|
|
lua_server_get_state (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
|
|
|
|
struct server *server = wrapper->object;
|
|
|
|
switch (server->state)
|
|
|
|
{
|
|
|
|
case IRC_DISCONNECTED: lua_pushstring (L, "disconnected"); break;
|
|
|
|
case IRC_CONNECTING: lua_pushstring (L, "connecting"); break;
|
|
|
|
case IRC_CONNECTED: lua_pushstring (L, "connected"); break;
|
|
|
|
case IRC_REGISTERED: lua_pushstring (L, "registered"); break;
|
|
|
|
case IRC_CLOSING: lua_pushstring (L, "closing"); break;
|
|
|
|
case IRC_HALF_CLOSED: lua_pushstring (L, "half_closed"); break;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
static int
|
|
|
|
lua_server_send (lua_State *L)
|
|
|
|
{
|
2016-04-21 10:02:33 +02:00
|
|
|
struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
|
|
|
|
struct server *server = wrapper->object;
|
|
|
|
irc_send (server, "%s", luaL_checkstring (L, 2));
|
2015-11-20 02:43:43 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static luaL_Reg lua_server_table[] =
|
|
|
|
{
|
2015-11-24 21:42:08 +01:00
|
|
|
{ "__gc", lua_server_gc },
|
2016-10-28 12:45:38 +02:00
|
|
|
{ "get_state", lua_server_get_state },
|
2015-11-24 21:42:08 +01:00
|
|
|
{ "send", lua_server_send },
|
|
|
|
{ NULL, NULL }
|
2015-11-20 02:43:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
#define XLUA_HOOK_METATABLE "hook" ///< Identifier for the Lua metatable
|
|
|
|
|
|
|
|
enum lua_hook_type
|
|
|
|
{
|
|
|
|
XLUA_HOOK_DEFUNCT, ///< No longer functional
|
|
|
|
XLUA_HOOK_INPUT, ///< Input hook
|
|
|
|
XLUA_HOOK_IRC, ///< IRC hook
|
2016-10-28 01:01:27 +02:00
|
|
|
XLUA_HOOK_PROMPT, ///< Prompt hook
|
2016-01-15 01:44:35 +01:00
|
|
|
XLUA_HOOK_COMPLETION, ///< Autocomplete
|
2015-11-20 02:43:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
struct lua_hook
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
|
|
|
enum lua_hook_type type; ///< Type of the hook
|
2016-01-04 22:24:05 +01:00
|
|
|
int ref_callback; ///< Reference to the callback
|
2015-11-20 02:43:43 +01:00
|
|
|
union
|
|
|
|
{
|
2015-11-21 19:48:15 +01:00
|
|
|
struct hook hook; ///< Hook base structure
|
2015-11-20 02:43:43 +01:00
|
|
|
struct input_hook input_hook; ///< Input hook
|
|
|
|
struct irc_hook irc_hook; ///< IRC hook
|
2016-10-28 01:01:27 +02:00
|
|
|
struct prompt_hook prompt_hook; ///< IRC hook
|
2016-01-15 01:44:35 +01:00
|
|
|
struct completion_hook c_hook; ///< Autocomplete hook
|
2015-11-20 02:43:43 +01:00
|
|
|
}
|
|
|
|
data; ///< Hook data
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_hook_unhook (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_hook *hook = luaL_checkudata (L, 1, XLUA_HOOK_METATABLE);
|
|
|
|
switch (hook->type)
|
|
|
|
{
|
|
|
|
case XLUA_HOOK_INPUT:
|
2016-01-15 01:44:35 +01:00
|
|
|
LIST_UNLINK (hook->plugin->ctx->input_hooks, &hook->data.hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
break;
|
|
|
|
case XLUA_HOOK_IRC:
|
2016-01-15 01:44:35 +01:00
|
|
|
LIST_UNLINK (hook->plugin->ctx->irc_hooks, &hook->data.hook);
|
|
|
|
break;
|
2016-10-28 01:01:27 +02:00
|
|
|
case XLUA_HOOK_PROMPT:
|
|
|
|
LIST_UNLINK (hook->plugin->ctx->prompt_hooks, &hook->data.hook);
|
2016-10-29 19:41:41 +02:00
|
|
|
refresh_prompt (hook->plugin->ctx);
|
2016-10-28 01:01:27 +02:00
|
|
|
break;
|
2016-01-15 01:44:35 +01:00
|
|
|
case XLUA_HOOK_COMPLETION:
|
|
|
|
LIST_UNLINK (hook->plugin->ctx->completion_hooks, &hook->data.hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
hard_assert (!"invalid hook type");
|
|
|
|
case XLUA_HOOK_DEFUNCT:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-01-04 22:24:05 +01:00
|
|
|
luaL_unref (L, LUA_REGISTRYINDEX, hook->ref_callback);
|
|
|
|
hook->ref_callback = LUA_REFNIL;
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
// The hook no longer has to stay alive
|
|
|
|
hook->type = XLUA_HOOK_DEFUNCT;
|
2015-11-21 15:15:49 +01:00
|
|
|
lua_cache_invalidate (L, hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The hook dies either when the plugin requests it or at plugin unload
|
|
|
|
static luaL_Reg lua_hook_table[] =
|
|
|
|
{
|
|
|
|
{ "unhook", lua_hook_unhook },
|
|
|
|
{ "__gc", lua_hook_unhook },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static char *
|
|
|
|
lua_input_hook_filter (struct input_hook *self, struct buffer *buffer,
|
|
|
|
char *input)
|
|
|
|
{
|
2015-11-21 15:15:49 +01:00
|
|
|
struct lua_hook *hook =
|
|
|
|
CONTAINER_OF (self, struct lua_hook, data.input_hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
struct lua_plugin *plugin = hook->plugin;
|
|
|
|
lua_State *L = plugin->L;
|
|
|
|
|
2016-01-04 22:24:05 +01:00
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
|
|
|
|
lua_weak_push (L, plugin, buffer, &lua_buffer_info); // 2: buffer
|
|
|
|
lua_pushstring (L, input); // 3: input
|
2015-11-20 02:43:43 +01:00
|
|
|
|
|
|
|
struct error *e = NULL;
|
2016-01-05 23:05:06 +01:00
|
|
|
if (lua_plugin_call (plugin, 3, 1, &e))
|
2015-11-20 02:43:43 +01:00
|
|
|
{
|
2016-01-05 23:05:06 +01:00
|
|
|
lua_plugin_handle_string_filter_result (plugin, &input, true, &e);
|
|
|
|
lua_pop (L, 1);
|
2015-11-20 02:43:43 +01:00
|
|
|
}
|
2016-01-05 23:05:06 +01:00
|
|
|
if (e)
|
|
|
|
lua_plugin_log_error (plugin, "input hook", e);
|
2015-11-20 02:43:43 +01:00
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
lua_irc_hook_filter (struct irc_hook *self, struct server *s, char *message)
|
|
|
|
{
|
2015-11-21 15:15:49 +01:00
|
|
|
struct lua_hook *hook =
|
|
|
|
CONTAINER_OF (self, struct lua_hook, data.irc_hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
struct lua_plugin *plugin = hook->plugin;
|
|
|
|
lua_State *L = plugin->L;
|
|
|
|
|
2016-01-04 22:24:05 +01:00
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
|
|
|
|
lua_weak_push (L, plugin, s, &lua_server_info); // 2: server
|
|
|
|
lua_pushstring (L, message); // 3: message
|
2015-11-20 02:43:43 +01:00
|
|
|
|
|
|
|
struct error *e = NULL;
|
2016-01-05 23:05:06 +01:00
|
|
|
if (lua_plugin_call (plugin, 3, 1, &e))
|
2015-11-20 02:43:43 +01:00
|
|
|
{
|
2016-01-05 23:05:06 +01:00
|
|
|
lua_plugin_handle_string_filter_result (plugin, &message, false, &e);
|
|
|
|
lua_pop (L, 1);
|
2015-11-20 02:43:43 +01:00
|
|
|
}
|
2016-01-05 23:05:06 +01:00
|
|
|
if (e)
|
|
|
|
lua_plugin_log_error (plugin, "IRC hook", e);
|
2015-11-20 02:43:43 +01:00
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
2016-10-28 01:01:27 +02:00
|
|
|
static char *
|
|
|
|
lua_prompt_hook_make (struct prompt_hook *self)
|
|
|
|
{
|
|
|
|
struct lua_hook *hook =
|
|
|
|
CONTAINER_OF (self, struct lua_hook, data.prompt_hook);
|
|
|
|
struct lua_plugin *plugin = hook->plugin;
|
|
|
|
lua_State *L = plugin->L;
|
|
|
|
|
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
|
|
|
|
lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
|
|
|
|
|
|
|
|
struct error *e = NULL;
|
|
|
|
char *prompt = xstrdup ("");
|
|
|
|
if (lua_plugin_call (plugin, 1, 1, &e))
|
|
|
|
{
|
2016-10-29 19:41:41 +02:00
|
|
|
lua_plugin_handle_string_filter_result (plugin, &prompt, true, &e);
|
2016-10-28 01:01:27 +02:00
|
|
|
lua_pop (L, 1);
|
|
|
|
}
|
|
|
|
if (e)
|
2016-10-29 19:41:41 +02:00
|
|
|
lua_plugin_log_error (plugin, "prompt hook", e);
|
2016-10-28 01:01:27 +02:00
|
|
|
return prompt;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-01-15 01:44:35 +01:00
|
|
|
static void
|
|
|
|
lua_plugin_push_completion (lua_State *L, struct completion *data)
|
|
|
|
{
|
|
|
|
lua_createtable (L, 0, 3);
|
|
|
|
|
|
|
|
lua_pushstring (L, data->line);
|
|
|
|
lua_setfield (L, -2, "line");
|
|
|
|
|
|
|
|
lua_createtable (L, data->words_len, 0);
|
|
|
|
for (size_t i = 0; i < data->words_len; i++)
|
|
|
|
{
|
|
|
|
lua_pushlstring (L, data->line + data->words[i].start,
|
|
|
|
data->words[i].end - data->words[i].start);
|
|
|
|
lua_rawseti (L, -2, i + 1);
|
|
|
|
}
|
|
|
|
lua_setfield (L, -2, "words");
|
|
|
|
|
|
|
|
lua_pushinteger (L, data->location);
|
|
|
|
lua_setfield (L, -2, "location");
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2017-01-23 23:50:27 +01:00
|
|
|
lua_completion_hook_process_value (lua_State *L, struct strv *output,
|
2016-01-15 01:44:35 +01:00
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
if (lua_type (L, -1) != LUA_TSTRING)
|
2016-10-11 10:52:49 +02:00
|
|
|
{
|
|
|
|
return error_set (e,
|
|
|
|
"%s: %s", "invalid type", lua_typename (L, lua_type (L, -1)));
|
|
|
|
}
|
2016-01-15 01:44:35 +01:00
|
|
|
|
|
|
|
size_t len;
|
|
|
|
const char *value = lua_tolstring (L, -1, &len);
|
|
|
|
if (!utf8_validate (value, len))
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "must be valid UTF-8");
|
2016-01-15 01:44:35 +01:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append (output, value);
|
2016-01-15 01:44:35 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2017-01-23 23:50:27 +01:00
|
|
|
lua_completion_hook_process (lua_State *L, struct strv *output,
|
2016-01-15 01:44:35 +01:00
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
if (lua_isnil (L, -1))
|
|
|
|
return true;
|
|
|
|
if (!lua_istable (L, -1))
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "must return either a table or nil");
|
2016-01-15 01:44:35 +01:00
|
|
|
|
|
|
|
bool success = true;
|
|
|
|
for (lua_Integer i = 1; success && lua_rawgeti (L, -1, i); i++)
|
|
|
|
if ((success = lua_completion_hook_process_value (L, output, e)))
|
|
|
|
lua_pop (L, 1);
|
|
|
|
lua_pop (L, 1);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_completion_hook_complete (struct completion_hook *self,
|
2017-01-23 23:50:27 +01:00
|
|
|
struct completion *data, const char *word, struct strv *output)
|
2016-01-15 01:44:35 +01:00
|
|
|
{
|
|
|
|
struct lua_hook *hook =
|
|
|
|
CONTAINER_OF (self, struct lua_hook, data.c_hook);
|
|
|
|
struct lua_plugin *plugin = hook->plugin;
|
|
|
|
lua_State *L = plugin->L;
|
|
|
|
|
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
|
|
|
|
lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook
|
|
|
|
lua_plugin_push_completion (L, data); // 2: data
|
|
|
|
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_weak_push (L, plugin, plugin->ctx->current_buffer, &lua_buffer_info);
|
2016-01-15 01:44:35 +01:00
|
|
|
lua_setfield (L, -2, "buffer");
|
|
|
|
|
|
|
|
lua_pushstring (L, word); // 3: word
|
|
|
|
|
|
|
|
struct error *e = NULL;
|
|
|
|
if (lua_plugin_call (plugin, 3, 1, &e))
|
|
|
|
{
|
|
|
|
lua_completion_hook_process (L, output, &e);
|
|
|
|
lua_pop (L, 1);
|
|
|
|
}
|
|
|
|
if (e)
|
|
|
|
lua_plugin_log_error (plugin, "autocomplete hook", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
static struct lua_hook *
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_push_hook (lua_State *L, struct lua_plugin *plugin,
|
|
|
|
int callback_index, enum lua_hook_type type, int priority)
|
2015-11-20 02:43:43 +01:00
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
luaL_checktype (L, callback_index, LUA_TFUNCTION);
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
struct lua_hook *hook = lua_newuserdata (L, sizeof *hook);
|
|
|
|
luaL_setmetatable (L, XLUA_HOOK_METATABLE);
|
|
|
|
memset (hook, 0, sizeof *hook);
|
2015-11-21 18:40:07 +01:00
|
|
|
hook->data.hook.priority = priority;
|
2015-11-20 02:43:43 +01:00
|
|
|
hook->type = type;
|
|
|
|
hook->plugin = plugin;
|
|
|
|
|
|
|
|
lua_pushvalue (L, callback_index);
|
2016-01-04 22:24:05 +01:00
|
|
|
hook->ref_callback = luaL_ref (L, LUA_REGISTRYINDEX);
|
2015-11-20 02:43:43 +01:00
|
|
|
|
|
|
|
// Make sure the hook doesn't get garbage collected and return it
|
2015-11-21 15:15:49 +01:00
|
|
|
lua_cache_store (L, hook, -1);
|
2015-11-20 02:43:43 +01:00
|
|
|
return hook;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_hook_input (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
2015-11-21 18:40:07 +01:00
|
|
|
struct lua_hook *hook = lua_plugin_push_hook
|
2016-11-04 18:51:32 +01:00
|
|
|
(L, plugin, 1, XLUA_HOOK_INPUT, luaL_optinteger (L, 2, 0));
|
2016-10-29 19:35:48 +02:00
|
|
|
hook->data.input_hook.filter = lua_input_hook_filter;
|
2015-11-21 18:40:07 +01:00
|
|
|
plugin->ctx->input_hooks =
|
|
|
|
hook_insert (plugin->ctx->input_hooks, &hook->data.hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_hook_irc (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
2015-11-21 18:40:07 +01:00
|
|
|
struct lua_hook *hook = lua_plugin_push_hook
|
2016-11-04 18:51:32 +01:00
|
|
|
(L, plugin, 1, XLUA_HOOK_IRC, luaL_optinteger (L, 2, 0));
|
2016-10-29 19:35:48 +02:00
|
|
|
hook->data.irc_hook.filter = lua_irc_hook_filter;
|
2015-11-21 18:40:07 +01:00
|
|
|
plugin->ctx->irc_hooks =
|
|
|
|
hook_insert (plugin->ctx->irc_hooks, &hook->data.hook);
|
2015-11-20 02:43:43 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-10-28 01:01:27 +02:00
|
|
|
static int
|
|
|
|
lua_plugin_hook_prompt (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
struct lua_hook *hook = lua_plugin_push_hook
|
2016-11-04 18:51:32 +01:00
|
|
|
(L, plugin, 1, XLUA_HOOK_PROMPT, luaL_optinteger (L, 2, 0));
|
2016-10-29 19:35:48 +02:00
|
|
|
hook->data.prompt_hook.make = lua_prompt_hook_make;
|
2016-10-28 01:01:27 +02:00
|
|
|
plugin->ctx->prompt_hooks =
|
|
|
|
hook_insert (plugin->ctx->prompt_hooks, &hook->data.hook);
|
2016-10-28 13:58:37 +02:00
|
|
|
refresh_prompt (plugin->ctx);
|
2016-10-28 01:01:27 +02:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-01-15 01:44:35 +01:00
|
|
|
static int
|
|
|
|
lua_plugin_hook_completion (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
struct lua_hook *hook = lua_plugin_push_hook
|
2016-11-04 18:51:32 +01:00
|
|
|
(L, plugin, 1, XLUA_HOOK_COMPLETION, luaL_optinteger (L, 2, 0));
|
2016-10-29 19:35:48 +02:00
|
|
|
hook->data.c_hook.complete = lua_completion_hook_complete;
|
2016-01-15 01:44:35 +01:00
|
|
|
plugin->ctx->completion_hooks =
|
|
|
|
hook_insert (plugin->ctx->completion_hooks, &hook->data.hook);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2015-12-28 02:03:26 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
#define XLUA_SCHEMA_METATABLE "schema" ///< Identifier for the Lua metatable
|
|
|
|
|
|
|
|
struct lua_schema_item
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct lua_schema_item)
|
|
|
|
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
|
|
|
struct config_item *item; ///< The item managed by the schema
|
|
|
|
struct config_schema schema; ///< Schema itself
|
|
|
|
|
|
|
|
int ref_validate; ///< Reference to "validate" callback
|
|
|
|
int ref_on_change; ///< Reference to "on_change" callback
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_schema_item_discard (struct lua_schema_item *self)
|
|
|
|
{
|
|
|
|
if (self->item)
|
|
|
|
{
|
|
|
|
self->item->schema = NULL;
|
|
|
|
self->item->user_data = NULL;
|
|
|
|
self->item = NULL;
|
|
|
|
LIST_UNLINK (self->plugin->schemas, self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now that we've disconnected from the item, allow garbage collection
|
|
|
|
lua_cache_invalidate (self->plugin->L, self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_schema_item_gc (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_schema_item *self =
|
|
|
|
luaL_checkudata (L, 1, XLUA_SCHEMA_METATABLE);
|
|
|
|
lua_schema_item_discard (self);
|
|
|
|
|
|
|
|
free ((char *) self->schema.name);
|
|
|
|
free ((char *) self->schema.comment);
|
|
|
|
free ((char *) self->schema.default_);
|
|
|
|
|
|
|
|
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_validate);
|
|
|
|
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_on_change);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-01-05 22:12:22 +01:00
|
|
|
static luaL_Reg lua_schema_table[] =
|
2015-12-28 02:03:26 +01:00
|
|
|
{
|
|
|
|
{ "__gc", lua_schema_item_gc },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
/// Unfortunately this has the same problem as JSON libraries in that Lua
|
|
|
|
/// cannot store null values in containers (it has no distinct "undefined" type)
|
|
|
|
static void
|
|
|
|
lua_plugin_push_config_item (lua_State *L, const struct config_item *item)
|
|
|
|
{
|
|
|
|
switch (item->type)
|
|
|
|
{
|
|
|
|
case CONFIG_ITEM_NULL:
|
|
|
|
lua_pushnil (L);
|
|
|
|
break;
|
|
|
|
case CONFIG_ITEM_OBJECT:
|
|
|
|
{
|
|
|
|
lua_createtable (L, 0, item->value.object.len);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&item->value.object);
|
2015-12-28 02:03:26 +01:00
|
|
|
struct config_item *child;
|
|
|
|
while ((child = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
lua_plugin_push_config_item (L, child);
|
|
|
|
lua_setfield (L, -2, iter.link->key);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case CONFIG_ITEM_BOOLEAN:
|
|
|
|
lua_pushboolean (L, item->value.boolean);
|
|
|
|
break;
|
|
|
|
case CONFIG_ITEM_INTEGER:
|
|
|
|
lua_pushinteger (L, item->value.integer);
|
|
|
|
break;
|
|
|
|
case CONFIG_ITEM_STRING:
|
|
|
|
case CONFIG_ITEM_STRING_ARRAY:
|
|
|
|
lua_pushlstring (L, item->value.string.str, item->value.string.len);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_schema_item_validate (const struct config_item *item, struct error **e)
|
|
|
|
{
|
|
|
|
struct lua_schema_item *self = item->user_data;
|
|
|
|
if (self->ref_validate == LUA_REFNIL)
|
|
|
|
return true;
|
|
|
|
|
2016-01-05 23:05:06 +01:00
|
|
|
lua_State *L = self->plugin->L;
|
2015-12-28 02:03:26 +01:00
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_validate);
|
|
|
|
lua_plugin_push_config_item (L, item);
|
|
|
|
|
|
|
|
// The callback can make use of error("...", 0) to produce nice messages
|
2016-01-05 23:05:06 +01:00
|
|
|
return lua_plugin_call (self->plugin, 1, 0, e);
|
2015-12-28 02:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_schema_item_on_change (struct config_item *item)
|
|
|
|
{
|
|
|
|
struct lua_schema_item *self = item->user_data;
|
|
|
|
if (self->ref_on_change == LUA_REFNIL)
|
|
|
|
return;
|
|
|
|
|
2016-01-05 23:05:06 +01:00
|
|
|
lua_State *L = self->plugin->L;
|
2015-12-28 02:03:26 +01:00
|
|
|
lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_change);
|
|
|
|
lua_plugin_push_config_item (L, item);
|
|
|
|
|
2016-01-05 23:05:06 +01:00
|
|
|
struct error *e = NULL;
|
|
|
|
if (!lua_plugin_call (self->plugin, 1, 0, &e))
|
|
|
|
lua_plugin_log_error (self->plugin, "schema on_change", e);
|
2015-12-28 02:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_decode_config_item_type (const char *type)
|
|
|
|
{
|
|
|
|
if (!strcmp (type, "null")) return CONFIG_ITEM_NULL;
|
|
|
|
if (!strcmp (type, "object")) return CONFIG_ITEM_OBJECT;
|
|
|
|
if (!strcmp (type, "boolean")) return CONFIG_ITEM_BOOLEAN;
|
|
|
|
if (!strcmp (type, "integer")) return CONFIG_ITEM_INTEGER;
|
|
|
|
if (!strcmp (type, "string")) return CONFIG_ITEM_STRING;
|
|
|
|
if (!strcmp (type, "string_array")) return CONFIG_ITEM_STRING_ARRAY;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_plugin_check_field (lua_State *L, int idx, const char *name,
|
|
|
|
int expected, bool optional)
|
|
|
|
{
|
|
|
|
int found = lua_getfield (L, idx, name);
|
|
|
|
if (found == expected)
|
|
|
|
return true;
|
|
|
|
if (optional && found == LUA_TNIL)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const char *message = optional
|
|
|
|
? "invalid field \"%s\" (found: %s, expected: %s or nil)"
|
|
|
|
: "invalid or missing field \"%s\" (found: %s, expected: %s)";
|
|
|
|
return luaL_error (L, message, name,
|
|
|
|
lua_typename (L, found), lua_typename (L, expected));
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_add_config_schema (lua_State *L, struct lua_plugin *plugin,
|
2015-12-28 02:03:26 +01:00
|
|
|
struct config_item *subtree, const char *name)
|
|
|
|
{
|
|
|
|
struct config_item *item = str_map_find (&subtree->value.object, name);
|
|
|
|
// This should only ever happen because of a conflict with another plugin;
|
|
|
|
// this is the price we pay for simplicity
|
|
|
|
if (item && item->schema)
|
|
|
|
{
|
|
|
|
struct lua_schema_item *owner = item->user_data;
|
|
|
|
return luaL_error (L, "conflicting schema item: %s (owned by: %s)",
|
|
|
|
name, owner->plugin->super.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create and initialize a full userdata wrapper for the schema item
|
|
|
|
struct lua_schema_item *self = lua_newuserdata (L, sizeof *self);
|
|
|
|
luaL_setmetatable (L, XLUA_SCHEMA_METATABLE);
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
|
|
|
|
self->plugin = plugin;
|
|
|
|
self->ref_on_change = LUA_REFNIL;
|
|
|
|
self->ref_validate = LUA_REFNIL;
|
|
|
|
|
|
|
|
struct config_schema *schema = &self->schema;
|
2016-01-09 02:35:05 +01:00
|
|
|
schema->name = xstrdup (name);
|
|
|
|
schema->comment = NULL;
|
|
|
|
schema->default_ = NULL;
|
|
|
|
schema->type = CONFIG_ITEM_NULL;
|
|
|
|
schema->on_change = lua_schema_item_on_change;
|
|
|
|
schema->validate = lua_schema_item_validate;
|
2015-12-28 02:03:26 +01:00
|
|
|
|
|
|
|
// Try to update the defaults with values provided by the plugin
|
|
|
|
int values = lua_absindex (L, -2);
|
|
|
|
(void) lua_plugin_check_field (L, values, "type", LUA_TSTRING, false);
|
|
|
|
int item_type = schema->type =
|
|
|
|
lua_plugin_decode_config_item_type (lua_tostring (L, -1));
|
|
|
|
if (item_type == -1)
|
|
|
|
return luaL_error (L, "invalid type of schema item");
|
|
|
|
|
|
|
|
if (lua_plugin_check_field (L, values, "comment", LUA_TSTRING, true))
|
|
|
|
schema->comment = xstrdup (lua_tostring (L, -1));
|
|
|
|
if (lua_plugin_check_field (L, values, "default", LUA_TSTRING, true))
|
|
|
|
schema->default_ = xstrdup (lua_tostring (L, -1));
|
|
|
|
|
2016-01-09 02:39:26 +01:00
|
|
|
lua_pop (L, 3);
|
|
|
|
|
|
|
|
(void) lua_plugin_check_field (L, values, "on_change", LUA_TFUNCTION, true);
|
|
|
|
self->ref_on_change = luaL_ref (L, LUA_REGISTRYINDEX);
|
|
|
|
(void) lua_plugin_check_field (L, values, "validate", LUA_TFUNCTION, true);
|
|
|
|
self->ref_validate = luaL_ref (L, LUA_REGISTRYINDEX);
|
2015-12-28 02:03:26 +01:00
|
|
|
|
|
|
|
// Try to install the created schema item into our configuration
|
|
|
|
struct error *warning = NULL, *e = NULL;
|
|
|
|
item = config_schema_initialize_item
|
|
|
|
(&self->schema, subtree, self, &warning, &e);
|
|
|
|
|
|
|
|
if (warning)
|
|
|
|
{
|
|
|
|
log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s",
|
|
|
|
plugin->super.name, warning->message);
|
|
|
|
error_free (warning);
|
|
|
|
}
|
|
|
|
if (e)
|
|
|
|
{
|
|
|
|
const char *error = lua_pushstring (L, e->message);
|
|
|
|
error_free (e);
|
|
|
|
return luaL_error (L, "%s", error);
|
|
|
|
}
|
|
|
|
|
|
|
|
self->item = item;
|
|
|
|
LIST_PREPEND (plugin->schemas, self);
|
|
|
|
|
|
|
|
// On the stack there should be the schema table and the resulting object;
|
|
|
|
// we need to make sure Lua doesn't GC the second and get rid of them both
|
|
|
|
lua_cache_store (L, self, -1);
|
|
|
|
lua_pop (L, 2);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_setup_config (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
luaL_checktype (L, 1, LUA_TTABLE);
|
|
|
|
|
|
|
|
struct app_context *ctx = plugin->ctx;
|
|
|
|
char *config_name = plugin_config_name (&plugin->super);
|
|
|
|
if (!config_name)
|
|
|
|
return luaL_error (L, "unsuitable plugin name");
|
|
|
|
|
|
|
|
struct str_map *plugins = get_plugins_config (ctx);
|
|
|
|
struct config_item *subtree = str_map_find (plugins, config_name);
|
|
|
|
if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
|
|
|
|
str_map_set (plugins, config_name, (subtree = config_item_object ()));
|
|
|
|
free (config_name);
|
|
|
|
|
|
|
|
LIST_FOR_EACH (struct lua_schema_item, iter, plugin->schemas)
|
|
|
|
lua_schema_item_discard (iter);
|
|
|
|
|
|
|
|
// Load all schema items and apply them to the plugin's subtree
|
|
|
|
lua_pushnil (L);
|
|
|
|
while (lua_next (L, 1))
|
|
|
|
{
|
|
|
|
if (lua_type (L, -2) != LUA_TSTRING
|
|
|
|
|| lua_type (L, -1) != LUA_TTABLE)
|
|
|
|
return luaL_error (L, "%s: %s -> %s", "invalid types",
|
2016-01-15 01:44:35 +01:00
|
|
|
lua_typename (L, lua_type (L, -2)),
|
|
|
|
lua_typename (L, lua_type (L, -1)));
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_add_config_schema (L, plugin, subtree, lua_tostring (L, -2));
|
2015-12-28 02:03:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Let the plugin read out configuration via on_change callbacks
|
|
|
|
config_schema_call_changed (subtree);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-01-05 22:12:22 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
/// Identifier for the Lua metatable
|
|
|
|
#define XLUA_CONNECTION_METATABLE "connection"
|
|
|
|
|
|
|
|
struct lua_connection
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
|
|
|
struct poller_fd socket_event; ///< Socket is ready
|
|
|
|
int socket_fd; ///< Underlying connected socket
|
2016-01-07 15:59:29 +01:00
|
|
|
|
|
|
|
bool got_eof; ///< Half-closed by remote host
|
|
|
|
bool closing; ///< We're closing the connection
|
|
|
|
|
|
|
|
struct str read_buffer; ///< Read buffer
|
|
|
|
struct str write_buffer; ///< Write buffer
|
2016-01-05 22:12:22 +01:00
|
|
|
};
|
|
|
|
|
2016-01-07 15:59:29 +01:00
|
|
|
static void
|
|
|
|
lua_connection_update_poller (struct lua_connection *self)
|
|
|
|
{
|
|
|
|
poller_fd_set (&self->socket_event,
|
|
|
|
self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_connection_send (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_connection *self =
|
|
|
|
luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
|
|
|
|
if (self->socket_fd == -1)
|
|
|
|
return luaL_error (L, "connection has been closed");
|
|
|
|
|
|
|
|
size_t len;
|
|
|
|
const char *s = luaL_checklstring (L, 2, &len);
|
|
|
|
str_append_data (&self->write_buffer, s, len);
|
|
|
|
lua_connection_update_poller (self);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-01-07 01:30:19 +01:00
|
|
|
static void
|
|
|
|
lua_connection_discard (struct lua_connection *self)
|
2016-01-05 22:12:22 +01:00
|
|
|
{
|
|
|
|
if (self->socket_fd != -1)
|
|
|
|
{
|
|
|
|
poller_fd_reset (&self->socket_event);
|
|
|
|
xclose (self->socket_fd);
|
|
|
|
self->socket_fd = -1;
|
2016-01-07 15:59:29 +01:00
|
|
|
|
|
|
|
str_free (&self->read_buffer);
|
|
|
|
str_free (&self->write_buffer);
|
2016-01-05 22:12:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Connection is dead, we don't need to hold onto any resources anymore
|
2016-01-07 01:30:19 +01:00
|
|
|
lua_cache_invalidate (self->plugin->L, self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_connection_close (lua_State *L)
|
2016-01-07 15:59:29 +01:00
|
|
|
{
|
|
|
|
struct lua_connection *self =
|
|
|
|
luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
|
|
|
|
if (self->socket_fd != -1)
|
|
|
|
{
|
|
|
|
self->closing = true;
|
2018-01-08 21:53:42 +01:00
|
|
|
// NOTE: this seems to do nothing on Linux
|
2016-01-07 15:59:29 +01:00
|
|
|
(void) shutdown (self->socket_fd, SHUT_RD);
|
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// Right now we want to wait until all data is flushed to the socket
|
|
|
|
// and can't call close() here immediately -- a rewrite to use async
|
|
|
|
// would enable the user to await on either :send() or :flush();
|
|
|
|
// a successful send() doesn't necessarily mean anything though
|
2016-01-07 15:59:29 +01:00
|
|
|
if (!self->write_buffer.len)
|
|
|
|
lua_connection_discard (self);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_connection_gc (lua_State *L)
|
2016-01-07 01:30:19 +01:00
|
|
|
{
|
|
|
|
lua_connection_discard (luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE));
|
2016-01-05 22:12:22 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static luaL_Reg lua_connection_table[] =
|
|
|
|
{
|
2016-01-07 15:59:29 +01:00
|
|
|
{ "send", lua_connection_send },
|
2016-01-05 22:12:22 +01:00
|
|
|
{ "close", lua_connection_close },
|
2016-01-07 15:59:29 +01:00
|
|
|
{ "__gc", lua_connection_gc },
|
2016-01-05 22:12:22 +01:00
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-01-07 15:59:29 +01:00
|
|
|
static int
|
|
|
|
lua_connection_check_fn (lua_State *L)
|
|
|
|
{
|
|
|
|
lua_plugin_check_field (L, 1, luaL_checkstring (L, 2), LUA_TFUNCTION, true);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to run it in a protected environment because of lua_getfield()
|
|
|
|
static bool
|
|
|
|
lua_connection_cb_lookup (struct lua_connection *self, const char *name,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
lua_State *L = self->plugin->L;
|
|
|
|
lua_pushcfunction (L, lua_connection_check_fn);
|
|
|
|
hard_assert (lua_cache_get (L, self));
|
|
|
|
lua_pushstring (L, name);
|
|
|
|
return lua_plugin_call (self->plugin, 2, 1, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ideally lua_connection_cb_lookup() would return a ternary value
|
|
|
|
static bool
|
|
|
|
lua_connection_eat_nil (struct lua_connection *self)
|
|
|
|
{
|
|
|
|
if (lua_toboolean (self->plugin->L, -1))
|
|
|
|
return false;
|
|
|
|
lua_pop (self->plugin->L, 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_connection_invoke_on_data (struct lua_connection *self, struct error **e)
|
|
|
|
{
|
|
|
|
if (!lua_connection_cb_lookup (self, "on_data", e))
|
|
|
|
return false;
|
|
|
|
if (lua_connection_eat_nil (self))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
lua_pushlstring (self->plugin->L,
|
|
|
|
self->read_buffer.str, self->read_buffer.len);
|
|
|
|
return lua_plugin_call (self->plugin, 1, 0, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_connection_invoke_on_eof (struct lua_connection *self, struct error **e)
|
|
|
|
{
|
|
|
|
if (!lua_connection_cb_lookup (self, "on_eof", e))
|
|
|
|
return false;
|
|
|
|
if (lua_connection_eat_nil (self))
|
|
|
|
return true;
|
|
|
|
return lua_plugin_call (self->plugin, 0, 0, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_connection_invoke_on_error (struct lua_connection *self,
|
2016-10-11 10:52:49 +02:00
|
|
|
const char *error, struct error **e)
|
2016-01-07 15:59:29 +01:00
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// XXX: not sure if ignoring errors after :close() is always desired;
|
|
|
|
// code might want to make sure that data are transferred successfully
|
2016-01-07 15:59:29 +01:00
|
|
|
if (!self->closing
|
|
|
|
&& lua_connection_cb_lookup (self, "on_error", e)
|
|
|
|
&& !lua_connection_eat_nil (self))
|
|
|
|
{
|
2016-10-11 10:52:49 +02:00
|
|
|
lua_pushstring (self->plugin->L, error);
|
2016-01-07 15:59:29 +01:00
|
|
|
lua_plugin_call (self->plugin, 1, 0, e);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_connection_try_read (struct lua_connection *self, struct error **e)
|
|
|
|
{
|
|
|
|
// Avoid the read call when it's obviously not going to return any data
|
|
|
|
// and would only cause unwanted invocation of callbacks
|
|
|
|
if (self->closing || self->got_eof)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
enum socket_io_result read_result =
|
2016-10-11 10:52:49 +02:00
|
|
|
socket_io_try_read (self->socket_fd, &self->read_buffer);
|
|
|
|
const char *error = strerror (errno);
|
2016-01-07 15:59:29 +01:00
|
|
|
|
|
|
|
// Dispatch any data that we got before an EOF or any error
|
|
|
|
if (self->read_buffer.len)
|
|
|
|
{
|
|
|
|
if (!lua_connection_invoke_on_data (self, e))
|
|
|
|
return false;
|
|
|
|
str_reset (&self->read_buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (read_result == SOCKET_IO_EOF)
|
|
|
|
{
|
|
|
|
if (!lua_connection_invoke_on_eof (self, e))
|
|
|
|
return false;
|
|
|
|
self->got_eof = true;
|
|
|
|
}
|
|
|
|
if (read_result == SOCKET_IO_ERROR)
|
|
|
|
return lua_connection_invoke_on_error (self, error, e);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_connection_try_write (struct lua_connection *self, struct error **e)
|
|
|
|
{
|
|
|
|
enum socket_io_result write_result =
|
2016-10-11 10:52:49 +02:00
|
|
|
socket_io_try_write (self->socket_fd, &self->write_buffer);
|
|
|
|
const char *error = strerror (errno);
|
2016-01-07 15:59:29 +01:00
|
|
|
|
|
|
|
if (write_result == SOCKET_IO_ERROR)
|
|
|
|
return lua_connection_invoke_on_error (self, error, e);
|
|
|
|
return !self->closing || self->write_buffer.len;
|
|
|
|
}
|
|
|
|
|
2016-01-05 22:12:22 +01:00
|
|
|
static void
|
|
|
|
lua_connection_on_ready (const struct pollfd *pfd, struct lua_connection *self)
|
|
|
|
{
|
2016-01-07 15:59:29 +01:00
|
|
|
(void) pfd;
|
|
|
|
|
|
|
|
// Hold a reference so that it doesn't get collected on close()
|
|
|
|
hard_assert (lua_cache_get (self->plugin->L, self));
|
|
|
|
|
|
|
|
struct error *e = NULL;
|
|
|
|
bool keep = lua_connection_try_read (self, &e)
|
|
|
|
&& lua_connection_try_write (self, &e);
|
|
|
|
if (e)
|
|
|
|
lua_plugin_log_error (self->plugin, "network I/O", e);
|
|
|
|
if (keep)
|
|
|
|
lua_connection_update_poller (self);
|
|
|
|
else
|
|
|
|
lua_connection_discard (self);
|
|
|
|
|
|
|
|
lua_pop (self->plugin->L, 1);
|
2016-01-05 22:12:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static struct lua_connection *
|
|
|
|
lua_plugin_push_connection (struct lua_plugin *plugin, int socket_fd)
|
|
|
|
{
|
|
|
|
lua_State *L = plugin->L;
|
|
|
|
|
|
|
|
struct lua_connection *self = lua_newuserdata (L, sizeof *self);
|
|
|
|
luaL_setmetatable (L, XLUA_CONNECTION_METATABLE);
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
self->plugin = plugin;
|
|
|
|
|
2016-01-09 05:47:24 +01:00
|
|
|
set_blocking (socket_fd, false);
|
2017-06-22 22:39:39 +02:00
|
|
|
self->socket_event = poller_fd_make
|
|
|
|
(&plugin->ctx->poller, (self->socket_fd = socket_fd));
|
2016-01-05 22:12:22 +01:00
|
|
|
self->socket_event.dispatcher = (poller_fd_fn) lua_connection_on_ready;
|
|
|
|
self->socket_event.user_data = self;
|
|
|
|
poller_fd_set (&self->socket_event, POLLIN);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->read_buffer = str_make ();
|
|
|
|
self->write_buffer = str_make ();
|
2016-01-07 15:59:29 +01:00
|
|
|
|
2016-01-05 22:12:22 +01:00
|
|
|
// Make sure the connection doesn't get garbage collected and return it
|
|
|
|
lua_cache_store (L, self, -1);
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
// The script can create as many wait channels as wanted. They only actually
|
|
|
|
// do anything once they get yielded to the main lua_resume() call.
|
|
|
|
|
|
|
|
/// Identifier for the Lua metatable
|
|
|
|
#define XLUA_WCHANNEL_METATABLE "wchannel"
|
|
|
|
|
|
|
|
struct lua_wait_channel
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct lua_wait_channel)
|
|
|
|
|
|
|
|
struct lua_task *task; ///< The task we're active in
|
|
|
|
|
|
|
|
/// Check if the event is ready and eventually push values to the thread;
|
|
|
|
/// the channel then may release any resources
|
|
|
|
bool (*check) (struct lua_wait_channel *self);
|
|
|
|
|
|
|
|
/// Release all resources held by the subclass
|
|
|
|
void (*cleanup) (struct lua_wait_channel *self);
|
|
|
|
};
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_wchannel_gc (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_wait_channel *self =
|
|
|
|
luaL_checkudata (L, 1, XLUA_WCHANNEL_METATABLE);
|
|
|
|
if (self->cleanup)
|
|
|
|
self->cleanup (self);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static luaL_Reg lua_wchannel_table[] =
|
|
|
|
{
|
|
|
|
{ "__gc", lua_wchannel_gc },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
// A task encapsulates a thread so that wait channels yielded from its main
|
|
|
|
// function get waited upon by the event loop
|
|
|
|
|
|
|
|
#define XLUA_TASK_METATABLE "task" ///< Identifier for the Lua metatable
|
|
|
|
|
|
|
|
struct lua_task
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct lua_task)
|
|
|
|
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
|
|
|
lua_State *thread; ///< Lua thread
|
|
|
|
struct lua_wait_channel *active; ///< Channels we're waiting on
|
|
|
|
struct poller_idle idle; ///< Idle job
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_task_unregister_channels (struct lua_task *self)
|
|
|
|
{
|
|
|
|
LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
|
|
|
|
{
|
|
|
|
iter->task = NULL;
|
|
|
|
LIST_UNLINK (self->active, iter);
|
|
|
|
lua_cache_invalidate (self->plugin->L, iter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_task_cancel_internal (struct lua_task *self)
|
|
|
|
{
|
|
|
|
if (self->thread)
|
|
|
|
{
|
|
|
|
lua_cache_invalidate (self->plugin->L, self->thread);
|
|
|
|
self->thread = NULL;
|
|
|
|
}
|
|
|
|
lua_task_unregister_channels (self);
|
|
|
|
poller_idle_reset (&self->idle);
|
|
|
|
|
|
|
|
// The task no longer has to stay alive
|
|
|
|
lua_cache_invalidate (self->plugin->L, self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_task_cancel (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_task *self = luaL_checkudata (L, 1, XLUA_TASK_METATABLE);
|
|
|
|
// We could also yield and make lua_task_resume() check "self->thread",
|
|
|
|
// however the main issue here is that the script should just return
|
|
|
|
luaL_argcheck (L, L != self->thread, 1,
|
|
|
|
"cannot cancel task from within itself");
|
|
|
|
lua_task_cancel_internal (self);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
#define lua_task_wakeup(self) poller_idle_set (&(self)->idle)
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_task_schedule (struct lua_task *self, int n, struct error **e)
|
|
|
|
{
|
|
|
|
lua_State *L = self->thread;
|
|
|
|
for (int i = -1; -i <= n; i--)
|
|
|
|
{
|
|
|
|
struct lua_wait_channel *channel =
|
|
|
|
luaL_testudata (L, i, XLUA_WCHANNEL_METATABLE);
|
|
|
|
if (!channel)
|
|
|
|
return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
|
|
|
|
"tasks can only yield wait channels");
|
|
|
|
if (channel->task)
|
|
|
|
return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
|
|
|
|
"wait channels can only be active in one task at most");
|
|
|
|
}
|
|
|
|
for (int i = -1; -i <= n; i--)
|
|
|
|
{
|
|
|
|
// Quietly ignore duplicate channels
|
|
|
|
struct lua_wait_channel *channel = lua_touserdata (L, i);
|
|
|
|
if (channel->task)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// By going in reverse the list ends up in the right order
|
|
|
|
channel->task = self;
|
|
|
|
LIST_PREPEND (self->active, channel);
|
|
|
|
lua_cache_store (self->plugin->L, channel, i);
|
|
|
|
}
|
|
|
|
lua_pop (L, n);
|
|
|
|
|
|
|
|
// There doesn't have to be a single channel
|
|
|
|
// We can also be waiting on a channel that is already ready
|
|
|
|
lua_task_wakeup (self);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_task_resume (struct lua_task *self, int index)
|
|
|
|
{
|
|
|
|
lua_State *L = self->thread;
|
|
|
|
bool waiting_on_multiple = self->active && self->active->next;
|
|
|
|
|
|
|
|
// Since we've ended the wait, we don't need to hold on to them anymore
|
|
|
|
lua_task_unregister_channels (self);
|
|
|
|
|
|
|
|
// On the first run we also have the main function on the stack,
|
|
|
|
// before any initial arguments
|
|
|
|
int n = lua_gettop (L) - (lua_status (L) == LUA_OK);
|
|
|
|
|
|
|
|
// Pack the values in a table and prepend the index of the channel, so that
|
|
|
|
// the caller doesn't need to care about the number of return values
|
|
|
|
if (waiting_on_multiple)
|
|
|
|
{
|
|
|
|
lua_plugin_pack (L, n);
|
|
|
|
lua_pushinteger (L, index);
|
|
|
|
lua_insert (L, -2);
|
|
|
|
n = 2;
|
|
|
|
}
|
|
|
|
|
2020-09-02 19:05:20 +02:00
|
|
|
#if LUA_VERSION_NUM >= 504
|
|
|
|
int nresults = 0;
|
|
|
|
int res = lua_resume (L, NULL, n, &nresults);
|
|
|
|
#else
|
2016-11-04 20:38:09 +01:00
|
|
|
int res = lua_resume (L, NULL, n);
|
2020-09-02 19:05:20 +02:00
|
|
|
int nresults = lua_gettop (L);
|
|
|
|
#endif
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
struct error *error = NULL;
|
|
|
|
if (res == LUA_YIELD)
|
|
|
|
{
|
|
|
|
// AFAIK we don't get any good error context information from here
|
2020-09-02 19:05:20 +02:00
|
|
|
if (lua_task_schedule (self, nresults, &error))
|
2016-11-04 20:38:09 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// For simplicity ignore any results from successful returns
|
|
|
|
else if (res != LUA_OK)
|
|
|
|
{
|
|
|
|
luaL_traceback (L, L, lua_tostring (L, -1), 0 /* or 1? */);
|
|
|
|
lua_plugin_process_error (self->plugin, lua_tostring (L, -1), &error);
|
|
|
|
lua_pop (L, 2);
|
|
|
|
}
|
|
|
|
if (error)
|
|
|
|
lua_plugin_log_error (self->plugin, "task", error);
|
|
|
|
lua_task_cancel_internal (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_task_check (struct lua_task *self)
|
|
|
|
{
|
|
|
|
poller_idle_reset (&self->idle);
|
|
|
|
|
|
|
|
lua_Integer i = 0;
|
|
|
|
LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
|
|
|
|
{
|
|
|
|
i++;
|
|
|
|
if (iter->check (iter))
|
|
|
|
{
|
|
|
|
lua_task_resume (self, i);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!self->active)
|
|
|
|
lua_task_resume (self, i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// The task dies either when it finishes, it is cancelled, or at plugin unload
|
|
|
|
static luaL_Reg lua_task_table[] =
|
|
|
|
{
|
|
|
|
{ "cancel", lua_task_cancel },
|
|
|
|
{ "__gc", lua_task_cancel },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct lua_wait_timer
|
|
|
|
{
|
|
|
|
struct lua_wait_channel super; ///< The structure we're deriving
|
|
|
|
struct poller_timer timer; ///< Timer event
|
|
|
|
bool expired; ///< Whether the timer has expired
|
|
|
|
};
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_wait_timer_check (struct lua_wait_channel *wchannel)
|
|
|
|
{
|
|
|
|
struct lua_wait_timer *self =
|
|
|
|
CONTAINER_OF (wchannel, struct lua_wait_timer, super);
|
|
|
|
return self->super.task && self->expired;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_timer_cleanup (struct lua_wait_channel *wchannel)
|
|
|
|
{
|
|
|
|
struct lua_wait_timer *self =
|
|
|
|
CONTAINER_OF (wchannel, struct lua_wait_timer, super);
|
|
|
|
poller_timer_reset (&self->timer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_timer_dispatch (struct lua_wait_timer *self)
|
|
|
|
{
|
|
|
|
self->expired = true;
|
|
|
|
if (self->super.task)
|
|
|
|
lua_task_wakeup (self->super.task);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_push_wait_timer (struct lua_plugin *plugin, lua_State *L,
|
|
|
|
lua_Integer timeout)
|
|
|
|
{
|
|
|
|
struct lua_wait_timer *self = lua_newuserdata (L, sizeof *self);
|
|
|
|
luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
|
|
|
|
self->super.check = lua_wait_timer_check;
|
|
|
|
self->super.cleanup = lua_wait_timer_cleanup;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->timer = poller_timer_make (&plugin->ctx->poller);
|
2016-11-04 20:38:09 +01:00
|
|
|
self->timer.dispatcher = (poller_timer_fn) lua_wait_timer_dispatch;
|
|
|
|
self->timer.user_data = self;
|
|
|
|
|
|
|
|
if (timeout)
|
|
|
|
poller_timer_set (&self->timer, timeout);
|
|
|
|
else
|
|
|
|
self->expired = true;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-11-04 21:50:46 +01:00
|
|
|
struct lua_wait_dial
|
|
|
|
{
|
|
|
|
struct lua_wait_channel super; ///< The structure we're deriving
|
|
|
|
|
|
|
|
struct lua_plugin *plugin; ///< The plugin we belong to
|
|
|
|
struct connector connector; ///< Connector object
|
|
|
|
bool active; ///< Whether the connector is alive
|
|
|
|
|
|
|
|
struct lua_connection *connection; ///< Established connection
|
|
|
|
char *hostname; ///< Target hostname
|
|
|
|
char *last_error; ///< Connecting error, if any
|
|
|
|
};
|
|
|
|
|
|
|
|
static bool
|
|
|
|
lua_wait_dial_check (struct lua_wait_channel *wchannel)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self =
|
|
|
|
CONTAINER_OF (wchannel, struct lua_wait_dial, super);
|
|
|
|
|
|
|
|
lua_State *L = self->super.task->thread;
|
|
|
|
if (self->connection)
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// FIXME: this way the connection may leak -- we pass the value to the
|
|
|
|
// task manager on the stack and forget about it but still leave the
|
|
|
|
// connection in the cache. That is because right now, when Lua code
|
|
|
|
// sets up callbacks in the connection object and returns, it might
|
|
|
|
// get otherwise GC'd since nothing else keeps referencing it.
|
|
|
|
// By rewriting lua_connection using async, tasks and wait channels
|
|
|
|
// would hold a reference, allowing us to remove it from the cache.
|
2016-11-04 21:50:46 +01:00
|
|
|
lua_cache_get (L, self->connection);
|
|
|
|
lua_pushstring (L, self->hostname);
|
|
|
|
self->connection = NULL;
|
|
|
|
}
|
|
|
|
else if (self->last_error)
|
|
|
|
{
|
|
|
|
lua_pushnil (L);
|
|
|
|
lua_pushnil (L);
|
|
|
|
lua_pushstring (L, self->last_error);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_dial_cancel (struct lua_wait_dial *self)
|
|
|
|
{
|
|
|
|
if (self->active)
|
|
|
|
{
|
|
|
|
connector_free (&self->connector);
|
|
|
|
self->active = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_dial_cleanup (struct lua_wait_channel *wchannel)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self =
|
|
|
|
CONTAINER_OF (wchannel, struct lua_wait_dial, super);
|
|
|
|
|
|
|
|
lua_wait_dial_cancel (self);
|
|
|
|
if (self->connection)
|
|
|
|
lua_connection_discard (self->connection);
|
|
|
|
|
|
|
|
free (self->hostname);
|
|
|
|
free (self->last_error);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_dial_on_connected (void *user_data, int socket, const char *hostname)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self = user_data;
|
|
|
|
if (self->super.task)
|
|
|
|
lua_task_wakeup (self->super.task);
|
|
|
|
|
|
|
|
self->connection = lua_plugin_push_connection (self->plugin, socket);
|
|
|
|
// TODO: use the hostname for SNI once TLS is implemented
|
|
|
|
self->hostname = xstrdup (hostname);
|
|
|
|
lua_wait_dial_cancel (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_dial_on_failure (void *user_data)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self = user_data;
|
|
|
|
if (self->super.task)
|
|
|
|
lua_task_wakeup (self->super.task);
|
|
|
|
lua_wait_dial_cancel (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
lua_wait_dial_on_error (void *user_data, const char *error)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self = user_data;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->last_error, xstrdup (error));
|
2016-11-04 21:50:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_push_wait_dial (struct lua_plugin *plugin, lua_State *L,
|
|
|
|
const char *host, const char *service)
|
|
|
|
{
|
|
|
|
struct lua_wait_dial *self = lua_newuserdata (L, sizeof *self);
|
|
|
|
luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
|
|
|
|
self->super.check = lua_wait_dial_check;
|
|
|
|
self->super.cleanup = lua_wait_dial_cleanup;
|
|
|
|
|
|
|
|
struct connector *connector = &self->connector;
|
|
|
|
connector_init (connector, &plugin->ctx->poller);
|
|
|
|
connector_add_target (connector, host, service);
|
|
|
|
|
|
|
|
connector->on_connected = lua_wait_dial_on_connected;
|
|
|
|
connector->on_connecting = NULL;
|
|
|
|
connector->on_error = lua_wait_dial_on_error;
|
|
|
|
connector->on_failure = lua_wait_dial_on_failure;
|
|
|
|
connector->user_data = self;
|
|
|
|
|
|
|
|
self->plugin = plugin;
|
|
|
|
self->active = true;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static int
|
|
|
|
lua_async_go (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
luaL_checktype (L, 1, LUA_TFUNCTION);
|
|
|
|
|
|
|
|
lua_State *thread = lua_newthread (L);
|
|
|
|
lua_cache_store (L, thread, -1);
|
|
|
|
lua_pop (L, 1);
|
|
|
|
|
|
|
|
// Move the main function w/ arguments to the thread
|
|
|
|
lua_xmove (L, thread, lua_gettop (L));
|
|
|
|
|
|
|
|
struct lua_task *task = lua_newuserdata (L, sizeof *task);
|
|
|
|
luaL_setmetatable (L, XLUA_TASK_METATABLE);
|
|
|
|
memset (task, 0, sizeof *task);
|
|
|
|
task->plugin = plugin;
|
|
|
|
task->thread = thread;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
task->idle = poller_idle_make (&plugin->ctx->poller);
|
2016-11-04 20:38:09 +01:00
|
|
|
task->idle.dispatcher = (poller_idle_fn) lua_task_check;
|
|
|
|
task->idle.user_data = task;
|
|
|
|
poller_idle_set (&task->idle);
|
|
|
|
|
|
|
|
// Make sure the task doesn't get garbage collected and return it
|
|
|
|
lua_cache_store (L, task, -1);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_async_timer_ms (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
lua_Integer timeout = luaL_checkinteger (L, 1);
|
|
|
|
if (timeout < 0)
|
|
|
|
luaL_argerror (L, 1, "timeout mustn't be negative");
|
|
|
|
return lua_plugin_push_wait_timer (plugin, L, timeout);
|
|
|
|
}
|
|
|
|
|
2016-11-04 21:50:46 +01:00
|
|
|
static int
|
|
|
|
lua_async_dial (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
return lua_plugin_push_wait_dial (plugin, L,
|
|
|
|
luaL_checkstring (L, 1), luaL_checkstring (L, 2));
|
|
|
|
}
|
|
|
|
|
2016-11-04 20:38:09 +01:00
|
|
|
static luaL_Reg lua_async_library[] =
|
|
|
|
{
|
|
|
|
{ "go", lua_async_go },
|
|
|
|
{ "timer_ms", lua_async_timer_ms },
|
2016-11-04 21:50:46 +01:00
|
|
|
{ "dial", lua_async_dial },
|
2016-11-04 20:38:09 +01:00
|
|
|
{ NULL, NULL },
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2016-10-28 03:53:26 +02:00
|
|
|
static int
|
|
|
|
lua_plugin_get_screen_size (lua_State *L)
|
|
|
|
{
|
|
|
|
lua_pushinteger (L, g_terminal.lines);
|
|
|
|
lua_pushinteger (L, g_terminal.columns);
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
|
2022-08-27 08:55:44 +02:00
|
|
|
static int
|
|
|
|
lua_plugin_measure (lua_State *L)
|
|
|
|
{
|
|
|
|
struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
const char *line = lua_plugin_check_utf8 (L, 1);
|
|
|
|
|
|
|
|
size_t term_len = 0, processed = 0, width = 0, len;
|
|
|
|
char *term = iconv_xstrdup (plugin->ctx->term_from_utf8,
|
|
|
|
(char *) line, strlen (line) + 1, &term_len);
|
|
|
|
|
|
|
|
mbstate_t ps;
|
|
|
|
memset (&ps, 0, sizeof ps);
|
|
|
|
|
|
|
|
wchar_t wch;
|
|
|
|
while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
|
|
|
|
{
|
|
|
|
hard_assert (len != (size_t) -2 && len != (size_t) -1);
|
|
|
|
hard_assert ((processed += len) <= term_len);
|
|
|
|
|
|
|
|
int wch_width = wcwidth (wch);
|
|
|
|
width += MAX (0, wch_width);
|
|
|
|
}
|
|
|
|
free (term);
|
|
|
|
lua_pushinteger (L, width);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-10-27 20:02:25 +02:00
|
|
|
static int
|
|
|
|
lua_ctx_gc (lua_State *L)
|
|
|
|
{
|
|
|
|
return lua_weak_gc (L, &lua_ctx_info);
|
|
|
|
}
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
static luaL_Reg lua_plugin_library[] =
|
|
|
|
{
|
2016-10-27 20:02:25 +02:00
|
|
|
// These are global functions:
|
|
|
|
|
2016-04-20 23:23:53 +02:00
|
|
|
{ "parse", lua_plugin_parse },
|
2016-01-15 01:44:35 +01:00
|
|
|
{ "hook_input", lua_plugin_hook_input },
|
|
|
|
{ "hook_irc", lua_plugin_hook_irc },
|
2016-10-28 01:01:27 +02:00
|
|
|
{ "hook_prompt", lua_plugin_hook_prompt },
|
2016-01-15 01:44:35 +01:00
|
|
|
{ "hook_completion", lua_plugin_hook_completion },
|
|
|
|
{ "setup_config", lua_plugin_setup_config },
|
2016-10-27 20:02:25 +02:00
|
|
|
|
|
|
|
// And these are methods:
|
2016-10-28 03:53:26 +02:00
|
|
|
|
|
|
|
{ "get_screen_size", lua_plugin_get_screen_size },
|
2022-08-27 08:55:44 +02:00
|
|
|
{ "measure", lua_plugin_measure },
|
2016-10-27 20:02:25 +02:00
|
|
|
{ "__gc", lua_ctx_gc },
|
2016-01-15 01:44:35 +01:00
|
|
|
{ NULL, NULL },
|
2015-11-20 02:43:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
static void *
|
|
|
|
lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
|
|
|
|
{
|
|
|
|
(void) ud;
|
|
|
|
(void) o_size;
|
|
|
|
|
|
|
|
if (n_size)
|
|
|
|
return realloc (ptr, n_size);
|
|
|
|
|
|
|
|
free (ptr);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_panic (lua_State *L)
|
|
|
|
{
|
|
|
|
// XXX: we might be able to do something better
|
|
|
|
print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
|
|
|
|
lua_close (L);
|
|
|
|
exit (EXIT_FAILURE);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2016-10-27 20:58:14 +02:00
|
|
|
static void
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_push_ref (lua_State *L, struct lua_plugin *self, void *object,
|
2016-10-28 00:07:49 +02:00
|
|
|
struct ispect_field *field)
|
2016-10-27 20:58:14 +02:00
|
|
|
{
|
2016-10-28 13:07:50 +02:00
|
|
|
// We create a mapping on object type registration
|
2016-11-04 18:51:32 +01:00
|
|
|
hard_assert (lua_rawgetp (L, LUA_REGISTRYINDEX, field->fields));
|
|
|
|
struct lua_weak_info *info = lua_touserdata (L, -1);
|
|
|
|
lua_pop (L, 1);
|
2016-10-28 00:07:49 +02:00
|
|
|
|
|
|
|
if (!field->is_list)
|
2016-10-27 20:58:14 +02:00
|
|
|
{
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_weak_push (L, self, object, info);
|
2016-10-27 20:58:14 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-10-28 13:07:50 +02:00
|
|
|
// As a rule in this codebase, these fields are right at the top of structs
|
|
|
|
struct list_header { LIST_HEADER (void) };
|
|
|
|
|
2016-10-27 20:58:14 +02:00
|
|
|
int i = 1;
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_newtable (L);
|
2016-10-27 20:58:14 +02:00
|
|
|
LIST_FOR_EACH (struct list_header, iter, object)
|
|
|
|
{
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_weak_push (L, self, iter, info);
|
|
|
|
lua_rawseti (L, -2, i++);
|
2016-10-27 20:58:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-04 18:51:32 +01:00
|
|
|
static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
|
2016-10-28 00:07:49 +02:00
|
|
|
const char *key, void *p, struct ispect_field *field);
|
|
|
|
|
|
|
|
static void
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_push_struct (lua_State *L, struct lua_plugin *self,
|
|
|
|
enum ispect_type type, void *value, struct ispect_field *field)
|
2016-10-28 00:07:49 +02:00
|
|
|
{
|
|
|
|
if (type == ISPECT_STR)
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
const struct str *s = value;
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_pushlstring (L, s->str, s->len);
|
2016-10-28 00:07:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (type == ISPECT_STR_MAP)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (value);
|
2016-10-28 00:07:49 +02:00
|
|
|
|
|
|
|
void *value;
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_newtable (L);
|
2016-10-28 00:07:49 +02:00
|
|
|
while ((value = str_map_iter_next (&iter)))
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_push_map_field (L, self, iter.link->key, value, field);
|
2016-10-28 00:07:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
hard_assert (!"unhandled introspection object type");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
|
2016-10-28 00:07:49 +02:00
|
|
|
const char *key, void *p, struct ispect_field *field)
|
|
|
|
{
|
|
|
|
// That would mean maps in maps ad infinitum
|
|
|
|
hard_assert (field->subtype != ISPECT_STR_MAP);
|
|
|
|
|
|
|
|
intptr_t n = (intptr_t) p;
|
|
|
|
switch (field->subtype)
|
|
|
|
{
|
|
|
|
// Here the types are generally casted to a void pointer
|
2016-11-04 18:51:32 +01:00
|
|
|
case ISPECT_BOOL: lua_pushboolean (L, (bool ) n); break;
|
|
|
|
case ISPECT_INT: lua_pushinteger (L, (int ) n); break;
|
|
|
|
case ISPECT_UINT: lua_pushinteger (L, (unsigned ) n); break;
|
|
|
|
case ISPECT_SIZE: lua_pushinteger (L, (size_t ) n); break;
|
|
|
|
case ISPECT_STRING: lua_pushstring (L, p); break;
|
|
|
|
case ISPECT_REF: lua_plugin_push_ref (L, self, p, field); break;
|
|
|
|
default: lua_plugin_push_struct (L, self, field->subtype, p, field);
|
2016-10-28 00:07:49 +02:00
|
|
|
}
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_setfield (L, -2, key);
|
2016-10-28 00:07:49 +02:00
|
|
|
}
|
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
static bool
|
|
|
|
lua_plugin_property_get_ispect (lua_State *L, const char *property_name)
|
|
|
|
{
|
|
|
|
struct lua_weak_info *info = lua_touserdata (L, lua_upvalueindex (1));
|
|
|
|
if (!info || !info->ispect)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
|
|
|
|
// TODO: I think we can do better than this, maybe binary search at least?
|
2016-10-28 00:07:49 +02:00
|
|
|
struct ispect_field *field;
|
|
|
|
for (field = info->ispect; field->name; field++)
|
|
|
|
if (!strcmp (property_name, field->name))
|
2016-10-27 17:03:53 +02:00
|
|
|
break;
|
2016-10-28 00:07:49 +02:00
|
|
|
if (!field->name)
|
2016-10-27 18:46:27 +02:00
|
|
|
return false;
|
2016-10-27 17:03:53 +02:00
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
struct lua_plugin *self = weak->plugin;
|
|
|
|
void *p = (uint8_t *) weak->object + field->offset;
|
|
|
|
switch (field->type)
|
2016-10-27 18:46:27 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
// Here the types are really what's under the pointer
|
|
|
|
case ISPECT_BOOL: lua_pushboolean (L, *(bool *) p); break;
|
|
|
|
case ISPECT_INT: lua_pushinteger (L, *(int *) p); break;
|
|
|
|
case ISPECT_UINT: lua_pushinteger (L, *(unsigned *) p); break;
|
|
|
|
case ISPECT_SIZE: lua_pushinteger (L, *(size_t *) p); break;
|
|
|
|
case ISPECT_STRING: lua_pushstring (L, *(char **) p); break;
|
2016-11-04 18:51:32 +01:00
|
|
|
case ISPECT_REF: lua_plugin_push_ref (L, self, *(void **) p, field); break;
|
|
|
|
default: lua_plugin_push_struct (L, self, field->type, p, field);
|
2016-10-27 17:03:53 +02:00
|
|
|
}
|
2016-10-28 00:07:49 +02:00
|
|
|
return true;
|
2016-10-27 17:03:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-24 21:42:08 +01:00
|
|
|
static int
|
|
|
|
lua_plugin_property_get (lua_State *L)
|
|
|
|
{
|
|
|
|
luaL_checktype (L, 1, LUA_TUSERDATA);
|
|
|
|
const char *property_name = luaL_checkstring (L, 2);
|
|
|
|
|
|
|
|
// Either it's directly present in the metatable
|
|
|
|
if (luaL_getmetafield (L, 1, property_name))
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
// Or we try to find and eventually call a getter method
|
|
|
|
char *getter_name = xstrdup_printf ("get_%s", property_name);
|
|
|
|
bool found = luaL_getmetafield (L, 1, getter_name);
|
|
|
|
free (getter_name);
|
|
|
|
|
|
|
|
if (found)
|
|
|
|
{
|
|
|
|
lua_pushvalue (L, 1);
|
|
|
|
lua_call (L, 1, 1);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
// Maybe we can find it via introspection
|
|
|
|
if (lua_plugin_property_get_ispect (L, property_name))
|
|
|
|
return 1;
|
|
|
|
|
2016-01-04 22:59:59 +01:00
|
|
|
// Or we look for a property set by the user (__gc cannot be overriden)
|
|
|
|
if (lua_getuservalue (L, 1) != LUA_TTABLE)
|
|
|
|
lua_pushnil (L);
|
|
|
|
else
|
|
|
|
lua_getfield (L, -1, property_name);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
lua_plugin_property_set (lua_State *L)
|
|
|
|
{
|
|
|
|
luaL_checktype (L, 1, LUA_TUSERDATA);
|
|
|
|
const char *property_name = luaL_checkstring (L, 2);
|
|
|
|
luaL_checkany (L, 3);
|
|
|
|
|
|
|
|
// We use the associated value to store user-defined properties
|
|
|
|
int type = lua_getuservalue (L, 1);
|
|
|
|
if (type == LUA_TNIL)
|
|
|
|
{
|
|
|
|
lua_pop (L, 1);
|
|
|
|
lua_newtable (L);
|
|
|
|
lua_pushvalue (L, -1);
|
|
|
|
lua_setuservalue (L, 1);
|
|
|
|
}
|
|
|
|
else if (type != LUA_TTABLE)
|
|
|
|
return luaL_error (L, "associated value is not a table");
|
|
|
|
|
|
|
|
// Beware that we do not check for conflicts here;
|
|
|
|
// if Lua code writes a conflicting field, it is effectively ignored
|
|
|
|
lua_pushvalue (L, 3);
|
|
|
|
lua_setfield (L, -2, property_name);
|
|
|
|
return 0;
|
2015-11-24 21:42:08 +01:00
|
|
|
}
|
|
|
|
|
2015-11-20 02:43:43 +01:00
|
|
|
static void
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_plugin_add_accessors (lua_State *L, struct lua_weak_info *info)
|
2015-11-19 15:45:32 +01:00
|
|
|
{
|
2015-11-24 21:42:08 +01:00
|
|
|
// Emulate properties for convenience
|
2016-10-27 18:46:27 +02:00
|
|
|
lua_pushlightuserdata (L, info);
|
2016-10-27 17:03:53 +02:00
|
|
|
lua_pushcclosure (L, lua_plugin_property_get, 1);
|
|
|
|
lua_setfield (L, -2, "__index");
|
|
|
|
lua_pushcfunction (L, lua_plugin_property_set);
|
|
|
|
lua_setfield (L, -2, "__newindex");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-10-27 18:46:27 +02:00
|
|
|
lua_plugin_reg_meta (lua_State *L, const char *name, luaL_Reg *fns)
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
2016-10-27 18:46:27 +02:00
|
|
|
luaL_newmetatable (L, name);
|
2016-10-27 17:03:53 +02:00
|
|
|
luaL_setfuncs (L, fns, 0);
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_plugin_add_accessors (L, NULL);
|
|
|
|
lua_pop (L, 1);
|
2016-10-27 18:46:27 +02:00
|
|
|
}
|
2016-10-27 17:03:53 +02:00
|
|
|
|
2016-10-27 18:46:27 +02:00
|
|
|
static void
|
|
|
|
lua_plugin_reg_weak (lua_State *L, struct lua_weak_info *info, luaL_Reg *fns)
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// Create a mapping from the object type (info->ispect) back to metadata
|
|
|
|
// so that we can figure out what to create from ISPECT_REF fields
|
2016-10-28 13:07:50 +02:00
|
|
|
lua_pushlightuserdata (L, info);
|
|
|
|
lua_rawsetp (L, LUA_REGISTRYINDEX, info->ispect);
|
|
|
|
|
2016-10-27 18:46:27 +02:00
|
|
|
luaL_newmetatable (L, info->name);
|
|
|
|
luaL_setfuncs (L, fns, 0);
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_plugin_add_accessors (L, info);
|
|
|
|
lua_pop (L, 1);
|
2015-11-20 02:43:43 +01:00
|
|
|
}
|
2015-11-19 15:45:32 +01:00
|
|
|
|
|
|
|
static struct plugin *
|
|
|
|
lua_plugin_load (struct app_context *ctx, const char *filename,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
lua_State *L = lua_newstate (lua_plugin_alloc, NULL);
|
|
|
|
if (!L)
|
|
|
|
{
|
|
|
|
error_set (e, "Lua initialization failed");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
lua_atpanic (L, lua_plugin_panic);
|
|
|
|
luaL_openlibs (L);
|
|
|
|
|
|
|
|
struct lua_plugin *plugin = xcalloc (1, sizeof *plugin);
|
2016-01-09 02:35:05 +01:00
|
|
|
plugin->super.name = xstrdup (filename);
|
2015-11-19 15:45:32 +01:00
|
|
|
plugin->super.vtable = &lua_plugin_vtable;
|
|
|
|
plugin->ctx = ctx;
|
|
|
|
plugin->L = L;
|
2015-11-20 02:43:43 +01:00
|
|
|
|
|
|
|
luaL_checkversion (L);
|
2016-10-27 20:02:25 +02:00
|
|
|
|
2021-08-06 16:12:15 +02:00
|
|
|
// Register the xC library as a singleton with "plugin" as an upvalue
|
2016-10-27 20:02:25 +02:00
|
|
|
// (mostly historical, but rather convenient)
|
|
|
|
luaL_newmetatable (L, lua_ctx_info.name);
|
2015-11-20 02:43:43 +01:00
|
|
|
lua_pushlightuserdata (L, plugin);
|
|
|
|
luaL_setfuncs (L, lua_plugin_library, 1);
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_plugin_add_accessors (L, &lua_ctx_info);
|
|
|
|
|
|
|
|
// Add the asynchronous library underneath
|
|
|
|
lua_newtable (L);
|
|
|
|
lua_pushlightuserdata (L, plugin);
|
|
|
|
luaL_setfuncs (L, lua_async_library, 1);
|
|
|
|
lua_setfield (L, -2, "async");
|
|
|
|
lua_pop (L, 1);
|
2016-10-27 20:02:25 +02:00
|
|
|
|
2016-11-04 18:51:32 +01:00
|
|
|
lua_weak_push (L, plugin, ctx, &lua_ctx_info);
|
2016-10-27 20:02:25 +02:00
|
|
|
lua_setglobal (L, lua_ctx_info.name);
|
2015-11-20 02:43:43 +01:00
|
|
|
|
|
|
|
// Create metatables for our objects
|
2016-10-27 18:46:27 +02:00
|
|
|
lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE, lua_hook_table);
|
|
|
|
lua_plugin_reg_weak (L, &lua_user_info, lua_user_table);
|
|
|
|
lua_plugin_reg_weak (L, &lua_channel_info, lua_channel_table);
|
|
|
|
lua_plugin_reg_weak (L, &lua_buffer_info, lua_buffer_table);
|
|
|
|
lua_plugin_reg_weak (L, &lua_server_info, lua_server_table);
|
|
|
|
lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_table);
|
|
|
|
lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
|
2015-11-20 02:43:43 +01:00
|
|
|
|
2016-11-01 04:07:53 +01:00
|
|
|
lua_plugin_reg_meta (L, XLUA_TASK_METATABLE, lua_task_table);
|
|
|
|
lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE, lua_wchannel_table);
|
|
|
|
|
2016-01-09 03:49:18 +01:00
|
|
|
struct error *error = NULL;
|
|
|
|
if (luaL_loadfile (L, filename))
|
|
|
|
error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
|
|
|
|
else if (!lua_plugin_call (plugin, 0, 0, &error))
|
|
|
|
{
|
|
|
|
error_set (e, "%s: %s", "Lua", error->message);
|
|
|
|
error_free (error);
|
|
|
|
}
|
|
|
|
else
|
2015-11-20 02:43:43 +01:00
|
|
|
return &plugin->super;
|
|
|
|
|
2016-01-17 22:57:16 +01:00
|
|
|
plugin_destroy (&plugin->super);
|
2015-11-20 02:43:43 +01:00
|
|
|
return NULL;
|
2015-11-19 15:45:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#endif // HAVE_LUA
|
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
// --- Plugins -----------------------------------------------------------------
|
|
|
|
|
|
|
|
typedef struct plugin *(*plugin_load_fn)
|
|
|
|
(struct app_context *ctx, const char *filename, struct error **e);
|
|
|
|
|
|
|
|
// We can potentially add support for other scripting languages if so desired,
|
|
|
|
// however this possibility is just a byproduct of abstraction
|
|
|
|
static plugin_load_fn g_plugin_loaders[] =
|
|
|
|
{
|
2015-11-19 15:45:32 +01:00
|
|
|
#ifdef HAVE_LUA
|
|
|
|
lua_plugin_load,
|
|
|
|
#endif // HAVE_LUA
|
2015-11-19 14:23:10 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
static struct plugin *
|
|
|
|
plugin_load_from_filename (struct app_context *ctx, const char *filename,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
struct plugin *plugin = NULL;
|
|
|
|
struct error *error = NULL;
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++)
|
|
|
|
if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (error)
|
|
|
|
error_propagate (e, error);
|
|
|
|
else if (!plugin)
|
2016-10-11 10:52:49 +02:00
|
|
|
{
|
|
|
|
error_set (e, "no plugin handler for \"%s\"", filename);
|
|
|
|
return NULL;
|
|
|
|
}
|
2015-11-19 14:23:10 +01:00
|
|
|
return plugin;
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct plugin *
|
|
|
|
plugin_find (struct app_context *ctx, const char *name)
|
|
|
|
{
|
|
|
|
LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
|
|
|
|
if (!strcmp (name, iter->name))
|
|
|
|
return iter;
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
plugin_resolve_relative_filename (const char *filename)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv paths = strv_make ();
|
2015-11-19 14:23:10 +01:00
|
|
|
get_xdg_data_dirs (&paths);
|
|
|
|
char *result = resolve_relative_filename_generic
|
|
|
|
(&paths, PROGRAM_NAME "/plugins/", filename);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&paths);
|
2015-11-19 14:23:10 +01:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct plugin *
|
|
|
|
plugin_load_by_name (struct app_context *ctx, const char *name,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
struct plugin *plugin = plugin_find (ctx, name);
|
|
|
|
if (plugin)
|
2016-10-11 10:52:49 +02:00
|
|
|
{
|
|
|
|
error_set (e, "plugin already loaded");
|
|
|
|
return NULL;
|
|
|
|
}
|
2015-11-19 14:23:10 +01:00
|
|
|
|
|
|
|
// As a side effect, a plugin can be loaded multiple times by giving
|
|
|
|
// various relative or non-relative paths to the function; this is not
|
|
|
|
// supposed to be fool-proof though, that requires other mechanisms
|
|
|
|
char *filename = resolve_filename (name, plugin_resolve_relative_filename);
|
|
|
|
if (!filename)
|
2016-10-11 10:52:49 +02:00
|
|
|
{
|
|
|
|
error_set (e, "file not found");
|
|
|
|
return NULL;
|
|
|
|
}
|
2015-11-19 14:23:10 +01:00
|
|
|
|
|
|
|
plugin = plugin_load_from_filename (ctx, filename, e);
|
|
|
|
free (filename);
|
|
|
|
return plugin;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
plugin_load (struct app_context *ctx, const char *name)
|
|
|
|
{
|
|
|
|
struct error *e = NULL;
|
|
|
|
struct plugin *plugin = plugin_load_by_name (ctx, name, &e);
|
|
|
|
if (plugin)
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// FIXME: this way the real name isn't available to the plugin on load,
|
|
|
|
// which has effect on e.g. plugin_config_name()
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&plugin->name, xstrdup (name));
|
2016-01-09 02:35:05 +01:00
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
log_global_status (ctx, "Plugin \"#s\" loaded", name);
|
|
|
|
LIST_PREPEND (ctx->plugins, plugin);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
log_global_error (ctx, "Can't load plugin \"#s\": #s",
|
|
|
|
name, e->message);
|
|
|
|
error_free (e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
plugin_unload (struct app_context *ctx, const char *name)
|
|
|
|
{
|
|
|
|
struct plugin *plugin = plugin_find (ctx, name);
|
|
|
|
if (!plugin)
|
|
|
|
log_global_error (ctx, "Can't unload plugin \"#s\": #s",
|
|
|
|
name, "plugin not loaded");
|
|
|
|
else
|
|
|
|
{
|
|
|
|
log_global_status (ctx, "Plugin \"#s\" unloaded", name);
|
|
|
|
LIST_UNLINK (ctx->plugins, plugin);
|
|
|
|
plugin_destroy (plugin);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
load_plugins (struct app_context *ctx)
|
|
|
|
{
|
2022-08-26 03:43:32 +02:00
|
|
|
const char *plugins =
|
|
|
|
get_config_string (ctx->config.root, "general.plugin_autoload");
|
2015-11-19 14:23:10 +01:00
|
|
|
if (plugins)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (plugins, ",", true, &v);
|
2015-11-19 14:23:10 +01:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
|
|
|
plugin_load (ctx, v.vector[i]);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-11-19 14:23:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-16 00:56:05 +02:00
|
|
|
// --- User input handling -----------------------------------------------------
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
// 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
|
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
struct app_context *ctx; ///< Application context
|
2015-06-19 23:53:57 +02:00
|
|
|
struct buffer *buffer; ///< Current buffer
|
|
|
|
struct server *s; ///< Related server
|
|
|
|
const char *channel_name; ///< Related channel name
|
|
|
|
char *arguments; ///< Command arguments
|
|
|
|
};
|
|
|
|
|
2015-04-17 21:29:18 +02:00
|
|
|
/// Cuts the longest non-whitespace portion of text and advances the pointer
|
|
|
|
static char *
|
|
|
|
cut_word (char **s)
|
|
|
|
{
|
|
|
|
char *start = *s;
|
2015-05-06 22:20:02 +02:00
|
|
|
size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);
|
2015-04-17 21:29:18 +02:00
|
|
|
char *end = start + word_len;
|
2015-05-06 22:20:02 +02:00
|
|
|
*s = end + strspn (end, WORD_BREAKING_CHARS);
|
2015-04-17 21:29:18 +02:00
|
|
|
*end = '\0';
|
|
|
|
return start;
|
|
|
|
}
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
/// Validates a word to be cut from a string
|
|
|
|
typedef bool (*word_validator_fn) (void *, char *);
|
|
|
|
|
2015-05-18 22:04:35 +02:00
|
|
|
static char *
|
2015-06-19 23:53:57 +02:00
|
|
|
maybe_cut_word (char **s, word_validator_fn validator, void *user_data)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
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
|
2018-01-08 21:53:42 +01:00
|
|
|
// Contrary to maybe_cut_word(), we ignore all whitespace at the end
|
2015-06-19 23:53:57 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-04-17 21:29:18 +02:00
|
|
|
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))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "#s: #s", "No such buffer", word);
|
2015-04-17 21:29:18 +02:00
|
|
|
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);
|
2016-03-26 13:00:10 +01:00
|
|
|
if (buffer || (buffer = buffer_by_name (ctx, word)))
|
|
|
|
return buffer;
|
|
|
|
|
|
|
|
// Basic case insensitive partial matching -- at most one buffer can match
|
|
|
|
int n_matches = 0;
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
|
|
{
|
|
|
|
char *string = xstrdup (iter->name);
|
|
|
|
char *pattern = xstrdup_printf ("*%s*", word);
|
|
|
|
for (char *p = string; *p; p++) *p = tolower_ascii (*p);
|
|
|
|
for (char *p = pattern; *p; p++) *p = tolower_ascii (*p);
|
|
|
|
if (!fnmatch (pattern, string, 0))
|
|
|
|
{
|
|
|
|
n_matches++;
|
|
|
|
buffer = iter;
|
|
|
|
}
|
|
|
|
free (string);
|
|
|
|
free (pattern);
|
|
|
|
}
|
|
|
|
return n_matches == 1 ? buffer : NULL;
|
2015-04-17 21:29:18 +02:00
|
|
|
}
|
2015-04-16 00:56:05 +02:00
|
|
|
|
2015-04-25 02:41:52 +02:00
|
|
|
static void
|
|
|
|
show_buffers_list (struct app_context *ctx)
|
|
|
|
{
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, "");
|
|
|
|
log_global_indent (ctx, "Buffers list:");
|
2015-04-25 02:41:52 +02:00
|
|
|
|
|
|
|
int i = 1;
|
|
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
2016-03-26 05:15:11 +01:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str s = str_make ();
|
2016-03-26 05:15:11 +01:00
|
|
|
int new = iter->new_messages_count - iter->new_unimportant_count;
|
|
|
|
if (new && iter != ctx->current_buffer)
|
|
|
|
str_append_printf (&s, " (%d%s)", new, &"!"[!iter->highlighted]);
|
|
|
|
log_global_indent (ctx,
|
|
|
|
" [#d] #s#&s", i++, iter->name, str_steal (&s));
|
|
|
|
}
|
2015-04-25 02:41:52 +02:00
|
|
|
}
|
|
|
|
|
2015-07-12 00:30:10 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-03-26 13:00:10 +01:00
|
|
|
static bool
|
|
|
|
handle_buffer_goto (struct app_context *ctx, struct handler_args *a)
|
|
|
|
{
|
|
|
|
if (!*a->arguments)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const char *which = cut_word (&a->arguments);
|
|
|
|
struct buffer *buffer = try_decode_buffer (ctx, which);
|
|
|
|
if (buffer)
|
|
|
|
buffer_activate (ctx, buffer);
|
|
|
|
else
|
|
|
|
log_global_error (ctx, "#s: #s", "No such buffer", which);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-04-25 02:41:52 +02:00
|
|
|
static void
|
2015-06-21 18:39:32 +02:00
|
|
|
handle_buffer_close (struct app_context *ctx, struct handler_args *a)
|
2015-04-25 02:41:52 +02:00
|
|
|
{
|
|
|
|
struct buffer *buffer = NULL;
|
|
|
|
const char *which = NULL;
|
2015-06-21 18:39:32 +02:00
|
|
|
if (!*a->arguments)
|
|
|
|
buffer = a->buffer;
|
2015-04-25 02:41:52 +02:00
|
|
|
else
|
2015-06-21 18:39:32 +02:00
|
|
|
buffer = try_decode_buffer (ctx, (which = cut_word (&a->arguments)));
|
2015-04-25 02:41:52 +02:00
|
|
|
|
|
|
|
if (!buffer)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "#s: #s", "No such buffer", which);
|
2015-04-25 02:41:52 +02:00
|
|
|
else if (buffer == ctx->global_buffer)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "Can't close the global buffer");
|
2015-04-30 00:02:14 +02:00
|
|
|
else if (buffer->type == BUFFER_SERVER)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "Can't close a server buffer");
|
2015-04-25 02:41:52 +02:00
|
|
|
else
|
|
|
|
{
|
2015-07-02 20:50:53 +02:00
|
|
|
// The user would be unable to recreate the buffer otherwise
|
2015-08-06 21:58:34 +02:00
|
|
|
if (buffer->type == BUFFER_CHANNEL
|
|
|
|
&& irc_channel_is_joined (buffer->channel))
|
2015-07-12 00:30:10 +02:00
|
|
|
part_channel (buffer->server, buffer->channel->name, "");
|
2015-07-14 06:11:14 +02:00
|
|
|
buffer_remove_safe (ctx, buffer);
|
2015-04-25 02:41:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-21 19:23:52 +02:00
|
|
|
static bool
|
|
|
|
handle_buffer_move (struct app_context *ctx, struct handler_args *a)
|
|
|
|
{
|
|
|
|
unsigned long request;
|
|
|
|
if (!xstrtoul (&request, a->arguments, 10))
|
|
|
|
return false;
|
|
|
|
|
2016-03-10 00:07:59 +01:00
|
|
|
if (request == 0 || request > (unsigned long) buffer_count (ctx))
|
2015-06-21 19:23:52 +02:00
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "#s: #s",
|
2015-06-21 19:23:52 +02:00
|
|
|
"Can't move buffer", "requested position is out of range");
|
|
|
|
return true;
|
|
|
|
}
|
2016-03-10 00:07:59 +01:00
|
|
|
buffer_move (ctx, a->buffer, request);
|
2015-06-21 19:23:52 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_buffer (struct handler_args *a)
|
2015-04-16 00:56:05 +02:00
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
struct app_context *ctx = a->ctx;
|
2015-06-19 23:53:57 +02:00
|
|
|
char *action = cut_word (&a->arguments);
|
2015-04-17 21:29:18 +02:00
|
|
|
if (try_handle_buffer_goto (ctx, action))
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-17 21:29:18 +02:00
|
|
|
|
2015-06-21 19:52:15 +02:00
|
|
|
bool result = true;
|
2015-11-21 14:07:07 +01:00
|
|
|
if (!*action || !strcasecmp_ascii (action, "list"))
|
2015-04-25 02:41:52 +02:00
|
|
|
show_buffers_list (ctx);
|
2015-04-17 21:29:18 +02:00
|
|
|
else if (!strcasecmp_ascii (action, "clear"))
|
|
|
|
{
|
2015-06-21 03:35:35 +02:00
|
|
|
buffer_clear (a->buffer);
|
|
|
|
buffer_print_backlog (ctx, a->buffer);
|
2015-04-17 21:29:18 +02:00
|
|
|
}
|
|
|
|
else if (!strcasecmp_ascii (action, "move"))
|
2015-06-21 19:52:15 +02:00
|
|
|
result = handle_buffer_move (ctx, a);
|
2016-03-26 13:00:10 +01:00
|
|
|
else if (!strcasecmp_ascii (action, "goto"))
|
|
|
|
result = handle_buffer_goto (ctx, a);
|
2015-04-17 21:29:18 +02:00
|
|
|
else if (!strcasecmp_ascii (action, "close"))
|
2015-06-21 18:39:32 +02:00
|
|
|
handle_buffer_close (ctx, a);
|
2015-04-17 21:29:18 +02:00
|
|
|
else
|
2015-06-21 19:52:15 +02:00
|
|
|
result = false;
|
2015-07-04 01:46:35 +02:00
|
|
|
return result;
|
2015-04-16 00:56:05 +02:00
|
|
|
}
|
|
|
|
|
2015-05-03 18:40:51 +02:00
|
|
|
static bool
|
2021-06-14 08:33:59 +02:00
|
|
|
handle_command_set_add
|
|
|
|
(struct strv *items, const struct strv *values, struct error **e)
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < values->len; i++)
|
|
|
|
{
|
|
|
|
const char *value = values->vector[i];
|
|
|
|
if (strv_find (items, values->vector[i]) != -1)
|
|
|
|
return error_set (e, "already present in the array: %s", value);
|
|
|
|
strv_append (items, value);
|
|
|
|
}
|
|
|
|
return true;
|
2015-05-03 18:40:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2021-06-14 08:33:59 +02:00
|
|
|
handle_command_set_remove
|
|
|
|
(struct strv *items, const struct strv *values, struct error **e)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2021-06-14 08:33:59 +02:00
|
|
|
for (size_t i = 0; i < values->len; i++)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2021-06-14 08:33:59 +02:00
|
|
|
const char *value = values->vector[i];
|
|
|
|
ssize_t i = strv_find (items, value);
|
|
|
|
if (i == -1)
|
|
|
|
return error_set (e, "not present in the array: %s", value);
|
|
|
|
strv_remove (items, i);
|
2015-05-03 18:40:51 +02:00
|
|
|
}
|
2021-06-14 08:33:59 +02:00
|
|
|
return true;
|
2015-05-03 18:40:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2021-06-14 08:33:59 +02:00
|
|
|
handle_command_set_modify
|
|
|
|
(struct config_item *item, const char *value, bool add, struct error **e)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv items = strv_make ();
|
2016-03-05 19:15:40 +01:00
|
|
|
if (item->type != CONFIG_ITEM_NULL)
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (item->value.string.str, ",", false, &items);
|
2015-05-03 19:11:57 +02:00
|
|
|
if (items.len == 1 && !*items.vector[0])
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_reset (&items);
|
2015-05-03 18:40:51 +02:00
|
|
|
|
2021-06-14 08:33:59 +02:00
|
|
|
struct strv values = strv_make ();
|
|
|
|
cstr_split (value, ",", false, &values);
|
|
|
|
bool result = add
|
|
|
|
? handle_command_set_add (&items, &values, e)
|
|
|
|
: handle_command_set_remove (&items, &values, e);
|
|
|
|
|
|
|
|
if (result)
|
2015-05-03 18:40:51 +02:00
|
|
|
{
|
2021-06-14 08:33:59 +02:00
|
|
|
char *changed = strv_join (&items, ",");
|
|
|
|
struct str tmp = { .str = changed, .len = strlen (changed) };
|
|
|
|
result = config_item_set_from (item,
|
|
|
|
config_item_string_array (&tmp), e);
|
|
|
|
free (changed);
|
2015-05-03 18:40:51 +02:00
|
|
|
}
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&items);
|
2021-06-14 08:33:59 +02:00
|
|
|
strv_free (&values);
|
2015-05-03 18:40:51 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-05-03 18:45:05 +02:00
|
|
|
static void
|
|
|
|
handle_command_set_assign_item (struct app_context *ctx,
|
2015-08-17 00:08:19 +02:00
|
|
|
char *key, struct config_item *new_, bool add, bool remove)
|
2015-05-03 18:45:05 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item =
|
2015-05-03 18:45:05 +02:00
|
|
|
config_item_get (ctx->config.root, key, NULL);
|
|
|
|
hard_assert (item);
|
|
|
|
|
|
|
|
struct error *e = NULL;
|
2015-05-14 06:45:24 +02:00
|
|
|
if (!item->schema)
|
|
|
|
error_set (&e, "option not recognized");
|
2016-03-05 19:15:40 +01:00
|
|
|
else if (!add && !remove)
|
|
|
|
config_item_set_from (item, config_item_clone (new_), &e);
|
|
|
|
else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY)
|
2015-05-03 18:45:05 +02:00
|
|
|
error_set (&e, "not a string array");
|
2021-06-14 08:33:59 +02:00
|
|
|
else
|
|
|
|
handle_command_set_modify (item, new_->value.string.str, add, &e);
|
2015-05-03 18:45:05 +02:00
|
|
|
|
|
|
|
if (e)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx,
|
|
|
|
"Failed to set option \"#s\": #s", key, e->message);
|
2015-05-03 18:45:05 +02:00
|
|
|
error_free (e);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv tmp = strv_make ();
|
2015-12-10 21:21:21 +01:00
|
|
|
dump_matching_options (ctx->config.root, key, &tmp);
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&tmp);
|
2015-05-03 18:45:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-03 17:38:01 +02:00
|
|
|
static bool
|
|
|
|
handle_command_set_assign
|
2017-01-23 23:50:27 +01:00
|
|
|
(struct app_context *ctx, struct strv *all, char *arguments)
|
2015-05-03 17:38:01 +02:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
|
2015-07-11 01:44:58 +02:00
|
|
|
if (!*arguments)
|
2015-05-03 17:38:01 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
struct error *e = NULL;
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *new_ =
|
2015-05-04 02:26:59 +02:00
|
|
|
config_item_parse (arguments, strlen (arguments), true, &e);
|
2015-05-03 17:38:01 +02:00
|
|
|
if (e)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "Invalid value: #s", e->message);
|
2015-05-03 17:38:01 +02:00
|
|
|
error_free (e);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((add | remove) && !config_item_type_is_string (new_->type))
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "+= / -= operators need a string argument");
|
2015-05-03 17:38:01 +02:00
|
|
|
config_item_destroy (new_);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
for (size_t i = 0; i < all->len; i++)
|
|
|
|
{
|
2015-07-11 17:54:38 +02:00
|
|
|
char *key = cstr_cut_until (all->vector[i], " ");
|
2015-05-03 18:45:05 +02:00
|
|
|
handle_command_set_assign_item (ctx, key, new_, add, remove);
|
2015-05-03 17:38:01 +02:00
|
|
|
free (key);
|
|
|
|
}
|
|
|
|
config_item_destroy (new_);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-03 16:47:16 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_set (struct handler_args *a)
|
2015-05-03 16:47:16 +02:00
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
struct app_context *ctx = a->ctx;
|
2015-05-03 16:47:16 +02:00
|
|
|
char *option = "*";
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
|
|
|
option = cut_word (&a->arguments);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv all = strv_make ();
|
2015-12-10 21:21:21 +01:00
|
|
|
dump_matching_options (ctx->config.root, option, &all);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
2015-05-03 17:38:01 +02:00
|
|
|
bool result = true;
|
|
|
|
if (!all.len)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "No matches: #s", option);
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (!*a->arguments)
|
2015-05-03 16:47:16 +02:00
|
|
|
{
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, "");
|
2015-05-03 16:47:16 +02:00
|
|
|
for (size_t i = 0; i < all.len; i++)
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, "#s", all.vector[i]);
|
2015-05-03 16:47:16 +02:00
|
|
|
}
|
2015-05-03 17:38:01 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
result = handle_command_set_assign (ctx, &all, a->arguments);
|
2015-05-03 16:47:16 +02:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&all);
|
2015-05-03 17:38:01 +02:00
|
|
|
return result;
|
2015-05-03 16:47:16 +02:00
|
|
|
}
|
|
|
|
|
2015-05-03 19:34:01 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_save (struct handler_args *a)
|
2015-05-03 19:34:01 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
2015-05-03 19:34:01 +02:00
|
|
|
return false;
|
|
|
|
|
2015-07-11 02:45:24 +02:00
|
|
|
save_configuration (a->ctx);
|
2015-05-03 19:34:01 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
static void
|
|
|
|
show_plugin_list (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
log_global_indent (ctx, "");
|
|
|
|
log_global_indent (ctx, "Plugins:");
|
|
|
|
LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
|
|
|
|
log_global_indent (ctx, " #s", iter->name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
handle_command_plugin (struct handler_args *a)
|
|
|
|
{
|
|
|
|
char *action = cut_word (&a->arguments);
|
|
|
|
if (!*action || !strcasecmp_ascii (action, "list"))
|
|
|
|
show_plugin_list (a->ctx);
|
|
|
|
else if (!strcasecmp_ascii (action, "load"))
|
|
|
|
{
|
|
|
|
if (!*a->arguments)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
plugin_load (a->ctx, cut_word (&a->arguments));
|
|
|
|
}
|
|
|
|
else if (!strcasecmp_ascii (action, "unload"))
|
|
|
|
{
|
|
|
|
if (!*a->arguments)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
plugin_unload (a->ctx, cut_word (&a->arguments));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-10 01:45:39 +02:00
|
|
|
static bool
|
|
|
|
show_aliases_list (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
log_global_indent (ctx, "");
|
|
|
|
log_global_indent (ctx, "Aliases:");
|
|
|
|
|
2015-07-11 03:27:59 +02:00
|
|
|
struct str_map *aliases = get_aliases_config (ctx);
|
|
|
|
if (!aliases->len)
|
|
|
|
{
|
|
|
|
log_global_indent (ctx, " (none)");
|
|
|
|
return true;
|
|
|
|
}
|
2015-07-10 01:45:39 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (aliases);
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *alias;
|
2015-07-10 01:45:39 +02:00
|
|
|
while ((alias = str_map_iter_next (&iter)))
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str definition = str_make ();
|
2015-07-11 03:58:53 +02:00
|
|
|
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");
|
2015-07-10 01:45:39 +02:00
|
|
|
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)
|
|
|
|
{
|
2015-07-11 03:27:59 +02:00
|
|
|
if (!*a->arguments)
|
2015-07-10 01:45:39 +02:00
|
|
|
return show_aliases_list (a->ctx);
|
|
|
|
|
2015-07-11 03:27:59 +02:00
|
|
|
// TODO: validate the name; maybe also while loading configuration
|
|
|
|
char *name = cut_word (&a->arguments);
|
|
|
|
if (!*a->arguments)
|
2015-07-10 01:45:39 +02:00
|
|
|
return false;
|
|
|
|
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *alias = config_item_string_from_cstr (a->arguments);
|
2015-07-11 03:27:59 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str definition = str_make ();
|
2015-07-11 03:27:59 +02:00
|
|
|
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);
|
2015-07-10 01:45:39 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-11 03:07:57 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_msg (struct handler_args *a)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-04-25 01:49:34 +02:00
|
|
|
return false;
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
|
|
|
if (!*a->arguments)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->s->buffer, "No text to send");
|
2015-04-23 02:48:25 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_query (struct handler_args *a)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-04-25 01:49:34 +02:00
|
|
|
return false;
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
2015-09-24 01:39:49 +02:00
|
|
|
if (irc_is_channel (a->s, irc_skip_statusmsg (a->s, target)))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->s->buffer, "Cannot query a channel");
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (!*a->arguments)
|
2016-12-06 13:51:16 +01:00
|
|
|
buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
|
2015-04-23 02:48:25 +02:00
|
|
|
else
|
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
|
2015-06-19 23:53:57 +02:00
|
|
|
SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_notice (struct handler_args *a)
|
2015-04-23 02:48:25 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-04-25 01:49:34 +02:00
|
|
|
return false;
|
2015-04-23 02:48:25 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
|
|
|
if (!*a->arguments)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->s->buffer, "No text to send");
|
2015-04-23 02:48:25 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
SEND_AUTOSPLIT_NOTICE (a->s, target, a->arguments);
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-23 02:48:25 +02:00
|
|
|
}
|
|
|
|
|
2021-05-22 19:38:20 +02:00
|
|
|
static bool
|
|
|
|
handle_command_squery (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
|
|
|
|
irc_send (a->s, "SQUERY %s :%s", target, a->arguments);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-04-26 22:53:38 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_ctcp (struct handler_args *a)
|
2015-04-26 22:53:38 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-04-26 22:53:38 +02:00
|
|
|
return false;
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
|
|
|
if (!*a->arguments)
|
2015-04-26 22:53:38 +02:00
|
|
|
return false;
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *tag = cut_word (&a->arguments);
|
2015-07-11 17:54:38 +02:00
|
|
|
cstr_transform (tag, toupper_ascii);
|
2015-04-26 22:53:38 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
|
|
|
irc_send (a->s, "PRIVMSG %s :\x01%s %s\x01", target, tag, a->arguments);
|
2015-04-26 22:53:38 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "PRIVMSG %s :\x01%s\x01", target, tag);
|
2015-04-26 22:53:38 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_me (struct handler_args *a)
|
2015-06-19 23:53:57 +02:00
|
|
|
{
|
|
|
|
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);
|
2015-04-26 23:30:54 +02:00
|
|
|
else
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->s->buffer,
|
|
|
|
"Can't do this from a server buffer (#s)",
|
2015-04-26 23:30:54 +02:00
|
|
|
"send CTCP actions");
|
2015-04-26 22:53:38 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_quit (struct handler_args *a)
|
2015-04-16 00:56:05 +02:00
|
|
|
{
|
2020-11-01 15:32:39 +01:00
|
|
|
request_quit (a->ctx, *a->arguments ? a->arguments : NULL);
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-16 00:56:05 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_join (struct handler_args *a)
|
2015-04-19 21:34:11 +02:00
|
|
|
{
|
2015-06-19 22:12:53 +02:00
|
|
|
// XXX: send the last known channel key?
|
2015-06-19 23:53:57 +02:00
|
|
|
if (irc_is_channel (a->s, a->arguments))
|
2015-06-19 22:12:53 +02:00
|
|
|
// XXX: we may want to split the list of channels
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "JOIN %s", a->arguments);
|
|
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
|
2015-06-19 22:12:53 +02:00
|
|
|
"no channel name given and this buffer is not a channel");
|
2015-08-06 21:58:34 +02:00
|
|
|
else if (irc_channel_is_joined (a->buffer->channel))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
|
|
|
|
"you already are on the channel");
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (*a->arguments)
|
|
|
|
irc_send (a->s, "JOIN %s :%s", a->buffer->channel->name, a->arguments);
|
2015-04-19 21:34:11 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "JOIN %s", a->buffer->channel->name);
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-19 21:34:11 +02:00
|
|
|
}
|
|
|
|
|
2015-04-25 01:49:34 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_part (struct handler_args *a)
|
2015-04-19 21:34:11 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (irc_is_channel (a->s, a->arguments))
|
2015-05-17 16:23:32 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (cut_word (&a->arguments), ",", true, &v);
|
2015-06-19 22:12:53 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
2015-06-20 20:13:37 +02:00
|
|
|
part_channel (a->s, v.vector[i], a->arguments);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-05-17 16:23:32 +02:00
|
|
|
}
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
|
2015-06-19 22:12:53 +02:00
|
|
|
"no channel name given and this buffer is not a channel");
|
2015-08-06 21:58:34 +02:00
|
|
|
else if (!irc_channel_is_joined (a->buffer->channel))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
|
|
|
|
"you're not on the channel");
|
2015-04-19 21:34:11 +02:00
|
|
|
else
|
2015-06-20 20:13:37 +02:00
|
|
|
part_channel (a->s, a->buffer->channel->name, a->arguments);
|
2015-04-25 01:49:34 +02:00
|
|
|
return true;
|
2015-04-19 21:34:11 +02:00
|
|
|
}
|
|
|
|
|
2015-06-19 22:12:53 +02:00
|
|
|
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");
|
|
|
|
|
2015-06-20 20:13:37 +02:00
|
|
|
if (*reason)
|
2015-06-19 22:12:53 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2015-05-17 16:25:52 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_cycle (struct handler_args *a)
|
2015-05-17 16:25:52 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (irc_is_channel (a->s, a->arguments))
|
2015-05-17 16:25:52 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (cut_word (&a->arguments), ",", true, &v);
|
2015-06-19 22:12:53 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
2015-06-20 20:13:37 +02:00
|
|
|
cycle_channel (a->s, v.vector[i], a->arguments);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-05-17 16:25:52 +02:00
|
|
|
}
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (a->buffer->type != BUFFER_CHANNEL)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
|
2015-06-19 22:12:53 +02:00
|
|
|
"no channel name given and this buffer is not a channel");
|
2015-08-06 21:58:34 +02:00
|
|
|
else if (!irc_channel_is_joined (a->buffer->channel))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
|
|
|
|
"you're not on the channel");
|
2015-05-17 16:25:52 +02:00
|
|
|
else
|
2015-06-20 20:13:37 +02:00
|
|
|
cycle_channel (a->s, a->buffer->channel->name, a->arguments);
|
2015-05-17 16:25:52 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-18 21:21:49 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_mode (struct handler_args *a)
|
2015-06-18 21:21:49 +02:00
|
|
|
{
|
2015-06-17 23:16:44 +02:00
|
|
|
// Channel names prefixed by "+" collide with mode strings,
|
|
|
|
// so we just disallow specifying these channels
|
2015-06-18 08:20:47 +02:00
|
|
|
char *target = NULL;
|
2015-06-19 23:53:57 +02:00
|
|
|
if (strchr ("+-\0", *a->arguments))
|
2015-06-18 07:54:46 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
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;
|
2015-06-18 07:54:46 +02:00
|
|
|
}
|
2015-06-18 21:54:26 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
// If there a->arguments and they don't begin with a mode string,
|
2015-06-18 21:54:26 +02:00
|
|
|
// they're either a user name or a channel name
|
2015-06-19 23:53:57 +02:00
|
|
|
target = cut_word (&a->arguments);
|
2015-06-18 07:54:46 +02:00
|
|
|
|
2015-06-18 08:20:47 +02:00
|
|
|
if (!target)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't change mode",
|
2015-06-18 08:20:47 +02:00
|
|
|
"no target given and this buffer is neither a PM nor a channel");
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (*a->arguments)
|
2015-06-18 08:20:47 +02:00
|
|
|
// XXX: split channel mode params as necessary using irc_max_modes?
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "MODE %s %s", target, a->arguments);
|
2015-06-18 08:20:47 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "MODE %s", target);
|
2015-05-18 22:04:35 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_topic (struct handler_args *a)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
2018-01-08 21:53:42 +01:00
|
|
|
// FIXME: there's no way to start the topic with whitespace
|
|
|
|
// FIXME: there's no way to unset the topic;
|
2021-06-16 21:17:34 +02:00
|
|
|
// we could adopt the Tcl style of "-switches" with "--" sentinels,
|
|
|
|
// or we could accept "strings" in the config format
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments);
|
2015-05-18 22:04:35 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "TOPIC %s", a->channel_name);
|
2015-05-18 22:04:35 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_kick (struct handler_args *a)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-06-19 23:09:46 +02:00
|
|
|
return false;
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
|
|
|
if (*a->arguments)
|
|
|
|
irc_send (a->s, "KICK %s %s :%s",
|
|
|
|
a->channel_name, target, a->arguments);
|
2015-05-18 22:04:35 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "KICK %s %s", a->channel_name, target);
|
2015-05-18 22:04:35 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_kickban (struct handler_args *a)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-06-19 23:09:46 +02:00
|
|
|
return false;
|
2015-06-18 08:47:52 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
char *target = cut_word (&a->arguments);
|
2015-06-19 23:09:46 +02:00
|
|
|
if (strpbrk (target, "!@*?"))
|
2015-05-18 22:04:35 +02:00
|
|
|
return false;
|
2015-06-19 23:09:46 +02:00
|
|
|
|
|
|
|
// XXX: how about other masks?
|
2015-06-19 23:53:57 +02:00
|
|
|
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);
|
2015-06-19 23:09:46 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "KICK %s %s", a->channel_name, target);
|
2015-05-18 22:04:35 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-18 21:21:49 +02:00
|
|
|
static void
|
2015-06-19 23:53:57 +02:00
|
|
|
mass_channel_mode (struct server *s, const char *channel_name,
|
2017-01-23 23:50:27 +01:00
|
|
|
bool adding, char mode_char, struct strv *v)
|
2015-06-19 23:53:57 +02:00
|
|
|
{
|
|
|
|
size_t n;
|
|
|
|
for (size_t i = 0; i < v->len; i += n)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str modes = str_make ();
|
|
|
|
struct str params = str_make ();
|
2015-06-19 23:53:57 +02:00
|
|
|
|
|
|
|
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)
|
2015-06-18 21:21:49 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (a->arguments, " ", true, &v);
|
2015-06-19 23:09:46 +02:00
|
|
|
|
2015-11-21 15:15:49 +01:00
|
|
|
// XXX: this may be a bit too trivial; we could also map nicknames
|
2015-06-18 21:21:49 +02:00
|
|
|
// to information from WHO polling or userhost-in-names
|
2015-06-19 23:09:46 +02:00
|
|
|
for (size_t i = 0; i < v.len; i++)
|
2015-06-18 21:21:49 +02:00
|
|
|
{
|
2015-06-19 23:09:46 +02:00
|
|
|
char *target = v.vector[i];
|
2021-06-17 12:08:08 +02:00
|
|
|
if (strpbrk (target, "!@*?") || irc_is_extban (a->s, target))
|
2015-06-18 21:21:49 +02:00
|
|
|
continue;
|
|
|
|
|
2015-06-19 23:09:46 +02:00
|
|
|
v.vector[i] = xstrdup_printf ("%s!*@*", target);
|
2015-06-18 21:21:49 +02:00
|
|
|
free (target);
|
|
|
|
}
|
2015-06-19 23:09:46 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-06-18 21:21:49 +02:00
|
|
|
}
|
|
|
|
|
2015-05-18 22:04:35 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_ban (struct handler_args *a)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
|
|
|
mass_channel_mode_mask_list (a, true, 'b');
|
|
|
|
else
|
|
|
|
irc_send (a->s, "MODE %s +b", a->channel_name);
|
2015-06-18 21:21:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_unban (struct handler_args *a)
|
2015-06-18 21:21:49 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
|
|
|
mass_channel_mode_mask_list (a, false, 'b');
|
|
|
|
else
|
2015-05-18 22:04:35 +02:00
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_invite (struct handler_args *a)
|
2015-05-18 22:04:35 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (a->arguments, " ", true, &v);
|
2015-06-18 22:30:18 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
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);
|
2015-06-18 22:30:18 +02:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-06-18 22:30:18 +02:00
|
|
|
return result;
|
2015-05-18 22:04:35 +02:00
|
|
|
}
|
|
|
|
|
2015-06-20 20:13:37 +02:00
|
|
|
static struct server *
|
|
|
|
resolve_server (struct app_context *ctx, struct handler_args *a,
|
|
|
|
const char *command_name)
|
2015-05-03 20:03:21 +02:00
|
|
|
{
|
2015-05-16 12:33:59 +02:00
|
|
|
struct server *s = NULL;
|
2015-06-19 23:53:57 +02:00
|
|
|
if (*a->arguments)
|
2015-05-16 12:33:59 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
char *server_name = cut_word (&a->arguments);
|
2015-06-19 20:47:37 +02:00
|
|
|
if (!(s = str_map_find (&ctx->servers, server_name)))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "/#s: #s: #s",
|
2015-06-20 20:13:37 +02:00
|
|
|
command_name, "no such server", server_name);
|
2015-05-16 12:33:59 +02:00
|
|
|
}
|
2015-06-19 23:53:57 +02:00
|
|
|
else if (a->buffer->type == BUFFER_GLOBAL)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "/#s: #s",
|
2015-06-20 20:13:37 +02:00
|
|
|
command_name, "no server name given and this buffer is global");
|
2015-05-16 12:33:59 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
s = a->buffer->server;
|
2015-06-20 20:13:37 +02:00
|
|
|
return s;
|
|
|
|
}
|
2015-05-16 12:33:59 +02:00
|
|
|
|
2015-06-20 20:13:37 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_connect (struct handler_args *a)
|
2015-06-20 20:13:37 +02:00
|
|
|
{
|
|
|
|
struct server *s = NULL;
|
2015-06-28 03:00:19 +02:00
|
|
|
if (!(s = resolve_server (a->ctx, a, "connect")))
|
2015-05-16 12:33:59 +02:00
|
|
|
return true;
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
if (irc_is_connected (s))
|
2015-05-03 20:03:21 +02:00
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer, "Already connected");
|
2015-05-03 20:03:21 +02:00
|
|
|
return true;
|
|
|
|
}
|
2015-05-09 23:14:07 +02:00
|
|
|
if (s->state == IRC_CONNECTING)
|
|
|
|
irc_destroy_connector (s);
|
2015-05-03 20:03:21 +02:00
|
|
|
|
|
|
|
irc_cancel_timers (s);
|
2015-07-18 13:39:30 +02:00
|
|
|
|
|
|
|
s->reconnect_attempt = 0;
|
2015-05-09 22:10:58 +02:00
|
|
|
irc_initiate_connect (s);
|
2015-05-03 20:03:21 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-09 23:30:04 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_disconnect (struct handler_args *a)
|
2015-05-09 23:30:04 +02:00
|
|
|
{
|
2015-05-16 12:33:59 +02:00
|
|
|
struct server *s = NULL;
|
2015-06-28 03:00:19 +02:00
|
|
|
if (!(s = resolve_server (a->ctx, a, "disconnect")))
|
2015-05-16 12:33:59 +02:00
|
|
|
return true;
|
|
|
|
|
2015-05-09 23:30:04 +02:00
|
|
|
if (s->state == IRC_CONNECTING)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_status (s, s->buffer, "Connecting aborted");
|
2015-05-09 23:30:04 +02:00
|
|
|
irc_destroy_connector (s);
|
|
|
|
}
|
2015-07-11 17:54:49 +02:00
|
|
|
else if (poller_timer_is_active (&s->reconnect_tmr))
|
|
|
|
{
|
|
|
|
log_server_status (s, s->buffer, "Connecting aborted");
|
|
|
|
poller_timer_reset (&s->reconnect_tmr);
|
|
|
|
}
|
2015-05-09 23:30:04 +02:00
|
|
|
else if (!irc_is_connected (s))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, s->buffer, "Not connected");
|
2015-05-09 23:30:04 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
|
2015-05-09 23:30:04 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-13 20:51:23 +02:00
|
|
|
static bool
|
2015-07-04 22:28:59 +02:00
|
|
|
show_servers_list (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
log_global_indent (ctx, "");
|
|
|
|
log_global_indent (ctx, "Servers list:");
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
2015-07-04 22:28:59 +02:00
|
|
|
struct server *s;
|
|
|
|
while ((s = str_map_iter_next (&iter)))
|
|
|
|
log_global_indent (ctx, " #s", s->name);
|
2015-07-13 20:51:23 +02:00
|
|
|
return true;
|
2015-07-04 22:28:59 +02:00
|
|
|
}
|
|
|
|
|
2015-07-05 20:32:48 +02:00
|
|
|
static bool
|
|
|
|
handle_server_add (struct handler_args *a)
|
|
|
|
{
|
2015-07-11 01:44:58 +02:00
|
|
|
if (!*a->arguments)
|
2015-07-05 20:32:48 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
struct app_context *ctx = a->ctx;
|
2015-07-12 23:32:03 +02:00
|
|
|
char *name = cut_word (&a->arguments);
|
2015-07-14 07:29:30 +02:00
|
|
|
const char *err;
|
|
|
|
if ((err = check_server_name_for_addition (ctx, name)))
|
|
|
|
log_global_error (ctx, "Cannot create server `#s': #s", name, err);
|
|
|
|
else
|
2015-07-14 06:50:39 +02:00
|
|
|
{
|
2015-07-05 20:32:48 +02:00
|
|
|
server_add_new (ctx, name);
|
2015-07-14 06:50:39 +02:00
|
|
|
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);
|
|
|
|
}
|
2015-07-05 20:32:48 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-04 22:28:59 +02:00
|
|
|
static bool
|
2015-07-14 20:48:17 +02:00
|
|
|
handle_server_rename (struct handler_args *a)
|
2015-07-04 22:28:59 +02:00
|
|
|
{
|
|
|
|
struct app_context *ctx = a->ctx;
|
2015-07-13 20:51:23 +02:00
|
|
|
if (!*a->arguments)
|
2015-07-14 20:48:17 +02:00
|
|
|
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);
|
2015-07-13 20:51:23 +02:00
|
|
|
|
2015-07-04 22:28:59 +02:00
|
|
|
char *action = cut_word (&a->arguments);
|
|
|
|
if (!strcasecmp_ascii (action, "list"))
|
2015-07-14 20:48:17 +02:00
|
|
|
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;
|
2015-07-04 22:28:59 +02:00
|
|
|
}
|
|
|
|
|
2015-04-26 23:06:19 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_names (struct handler_args *a)
|
2015-04-26 23:06:19 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
char *channel_name = try_get_channel (a, maybe_cut_word);
|
|
|
|
if (channel_name)
|
|
|
|
irc_send (a->s, "NAMES %s", channel_name);
|
2015-05-19 21:02:49 +02:00
|
|
|
else
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "NAMES");
|
2015-05-19 21:02:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_whois (struct handler_args *a)
|
2015-06-19 23:53:57 +02:00
|
|
|
{
|
|
|
|
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);
|
2015-05-19 21:02:49 +02:00
|
|
|
else
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
|
2015-11-21 15:15:49 +01:00
|
|
|
"no target given and this buffer is neither a PM nor a server");
|
2015-05-19 21:02:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_whowas (struct handler_args *a)
|
2015-05-19 21:02:49 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
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);
|
2015-05-19 21:02:49 +02:00
|
|
|
else
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
|
2015-06-18 22:05:32 +02:00
|
|
|
"no target given and this buffer is not a PM");
|
2015-05-19 21:02:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-09-23 22:50:30 +02:00
|
|
|
static bool
|
|
|
|
handle_command_kill (struct handler_args *a)
|
|
|
|
{
|
|
|
|
if (!*a->arguments)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
char *target = cut_word (&a->arguments);
|
|
|
|
if (*a->arguments)
|
|
|
|
irc_send (a->s, "KILL %s :%s", target, a->arguments);
|
|
|
|
else
|
|
|
|
irc_send (a->s, "KILL %s", target);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-19 21:02:49 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_nick (struct handler_args *a)
|
2015-05-19 21:02:49 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
irc_send (a->s, "NICK %s", cut_word (&a->arguments));
|
2015-05-19 21:02:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-17 20:29:17 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_quote (struct handler_args *a)
|
2015-06-17 20:29:17 +02:00
|
|
|
{
|
2015-06-19 23:53:57 +02:00
|
|
|
irc_send (a->s, "%s", a->arguments);
|
2015-06-17 20:29:17 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-19 21:02:49 +02:00
|
|
|
static bool
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_channel_mode
|
|
|
|
(struct handler_args *a, bool adding, char mode_char)
|
2015-05-19 21:02:49 +02:00
|
|
|
{
|
2021-06-03 00:07:11 +02:00
|
|
|
const char *targets = a->arguments;
|
|
|
|
if (!*targets)
|
|
|
|
{
|
|
|
|
if (adding)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
targets = a->s->irc_user->nickname;
|
|
|
|
}
|
2015-05-19 21:02:49 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2021-06-03 00:07:11 +02:00
|
|
|
cstr_split (targets, " ", true, &v);
|
2015-06-19 23:53:57 +02:00
|
|
|
mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-05-19 21:02:49 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
#define CHANMODE_HANDLER(name, adding, mode_char) \
|
|
|
|
static bool \
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_ ## name (struct handler_args *a) \
|
2015-06-19 23:53:57 +02:00
|
|
|
{ \
|
|
|
|
return handle_command_channel_mode (a, (adding), (mode_char)); \
|
|
|
|
}
|
2015-06-19 22:54:10 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
CHANMODE_HANDLER (op, true, 'o') CHANMODE_HANDLER (deop, false, 'o')
|
|
|
|
CHANMODE_HANDLER (voice, true, 'v') CHANMODE_HANDLER (devoice, false, 'v')
|
2015-04-27 23:06:20 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
#define TRIVIAL_HANDLER(name, command) \
|
|
|
|
static bool \
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_ ## name (struct handler_args *a) \
|
2015-06-19 23:53:57 +02:00
|
|
|
{ \
|
|
|
|
if (*a->arguments) \
|
2015-06-20 20:23:14 +02:00
|
|
|
irc_send (a->s, command " %s", a->arguments); \
|
2015-06-19 23:53:57 +02:00
|
|
|
else \
|
2015-06-20 20:23:14 +02:00
|
|
|
irc_send (a->s, command); \
|
2015-06-19 23:53:57 +02:00
|
|
|
return true; \
|
|
|
|
}
|
2015-04-27 23:06:20 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
TRIVIAL_HANDLER (list, "LIST")
|
|
|
|
TRIVIAL_HANDLER (who, "WHO")
|
|
|
|
TRIVIAL_HANDLER (motd, "MOTD")
|
2015-10-01 21:39:47 +02:00
|
|
|
TRIVIAL_HANDLER (oper, "OPER")
|
2015-06-19 23:53:57 +02:00
|
|
|
TRIVIAL_HANDLER (stats, "STATS")
|
|
|
|
TRIVIAL_HANDLER (away, "AWAY")
|
2015-04-17 23:18:07 +02:00
|
|
|
|
2015-05-09 06:11:36 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-06-28 03:00:19 +02:00
|
|
|
static bool handle_command_help (struct handler_args *);
|
2015-05-09 06:11:36 +02:00
|
|
|
|
2015-04-16 00:56:05 +02:00
|
|
|
static struct command_handler
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-04-18 17:19:56 +02:00
|
|
|
const char *name;
|
|
|
|
const char *description;
|
|
|
|
const char *usage;
|
2015-06-28 03:00:19 +02:00
|
|
|
bool (*handler) (struct handler_args *a);
|
2015-06-19 23:53:57 +02:00
|
|
|
enum handler_flags flags;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
2015-04-16 00:56:05 +02:00
|
|
|
g_command_handlers[] =
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "help", "Show help",
|
|
|
|
"[<command> | <option>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_help, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "quit", "Quit the program",
|
|
|
|
"[<message>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_quit, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "buffer", "Manage buffers",
|
2016-03-26 13:00:10 +01:00
|
|
|
"<N> | list | clear | move <N> | goto <N or name> | close [<N or name>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_buffer, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "set", "Manage configuration",
|
|
|
|
"[<option>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_set, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "save", "Save configuration",
|
|
|
|
NULL,
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_save, 0 },
|
2015-11-19 14:23:10 +01:00
|
|
|
{ "plugin", "Manage plugins",
|
|
|
|
"list | load <name> | unload <name>",
|
|
|
|
handle_command_plugin, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
|
2015-07-10 01:45:39 +02:00
|
|
|
{ "alias", "List or set aliases",
|
|
|
|
"[<name> <definition>]",
|
|
|
|
handle_command_alias, 0 },
|
2015-07-11 03:07:57 +02:00
|
|
|
{ "unalias", "Unset aliases",
|
|
|
|
"<name>...",
|
|
|
|
handle_command_unalias, 0 },
|
2015-07-10 01:45:39 +02:00
|
|
|
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "msg", "Send message to a nick or channel",
|
|
|
|
"<target> <message>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_msg, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "query", "Send a private message to a nick",
|
|
|
|
"<nick> <message>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_query, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "notice", "Send notice to a nick or channel",
|
|
|
|
"<target> <message>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2021-05-22 19:38:20 +02:00
|
|
|
{ "squery", "Send a message to a service",
|
|
|
|
"<service> <message>",
|
|
|
|
handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "ctcp", "Send a CTCP query",
|
|
|
|
"<target> <tag>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "me", "Send a CTCP action",
|
|
|
|
"<message>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_me, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
2015-05-09 23:07:48 +02:00
|
|
|
|
|
|
|
{ "join", "Join channels",
|
2015-06-19 22:12:53 +02:00
|
|
|
"[<channel>[,<channel>...]] [<key>[,<key>...]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_join, HANDLER_SERVER },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "part", "Leave channels",
|
2015-05-18 22:04:35 +02:00
|
|
|
"[<channel>[,<channel>...]] [<reason>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_part, HANDLER_SERVER },
|
2015-05-17 16:25:52 +02:00
|
|
|
{ "cycle", "Rejoin channels",
|
2015-05-18 22:04:35 +02:00
|
|
|
"[<channel>[,<channel>...]] [<reason>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_cycle, HANDLER_SERVER },
|
2015-05-09 23:07:48 +02:00
|
|
|
|
2015-06-17 21:34:04 +02:00
|
|
|
{ "op", "Give channel operator status",
|
2015-06-19 23:53:57 +02:00
|
|
|
"<nick>...",
|
|
|
|
handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-06-17 21:34:04 +02:00
|
|
|
{ "deop", "Remove channel operator status",
|
2021-06-03 00:07:11 +02:00
|
|
|
"[<nick>...]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-06-17 21:34:04 +02:00
|
|
|
{ "voice", "Give voice",
|
2015-06-19 23:53:57 +02:00
|
|
|
"<nick>...",
|
|
|
|
handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-06-17 21:34:04 +02:00
|
|
|
{ "devoice", "Remove voice",
|
2021-06-03 00:07:11 +02:00
|
|
|
"[<nick>...]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-05-19 21:02:49 +02:00
|
|
|
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "mode", "Change mode",
|
|
|
|
"[<channel>] [<mode>...]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_mode, HANDLER_SERVER },
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "topic", "Change topic",
|
|
|
|
"[<channel>] [<topic>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_topic, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "kick", "Kick user from channel",
|
|
|
|
"[<channel>] <user> [<reason>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_kick, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "kickban", "Kick and ban user from channel",
|
|
|
|
"[<channel>] <user> [<reason>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_kickban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "ban", "Ban user from channel",
|
2015-06-18 21:21:49 +02:00
|
|
|
"[<channel>] [<mask>...]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_ban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-06-18 21:21:49 +02:00
|
|
|
{ "unban", "Unban user from channel",
|
|
|
|
"[<channel>] <mask>...",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_unban, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
2015-05-18 22:04:35 +02:00
|
|
|
{ "invite", "Invite user to channel",
|
2015-06-18 22:30:18 +02:00
|
|
|
"<user>... [<channel>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_invite, HANDLER_SERVER | HANDLER_CHANNEL_LAST },
|
2015-05-09 23:07:48 +02:00
|
|
|
|
2015-07-04 22:28:59 +02:00
|
|
|
{ "server", "Manage servers",
|
2015-08-06 23:27:35 +02:00
|
|
|
"list | add <name> | remove <name> | rename <old> <new>",
|
2015-07-04 22:28:59 +02:00
|
|
|
handle_command_server, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "connect", "Connect to the server",
|
2015-06-18 22:46:51 +02:00
|
|
|
"[<server>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_connect, 0 },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "disconnect", "Disconnect from the server",
|
2015-06-19 20:47:37 +02:00
|
|
|
"[<server> [<reason>]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_disconnect, 0 },
|
2015-07-04 22:28:59 +02:00
|
|
|
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "list", "List channels and their topic",
|
2015-06-18 22:46:51 +02:00
|
|
|
"[<channel>[,<channel>...]] [<target>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_list, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
{ "names", "List users on channel",
|
|
|
|
"[<channel>[,<channel>...]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_names, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
{ "who", "List users",
|
2015-06-18 22:46:51 +02:00
|
|
|
"[<mask> [o]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_who, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
{ "whois", "Get user information",
|
2015-06-18 22:05:32 +02:00
|
|
|
"[<target>] <mask>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_whois, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
{ "whowas", "Get user information",
|
2015-06-18 22:05:32 +02:00
|
|
|
"<user> [<count> [<target>]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_whowas, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
|
|
|
|
{ "motd", "Get the Message of The Day",
|
2015-06-18 22:46:51 +02:00
|
|
|
"[<target>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_motd, HANDLER_SERVER },
|
2015-10-01 21:39:47 +02:00
|
|
|
{ "oper", "Authenticate as an IRC operator",
|
|
|
|
"<name> <password>",
|
|
|
|
handle_command_oper, HANDLER_SERVER },
|
2016-09-23 22:50:30 +02:00
|
|
|
{ "kill", "Kick another user from the server",
|
|
|
|
"<user> <comment>",
|
|
|
|
handle_command_kill, HANDLER_SERVER },
|
2015-06-17 20:29:17 +02:00
|
|
|
{ "stats", "Query server statistics",
|
|
|
|
"[<query> [<target>]]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_stats, HANDLER_SERVER },
|
2015-05-19 21:02:49 +02:00
|
|
|
{ "away", "Set away status",
|
|
|
|
"[<text>]",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_away, HANDLER_SERVER },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "nick", "Change current nick",
|
|
|
|
"<nickname>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_nick, HANDLER_SERVER },
|
2015-05-09 23:07:48 +02:00
|
|
|
{ "quote", "Send a raw command to the server",
|
|
|
|
"<command>",
|
2015-06-19 23:53:57 +02:00
|
|
|
handle_command_quote, HANDLER_SERVER },
|
2015-04-12 04:52:39 +02:00
|
|
|
};
|
|
|
|
|
2015-05-03 16:47:31 +02:00
|
|
|
static bool
|
|
|
|
try_handle_command_help_option (struct app_context *ctx, const char *name)
|
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item =
|
2015-05-03 16:47:31 +02:00
|
|
|
config_item_get (ctx->config.root, name, NULL);
|
|
|
|
if (!item)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
struct config_schema *schema = item->schema;
|
|
|
|
if (!schema)
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "#s: #s", "Option not recognized", name);
|
2015-05-03 16:47:31 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, "");
|
|
|
|
log_global_indent (ctx, "Option \"#s\":", name);
|
2015-12-28 02:03:26 +01:00
|
|
|
log_global_indent (ctx, " Description: #s",
|
|
|
|
schema->comment ? schema->comment : "(none)");
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type));
|
|
|
|
log_global_indent (ctx, " Default: #s",
|
2015-06-28 02:49:28 +02:00
|
|
|
schema->default_ ? schema->default_ : "null");
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str tmp = str_make ();
|
2015-05-09 22:10:58 +02:00
|
|
|
config_item_write (item, false, &tmp);
|
2015-06-28 19:56:05 +02:00
|
|
|
log_global_indent (ctx, " Current value: #s", tmp.str);
|
2015-05-09 22:10:58 +02:00
|
|
|
str_free (&tmp);
|
|
|
|
return true;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-07-09 00:10:46 +02:00
|
|
|
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, "");
|
2015-07-11 03:58:53 +02:00
|
|
|
log_global_indent (ctx, "/#s: #s", handler->name, handler->description);
|
2015-07-09 00:10:46 +02:00
|
|
|
log_global_indent (ctx, " Arguments: #s",
|
|
|
|
handler->usage ? handler->usage : "(none)");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static bool
|
2015-06-28 03:00:19 +02:00
|
|
|
handle_command_help (struct handler_args *a)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
struct app_context *ctx = a->ctx;
|
2015-06-19 23:53:57 +02:00
|
|
|
if (!*a->arguments)
|
2015-07-09 00:10:46 +02:00
|
|
|
return show_command_list (ctx);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2021-06-16 20:24:41 +02:00
|
|
|
const char *word = cut_word (&a->arguments);
|
|
|
|
|
|
|
|
const char *command = word + (*word == '/');
|
2015-05-09 22:10:58 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
|
|
{
|
|
|
|
struct command_handler *handler = &g_command_handlers[i];
|
2015-07-09 00:10:46 +02:00
|
|
|
if (!strcasecmp_ascii (command, handler->name))
|
|
|
|
return show_command_help (ctx, handler);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
|
2021-06-16 20:24:41 +02:00
|
|
|
if (try_handle_command_help_option (ctx, word))
|
2015-07-11 03:58:53 +02:00
|
|
|
return true;
|
|
|
|
|
|
|
|
if (str_map_find (get_aliases_config (ctx), command))
|
|
|
|
log_global_status (ctx, "/#s is an alias", command);
|
|
|
|
else
|
2021-06-16 20:24:41 +02:00
|
|
|
log_global_error (ctx, "#s: #s", "No such command or option", word);
|
2015-05-09 22:10:58 +02:00
|
|
|
return true;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-07-09 22:32:55 +02:00
|
|
|
init_user_command_map (struct str_map *map)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
*map = str_map_make (NULL);
|
2015-07-09 22:32:55 +02:00
|
|
|
map->key_xfrm = tolower_ascii_strxfrm;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-09 22:32:55 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-07-09 22:32:55 +02:00
|
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
|
|
str_map_set (map, handler->name, handler);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-09 22:32:14 +02:00
|
|
|
static bool
|
2015-07-18 13:23:22 +02:00
|
|
|
process_user_command (struct app_context *ctx, struct buffer *buffer,
|
|
|
|
const char *command_name, char *input)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-05-09 22:10:58 +02:00
|
|
|
static bool initialized = false;
|
2015-07-09 22:32:55 +02:00
|
|
|
static struct str_map map;
|
2015-05-09 22:10:58 +02:00
|
|
|
if (!initialized)
|
2015-05-02 23:49:38 +02:00
|
|
|
{
|
2015-07-09 22:32:55 +02:00
|
|
|
init_user_command_map (&map);
|
2015-05-09 22:10:58 +02:00
|
|
|
initialized = true;
|
2015-05-02 23:49:38 +02:00
|
|
|
}
|
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
if (try_handle_buffer_goto (ctx, command_name))
|
2015-07-09 22:32:14 +02:00
|
|
|
return true;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-06-19 23:53:57 +02:00
|
|
|
struct handler_args args =
|
|
|
|
{
|
2015-06-28 03:00:19 +02:00
|
|
|
.ctx = ctx,
|
2015-07-18 13:23:22 +02:00
|
|
|
.buffer = buffer,
|
2015-06-19 23:53:57 +02:00
|
|
|
.arguments = input,
|
|
|
|
};
|
|
|
|
|
2015-07-09 22:32:14 +02:00
|
|
|
struct command_handler *handler;
|
|
|
|
if (!(handler = str_map_find (&map, command_name)))
|
|
|
|
return false;
|
2015-07-11 14:47:45 +02:00
|
|
|
hard_assert (handler->flags == 0 || (handler->flags & HANDLER_SERVER));
|
2015-07-09 22:32:14 +02:00
|
|
|
|
|
|
|
if ((handler->flags & HANDLER_SERVER)
|
2015-06-19 23:53:57 +02:00
|
|
|
&& args.buffer->type == BUFFER_GLOBAL)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx, "/#s: #s",
|
|
|
|
command_name, "can't do this from a global buffer");
|
2015-06-19 23:53:57 +02:00
|
|
|
else if ((handler->flags & HANDLER_SERVER)
|
|
|
|
&& !irc_is_connected ((args.s = args.buffer->server)))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (args.s, args.s->buffer, "Not connected");
|
2015-06-19 23:53:57 +02:00
|
|
|
else if ((handler->flags & HANDLER_NEEDS_REG)
|
|
|
|
&& args.s->state != IRC_REGISTERED)
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (args.s, args.s->buffer, "Not registered");
|
2015-06-19 23:53:57 +02:00
|
|
|
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))))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (args.s, args.buffer, "/#s: #s", command_name,
|
2015-06-19 23:53:57 +02:00
|
|
|
"no channel name given and this buffer is not a channel");
|
2015-06-28 03:00:19 +02:00
|
|
|
else if (!handler->handler (&args))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_global_error (ctx,
|
|
|
|
"#s: /#s #s", "Usage", handler->name, handler->usage);
|
2015-07-09 22:32:14 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-16 21:58:49 +02:00
|
|
|
static const char *
|
|
|
|
expand_alias_escape (const char *p, const char *arguments, struct str *output)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv words = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (arguments, " ", true, &words);
|
2015-07-16 21:58:49 +02:00
|
|
|
|
|
|
|
// TODO: eventually also add support for argument ranges
|
2021-06-16 21:17:34 +02:00
|
|
|
// - Can use ${0}, ${0:}, ${:0}, ${1:-1} with strtol, dispose of $1 syntax
|
|
|
|
// (default aliases don't use numeric arguments).
|
|
|
|
// - Start numbering from zero, since we'd have to figure out what to do
|
|
|
|
// in case we encounter a zero if we keep the current approach.
|
|
|
|
// - Ignore the sequence altogether if no closing '}' can be found,
|
|
|
|
// or if the internal format doesn't fit the above syntax.
|
2015-12-10 20:01:41 +01:00
|
|
|
if (*p >= '1' && *p <= '9')
|
|
|
|
{
|
|
|
|
size_t offset = *p - '1';
|
|
|
|
if (offset < words.len)
|
|
|
|
str_append (output, words.vector[offset]);
|
|
|
|
}
|
2015-07-16 21:58:49 +02:00
|
|
|
else if (*p == '*')
|
|
|
|
str_append (output, arguments);
|
|
|
|
else if (strchr ("$;", *p))
|
|
|
|
str_append_c (output, *p);
|
|
|
|
else
|
|
|
|
str_append_printf (output, "$%c", *p);
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&words);
|
2015-07-16 21:58:49 +02:00
|
|
|
return ++p;
|
|
|
|
}
|
2015-07-09 22:32:14 +02:00
|
|
|
|
2015-07-16 21:58:49 +02:00
|
|
|
static void
|
2015-12-10 20:01:41 +01:00
|
|
|
expand_alias_definition (const char *definition, const char *arguments,
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv *commands)
|
2015-07-16 21:58:49 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str expanded = str_make ();
|
2015-07-09 22:32:14 +02:00
|
|
|
bool escape = false;
|
2015-12-10 20:01:41 +01:00
|
|
|
for (const char *p = definition; *p; p++)
|
2015-07-09 22:32:14 +02:00
|
|
|
{
|
2015-07-16 21:58:49 +02:00
|
|
|
if (escape)
|
2015-07-09 22:32:14 +02:00
|
|
|
{
|
2015-07-23 21:44:04 +02:00
|
|
|
p = expand_alias_escape (p, arguments, &expanded) - 1;
|
2015-07-16 21:58:49 +02:00
|
|
|
escape = false;
|
2015-07-09 22:32:14 +02:00
|
|
|
}
|
2015-07-16 21:58:49 +02:00
|
|
|
else if (*p == ';')
|
|
|
|
{
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (commands, str_steal (&expanded));
|
2017-06-22 22:39:39 +02:00
|
|
|
expanded = str_make ();
|
2015-07-16 21:58:49 +02:00
|
|
|
}
|
|
|
|
else if (*p == '$' && p[1])
|
|
|
|
escape = true;
|
2015-07-09 22:32:14 +02:00
|
|
|
else
|
2015-07-16 21:58:49 +02:00
|
|
|
str_append_c (&expanded, *p);
|
2015-07-09 22:32:14 +02:00
|
|
|
}
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (commands, str_steal (&expanded));
|
2015-07-09 22:32:14 +02:00
|
|
|
}
|
|
|
|
|
2015-07-16 21:58:49 +02:00
|
|
|
static bool
|
|
|
|
expand_alias (struct app_context *ctx,
|
2017-01-23 23:50:27 +01:00
|
|
|
const char *alias_name, char *input, struct strv *commands)
|
2015-07-09 22:32:14 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *entry =
|
2015-07-10 01:44:02 +02:00
|
|
|
str_map_find (get_aliases_config (ctx), alias_name);
|
2015-07-09 22:32:14 +02:00
|
|
|
if (!entry)
|
2015-07-16 21:58:49 +02:00
|
|
|
return false;
|
2015-07-09 22:32:14 +02:00
|
|
|
|
2015-07-16 21:58:49 +02:00
|
|
|
if (!config_item_type_is_string (entry->type))
|
|
|
|
{
|
2018-06-21 22:02:26 +02:00
|
|
|
log_global_error (ctx, "Error executing `/#s': #s",
|
2015-07-16 21:58:49 +02:00
|
|
|
alias_name, "alias definition is not a string");
|
|
|
|
return false;
|
|
|
|
}
|
2015-07-09 22:32:14 +02:00
|
|
|
|
2015-12-10 20:01:41 +01:00
|
|
|
expand_alias_definition (entry->value.string.str, input, commands);
|
2015-07-16 21:58:49 +02:00
|
|
|
return true;
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
send_message_to_target (struct server *s,
|
|
|
|
const char *target, char *message, struct buffer *buffer)
|
|
|
|
{
|
|
|
|
if (!irc_is_connected (s))
|
2015-06-28 02:49:28 +02:00
|
|
|
log_server_error (s, buffer, "Not connected");
|
|
|
|
else
|
|
|
|
SEND_AUTOSPLIT_PRIVMSG (s, target, message);
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
2015-05-08 17:39:26 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
2015-07-18 13:23:22 +02:00
|
|
|
send_message_to_buffer (struct app_context *ctx, struct buffer *buffer,
|
|
|
|
char *message)
|
2015-05-09 22:10:58 +02:00
|
|
|
{
|
|
|
|
hard_assert (buffer != NULL);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
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;
|
2015-07-06 01:54:02 +02:00
|
|
|
default:
|
|
|
|
log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR,
|
|
|
|
"This buffer is not a channel");
|
2015-05-09 22:10:58 +02:00
|
|
|
}
|
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-16 21:58:49 +02:00
|
|
|
static bool
|
2015-07-18 13:23:22 +02:00
|
|
|
process_alias (struct app_context *ctx, struct buffer *buffer,
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv *commands, int level)
|
2015-07-16 21:58:49 +02:00
|
|
|
{
|
|
|
|
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++)
|
2015-07-18 13:23:22 +02:00
|
|
|
if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level))
|
2015-07-16 21:58:49 +02:00
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
2015-11-19 19:09:05 +01:00
|
|
|
process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer,
|
2015-07-18 13:23:22 +02:00
|
|
|
char *input, int alias_level)
|
2015-07-09 22:32:14 +02:00
|
|
|
{
|
|
|
|
if (*input != '/' || *++input == '/')
|
|
|
|
{
|
2015-07-18 13:23:22 +02:00
|
|
|
send_message_to_buffer (ctx, buffer, input);
|
2015-07-16 21:58:49 +02:00
|
|
|
return true;
|
2015-07-09 22:32:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
char *name = cut_word (&input);
|
2015-07-18 13:23:22 +02:00
|
|
|
if (process_user_command (ctx, buffer, name, input))
|
2015-07-16 21:58:49 +02:00
|
|
|
return true;
|
2015-07-09 22:32:14 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv commands = strv_make ();
|
2015-07-16 21:58:49 +02:00
|
|
|
bool result = false;
|
|
|
|
if (!expand_alias (ctx, name, input, &commands))
|
2015-07-09 22:32:14 +02:00
|
|
|
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
|
2015-07-18 13:23:22 +02:00
|
|
|
result = process_alias (ctx, buffer, &commands, alias_level);
|
2015-07-16 21:58:49 +02:00
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&commands);
|
2015-07-16 21:58:49 +02:00
|
|
|
return result;
|
2015-07-09 22:32:14 +02:00
|
|
|
}
|
|
|
|
|
2015-11-19 19:09:05 +01:00
|
|
|
static char *
|
|
|
|
process_input_hooks (struct app_context *ctx, struct buffer *buffer,
|
|
|
|
char *input)
|
|
|
|
{
|
2015-11-22 02:52:07 +01:00
|
|
|
uint64_t hash = siphash_wrapper (input, strlen (input));
|
2015-11-21 18:40:07 +01:00
|
|
|
LIST_FOR_EACH (struct hook, iter, ctx->input_hooks)
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
struct input_hook *hook = (struct input_hook *) iter;
|
2016-10-29 19:35:48 +02:00
|
|
|
if (!(input = hook->filter (hook, buffer, input)))
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
|
|
|
log_global_debug (ctx, "Input thrown away by hook");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2015-11-22 02:52:07 +01:00
|
|
|
uint64_t new_hash = siphash_wrapper (input, strlen (input));
|
|
|
|
if (new_hash != hash)
|
|
|
|
log_global_debug (ctx, "Input transformed to \"#s\"#r", input);
|
|
|
|
hash = new_hash;
|
2015-11-19 19:09:05 +01:00
|
|
|
}
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
process_input_utf8 (struct app_context *ctx, struct buffer *buffer,
|
|
|
|
const char *input, int alias_level)
|
|
|
|
{
|
|
|
|
// Note that this also gets called on expanded aliases,
|
|
|
|
// which might or might not be desirable (we can forward "alias_level")
|
|
|
|
char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
|
|
|
|
bool result = !processed
|
|
|
|
|| process_input_utf8_posthook (ctx, buffer, processed, alias_level);
|
|
|
|
free (processed);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
static void
|
|
|
|
process_input (struct app_context *ctx, char *user_input)
|
|
|
|
{
|
|
|
|
char *input;
|
2015-07-09 22:32:14 +02:00
|
|
|
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
|
2015-12-25 21:22:59 +01:00
|
|
|
print_error ("character conversion failed for: %s", "user input");
|
2015-05-09 22:10:58 +02:00
|
|
|
else
|
2015-11-15 15:56:33 +01:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv lines = strv_make ();
|
2016-10-11 10:52:49 +02:00
|
|
|
cstr_split (input, "\r\n", false, &lines);
|
2015-11-15 15:56:33 +01:00
|
|
|
for (size_t i = 0; i < lines.len; i++)
|
|
|
|
(void) process_input_utf8 (ctx,
|
|
|
|
ctx->current_buffer, lines.vector[i], 0);
|
|
|
|
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&lines);
|
2015-11-15 15:56:33 +01:00
|
|
|
}
|
2015-05-09 22:10:58 +02:00
|
|
|
free (input);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-05-07 05:19:13 +02:00
|
|
|
// --- Word completion ---------------------------------------------------------
|
|
|
|
|
|
|
|
// The amount of crap that goes into this is truly insane.
|
|
|
|
// It's mostly because of Editline's total ignorance of this task.
|
|
|
|
|
|
|
|
static void
|
|
|
|
completion_init (struct completion *self)
|
|
|
|
{
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
completion_free (struct completion *self)
|
|
|
|
{
|
|
|
|
free (self->line);
|
|
|
|
free (self->words);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
completion_add_word (struct completion *self, size_t start, size_t end)
|
|
|
|
{
|
|
|
|
if (!self->words)
|
|
|
|
self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
|
|
|
|
if (self->words_len == self->words_alloc)
|
2015-05-10 00:22:19 +02:00
|
|
|
self->words = xreallocarray (self->words,
|
|
|
|
(self->words_alloc <<= 1), sizeof *self->words);
|
2015-05-07 06:37:50 +02:00
|
|
|
self->words[self->words_len++] = (struct completion_word) { start, end };
|
2015-05-07 05:19:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-10-31 23:06:43 +01:00
|
|
|
static char *
|
|
|
|
completion_word (struct completion *self, int word)
|
2015-05-07 06:37:50 +02:00
|
|
|
{
|
|
|
|
hard_assert (word >= 0 && word < (int) self->words_len);
|
2020-10-31 23:06:43 +01:00
|
|
|
return xstrndup (self->line + self->words[word].start,
|
2015-05-07 06:37:50 +02:00
|
|
|
self->words[word].end - self->words[word].start);
|
|
|
|
}
|
|
|
|
|
2015-05-07 05:19:13 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
// 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;
|
2015-07-11 14:47:45 +02:00
|
|
|
if (!vector || !len)
|
2015-05-07 05:19:13 +02:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
struct utf8_iter a[len];
|
|
|
|
for (size_t i = 0; i < len; i++)
|
2017-06-22 22:39:39 +02:00
|
|
|
a[i] = utf8_iter_make (vector[i]);
|
2015-05-07 05:19:13 +02:00
|
|
|
|
|
|
|
size_t ch_len;
|
2015-05-07 08:36:22 +02:00
|
|
|
int32_t ch;
|
2020-10-12 23:00:43 +02:00
|
|
|
while ((ch = utf8_iter_next (&a[0], &ch_len)) >= 0)
|
2015-05-07 05:19:13 +02:00
|
|
|
{
|
|
|
|
for (size_t i = 1; i < len; i++)
|
|
|
|
{
|
2015-05-07 08:36:22 +02:00
|
|
|
int32_t other = utf8_iter_next (&a[i], NULL);
|
2015-05-07 18:27:02 +02:00
|
|
|
if (ch == other)
|
|
|
|
continue;
|
|
|
|
// Not bothering with lowercasing non-ASCII
|
|
|
|
if (ch >= 0x80 || other >= 0x80
|
2015-05-07 08:36:22 +02:00
|
|
|
|| tolower_ascii (ch) != tolower_ascii (other))
|
2015-05-07 05:19:13 +02:00
|
|
|
return prefix;
|
|
|
|
}
|
|
|
|
prefix += ch_len;
|
|
|
|
}
|
|
|
|
return prefix;
|
|
|
|
}
|
|
|
|
|
2015-05-07 06:37:50 +02:00
|
|
|
static void
|
|
|
|
complete_command (struct app_context *ctx, struct completion *data,
|
2017-01-23 23:50:27 +01:00
|
|
|
const char *word, struct strv *output)
|
2015-05-07 06:37:50 +02:00
|
|
|
{
|
|
|
|
(void) data;
|
|
|
|
|
|
|
|
const char *prefix = "";
|
|
|
|
if (*word == '/')
|
|
|
|
{
|
|
|
|
word++;
|
|
|
|
prefix = "/";
|
|
|
|
}
|
|
|
|
|
2015-06-02 21:28:41 +02:00
|
|
|
size_t word_len = strlen (word);
|
2015-05-07 06:37:50 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
|
|
{
|
|
|
|
struct command_handler *handler = &g_command_handlers[i];
|
2015-06-02 21:28:41 +02:00
|
|
|
if (!strncasecmp_ascii (word, handler->name, word_len))
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output,
|
2015-05-07 06:37:50 +02:00
|
|
|
xstrdup_printf ("%s%s", prefix, handler->name));
|
|
|
|
}
|
2015-07-11 03:58:53 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (get_aliases_config (ctx));
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *alias;
|
2015-07-11 03:58:53 +02:00
|
|
|
while ((alias = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
if (!strncasecmp_ascii (word, iter.link->key, word_len))
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output,
|
2015-07-11 03:58:53 +02:00
|
|
|
xstrdup_printf ("%s%s", prefix, iter.link->key));
|
|
|
|
}
|
2015-05-07 06:37:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
complete_option (struct app_context *ctx, struct completion *data,
|
2017-01-23 23:50:27 +01:00
|
|
|
const char *word, struct strv *output)
|
2015-05-07 06:37:50 +02:00
|
|
|
{
|
|
|
|
(void) data;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv options = strv_make ();
|
2015-05-07 07:47:58 +02:00
|
|
|
config_dump (ctx->config.root, &options);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_sort (&options);
|
2015-05-07 06:37:50 +02:00
|
|
|
|
2015-05-07 07:47:58 +02:00
|
|
|
// Wildcard expansion is an interesting side-effect
|
|
|
|
char *mask = xstrdup_printf ("%s*", word);
|
|
|
|
for (size_t i = 0; i < options.len; i++)
|
|
|
|
{
|
2015-07-11 17:54:38 +02:00
|
|
|
char *key = cstr_cut_until (options.vector[i], " ");
|
2015-05-07 07:47:58 +02:00
|
|
|
if (!fnmatch (mask, key, 0))
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output, key);
|
2015-05-07 07:47:58 +02:00
|
|
|
else
|
|
|
|
free (key);
|
|
|
|
}
|
|
|
|
free (mask);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&options);
|
2015-05-07 06:37:50 +02:00
|
|
|
}
|
|
|
|
|
2020-10-31 23:06:43 +01:00
|
|
|
static void
|
|
|
|
complete_set_value (struct config_item *item, const char *word,
|
|
|
|
struct strv *output)
|
|
|
|
{
|
|
|
|
struct str serialized = str_make ();
|
|
|
|
config_item_write (item, false, &serialized);
|
|
|
|
if (!strncmp (serialized.str, word, strlen (word)))
|
|
|
|
strv_append_owned (output, str_steal (&serialized));
|
|
|
|
else
|
|
|
|
str_free (&serialized);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
complete_set_value_array (struct config_item *item, const char *word,
|
|
|
|
struct strv *output)
|
|
|
|
{
|
|
|
|
if (!item->schema || item->schema->type != CONFIG_ITEM_STRING_ARRAY)
|
|
|
|
return;
|
|
|
|
|
|
|
|
struct strv items = strv_make ();
|
|
|
|
cstr_split (item->value.string.str, ",", false, &items);
|
|
|
|
for (size_t i = 0; i < items.len; i++)
|
|
|
|
{
|
|
|
|
struct str wrapped = str_make (), serialized = str_make ();
|
|
|
|
str_append (&wrapped, items.vector[i]);
|
|
|
|
config_item_write_string (&serialized, &wrapped);
|
|
|
|
str_free (&wrapped);
|
|
|
|
|
|
|
|
if (!strncmp (serialized.str, word, strlen (word)))
|
|
|
|
strv_append_owned (output, str_steal (&serialized));
|
|
|
|
else
|
|
|
|
str_free (&serialized);
|
|
|
|
}
|
|
|
|
strv_free (&items);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
complete_set (struct app_context *ctx, struct completion *data,
|
|
|
|
const char *word, struct strv *output)
|
|
|
|
{
|
|
|
|
if (data->location == 1)
|
|
|
|
{
|
|
|
|
complete_option (ctx, data, word, output);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data->location != 3)
|
|
|
|
return;
|
|
|
|
|
|
|
|
char *key = completion_word (data, 1);
|
|
|
|
struct config_item *item = config_item_get (ctx->config.root, key, NULL);
|
|
|
|
if (item)
|
|
|
|
{
|
|
|
|
char *op = completion_word (data, 2);
|
|
|
|
if (!strcmp (op, "-=")) complete_set_value_array (item, word, output);
|
|
|
|
if (!strcmp (op, "=")) complete_set_value (item, word, output);
|
|
|
|
free (op);
|
|
|
|
}
|
|
|
|
free (key);
|
|
|
|
}
|
|
|
|
|
2016-01-04 22:06:29 +01:00
|
|
|
static void
|
|
|
|
complete_topic (struct app_context *ctx, struct completion *data,
|
2017-01-23 23:50:27 +01:00
|
|
|
const char *word, struct strv *output)
|
2016-01-04 22:06:29 +01:00
|
|
|
{
|
|
|
|
(void) data;
|
|
|
|
|
|
|
|
// TODO: make it work in other server-related buffers, too, i.e. when we're
|
|
|
|
// completing the third word and the second word is a known channel name
|
|
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
|
|
if (buffer->type != BUFFER_CHANNEL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const char *topic = buffer->channel->topic;
|
|
|
|
if (topic && !strncasecmp_ascii (word, topic, strlen (word)))
|
|
|
|
{
|
|
|
|
// We must prepend the channel name if the topic itself starts
|
|
|
|
// with something that could be regarded as a channel name
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output, irc_is_channel (buffer->server, topic)
|
2016-01-04 22:06:29 +01:00
|
|
|
? xstrdup_printf ("%s %s", buffer->channel->name, topic)
|
|
|
|
: xstrdup (topic));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-07 06:37:50 +02:00
|
|
|
static void
|
|
|
|
complete_nicknames (struct app_context *ctx, struct completion *data,
|
2017-01-23 23:50:27 +01:00
|
|
|
const char *word, struct strv *output)
|
2015-05-07 06:37:50 +02:00
|
|
|
{
|
2015-06-02 21:03:10 +02:00
|
|
|
struct buffer *buffer = ctx->current_buffer;
|
2015-08-13 00:23:56 +02:00
|
|
|
if (buffer->type == BUFFER_SERVER)
|
|
|
|
{
|
|
|
|
struct user *self_user = buffer->server->irc_user;
|
|
|
|
if (self_user)
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append (output, self_user->nickname);
|
2015-08-13 00:23:56 +02:00
|
|
|
}
|
2015-06-02 21:03:10 +02:00
|
|
|
if (buffer->type != BUFFER_CHANNEL)
|
2015-05-10 00:23:23 +02:00
|
|
|
return;
|
|
|
|
|
2015-06-02 21:03:10 +02:00
|
|
|
size_t word_len = strlen (word);
|
|
|
|
LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
|
2015-05-10 00:23:23 +02:00
|
|
|
{
|
|
|
|
const char *nickname = iter->user->nickname;
|
2015-06-02 21:03:10 +02:00
|
|
|
if (irc_server_strncmp (buffer->server, word, nickname, word_len))
|
2015-05-10 00:23:23 +02:00
|
|
|
continue;
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (output, data->location == 0
|
2015-05-10 00:23:23 +02:00
|
|
|
? xstrdup_printf ("%s:", nickname)
|
|
|
|
: xstrdup (nickname));
|
|
|
|
}
|
2015-05-07 06:37:50 +02:00
|
|
|
}
|
|
|
|
|
2015-05-07 05:19:13 +02:00
|
|
|
static char **
|
|
|
|
complete_word (struct app_context *ctx, struct completion *data,
|
|
|
|
const char *word)
|
|
|
|
{
|
2020-10-31 23:06:43 +01:00
|
|
|
char *initial = completion_word (data, 0);
|
2015-05-07 06:37:50 +02:00
|
|
|
|
2020-10-31 23:06:43 +01:00
|
|
|
// Start with a placeholder for the longest common prefix
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv words = strv_make ();
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (&words, NULL);
|
2015-05-07 06:37:50 +02:00
|
|
|
|
2020-10-31 23:06:43 +01:00
|
|
|
if (data->location == 0 && *initial == '/')
|
|
|
|
complete_command (ctx, data, word, &words);
|
|
|
|
else if (data->location >= 1 && !strcmp (initial, "/set"))
|
|
|
|
complete_set (ctx, data, word, &words);
|
|
|
|
else if (data->location == 1 && !strcmp (initial, "/help"))
|
|
|
|
{
|
|
|
|
complete_command (ctx, data, word, &words);
|
|
|
|
complete_option (ctx, data, word, &words);
|
|
|
|
}
|
|
|
|
else if (data->location == 1 && !strcmp (initial, "/topic"))
|
|
|
|
{
|
|
|
|
complete_topic (ctx, data, word, &words);
|
|
|
|
complete_nicknames (ctx, data, word, &words);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
complete_nicknames (ctx, data, word, &words);
|
2015-05-07 06:37:50 +02:00
|
|
|
|
2020-10-31 23:06:43 +01:00
|
|
|
cstr_set (&initial, NULL);
|
2016-01-15 01:44:35 +01:00
|
|
|
LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
|
|
|
|
{
|
|
|
|
struct completion_hook *hook = (struct completion_hook *) iter;
|
2016-10-29 19:35:48 +02:00
|
|
|
hook->complete (hook, data, word, &words);
|
2016-01-15 01:44:35 +01:00
|
|
|
}
|
|
|
|
|
2015-05-07 06:37:50 +02:00
|
|
|
if (words.len == 1)
|
|
|
|
{
|
|
|
|
// Nothing matched
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&words);
|
2015-05-07 06:37:50 +02:00
|
|
|
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;
|
2015-05-07 05:19:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
/// 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)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
mbstate_t state;
|
|
|
|
memset (&state, 0, sizeof state);
|
2015-05-07 05:19:13 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str utf8 = str_make ();
|
2015-05-07 05:19:13 +02:00
|
|
|
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 ("");
|
|
|
|
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (vector, converted);
|
2015-05-07 05:19:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
/// 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;
|
|
|
|
}
|
|
|
|
|
2015-05-08 05:03:36 +02:00
|
|
|
// --- Common code for user actions --------------------------------------------
|
|
|
|
|
2015-11-15 01:03:29 +01:00
|
|
|
static void
|
|
|
|
toggle_bracketed_paste (bool enable)
|
|
|
|
{
|
|
|
|
fprintf (stdout, "\x1b[?2004%c", "lh"[enable]);
|
|
|
|
fflush (stdout);
|
|
|
|
}
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
static void
|
|
|
|
suspend_terminal (struct app_context *ctx)
|
|
|
|
{
|
2015-11-15 15:36:03 +01:00
|
|
|
// Terminal can get suspended by both backlog helper and SIGTSTP handling
|
|
|
|
if (ctx->terminal_suspended++ > 0)
|
|
|
|
return;
|
|
|
|
|
2015-11-15 01:03:29 +01:00
|
|
|
toggle_bracketed_paste (false);
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
2015-08-08 19:36:34 +02:00
|
|
|
poller_fd_reset (&ctx->tty_event);
|
2016-03-07 01:08:52 +01:00
|
|
|
|
|
|
|
CALL_ (ctx->input, prepare, false);
|
2015-08-08 19:36:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
resume_terminal (struct app_context *ctx)
|
|
|
|
{
|
2015-11-15 15:36:03 +01:00
|
|
|
if (--ctx->terminal_suspended > 0)
|
|
|
|
return;
|
|
|
|
|
2016-02-10 23:02:33 +01:00
|
|
|
update_screen_size ();
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (ctx->input, prepare, true);
|
2020-03-21 22:02:02 +01:00
|
|
|
CALL (ctx->input, on_tty_resized);
|
2015-08-08 19:36:34 +02:00
|
|
|
|
2015-11-15 01:03:29 +01:00
|
|
|
toggle_bracketed_paste (true);
|
2015-08-08 19:36:34 +02:00
|
|
|
// In theory we could just print all unseen messages but this is safer
|
|
|
|
buffer_print_backlog (ctx, ctx->current_buffer);
|
|
|
|
// Now it's safe to process any user input
|
|
|
|
poller_fd_set (&ctx->tty_event, POLLIN);
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-08-08 19:36:34 +02:00
|
|
|
}
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static pid_t
|
|
|
|
spawn_helper_child (struct app_context *ctx)
|
2015-08-08 19:36:34 +02:00
|
|
|
{
|
|
|
|
suspend_terminal (ctx);
|
|
|
|
pid_t child = fork ();
|
2015-12-25 04:21:18 +01:00
|
|
|
switch (child)
|
|
|
|
{
|
|
|
|
case -1:
|
2015-08-08 19:36:34 +02:00
|
|
|
{
|
|
|
|
int saved_errno = errno;
|
|
|
|
resume_terminal (ctx);
|
2015-12-25 04:21:18 +01:00
|
|
|
errno = saved_errno;
|
|
|
|
break;
|
2015-08-08 19:36:34 +02:00
|
|
|
}
|
2015-12-25 04:21:18 +01:00
|
|
|
case 0:
|
|
|
|
// Put the child in a new foreground process group
|
|
|
|
hard_assert (setpgid (0, 0) != -1);
|
|
|
|
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Make sure of it in the parent as well before continuing
|
2015-08-08 19:36:34 +02:00
|
|
|
(void) setpgid (child, child);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
return child;
|
|
|
|
}
|
2015-08-08 19:36:34 +02:00
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static void
|
2016-03-06 17:59:45 +01:00
|
|
|
redraw_screen (struct app_context *ctx)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-06 17:59:45 +01:00
|
|
|
// If by some circumstance we had the wrong idea
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, on_tty_resized);
|
2016-03-06 17:59:45 +01:00
|
|
|
update_screen_size ();
|
2015-09-24 16:12:07 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
2016-03-06 17:59:45 +01:00
|
|
|
buffer_print_backlog (ctx, ctx->current_buffer);
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-09-24 16:12:07 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2015-12-25 04:21:18 +01:00
|
|
|
|
|
|
|
static bool
|
|
|
|
dump_input_to_file (struct app_context *ctx, char *template, struct error **e)
|
|
|
|
{
|
2016-02-09 13:52:56 +01:00
|
|
|
mode_t mask = umask (S_IXUSR | S_IRWXG | S_IRWXO);
|
2015-12-25 04:21:18 +01:00
|
|
|
int fd = mkstemp (template);
|
2016-02-09 13:52:56 +01:00
|
|
|
(void) umask (mask);
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
if (fd < 0)
|
2016-10-11 10:52:49 +02:00
|
|
|
return error_set (e, "%s", strerror (errno));
|
2015-12-25 04:21:18 +01:00
|
|
|
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
char *input = CALL_ (ctx->input, get_line, NULL);
|
2015-12-25 04:21:18 +01:00
|
|
|
bool success = xwrite (fd, input, strlen (input), e);
|
|
|
|
free (input);
|
|
|
|
|
|
|
|
if (!success)
|
|
|
|
(void) unlink (template);
|
|
|
|
|
|
|
|
xclose (fd);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
try_dump_input_to_file (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
char *template = resolve_filename
|
2020-10-10 04:37:08 +02:00
|
|
|
("input.XXXXXX", resolve_relative_runtime_template);
|
2015-12-25 04:21:18 +01:00
|
|
|
|
|
|
|
struct error *e = NULL;
|
|
|
|
if (dump_input_to_file (ctx, template, &e))
|
|
|
|
return template;
|
|
|
|
|
|
|
|
log_global_error (ctx, "#s: #s",
|
|
|
|
"Failed to create a temporary file for editing", e->message);
|
|
|
|
error_free (e);
|
|
|
|
free (template);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
static struct strv
|
|
|
|
build_editor_command (struct app_context *ctx, const char *filename)
|
|
|
|
{
|
|
|
|
struct strv argv = strv_make ();
|
2022-08-26 03:43:32 +02:00
|
|
|
const char *editor = get_config_string (ctx->config.root, "general.editor");
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
if (!editor)
|
|
|
|
{
|
|
|
|
const char *command;
|
|
|
|
if (!(command = getenv ("VISUAL"))
|
|
|
|
&& !(command = getenv ("EDITOR")))
|
|
|
|
command = "vi";
|
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
// Although most visual editors support a "+LINE" argument
|
|
|
|
// (every editor mentioned in the default value of general.editor,
|
2022-08-14 20:26:38 +02:00
|
|
|
// plus vi, mcedit, vis, ...), it isn't particularly useful by itself.
|
|
|
|
// We need to be able to specify the column number.
|
|
|
|
//
|
|
|
|
// Seeing as less popular software may try to process this as a filename
|
|
|
|
// and fail, do not bother with this "undocumented standard feature".
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
strv_append (&argv, command);
|
|
|
|
strv_append (&argv, filename);
|
|
|
|
return argv;
|
|
|
|
}
|
|
|
|
|
|
|
|
int cursor = 0;
|
|
|
|
char *input = CALL_ (ctx->input, get_line, &cursor);
|
|
|
|
hard_assert (cursor >= 0);
|
|
|
|
|
|
|
|
mbstate_t ps;
|
|
|
|
memset (&ps, 0, sizeof ps);
|
|
|
|
|
|
|
|
wchar_t wch;
|
|
|
|
size_t len, processed = 0, line_one_based = 1, column = 0;
|
|
|
|
while (processed < (size_t) cursor
|
|
|
|
&& (len = mbrtowc (&wch, input + processed, cursor - processed, &ps))
|
|
|
|
&& len != (size_t) -2 && len != (size_t) -1)
|
|
|
|
{
|
|
|
|
// Both VIM and Emacs use the caret notation with columns.
|
|
|
|
// Consciously leaving tabs broken, they're too difficult to handle.
|
|
|
|
int width = wcwidth (wch);
|
|
|
|
if (width < 0)
|
|
|
|
width = 2;
|
|
|
|
|
|
|
|
processed += len;
|
|
|
|
if (wch == '\n')
|
|
|
|
{
|
|
|
|
line_one_based++;
|
|
|
|
column = 0;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
column += width;
|
|
|
|
}
|
|
|
|
free (input);
|
|
|
|
|
|
|
|
// Trivially split the command on spaces and substitute our values
|
|
|
|
struct str argument = str_make ();
|
|
|
|
for (; *editor; editor++)
|
|
|
|
{
|
|
|
|
if (*editor == ' ')
|
|
|
|
{
|
|
|
|
if (argument.len)
|
|
|
|
{
|
|
|
|
strv_append_owned (&argv, str_steal (&argument));
|
|
|
|
argument = str_make ();
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (*editor != '%' || !editor[1])
|
|
|
|
{
|
|
|
|
str_append_c (&argument, *editor);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// None of them are zero-length, thus words don't get lost
|
|
|
|
switch (*++editor)
|
|
|
|
{
|
|
|
|
case 'F':
|
|
|
|
str_append (&argument, filename);
|
|
|
|
break;
|
|
|
|
case 'L':
|
|
|
|
str_append_printf (&argument, "%zu", line_one_based);
|
|
|
|
break;
|
|
|
|
case 'C':
|
|
|
|
str_append_printf (&argument, "%zu", column + 1);
|
|
|
|
break;
|
|
|
|
case 'B':
|
|
|
|
str_append_printf (&argument, "%d", cursor + 1);
|
|
|
|
break;
|
|
|
|
case '%':
|
|
|
|
case ' ':
|
|
|
|
str_append_c (&argument, *editor);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
print_warning ("unknown substitution variable");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (argument.len)
|
|
|
|
strv_append_owned (&argv, str_steal (&argument));
|
|
|
|
else
|
|
|
|
str_free (&argument);
|
|
|
|
return argv;
|
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_edit_input (int count, int key, void *user_data)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-06 17:59:45 +01:00
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
struct app_context *ctx = user_data;
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
char *filename;
|
|
|
|
if (!(filename = try_dump_input_to_file (ctx)))
|
2016-03-06 17:59:45 +01:00
|
|
|
return false;
|
2015-12-25 04:21:18 +01:00
|
|
|
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
struct strv argv = build_editor_command (ctx, filename);
|
|
|
|
if (!argv.len)
|
|
|
|
strv_append (&argv, "true");
|
2015-12-25 04:21:18 +01:00
|
|
|
|
|
|
|
hard_assert (!ctx->running_editor);
|
|
|
|
switch (spawn_helper_child (ctx))
|
|
|
|
{
|
|
|
|
case 0:
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
execvp (argv.vector[0], argv.vector);
|
2015-12-25 04:21:18 +01:00
|
|
|
print_error ("%s: %s",
|
|
|
|
"Failed to launch editor", strerror (errno));
|
|
|
|
_exit (EXIT_FAILURE);
|
|
|
|
case -1:
|
2015-12-25 21:22:59 +01:00
|
|
|
log_global_error (ctx, "#s: #l",
|
2015-12-25 04:21:18 +01:00
|
|
|
"Failed to launch editor", strerror (errno));
|
|
|
|
free (filename);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ctx->running_editor = true;
|
|
|
|
ctx->editor_filename = filename;
|
|
|
|
}
|
xC: allow passing the cursor position to editors
Add a configuration option to set a custom editor command,
different from EDITOR or VISUAL--those remain as defaults.
Implement substitutions allowing to convey cursor information
to VIM and Emacs (the latter of which is fairly painful to cater to),
and put usage hints in the configuration option's description.
This should make the editing experience a bit more seamless
for users, even though the position is carried over in one way only.
No sophisticated quoting capabilities were deemed necessary,
it is a lot of code already. The particular syntax is inspired
by .desktop files and systemd.
["/bin/sh", "-c", "vim +$2go \"$1\"", filename, position, line, column]
would be a slightly simpler but cryptic way of implementing this.
2021-10-30 08:34:14 +02:00
|
|
|
strv_free (&argv);
|
2016-03-06 17:59:45 +01:00
|
|
|
return true;
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-01-07 22:47:57 +01:00
|
|
|
input_editor_process (struct app_context *ctx)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str input = str_make ();
|
2015-12-25 04:21:18 +01:00
|
|
|
struct error *e = NULL;
|
|
|
|
if (!read_file (ctx->editor_filename, &input, &e))
|
|
|
|
{
|
|
|
|
log_global_error (ctx, "#s: #s", "Input editing failed", e->message);
|
|
|
|
error_free (e);
|
|
|
|
}
|
|
|
|
else
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, clear_line);
|
2015-12-25 04:21:18 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
if (!CALL_ (ctx->input, insert, input.str))
|
2015-12-25 04:21:18 +01:00
|
|
|
log_global_error (ctx, "#s: #s", "Input editing failed",
|
|
|
|
"could not re-insert the modified text");
|
|
|
|
|
2016-01-07 22:47:57 +01:00
|
|
|
str_free (&input);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
input_editor_cleanup (struct app_context *ctx)
|
|
|
|
{
|
2015-12-25 04:21:18 +01:00
|
|
|
if (unlink (ctx->editor_filename))
|
2015-12-25 21:22:59 +01:00
|
|
|
log_global_error (ctx, "Could not unlink `#s': #l",
|
2015-12-25 04:21:18 +01:00
|
|
|
ctx->editor_filename, strerror (errno));
|
|
|
|
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&ctx->editor_filename, NULL);
|
2015-12-25 04:21:18 +01:00
|
|
|
ctx->running_editor = false;
|
2015-09-24 16:12:07 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
static void
|
2022-08-26 03:43:32 +02:00
|
|
|
launch_pager (struct app_context *ctx,
|
|
|
|
int fd, const char *name, const char *path)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2022-08-26 03:43:32 +02:00
|
|
|
hard_assert (!ctx->running_pager);
|
2016-03-06 17:59:45 +01:00
|
|
|
switch (spawn_helper_child (ctx))
|
|
|
|
{
|
|
|
|
case 0:
|
2022-08-26 03:43:32 +02:00
|
|
|
dup2 (fd, STDIN_FILENO);
|
2022-08-14 13:22:07 +02:00
|
|
|
char *localized_name =
|
|
|
|
iconv_xstrdup (ctx->term_from_utf8, (char *) name, -1, NULL);
|
|
|
|
execl ("/bin/sh", "/bin/sh", "-c",
|
2022-08-26 03:43:32 +02:00
|
|
|
get_config_string (ctx->config.root, "general.pager"),
|
2022-08-14 13:22:07 +02:00
|
|
|
PROGRAM_NAME, localized_name, path, NULL);
|
2022-08-26 03:43:32 +02:00
|
|
|
print_error ("%s: %s", "Failed to launch pager", strerror (errno));
|
2016-03-06 17:59:45 +01:00
|
|
|
_exit (EXIT_FAILURE);
|
|
|
|
case -1:
|
|
|
|
log_global_error (ctx, "#s: #l",
|
2022-08-26 03:43:32 +02:00
|
|
|
"Failed to launch pager", strerror (errno));
|
2016-03-06 17:59:45 +01:00
|
|
|
break;
|
|
|
|
default:
|
2022-08-26 03:43:32 +02:00
|
|
|
ctx->running_pager = true;
|
2016-03-06 17:59:45 +01:00
|
|
|
}
|
2015-05-08 06:23:38 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
2016-04-10 21:37:36 +02:00
|
|
|
display_backlog (struct app_context *ctx, int flush_opts)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-06 17:59:45 +01:00
|
|
|
FILE *backlog = tmpfile ();
|
|
|
|
if (!backlog)
|
|
|
|
{
|
|
|
|
log_global_error (ctx, "#s: #l",
|
|
|
|
"Failed to create a temporary file", strerror (errno));
|
|
|
|
return false;
|
|
|
|
}
|
2016-04-10 21:37:36 +02:00
|
|
|
|
|
|
|
if (!get_config_boolean (ctx->config.root,
|
2022-08-26 03:43:32 +02:00
|
|
|
"general.pager_strip_formatting"))
|
2016-04-10 21:37:36 +02:00
|
|
|
flush_opts |= FLUSH_OPT_RAW;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-26 03:09:29 +01:00
|
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
|
|
int until_marker =
|
|
|
|
(int) buffer->lines_count - (int) buffer->new_messages_count;
|
|
|
|
for (struct buffer_line *line = buffer->lines; line; line = line->next)
|
|
|
|
{
|
|
|
|
if (until_marker-- == 0
|
|
|
|
&& buffer->new_messages_count != buffer->lines_count)
|
2016-04-10 21:37:36 +02:00
|
|
|
buffer_print_read_marker (ctx, backlog, flush_opts);
|
2016-10-30 16:24:23 +01:00
|
|
|
if (buffer_line_will_show_up (buffer, line))
|
|
|
|
buffer_line_write_to_backlog (ctx, line, backlog, flush_opts);
|
2016-03-26 03:09:29 +01:00
|
|
|
}
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-04-10 22:13:53 +02:00
|
|
|
// So that it is obvious if the last line in the buffer is not from today
|
|
|
|
buffer_update_time (ctx, time (NULL), backlog, flush_opts);
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
rewind (backlog);
|
|
|
|
set_cloexec (fileno (backlog));
|
2022-08-26 03:43:32 +02:00
|
|
|
launch_pager (ctx, fileno (backlog), buffer->name, NULL);
|
2016-03-06 17:59:45 +01:00
|
|
|
fclose (backlog);
|
|
|
|
return true;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2016-04-10 21:37:36 +02:00
|
|
|
static bool
|
|
|
|
on_display_backlog (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
return display_backlog (user_data, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
on_display_backlog_nowrap (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
return display_backlog (user_data, FLUSH_OPT_NOWRAP);
|
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_display_full_log (int count, int key, void *user_data)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-06 17:59:45 +01:00
|
|
|
(void) count;
|
2015-05-05 03:23:53 +02:00
|
|
|
(void) key;
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = user_data;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2022-08-14 13:22:07 +02:00
|
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
|
|
char *path = buffer_get_log_path (buffer);
|
2016-03-06 17:59:45 +01:00
|
|
|
FILE *full_log = fopen (path, "rb");
|
|
|
|
if (!full_log)
|
|
|
|
{
|
|
|
|
log_global_error (ctx, "Failed to open log file for #s: #l",
|
|
|
|
ctx->current_buffer->name, strerror (errno));
|
2022-08-14 13:22:07 +02:00
|
|
|
free (path);
|
2016-03-06 17:59:45 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-08-14 13:22:07 +02:00
|
|
|
if (buffer->log_file)
|
2018-01-07 05:54:45 +01:00
|
|
|
// The regular flush will log any error eventually
|
2022-08-14 13:22:07 +02:00
|
|
|
(void) fflush (buffer->log_file);
|
2016-03-06 17:59:45 +01:00
|
|
|
|
|
|
|
set_cloexec (fileno (full_log));
|
2022-08-26 03:43:32 +02:00
|
|
|
launch_pager (ctx, fileno (full_log), buffer->name, path);
|
2016-03-06 17:59:45 +01:00
|
|
|
fclose (full_log);
|
2022-08-14 13:22:07 +02:00
|
|
|
free (path);
|
2016-03-06 17:59:45 +01:00
|
|
|
return true;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2016-10-23 16:53:31 +02:00
|
|
|
static bool
|
|
|
|
on_toggle_unimportant (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
struct app_context *ctx = user_data;
|
|
|
|
ctx->current_buffer->hide_unimportant ^= true;
|
|
|
|
buffer_print_backlog (ctx, ctx->current_buffer);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static bool
|
|
|
|
on_goto_buffer (int count, int key, void *user_data)
|
2015-12-31 03:55:32 +01:00
|
|
|
{
|
|
|
|
(void) count;
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = user_data;
|
2015-12-31 03:55:32 +01:00
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
int n = key - '0';
|
|
|
|
if (n < 0 || n > 9)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// There's no buffer zero
|
|
|
|
if (n == 0)
|
|
|
|
n = 10;
|
|
|
|
|
|
|
|
if (!ctx->last_buffer || buffer_get_index (ctx, ctx->current_buffer) != n)
|
|
|
|
return buffer_goto (ctx, n);
|
|
|
|
|
|
|
|
// Fast switching between two buffers
|
|
|
|
buffer_activate (ctx, ctx->last_buffer);
|
|
|
|
return true;
|
2015-12-31 03:55:32 +01:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_previous_buffer (int count, int key, void *user_data)
|
2015-08-08 19:36:34 +02:00
|
|
|
{
|
|
|
|
(void) key;
|
2016-03-06 17:59:45 +01:00
|
|
|
buffer_activate (user_data, buffer_previous (user_data, count));
|
|
|
|
return true;
|
2015-08-08 19:36:34 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_next_buffer (int count, int key, void *user_data)
|
2015-09-24 16:12:07 +02:00
|
|
|
{
|
|
|
|
(void) key;
|
2016-03-06 17:59:45 +01:00
|
|
|
buffer_activate (user_data, buffer_next (user_data, count));
|
|
|
|
return true;
|
2015-09-24 16:12:07 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_switch_buffer (int count, int key, void *user_data)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = user_data;
|
2015-12-25 04:21:18 +01:00
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
if (!ctx->last_buffer)
|
|
|
|
return false;
|
|
|
|
buffer_activate (ctx, ctx->last_buffer);
|
|
|
|
return true;
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
2016-03-08 22:27:59 +01:00
|
|
|
static bool
|
|
|
|
on_goto_highlight (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
|
|
|
struct app_context *ctx = user_data;
|
2016-03-26 03:09:29 +01:00
|
|
|
struct buffer *iter = ctx->current_buffer;;
|
|
|
|
do
|
2016-03-08 22:27:59 +01:00
|
|
|
{
|
2016-03-26 03:09:29 +01:00
|
|
|
if (!(iter = iter->next))
|
|
|
|
iter = ctx->buffers;
|
|
|
|
if (iter == ctx->current_buffer)
|
2016-03-08 22:27:59 +01:00
|
|
|
return false;
|
|
|
|
}
|
2016-03-26 03:09:29 +01:00
|
|
|
while (!iter->highlighted);
|
|
|
|
buffer_activate (ctx, iter);
|
2016-03-08 22:27:59 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
on_goto_activity (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
|
|
|
struct app_context *ctx = user_data;
|
2016-03-26 03:09:29 +01:00
|
|
|
struct buffer *iter = ctx->current_buffer;
|
|
|
|
do
|
2016-03-08 22:27:59 +01:00
|
|
|
{
|
2016-03-26 03:09:29 +01:00
|
|
|
if (!(iter = iter->next))
|
|
|
|
iter = ctx->buffers;
|
|
|
|
if (iter == ctx->current_buffer)
|
2016-03-08 22:27:59 +01:00
|
|
|
return false;
|
|
|
|
}
|
2016-03-26 03:09:29 +01:00
|
|
|
while (iter->new_messages_count == iter->new_unimportant_count);
|
|
|
|
buffer_activate (ctx, iter);
|
2016-03-08 22:27:59 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-03-10 00:07:59 +01:00
|
|
|
static bool
|
|
|
|
on_move_buffer_left (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
(void) key;
|
|
|
|
|
|
|
|
struct app_context *ctx = user_data;
|
|
|
|
int total = buffer_count (ctx);
|
|
|
|
int n = buffer_get_index (ctx, ctx->current_buffer) - count;
|
2018-01-07 05:51:44 +01:00
|
|
|
buffer_move (ctx, ctx->current_buffer, n <= 0
|
|
|
|
? (total + n % total)
|
|
|
|
: ((n - 1) % total + 1));
|
2016-03-10 00:07:59 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
on_move_buffer_right (int count, int key, void *user_data)
|
|
|
|
{
|
|
|
|
return on_move_buffer_left (-count, key, user_data);
|
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_redraw_screen (int count, int key, void *user_data)
|
2015-05-08 05:03:36 +02:00
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
redraw_screen (user_data);
|
|
|
|
return true;
|
2015-05-08 05:03:36 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_insert_attribute (int count, int key, void *user_data)
|
2015-05-12 03:48:52 +02:00
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = user_data;
|
2021-08-29 12:07:31 +02:00
|
|
|
ctx->awaiting_formatting_escape = true;
|
2016-03-06 17:59:45 +01:00
|
|
|
return true;
|
2015-05-12 03:48:52 +02:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static bool
|
|
|
|
on_start_paste_mode (int count, int key, void *user_data)
|
2015-11-15 01:03:29 +01:00
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = user_data;
|
2015-11-15 01:03:29 +01:00
|
|
|
ctx->in_bracketed_paste = true;
|
2016-03-06 17:59:45 +01:00
|
|
|
return true;
|
2015-11-15 01:03:29 +01:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static void
|
2016-03-10 00:06:28 +01:00
|
|
|
input_add_functions (void *user_data)
|
2016-03-06 17:59:45 +01:00
|
|
|
{
|
2016-03-10 00:06:28 +01:00
|
|
|
struct app_context *ctx = user_data;
|
|
|
|
#define XX(...) CALL_ (ctx->input, register_fn, __VA_ARGS__, ctx);
|
2016-04-10 21:37:36 +02:00
|
|
|
XX ("previous-buffer", "Previous buffer", on_previous_buffer)
|
|
|
|
XX ("next-buffer", "Next buffer", on_next_buffer)
|
|
|
|
XX ("goto-buffer", "Go to buffer", on_goto_buffer)
|
|
|
|
XX ("switch-buffer", "Switch buffer", on_switch_buffer)
|
|
|
|
XX ("goto-highlight", "Go to highlight", on_goto_highlight)
|
|
|
|
XX ("goto-activity", "Go to activity", on_goto_activity)
|
|
|
|
XX ("move-buffer-left", "Move buffer left", on_move_buffer_left)
|
|
|
|
XX ("move-buffer-right", "Move buffer right", on_move_buffer_right)
|
|
|
|
XX ("display-backlog", "Show backlog", on_display_backlog)
|
|
|
|
XX ("display-backlog-nw", "Non-wrapped log", on_display_backlog_nowrap)
|
|
|
|
XX ("display-full-log", "Show full log", on_display_full_log)
|
2016-10-23 16:53:31 +02:00
|
|
|
XX ("toggle-unimportant", "Toggle junk msgs", on_toggle_unimportant)
|
2016-04-10 21:37:36 +02:00
|
|
|
XX ("edit-input", "Edit input", on_edit_input)
|
|
|
|
XX ("redraw-screen", "Redraw screen", on_redraw_screen)
|
2021-08-29 12:07:31 +02:00
|
|
|
XX ("insert-attribute", "IRC formatting", on_insert_attribute)
|
2016-04-10 21:37:36 +02:00
|
|
|
XX ("start-paste-mode", "Bracketed paste", on_start_paste_mode)
|
2016-03-06 17:59:45 +01:00
|
|
|
#undef XX
|
2016-03-10 00:06:28 +01:00
|
|
|
}
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-10 00:06:28 +01:00
|
|
|
static void
|
|
|
|
bind_common_keys (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
struct input *self = ctx->input;
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_control, 'p', "previous-buffer");
|
|
|
|
CALL_ (self, bind_control, 'n', "next-buffer");
|
2016-03-06 17:59:45 +01:00
|
|
|
|
|
|
|
// Redefine M-0 through M-9 to switch buffers
|
|
|
|
for (int i = 0; i <= 9; i++)
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_meta, '0' + i, "goto-buffer");
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_meta, '\t', "switch-buffer");
|
2016-03-08 22:27:59 +01:00
|
|
|
CALL_ (self, bind_meta, '!', "goto-highlight");
|
2016-03-26 13:10:23 +01:00
|
|
|
CALL_ (self, bind_meta, 'a', "goto-activity");
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_meta, 'm', "insert-attribute");
|
|
|
|
CALL_ (self, bind_meta, 'h', "display-full-log");
|
2016-10-23 16:53:31 +02:00
|
|
|
CALL_ (self, bind_meta, 'H', "toggle-unimportant");
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_meta, 'e', "edit-input");
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
if (key_f5) CALL_ (self, bind, key_f5, "previous-buffer");
|
|
|
|
if (key_f6) CALL_ (self, bind, key_f6, "next-buffer");
|
2020-10-04 12:17:09 +02:00
|
|
|
if (key_ppage) CALL_ (self, bind, key_ppage, "display-backlog");
|
2016-03-06 17:59:45 +01:00
|
|
|
|
|
|
|
if (clear_screen)
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_control, 'l', "redraw-screen");
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind, "\x1b[200~", "start-paste-mode");
|
2016-03-06 17:59:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- GNU Readline user actions -----------------------------------------------
|
|
|
|
|
|
|
|
#ifdef HAVE_READLINE
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
static int
|
|
|
|
on_readline_return (int count, int key)
|
|
|
|
{
|
|
|
|
(void) count;
|
|
|
|
(void) key;
|
|
|
|
|
|
|
|
// Let readline pass the line to our input handler
|
|
|
|
rl_done = 1;
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
struct app_context *ctx = g_ctx;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = (struct input_rl *) ctx->input;
|
|
|
|
|
|
|
|
// Hide the line, don't redisplay it
|
|
|
|
CALL (ctx->input, hide);
|
|
|
|
input_rl__restore (self);
|
2015-05-05 03:23:53 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_readline_input (char *line)
|
|
|
|
{
|
2015-05-07 18:39:43 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = (struct input_rl *) ctx->input;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
if (line)
|
|
|
|
{
|
|
|
|
if (*line)
|
|
|
|
add_history (line);
|
|
|
|
|
2016-01-07 22:12:29 +01:00
|
|
|
// readline always erases the input line after returning from here,
|
2018-01-08 21:53:42 +01:00
|
|
|
// but we don't want that to happen if the command to be executed
|
|
|
|
// would switch the buffer (we'd keep the already executed command in
|
|
|
|
// the old buffer and delete any input restored from the new buffer)
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (&ctx->pending_input, line);
|
2016-01-07 22:12:29 +01:00
|
|
|
poller_idle_set (&ctx->input_event);
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2016-01-07 22:12:29 +01:00
|
|
|
// Prevent readline from showing the prompt twice for w/e reason
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
|
|
|
input_rl__restore (self);
|
2016-01-07 22:12:29 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, ding);
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
if (self->active)
|
2015-05-05 03:23:53 +02:00
|
|
|
// Readline automatically redisplays it
|
2016-03-07 01:08:52 +01:00
|
|
|
self->prompt_shown = 1;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
2015-05-06 17:32:29 +02:00
|
|
|
static char **
|
|
|
|
app_readline_completion (const char *text, int start, int end)
|
|
|
|
{
|
2015-05-07 05:19:13 +02:00
|
|
|
// We will reconstruct that ourselves
|
2015-05-06 17:32:29 +02:00
|
|
|
(void) text;
|
|
|
|
|
|
|
|
// Don't iterate over filenames and stuff
|
|
|
|
rl_attempted_completion_over = true;
|
2015-05-07 05:19:13 +02:00
|
|
|
|
|
|
|
return make_completions (g_ctx, rl_line_buffer, start, end);
|
2015-05-06 17:32:29 +02:00
|
|
|
}
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
static int
|
|
|
|
app_readline_init (void)
|
|
|
|
{
|
2015-05-08 06:23:38 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *self = ctx->input;
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// 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
|
|
|
|
|
2016-07-23 19:13:55 +02:00
|
|
|
// For vi mode, enabling "show-mode-in-prompt" is recommended as there is
|
|
|
|
// no easy way to indicate mode changes otherwise.
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
rl_add_defun ("send-line", on_readline_return, -1);
|
2015-05-08 06:23:38 +02:00
|
|
|
bind_common_keys (ctx);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// Move native history commands
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind_meta, 'p', "previous-history");
|
|
|
|
CALL_ (self, bind_meta, 'n', "next-history");
|
2015-05-08 05:03:36 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// We need to hide the prompt and input first
|
|
|
|
rl_bind_key (RETURN, rl_named_function ("send-line"));
|
2016-07-23 19:13:55 +02:00
|
|
|
CALL_ (self, bind_control, 'j', "send-line");
|
2015-05-06 17:32:29 +02:00
|
|
|
|
|
|
|
rl_variable_bind ("completion-ignore-case", "on");
|
|
|
|
rl_bind_key (TAB, rl_named_function ("menu-complete"));
|
|
|
|
if (key_btab)
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (self, bind, key_btab, "menu-complete-backward");
|
2015-05-05 03:23:53 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#endif // HAVE_READLINE
|
|
|
|
|
2015-05-05 19:35:51 +02:00
|
|
|
// --- BSD Editline user actions -----------------------------------------------
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
#ifdef HAVE_EDITLINE
|
|
|
|
|
2015-05-06 17:32:29 +02:00
|
|
|
static unsigned char
|
|
|
|
on_editline_complete (EditLine *editline, int key)
|
|
|
|
{
|
|
|
|
(void) key;
|
|
|
|
(void) editline;
|
|
|
|
|
2015-05-07 05:19:13 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
|
|
|
|
|
|
|
// First prepare what Readline would have normally done for us...
|
2015-05-07 07:23:11 +02:00
|
|
|
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);
|
2015-05-07 05:19:13 +02:00
|
|
|
|
|
|
|
// 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';
|
2015-05-07 07:40:58 +02:00
|
|
|
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);
|
2015-05-07 05:19:13 +02:00
|
|
|
|
|
|
|
// Insert the best match instead
|
|
|
|
el_insertstr (editline, completions[0]);
|
2021-10-30 08:28:05 +02:00
|
|
|
|
|
|
|
// I'm not sure if Readline's menu-complete can at all be implemented
|
|
|
|
// with Editline--we have no way of detecting what the last executed handler
|
|
|
|
// was. Employ the formatter's wrapping feature to spew all options.
|
2015-05-07 07:40:58 +02:00
|
|
|
bool only_match = !completions[1];
|
2021-10-30 08:28:05 +02:00
|
|
|
if (!only_match)
|
|
|
|
{
|
|
|
|
CALL (ctx->input, hide);
|
|
|
|
redraw_screen (ctx);
|
|
|
|
|
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
|
|
|
for (char **p = completions; *++p; )
|
|
|
|
formatter_add (&f, " #l", *p);
|
|
|
|
formatter_add (&f, "\n");
|
|
|
|
formatter_flush (&f, stdout, 0);
|
|
|
|
formatter_free (&f);
|
|
|
|
|
|
|
|
CALL (ctx->input, show);
|
|
|
|
}
|
|
|
|
|
2015-05-07 05:19:13 +02:00
|
|
|
for (char **p = completions; *p; p++)
|
|
|
|
free (*p);
|
|
|
|
free (completions);
|
2015-05-07 07:40:58 +02:00
|
|
|
if (!only_match)
|
|
|
|
return CC_REFRESH_BEEP;
|
|
|
|
|
2021-10-30 08:28:05 +02:00
|
|
|
// If there actually is just one match, finish the word
|
2015-05-07 07:40:58 +02:00
|
|
|
el_insertstr (editline, " ");
|
|
|
|
return CC_REFRESH;
|
2015-05-06 17:32:29 +02:00
|
|
|
}
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static unsigned char
|
|
|
|
on_editline_return (EditLine *editline, int key)
|
|
|
|
{
|
|
|
|
(void) key;
|
2015-05-07 18:39:43 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = (struct input_el *) ctx->input;
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
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;
|
2016-03-07 01:08:52 +01:00
|
|
|
history_w (self->current->history, &ev, H_ENTER, line);
|
2015-05-05 08:46:59 +02:00
|
|
|
print_debug ("history: %d %ls", ev.num, ev.str);
|
|
|
|
}
|
|
|
|
free (line);
|
|
|
|
|
2015-05-07 07:23:11 +02:00
|
|
|
// process_input() expects a multibyte string
|
|
|
|
const LineInfo *info_mb = el_line (editline);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_append_owned (&ctx->pending_input,
|
2016-01-07 22:12:29 +01:00
|
|
|
xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
|
|
|
|
poller_idle_set (&ctx->input_event);
|
2015-05-07 07:23:11 +02:00
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
el_cursor (editline, len - point);
|
|
|
|
el_wdeletestr (editline, len);
|
|
|
|
return CC_REFRESH;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
app_editline_init (struct input_el *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-01-01 01:17:56 +01:00
|
|
|
// el_set() leaks memory in 20150325 and other versions, we need wchar_t
|
2016-03-06 17:59:45 +01:00
|
|
|
el_wset (self->editline, EL_ADDFN,
|
|
|
|
L"send-line", L"Send line", on_editline_return);
|
|
|
|
el_wset (self->editline, EL_ADDFN,
|
|
|
|
L"complete", L"Complete word", on_editline_complete);
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *input = &self->super;
|
2016-03-10 00:06:28 +01:00
|
|
|
input->add_functions (input->user_data);
|
|
|
|
bind_common_keys (g_ctx);
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// Move native history commands
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (input, bind_meta, 'p', "ed-prev-history");
|
|
|
|
CALL_ (input, bind_meta, 'n', "ed-next-history");
|
2015-05-07 19:48:10 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// No, editline, it's not supposed to kill the entire line
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (input, bind_control, 'w', "ed-delete-prev-word");
|
2015-05-08 06:23:38 +02:00
|
|
|
// Just what are you doing?
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (input, bind_control, 'u', "vi-kill-line-prev");
|
2021-10-28 08:18:15 +02:00
|
|
|
// See input_el__redisplay(), functionally important
|
|
|
|
CALL_ (input, bind_control, 'q', "ed-redisplay");
|
2015-05-08 05:03:36 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// We need to hide the prompt and input first
|
2021-10-28 08:49:01 +02:00
|
|
|
CALL_ (input, bind, "\r", "send-line");
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (input, bind, "\n", "send-line");
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (input, bind_control, 'i', "complete");
|
2015-05-06 17:32:29 +02:00
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
// Source the user's defaults file
|
|
|
|
el_source (self->editline, NULL);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#endif // HAVE_EDITLINE
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// --- Configuration loading ---------------------------------------------------
|
|
|
|
|
2015-07-09 23:55:41 +02:00
|
|
|
static const char *g_first_time_help[] =
|
|
|
|
{
|
|
|
|
"",
|
2021-08-06 16:12:15 +02:00
|
|
|
"\x02Welcome to xC!",
|
2015-07-09 23:55:41 +02:00
|
|
|
"",
|
2015-10-01 21:05:32 +02:00
|
|
|
"To get a list of all commands, type \x02/help\x02. To obtain",
|
2015-07-09 23:55:41 +02:00
|
|
|
"more information on a command or option, simply add it as",
|
2022-08-26 03:43:32 +02:00
|
|
|
"a parameter, e.g. \x02/help set\x02 or \x02/help general.logging\x02.",
|
2015-07-09 23:55:41 +02:00
|
|
|
"",
|
|
|
|
"To switch between buffers, press \x02"
|
2015-10-01 21:05:32 +02:00
|
|
|
"F5/Ctrl-P\x02 or \x02" "F6/Ctrl-N\x02.",
|
2015-07-09 23:55:41 +02:00
|
|
|
"",
|
|
|
|
"Finally, adding a network is as simple as:",
|
2021-08-29 15:07:09 +02:00
|
|
|
" - \x02/server add IRCnet\x02",
|
|
|
|
" - \x02/set servers.IRCnet.addresses = \"open.ircnet.net\"\x02",
|
|
|
|
" - \x02/connect IRCnet\x02",
|
2015-07-09 23:55:41 +02:00
|
|
|
"",
|
|
|
|
"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]);
|
|
|
|
}
|
|
|
|
|
2015-07-10 00:54:54 +02:00
|
|
|
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)
|
|
|
|
{
|
2015-07-10 01:44:02 +02:00
|
|
|
struct str_map *aliases = get_aliases_config (ctx);
|
2015-07-10 00:54:54 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
|
|
|
|
{
|
|
|
|
const char **pair = g_default_aliases[i];
|
2015-07-11 03:27:17 +02:00
|
|
|
str_map_set (aliases, pair[0], config_item_string_from_cstr (pair[1]));
|
2015-07-10 00:54:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-02 23:49:38 +02:00
|
|
|
static void
|
|
|
|
load_configuration (struct app_context *ctx)
|
|
|
|
{
|
2016-07-23 20:00:40 +02:00
|
|
|
// In theory, we could ensure that only one instance is running by locking
|
|
|
|
// the configuration file and ensuring here that it exists. This is
|
|
|
|
// however brittle, as it may be unlinked without the application noticing.
|
|
|
|
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *root = NULL;
|
2015-05-02 23:49:38 +02:00
|
|
|
struct error *e = NULL;
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-11 17:54:38 +02:00
|
|
|
char *filename = resolve_filename
|
|
|
|
(PROGRAM_NAME ".conf", resolve_relative_config_filename);
|
2015-05-02 23:49:38 +02:00
|
|
|
if (filename)
|
2016-10-11 10:52:49 +02:00
|
|
|
root = config_read_from_file (filename, &e);
|
2015-05-02 23:49:38 +02:00
|
|
|
else
|
2015-07-05 15:57:53 +02:00
|
|
|
log_global_error (ctx, "Configuration file not found");
|
2015-05-02 23:49:38 +02:00
|
|
|
free (filename);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-02 23:49:38 +02:00
|
|
|
if (e)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2016-10-11 10:52:49 +02:00
|
|
|
log_global_error (ctx, "Cannot load configuration: #s", e->message);
|
2016-07-23 20:00:40 +02:00
|
|
|
log_global_error (ctx,
|
2016-10-29 21:08:15 +02:00
|
|
|
"Please either fix the configuration file or remove it");
|
2015-05-02 23:49:38 +02:00
|
|
|
error_free (e);
|
2016-07-23 20:00:40 +02:00
|
|
|
exit (EXIT_FAILURE);
|
2015-05-02 23:49:38 +02:00
|
|
|
}
|
2015-07-09 23:55:41 +02:00
|
|
|
|
2015-07-05 15:57:53 +02:00
|
|
|
if (root)
|
|
|
|
{
|
|
|
|
config_load (&ctx->config, root);
|
|
|
|
log_global_status (ctx, "Configuration loaded");
|
|
|
|
}
|
2015-07-09 23:55:41 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
show_first_time_help (ctx);
|
2015-07-10 00:54:54 +02:00
|
|
|
load_default_aliases (ctx);
|
2015-07-09 23:55:41 +02:00
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-07-05 01:11:20 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
2015-07-05 15:57:53 +02:00
|
|
|
load_servers (struct app_context *ctx)
|
2015-07-05 01:11:20 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (get_servers_config (ctx));
|
2015-07-05 01:11:20 +02:00
|
|
|
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *subtree;
|
2015-07-05 20:32:48 +02:00
|
|
|
while ((subtree = str_map_iter_next (&iter)))
|
2015-07-05 01:11:20 +02:00
|
|
|
{
|
2015-07-05 20:32:48 +02:00
|
|
|
const char *name = iter.link->key;
|
2015-07-14 07:49:28 +02:00
|
|
|
const char *err;
|
2015-07-05 20:32:48 +02:00
|
|
|
if (subtree->type != CONFIG_ITEM_OBJECT)
|
2015-07-05 01:11:20 +02:00
|
|
|
log_global_error (ctx, "Error in configuration: "
|
2015-07-05 20:32:48 +02:00
|
|
|
"ignoring server `#s' as it's not an object", name);
|
2015-07-14 07:49:28 +02:00
|
|
|
else if ((err = check_server_name_for_addition (ctx, name)))
|
|
|
|
log_global_error (ctx, "Cannot load server `#s': #s", name, err);
|
|
|
|
else
|
2015-07-05 20:32:48 +02:00
|
|
|
server_add (ctx, name, subtree);
|
2015-07-05 01:11:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-09 06:00:40 +02:00
|
|
|
// --- 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
|
2015-08-08 19:36:34 +02:00
|
|
|
postpone_signal_handling (char id)
|
2015-05-09 06:00:40 +02:00
|
|
|
{
|
|
|
|
int original_errno = errno;
|
2015-08-08 19:36:34 +02:00
|
|
|
if (write (g_signal_pipe[1], &id, 1) == -1)
|
2015-05-09 06:00:40 +02:00
|
|
|
soft_assert (errno == EAGAIN);
|
|
|
|
errno = original_errno;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2015-08-08 19:36:34 +02:00
|
|
|
signal_superhandler (int signum)
|
2015-05-09 06:00:40 +02:00
|
|
|
{
|
2015-08-08 19:36:34 +02:00
|
|
|
switch (signum)
|
|
|
|
{
|
|
|
|
case SIGWINCH:
|
|
|
|
g_winch_received = true;
|
|
|
|
postpone_signal_handling ('w');
|
|
|
|
break;
|
|
|
|
case SIGINT:
|
|
|
|
case SIGTERM:
|
|
|
|
g_termination_requested = true;
|
|
|
|
postpone_signal_handling ('t');
|
|
|
|
break;
|
|
|
|
case SIGCHLD:
|
|
|
|
postpone_signal_handling ('c');
|
|
|
|
break;
|
2015-11-15 15:36:03 +01:00
|
|
|
case SIGTSTP:
|
|
|
|
postpone_signal_handling ('s');
|
|
|
|
break;
|
2015-08-08 19:36:34 +02:00
|
|
|
default:
|
|
|
|
hard_assert (!"unhandled signal");
|
|
|
|
}
|
2015-05-09 06:00:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
// So that we can write to the terminal while we're running a backlog
|
|
|
|
// helper. This is also inherited by the child so that it doesn't stop
|
|
|
|
// when it calls tcsetpgrp().
|
|
|
|
signal (SIGTTOU, SIG_IGN);
|
|
|
|
|
2015-05-09 06:00:40 +02:00
|
|
|
struct sigaction sa;
|
|
|
|
sa.sa_flags = SA_RESTART;
|
2015-08-08 19:36:34 +02:00
|
|
|
sa.sa_handler = signal_superhandler;
|
2015-05-09 06:00:40 +02:00
|
|
|
sigemptyset (&sa.sa_mask);
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
if (sigaction (SIGWINCH, &sa, NULL) == -1
|
|
|
|
|| sigaction (SIGINT, &sa, NULL) == -1
|
|
|
|
|| sigaction (SIGTERM, &sa, NULL) == -1
|
2015-11-15 15:36:03 +01:00
|
|
|
|| sigaction (SIGTSTP, &sa, NULL) == -1
|
2015-08-08 19:36:34 +02:00
|
|
|
|| sigaction (SIGCHLD, &sa, NULL) == -1)
|
2015-05-09 06:00:40 +02:00
|
|
|
exit_fatal ("sigaction: %s", strerror (errno));
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- I/O event handlers ------------------------------------------------------
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
static bool
|
|
|
|
try_reap_child (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
int status;
|
|
|
|
pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED);
|
|
|
|
|
|
|
|
if (zombie == -1)
|
|
|
|
{
|
|
|
|
if (errno == ECHILD) return false;
|
|
|
|
if (errno == EINTR) return true;
|
|
|
|
exit_fatal ("%s: %s", "waitpid", strerror (errno));
|
|
|
|
}
|
|
|
|
if (!zombie)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (WIFSTOPPED (status))
|
|
|
|
{
|
|
|
|
// We could also send SIGCONT but what's the point
|
2015-12-25 04:21:18 +01:00
|
|
|
print_debug ("a child has been stopped, killing its process group");
|
2015-08-08 19:36:34 +02:00
|
|
|
kill (-zombie, SIGKILL);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
if (ctx->running_pager)
|
|
|
|
ctx->running_pager = false;
|
2015-12-25 04:21:18 +01:00
|
|
|
else if (!ctx->running_editor)
|
|
|
|
{
|
2016-01-07 22:47:57 +01:00
|
|
|
log_global_debug (ctx, "An unknown child has died");
|
2015-12-25 04:21:18 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
|
|
|
|
resume_terminal (ctx);
|
|
|
|
|
|
|
|
if (WIFSIGNALED (status))
|
|
|
|
log_global_error (ctx,
|
|
|
|
"Child died from signal #d", WTERMSIG (status));
|
|
|
|
else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
|
|
|
|
log_global_error (ctx,
|
|
|
|
"Child returned status #d", WEXITSTATUS (status));
|
2015-12-25 04:21:18 +01:00
|
|
|
else if (ctx->running_editor)
|
2016-01-07 22:47:57 +01:00
|
|
|
input_editor_process (ctx);
|
|
|
|
|
|
|
|
if (ctx->running_editor)
|
|
|
|
input_editor_cleanup (ctx);
|
2015-08-08 19:36:34 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
static void
|
|
|
|
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
|
|
|
|
{
|
2015-11-15 15:36:03 +01:00
|
|
|
char id = 0;
|
|
|
|
(void) read (fd->fd, &id, 1);
|
|
|
|
|
|
|
|
// Stop ourselves cleanly, even if it makes little sense to do this
|
|
|
|
if (id == 's')
|
|
|
|
{
|
|
|
|
suspend_terminal (ctx);
|
|
|
|
kill (getpid (), SIGSTOP);
|
|
|
|
g_winch_received = true;
|
|
|
|
resume_terminal (ctx);
|
|
|
|
}
|
2015-05-16 12:33:59 +02:00
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
// Reap all dead children (since the signal pipe may overflow etc. we run
|
|
|
|
// waitpid() in a loop to return all the zombies it knows about).
|
|
|
|
while (try_reap_child (ctx))
|
|
|
|
;
|
|
|
|
|
2020-11-01 15:32:39 +01:00
|
|
|
if (g_termination_requested)
|
|
|
|
{
|
|
|
|
g_termination_requested = false;
|
|
|
|
request_quit (ctx, NULL);
|
|
|
|
}
|
2015-05-09 06:00:40 +02:00
|
|
|
if (g_winch_received)
|
|
|
|
{
|
2015-05-10 06:44:34 +02:00
|
|
|
g_winch_received = false;
|
2020-11-01 15:32:39 +01:00
|
|
|
redraw_screen (ctx);
|
2015-05-09 06:00:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-12 03:48:52 +02:00
|
|
|
static void
|
2021-08-29 12:07:31 +02:00
|
|
|
process_formatting_escape (const struct pollfd *fd, struct app_context *ctx)
|
2015-05-12 03:48:52 +02:00
|
|
|
{
|
|
|
|
// There's no other way with libedit, as both el_getc() in a function
|
|
|
|
// handler and CC_ARGHACK would block execution
|
2015-11-15 01:23:32 +01:00
|
|
|
struct str *buf = &ctx->input_buffer;
|
2017-01-23 23:50:27 +01:00
|
|
|
str_reserve (buf, 1);
|
2015-11-15 01:23:32 +01:00
|
|
|
if (read (fd->fd, buf->str + buf->len, 1) != 1)
|
2015-05-12 03:48:52 +02:00
|
|
|
goto error;
|
2015-11-15 01:23:32 +01:00
|
|
|
buf->str[++buf->len] = '\0';
|
2015-05-12 03:48:52 +02:00
|
|
|
|
2018-01-08 21:53:42 +01:00
|
|
|
// XXX: I think this should be global and shared with Readline/libedit
|
2015-05-12 03:48:52 +02:00
|
|
|
mbstate_t state;
|
|
|
|
memset (&state, 0, sizeof state);
|
|
|
|
|
2015-11-15 01:23:32 +01:00
|
|
|
size_t len = mbrlen (buf->str, buf->len, &state);
|
2015-05-12 03:48:52 +02:00
|
|
|
|
|
|
|
// Illegal sequence
|
|
|
|
if (len == (size_t) -1)
|
|
|
|
goto error;
|
|
|
|
|
|
|
|
// Incomplete multibyte character
|
|
|
|
if (len == (size_t) -2)
|
|
|
|
return;
|
|
|
|
|
2015-11-15 01:23:32 +01:00
|
|
|
if (buf->len != 1)
|
2015-05-12 03:48:52 +02:00
|
|
|
goto error;
|
2015-11-15 01:23:32 +01:00
|
|
|
switch (buf->str[0])
|
2015-05-12 03:48:52 +02:00
|
|
|
{
|
2016-10-23 13:40:04 +02:00
|
|
|
case 'b' ^ 96:
|
2016-03-07 01:08:52 +01:00
|
|
|
case 'b': CALL_ (ctx->input, insert, "\x02"); break;
|
|
|
|
case 'c': CALL_ (ctx->input, insert, "\x03"); break;
|
2016-10-23 13:40:04 +02:00
|
|
|
case 'i' ^ 96:
|
2015-05-12 03:48:52 +02:00
|
|
|
case 'i':
|
2016-03-07 01:08:52 +01:00
|
|
|
case ']': CALL_ (ctx->input, insert, "\x1d"); break;
|
2020-10-11 17:58:35 +02:00
|
|
|
case 'x' ^ 96:
|
|
|
|
case 'x':
|
|
|
|
case '^': CALL_ (ctx->input, insert, "\x1e"); break;
|
2016-10-23 13:40:04 +02:00
|
|
|
case 'u' ^ 96:
|
2015-05-12 03:48:52 +02:00
|
|
|
case 'u':
|
2016-03-07 01:08:52 +01:00
|
|
|
case '_': CALL_ (ctx->input, insert, "\x1f"); break;
|
|
|
|
case 'v': CALL_ (ctx->input, insert, "\x16"); break;
|
|
|
|
case 'o': CALL_ (ctx->input, insert, "\x0f"); break;
|
2015-05-12 03:48:52 +02:00
|
|
|
|
|
|
|
default:
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
goto done;
|
|
|
|
|
|
|
|
error:
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, ding);
|
2015-05-12 03:48:52 +02:00
|
|
|
done:
|
2015-11-15 01:23:32 +01:00
|
|
|
str_reset (buf);
|
2021-08-29 12:07:31 +02:00
|
|
|
ctx->awaiting_formatting_escape = false;
|
2015-05-12 03:48:52 +02:00
|
|
|
}
|
|
|
|
|
2015-11-15 01:03:29 +01:00
|
|
|
#define BRACKETED_PASTE_LIMIT 102400 ///< How much text can be pasted
|
|
|
|
|
2021-10-30 04:18:52 +02:00
|
|
|
static bool
|
|
|
|
insert_paste (struct app_context *ctx, char *paste, size_t len)
|
|
|
|
{
|
2022-08-26 03:43:32 +02:00
|
|
|
if (!get_config_boolean (ctx->config.root, "general.process_pasted_text"))
|
2021-10-30 04:18:52 +02:00
|
|
|
return CALL_ (ctx->input, insert, paste);
|
|
|
|
|
|
|
|
// Without ICRNL, which Editline keeps but Readline doesn't,
|
|
|
|
// the terminal sends newlines as carriage returns (seen on urxvt)
|
|
|
|
for (size_t i = 0; i < len; i++)
|
|
|
|
if (paste[i] == '\r')
|
|
|
|
paste[i] = '\n';
|
|
|
|
|
|
|
|
int position = 0;
|
|
|
|
char *input = CALL_ (ctx->input, get_line, &position);
|
|
|
|
bool quote_first_slash = !position || strchr ("\r\n", input[position - 1]);
|
|
|
|
free (input);
|
|
|
|
|
|
|
|
// Executing commands by accident is much more common than pasting them
|
|
|
|
// intentionally, although the latter may also have security consequences
|
|
|
|
struct str processed = str_make ();
|
|
|
|
str_reserve (&processed, len);
|
|
|
|
for (size_t i = 0; i < len; i++)
|
|
|
|
{
|
|
|
|
if (paste[i] == '/'
|
|
|
|
&& ((!i && quote_first_slash) || (i && paste[i - 1] == '\n')))
|
|
|
|
str_append_c (&processed, paste[i]);
|
|
|
|
str_append_c (&processed, paste[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool success = CALL_ (ctx->input, insert, processed.str);
|
|
|
|
str_free (&processed);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
2015-11-15 01:03:29 +01:00
|
|
|
static void
|
|
|
|
process_bracketed_paste (const struct pollfd *fd, struct app_context *ctx)
|
|
|
|
{
|
2015-11-15 01:23:32 +01:00
|
|
|
struct str *buf = &ctx->input_buffer;
|
2017-01-23 23:50:27 +01:00
|
|
|
str_reserve (buf, 1);
|
2015-11-15 01:03:29 +01:00
|
|
|
if (read (fd->fd, buf->str + buf->len, 1) != 1)
|
|
|
|
goto error;
|
|
|
|
buf->str[++buf->len] = '\0';
|
|
|
|
|
|
|
|
static const char stop_mark[] = "\x1b[201~";
|
|
|
|
static const size_t stop_mark_len = sizeof stop_mark - 1;
|
|
|
|
if (buf->len < stop_mark_len)
|
|
|
|
return;
|
|
|
|
|
|
|
|
size_t text_len = buf->len - stop_mark_len;
|
|
|
|
if (memcmp (buf->str + text_len, stop_mark, stop_mark_len))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Avoid endless flooding of the buffer
|
|
|
|
if (text_len > BRACKETED_PASTE_LIMIT)
|
2018-06-21 22:02:26 +02:00
|
|
|
log_global_error (ctx, "Paste trimmed to #d bytes",
|
|
|
|
(int) (text_len = BRACKETED_PASTE_LIMIT));
|
2015-11-15 01:03:29 +01:00
|
|
|
|
|
|
|
buf->str[text_len] = '\0';
|
2021-10-30 04:18:52 +02:00
|
|
|
if (insert_paste (ctx, buf->str, text_len))
|
2015-11-15 01:03:29 +01:00
|
|
|
goto done;
|
|
|
|
|
|
|
|
error:
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, ding);
|
2015-11-15 01:03:29 +01:00
|
|
|
log_global_error (ctx, "Paste failed");
|
|
|
|
done:
|
|
|
|
str_reset (buf);
|
|
|
|
ctx->in_bracketed_paste = false;
|
|
|
|
}
|
|
|
|
|
2015-05-09 06:00:40 +02:00
|
|
|
static void
|
2015-11-22 23:04:51 +01:00
|
|
|
reset_autoaway (struct app_context *ctx)
|
2015-05-09 06:00:40 +02:00
|
|
|
{
|
2015-11-22 23:04:51 +01:00
|
|
|
// Stop the last one if it's been disabled altogether in the meantime
|
|
|
|
poller_timer_reset (&ctx->autoaway_tmr);
|
|
|
|
|
|
|
|
// Unset any automated statuses that are active right at this moment
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
2015-11-22 23:04:51 +01:00
|
|
|
struct server *s;
|
|
|
|
while ((s = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
if (s->autoaway_active
|
|
|
|
&& s->irc_user
|
|
|
|
&& s->irc_user->away)
|
|
|
|
irc_send (s, "AWAY");
|
2015-05-09 06:00:40 +02:00
|
|
|
|
2015-11-22 23:04:51 +01:00
|
|
|
s->autoaway_active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// And potentially start a new auto-away timer
|
|
|
|
int64_t delay = get_config_integer
|
2022-08-26 03:43:32 +02:00
|
|
|
(ctx->config.root, "general.autoaway_delay");
|
2015-11-22 23:04:51 +01:00
|
|
|
if (delay)
|
|
|
|
poller_timer_set (&ctx->autoaway_tmr, delay * 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_autoaway_timer (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
// An empty message would unset any away status, so let's ignore that
|
|
|
|
const char *message = get_config_string
|
2022-08-26 03:43:32 +02:00
|
|
|
(ctx->config.root, "general.autoaway_message");
|
2015-11-22 23:04:51 +01:00
|
|
|
if (!message || !*message)
|
|
|
|
return;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
2015-11-22 23:04:51 +01:00
|
|
|
struct server *s;
|
|
|
|
while ((s = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
// If the user has already been marked as away,
|
|
|
|
// don't override his current away status
|
|
|
|
if (s->irc_user
|
|
|
|
&& s->irc_user->away)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
irc_send (s, "AWAY :%s", message);
|
|
|
|
s->autoaway_active = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
|
|
|
|
{
|
2015-05-09 06:00:40 +02:00
|
|
|
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
|
|
|
|
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
|
|
|
|
|
2021-08-29 12:07:31 +02:00
|
|
|
if (ctx->awaiting_formatting_escape)
|
|
|
|
process_formatting_escape (fd, ctx);
|
2015-11-15 01:03:29 +01:00
|
|
|
else if (ctx->in_bracketed_paste)
|
|
|
|
process_bracketed_paste (fd, ctx);
|
2015-12-31 23:42:22 +01:00
|
|
|
else if (!ctx->quitting)
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, on_tty_readable);
|
2015-11-22 23:04:51 +01:00
|
|
|
|
|
|
|
// User activity detected, stop current auto-away and start anew;
|
|
|
|
// since they might have just changed the settings, do this last
|
|
|
|
reset_autoaway (ctx);
|
2015-05-09 06:00:40 +02:00
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-04 15:43:45 +02:00
|
|
|
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);
|
2018-01-07 05:54:45 +01:00
|
|
|
|
|
|
|
// It would be a bit problematic to handle it properly, so do this at least
|
|
|
|
LIST_FOR_EACH (struct buffer, buffer, ctx->buffers)
|
|
|
|
{
|
|
|
|
if (!buffer->log_file || !ferror (buffer->log_file))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Might be a transient error such as running out of disk space,
|
|
|
|
// keep notifying of the problem until it disappears
|
|
|
|
clearerr (buffer->log_file);
|
|
|
|
log_global (ctx, BUFFER_LINE_ERROR | BUFFER_LINE_SKIP_FILE,
|
|
|
|
"Log write failure detected for #s", buffer->name);
|
|
|
|
}
|
|
|
|
|
2020-10-20 02:02:09 +02:00
|
|
|
#ifdef LOMEM
|
|
|
|
// Lua should normally be reasonable and collect garbage when needed,
|
|
|
|
// though we can try to push it. This is a reasonable place.
|
|
|
|
LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
|
|
|
|
if (iter->vtable->gc)
|
|
|
|
iter->vtable->gc (iter);
|
|
|
|
#endif // LOMEM
|
|
|
|
|
2015-07-04 15:43:45 +02:00
|
|
|
rearm_flush_timer (ctx);
|
|
|
|
}
|
|
|
|
|
2015-07-11 06:10:46 +02:00
|
|
|
static void
|
|
|
|
rearm_date_change_timer (struct app_context *ctx)
|
|
|
|
{
|
2015-07-14 06:43:15 +02:00
|
|
|
struct tm tm_;
|
2015-07-12 00:08:56 +02:00
|
|
|
const time_t now = time (NULL);
|
2015-07-14 06:43:15 +02:00
|
|
|
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;
|
2015-07-12 00:08:56 +02:00
|
|
|
poller_timer_set (&ctx->date_chg_tmr, (midnight - now) * 1000);
|
2015-07-11 06:10:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
on_date_change_timer (struct app_context *ctx)
|
|
|
|
{
|
2015-12-24 16:02:40 +01:00
|
|
|
if (ctx->terminal_suspended <= 0)
|
2016-04-10 22:13:53 +02:00
|
|
|
{
|
|
|
|
CALL (ctx->input, hide);
|
|
|
|
buffer_update_time (ctx, time (NULL), stdout, 0);
|
|
|
|
CALL (ctx->input, show);
|
|
|
|
}
|
2015-07-11 06:10:46 +02:00
|
|
|
rearm_date_change_timer (ctx);
|
|
|
|
}
|
|
|
|
|
2016-01-07 22:12:29 +01:00
|
|
|
static void
|
|
|
|
on_pending_input (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
poller_idle_reset (&ctx->input_event);
|
|
|
|
for (size_t i = 0; i < ctx->pending_input.len; i++)
|
|
|
|
process_input (ctx, ctx->pending_input.vector[i]);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_reset (&ctx->pending_input);
|
2016-01-07 22:12:29 +01:00
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
|
|
|
init_poller_events (struct app_context *ctx)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->signal_event = poller_fd_make (&ctx->poller, g_signal_pipe[0]);
|
2015-04-12 04:52:39 +02:00
|
|
|
ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
|
2015-04-18 03:25:10 +02:00
|
|
|
ctx->signal_event.user_data = ctx;
|
2015-04-12 04:52:39 +02:00
|
|
|
poller_fd_set (&ctx->signal_event, POLLIN);
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->tty_event = poller_fd_make (&ctx->poller, STDIN_FILENO);
|
2015-04-12 04:52:39 +02:00
|
|
|
ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
|
2015-05-05 05:15:08 +02:00
|
|
|
ctx->tty_event.user_data = ctx;
|
2015-04-12 04:52:39 +02:00
|
|
|
poller_fd_set (&ctx->tty_event, POLLIN);
|
2015-07-04 15:43:45 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->flush_timer = poller_timer_make (&ctx->poller);
|
2015-07-04 15:43:45 +02:00
|
|
|
ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
|
|
|
|
ctx->flush_timer.user_data = ctx;
|
|
|
|
rearm_flush_timer (ctx);
|
2015-07-11 06:10:46 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->date_chg_tmr = poller_timer_make (&ctx->poller);
|
2015-07-13 02:39:28 +02:00
|
|
|
ctx->date_chg_tmr.dispatcher = (poller_timer_fn) on_date_change_timer;
|
|
|
|
ctx->date_chg_tmr.user_data = ctx;
|
2015-07-11 06:10:46 +02:00
|
|
|
rearm_date_change_timer (ctx);
|
2015-11-22 23:04:51 +01:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->autoaway_tmr = poller_timer_make (&ctx->poller);
|
2015-11-22 23:04:51 +01:00
|
|
|
ctx->autoaway_tmr.dispatcher = (poller_timer_fn) on_autoaway_timer;
|
|
|
|
ctx->autoaway_tmr.user_data = ctx;
|
2016-01-07 22:12:29 +01:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->prompt_event = poller_idle_make (&ctx->poller);
|
2016-10-23 17:34:52 +02:00
|
|
|
ctx->prompt_event.dispatcher = (poller_idle_fn) on_refresh_prompt;
|
|
|
|
ctx->prompt_event.user_data = ctx;
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
ctx->input_event = poller_idle_make (&ctx->poller);
|
2016-01-07 22:12:29 +01:00
|
|
|
ctx->input_event.dispatcher = (poller_idle_fn) on_pending_input;
|
|
|
|
ctx->input_event.user_data = ctx;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
2015-12-08 23:38:02 +01:00
|
|
|
// --- Tests -------------------------------------------------------------------
|
|
|
|
|
2015-12-10 20:01:41 +01:00
|
|
|
// The application is quite monolithic and can only be partially unit-tested.
|
|
|
|
// Locale-, terminal- and filesystem-dependent tests are also somewhat tricky.
|
2015-12-08 23:38:02 +01:00
|
|
|
|
|
|
|
#ifdef TESTING
|
|
|
|
|
2015-12-10 21:21:21 +01:00
|
|
|
static struct config_schema g_config_test[] =
|
|
|
|
{
|
2020-10-12 04:08:09 +02:00
|
|
|
{ .name = "foo", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" },
|
|
|
|
{ .name = "bar", .type = CONFIG_ITEM_INTEGER, .default_ = "1" },
|
|
|
|
{ .name = "foobar", .type = CONFIG_ITEM_STRING, .default_ = "\"x\\x01\"" },
|
2015-12-10 21:21:21 +01:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
test_config (void)
|
|
|
|
{
|
2020-10-12 04:08:09 +02:00
|
|
|
struct config_item *foo = config_item_object ();
|
|
|
|
config_schema_apply_to_object (g_config_test, foo, NULL);
|
|
|
|
struct config_item *root = config_item_object ();
|
|
|
|
str_map_set (&root->value.object, "top", foo);
|
2015-12-10 21:21:21 +01:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2020-10-12 04:08:09 +02:00
|
|
|
dump_matching_options (root, "*foo*", &v);
|
2015-12-10 21:21:21 +01:00
|
|
|
hard_assert (v.len == 2);
|
|
|
|
hard_assert (!strcmp (v.vector[0], "top.foo = off"));
|
2020-10-12 04:08:09 +02:00
|
|
|
hard_assert (!strcmp (v.vector[1], "top.foobar = \"x\\x01\""));
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-12-10 21:21:21 +01:00
|
|
|
|
2020-10-12 04:08:09 +02:00
|
|
|
config_item_destroy (root);
|
2015-12-10 21:21:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
2015-12-10 20:01:41 +01:00
|
|
|
static void
|
|
|
|
test_aliases (void)
|
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2015-12-10 20:01:41 +01:00
|
|
|
expand_alias_definition ("/foo; /bar $* $$$;;;$1$2$3$4", "foo bar baz", &v);
|
|
|
|
hard_assert (v.len == 4);
|
|
|
|
hard_assert (!strcmp (v.vector[0], "/foo"));
|
|
|
|
hard_assert (!strcmp (v.vector[1], " /bar foo bar baz $;"));
|
|
|
|
hard_assert (!strcmp (v.vector[2], ""));
|
|
|
|
hard_assert (!strcmp (v.vector[3], "foobarbaz"));
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-12-10 20:01:41 +01:00
|
|
|
}
|
|
|
|
|
2015-12-08 23:38:02 +01:00
|
|
|
static void
|
|
|
|
test_wrapping (void)
|
|
|
|
{
|
2021-08-28 14:44:38 +02:00
|
|
|
static const char *message = " foo bar foobar fóóbárbáz\002 a\0031 b";
|
|
|
|
// XXX: formatting continuation order is implementation-dependent here
|
|
|
|
// (irc_serialize_char_attrs() makes a choice in serialization)
|
|
|
|
static const char *split[] = { " foo", "bar", "foob", "ar",
|
|
|
|
"fó", "ób", "árb", "áz\x02", "\002a\0031", "\0031\002b" };
|
2015-12-08 23:38:02 +01:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct strv v = strv_make ();
|
2015-12-08 23:38:02 +01:00
|
|
|
hard_assert (wrap_message (message, 4, &v, NULL));
|
|
|
|
hard_assert (v.len == N_ELEMENTS (split));
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (split); i++)
|
|
|
|
hard_assert (!strcmp (v.vector[i], split[i]));
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&v);
|
2015-12-08 23:38:02 +01:00
|
|
|
}
|
|
|
|
|
2015-12-10 20:01:41 +01:00
|
|
|
static void
|
2020-10-21 05:39:05 +02:00
|
|
|
test_utf8 (void)
|
2015-12-10 20:01:41 +01:00
|
|
|
{
|
|
|
|
static const char *a[] = { "fřoo", "Fřooř", "fřOOŘ" };
|
|
|
|
hard_assert (utf8_common_prefix (a, N_ELEMENTS (a)) == 5);
|
2020-10-21 05:39:05 +02:00
|
|
|
|
|
|
|
char *cut_off = xstrdup ("ё\xD0");
|
|
|
|
irc_sanitize_cut_off_utf8 (&cut_off);
|
|
|
|
hard_assert (!strcmp (cut_off, "ё\xEF\xBF\xBD"));
|
|
|
|
free (cut_off);
|
2015-12-10 20:01:41 +01:00
|
|
|
}
|
|
|
|
|
2015-12-08 23:38:02 +01:00
|
|
|
int
|
|
|
|
main (int argc, char *argv[])
|
|
|
|
{
|
|
|
|
struct test test;
|
|
|
|
test_init (&test, argc, argv);
|
2020-10-21 05:39:05 +02:00
|
|
|
test_add_simple (&test, "/config", NULL, test_config);
|
|
|
|
test_add_simple (&test, "/aliases", NULL, test_aliases);
|
|
|
|
test_add_simple (&test, "/wrapping", NULL, test_wrapping);
|
|
|
|
test_add_simple (&test, "/utf8", NULL, test_utf8);
|
2015-12-08 23:38:02 +01:00
|
|
|
return test_run (&test);
|
|
|
|
}
|
|
|
|
|
|
|
|
#define main main_shadowed
|
|
|
|
#endif // TESTING
|
|
|
|
|
2015-05-09 06:00:40 +02:00
|
|
|
// --- Main program ------------------------------------------------------------
|
|
|
|
|
2015-07-10 00:26:50 +02:00
|
|
|
static const char *g_logo[] =
|
|
|
|
{
|
2021-08-06 16:12:15 +02:00
|
|
|
"",
|
|
|
|
"\x02" PROGRAM_NAME "\x02 " PROGRAM_VERSION,
|
|
|
|
"",
|
2015-07-10 00:26:50 +02:00
|
|
|
};
|
|
|
|
|
2015-05-07 22:55:11 +02:00
|
|
|
static void
|
2015-07-10 00:26:50 +02:00
|
|
|
show_logo (struct app_context *ctx)
|
2015-05-07 22:55:11 +02:00
|
|
|
{
|
2015-07-10 00:26:50 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
|
|
|
|
log_global_indent (ctx, "#m", g_logo[i]);
|
2015-05-07 22:55:11 +02:00
|
|
|
}
|
|
|
|
|
2016-03-12 14:28:17 +01:00
|
|
|
static void
|
|
|
|
format_input_and_die (struct app_context *ctx)
|
|
|
|
{
|
|
|
|
char buf[513];
|
|
|
|
while (fgets (buf, sizeof buf, stdin))
|
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter f = formatter_make (ctx, NULL);
|
2016-03-12 14:28:17 +01:00
|
|
|
formatter_add (&f, "#m", buf);
|
2016-04-10 21:37:36 +02:00
|
|
|
formatter_flush (&f, stdout, FLUSH_OPT_NOWRAP);
|
2016-03-12 14:28:17 +01:00
|
|
|
formatter_free (&f);
|
|
|
|
}
|
|
|
|
exit (EXIT_SUCCESS);
|
|
|
|
}
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
int
|
|
|
|
main (int argc, char *argv[])
|
|
|
|
{
|
2021-08-06 16:12:15 +02:00
|
|
|
// We include a generated file from xD including this array we don't use;
|
2015-04-21 22:08:18 +02:00
|
|
|
// let's just keep it there and silence the compiler warning instead
|
|
|
|
(void) g_default_replies;
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static const struct opt opts[] =
|
|
|
|
{
|
|
|
|
{ 'h', "help", NULL, 0, "display this help and exit" },
|
|
|
|
{ 'V', "version", NULL, 0, "output version information and exit" },
|
2016-03-12 14:28:17 +01:00
|
|
|
// This is mostly intended for previewing formatted MOTD files
|
|
|
|
{ 'f', "format", NULL, OPT_LONG_ONLY, "format IRC text from stdin" },
|
2015-04-12 04:52:39 +02:00
|
|
|
{ 0, NULL, NULL, 0, NULL }
|
|
|
|
};
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct opt_handler oh =
|
2020-10-31 13:42:56 +01:00
|
|
|
opt_handler_make (argc, argv, opts, NULL, "Terminal-based IRC client.");
|
2016-03-12 14:28:17 +01:00
|
|
|
bool format_mode = false;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
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);
|
2016-03-12 14:28:17 +01:00
|
|
|
case 'f':
|
|
|
|
format_mode = true;
|
|
|
|
break;
|
2015-04-12 04:52:39 +02:00
|
|
|
default:
|
|
|
|
print_error ("wrong options");
|
|
|
|
opt_handler_usage (&oh, stderr);
|
|
|
|
exit (EXIT_FAILURE);
|
|
|
|
}
|
2016-09-29 13:40:15 +02:00
|
|
|
if (optind != argc)
|
|
|
|
{
|
|
|
|
opt_handler_usage (&oh, stderr);
|
|
|
|
exit (EXIT_FAILURE);
|
|
|
|
}
|
2015-04-12 04:52:39 +02:00
|
|
|
opt_handler_free (&oh);
|
|
|
|
|
|
|
|
// We only need to convert to and from the terminal encoding
|
|
|
|
setlocale (LC_CTYPE, "");
|
|
|
|
|
2015-04-19 23:07:23 +02:00
|
|
|
struct app_context ctx;
|
|
|
|
app_context_init (&ctx);
|
|
|
|
g_ctx = &ctx;
|
|
|
|
|
2016-12-28 12:44:27 +01:00
|
|
|
init_openssl ();
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-05 15:57:53 +02:00
|
|
|
// Bootstrap configuration, so that we can access schema items at all
|
2015-05-02 23:00:34 +02:00
|
|
|
register_config_modules (&ctx);
|
2015-07-05 15:57:53 +02:00
|
|
|
config_load (&ctx.config, config_item_object ());
|
2015-04-18 03:25:10 +02:00
|
|
|
|
2015-07-05 15:57:53 +02:00
|
|
|
// The following part is a bit brittle because of interdependencies
|
2015-04-12 04:52:39 +02:00
|
|
|
init_colors (&ctx);
|
2016-03-12 14:28:17 +01:00
|
|
|
if (format_mode) format_input_and_die (&ctx);
|
2015-07-05 01:11:20 +02:00
|
|
|
init_global_buffer (&ctx);
|
2015-07-10 00:26:50 +02:00
|
|
|
show_logo (&ctx);
|
|
|
|
setup_signal_handlers ();
|
2015-07-05 15:57:53 +02:00
|
|
|
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);
|
|
|
|
|
2015-11-15 00:57:58 +01:00
|
|
|
// Initialize input so that we can switch to new buffers
|
2016-10-23 17:34:52 +02:00
|
|
|
on_refresh_prompt (&ctx);
|
2016-03-10 00:06:28 +01:00
|
|
|
ctx.input->add_functions = input_add_functions;
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL_ (ctx.input, start, argv[0]);
|
2015-11-15 01:03:29 +01:00
|
|
|
toggle_bracketed_paste (true);
|
2015-11-22 23:04:51 +01:00
|
|
|
reset_autoaway (&ctx);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-11-15 00:57:58 +01:00
|
|
|
// Finally, we juice the configuration for some servers to create
|
2015-11-19 14:23:10 +01:00
|
|
|
load_plugins (&ctx);
|
2015-11-15 00:57:58 +01:00
|
|
|
load_servers (&ctx);
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
ctx.polling = true;
|
|
|
|
while (ctx.polling)
|
|
|
|
poller_run (&ctx.poller);
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx.input, stop);
|
2015-12-31 23:42:22 +01:00
|
|
|
|
2022-08-26 03:43:32 +02:00
|
|
|
if (get_config_boolean (ctx.config.root, "general.save_on_quit"))
|
2015-07-11 02:45:24 +02:00
|
|
|
save_configuration (&ctx);
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
app_context_free (&ctx);
|
2015-11-15 01:03:29 +01:00
|
|
|
toggle_bracketed_paste (false);
|
2015-04-12 04:52:39 +02:00
|
|
|
free_terminal ();
|
|
|
|
return EXIT_SUCCESS;
|
|
|
|
}
|