4424 lines
117 KiB
C
4424 lines
117 KiB
C
/*
|
|
* degesch.c: the experimental IRC client
|
|
*
|
|
* Copyright (c) 2015, Přemysl Janouch <p.janouch@gmail.com>
|
|
* All rights reserved.
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
/// Some arbitrary limit for the history file
|
|
#define HISTORY_LIMIT 10000
|
|
|
|
// String constants for all attributes we use for output
|
|
#define ATTR_PROMPT "attr_prompt"
|
|
#define ATTR_RESET "attr_reset"
|
|
#define ATTR_WARNING "attr_warning"
|
|
#define ATTR_ERROR "attr_error"
|
|
|
|
#define ATTR_EXTERNAL "attr_external"
|
|
#define ATTR_TIMESTAMP "attr_timestamp"
|
|
#define ATTR_ACTION "attr_action"
|
|
#define ATTR_JOIN "attr_join"
|
|
#define ATTR_PART "attr_part"
|
|
|
|
// User data for logger functions to enable formatted logging
|
|
#define print_fatal_data ATTR_ERROR
|
|
#define print_error_data ATTR_ERROR
|
|
#define print_warning_data ATTR_WARNING
|
|
|
|
#include "config.h"
|
|
#undef PROGRAM_NAME
|
|
#define PROGRAM_NAME "degesch"
|
|
|
|
#include "common.c"
|
|
#include "kike-replies.c"
|
|
|
|
#include <langinfo.h>
|
|
#include <locale.h>
|
|
#include <pwd.h>
|
|
#include <sys/utsname.h>
|
|
|
|
#include <curses.h>
|
|
#include <term.h>
|
|
|
|
// Literally cancer
|
|
#undef lines
|
|
#undef columns
|
|
|
|
#include <readline/readline.h>
|
|
#include <readline/history.h>
|
|
|
|
// --- Configuration (application-specific) ------------------------------------
|
|
|
|
// TODO: reject all junk present in the configuration; there can be newlines
|
|
|
|
static struct config_item g_config_table[] =
|
|
{
|
|
{ "nickname", NULL, "IRC nickname" },
|
|
{ "username", NULL, "IRC user name" },
|
|
{ "realname", NULL, "IRC real name/e-mail" },
|
|
|
|
{ "irc_host", NULL, "Address of the IRC server" },
|
|
{ "irc_port", "6667", "Port of the IRC server" },
|
|
{ "ssl", "off", "Whether to use SSL" },
|
|
{ "ssl_cert", NULL, "Client SSL certificate (PEM)" },
|
|
{ "ssl_verify", "on", "Whether to verify certificates" },
|
|
{ "ssl_ca_file", NULL, "OpenSSL CA bundle file" },
|
|
{ "ssl_ca_path", NULL, "OpenSSL CA bundle path" },
|
|
{ "autojoin", NULL, "Channels to join on start" },
|
|
{ "reconnect", "on", "Whether to reconnect on error" },
|
|
{ "reconnect_delay", "5", "Time between reconnecting" },
|
|
|
|
{ "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" },
|
|
{ "socks_port", "1080", "SOCKS port number" },
|
|
{ "socks_username", NULL, "SOCKS auth. username" },
|
|
{ "socks_password", NULL, "SOCKS auth. password" },
|
|
|
|
{ "isolate_buffers", "off", "Isolate global/server buffers" },
|
|
|
|
{ ATTR_PROMPT, NULL, "Terminal attributes for the prompt" },
|
|
{ ATTR_RESET, NULL, "String to reset terminal attributes" },
|
|
{ ATTR_WARNING, NULL, "Terminal attributes for warnings" },
|
|
{ ATTR_ERROR, NULL, "Terminal attributes for errors" },
|
|
|
|
{ ATTR_EXTERNAL, NULL, "Terminal attributes for external lines" },
|
|
{ ATTR_TIMESTAMP, NULL, "Terminal attributes for timestamps" },
|
|
{ ATTR_ACTION, NULL, "Terminal attributes for user actions" },
|
|
{ ATTR_JOIN, NULL, "Terminal attributes for joins" },
|
|
{ ATTR_PART, NULL, "Terminal attributes for parts" },
|
|
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
// --- Application data --------------------------------------------------------
|
|
|
|
// All text stored in our data structures is encoded in UTF-8.
|
|
// Or at least should be. The exception is IRC identifiers.
|
|
|
|
static bool
|
|
isdigit_ascii (int c)
|
|
{
|
|
return c >= '0' && c <= '9';
|
|
}
|
|
|
|
static int
|
|
toupper_ascii (int c)
|
|
{
|
|
return c >= 'A' && c <= 'Z' ? c : c - ('a' - 'A');
|
|
}
|
|
|
|
/// Shorthand to set an error and return failure from the function
|
|
#define FAIL(...) \
|
|
BLOCK_START \
|
|
error_set (e, __VA_ARGS__); \
|
|
return false; \
|
|
BLOCK_END
|
|
|
|
// A few other debugging shorthands
|
|
#define LOG_FUNC_FAILURE(name, desc) \
|
|
print_debug ("%s: %s: %s", __func__, (name), (desc))
|
|
#define LOG_LIBC_FAILURE(name) \
|
|
print_debug ("%s: %s: %s", __func__, (name), strerror (errno))
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// We need a few reference countable objects with support
|
|
// for both strong and weak references
|
|
|
|
/// Callback just before a reference counted object is destroyed
|
|
typedef void (*destroy_cb_fn) (void *object, void *user_data);
|
|
|
|
#define REF_COUNTABLE_HEADER \
|
|
size_t ref_count; /**< Reference count */ \
|
|
destroy_cb_fn on_destroy; /**< To remove any weak references */ \
|
|
void *user_data; /**< User data for callbacks */
|
|
|
|
#define REF_COUNTABLE_METHODS(name) \
|
|
static struct name * \
|
|
name ## _ref (struct name *self) \
|
|
{ \
|
|
self->ref_count++; \
|
|
return self; \
|
|
} \
|
|
\
|
|
static void \
|
|
name ## _unref (struct name *self) \
|
|
{ \
|
|
if (--self->ref_count) \
|
|
return; \
|
|
if (self->on_destroy) \
|
|
self->on_destroy (self, self->user_data); \
|
|
name ## _destroy (self); \
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct user_channel
|
|
{
|
|
LIST_HEADER (struct user_channel)
|
|
|
|
struct channel *channel; ///< Reference to channel
|
|
};
|
|
|
|
static struct user_channel *
|
|
user_channel_new (void)
|
|
{
|
|
struct user_channel *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
user_channel_destroy (struct user_channel *self)
|
|
{
|
|
// The "channel" reference is weak and this object should get
|
|
// destroyed whenever the user stops being in the channel.
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// We keep references to user information in channels and buffers,
|
|
// and weak references in the name lookup table.
|
|
|
|
struct user
|
|
{
|
|
REF_COUNTABLE_HEADER
|
|
|
|
// TODO: eventually a reference to the server
|
|
|
|
char *nickname; ///< Literal nickname
|
|
// TODO: write code to poll for the away status
|
|
bool away; ///< User is away
|
|
|
|
struct user_channel *channels; ///< Channels the user is on
|
|
};
|
|
|
|
static struct user *
|
|
user_new (void)
|
|
{
|
|
struct user *self = xcalloc (1, sizeof *self);
|
|
self->ref_count = 1;
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
user_destroy (struct user *self)
|
|
{
|
|
free (self->nickname);
|
|
LIST_FOR_EACH (struct user_channel, iter, self->channels)
|
|
user_channel_destroy (iter);
|
|
free (self);
|
|
}
|
|
|
|
REF_COUNTABLE_METHODS (user)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct channel_user
|
|
{
|
|
LIST_HEADER (struct channel_user)
|
|
|
|
struct user *user; ///< Reference to user
|
|
char *modes; ///< Op/voice/... characters
|
|
};
|
|
|
|
static struct channel_user *
|
|
channel_user_new (void)
|
|
{
|
|
struct channel_user *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
channel_user_destroy (struct channel_user *self)
|
|
{
|
|
user_unref (self->user);
|
|
free (self->modes);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// We keep references to channels in their buffers,
|
|
// and weak references in their users and the name lookup table.
|
|
|
|
// XXX: this doesn't really have to be reference countable
|
|
|
|
struct channel
|
|
{
|
|
REF_COUNTABLE_HEADER
|
|
|
|
// TODO: eventually a reference to the server
|
|
|
|
char *name; ///< Channel name
|
|
char *mode; ///< Channel mode
|
|
char *topic; ///< Channel topic
|
|
|
|
struct channel_user *users; ///< Channel users
|
|
struct str_vector names_buf; ///< Buffer for RPL_NAMREPLY
|
|
};
|
|
|
|
static struct channel *
|
|
channel_new (void)
|
|
{
|
|
struct channel *self = xcalloc (1, sizeof *self);
|
|
self->ref_count = 1;
|
|
str_vector_init (&self->names_buf);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
channel_destroy (struct channel *self)
|
|
{
|
|
free (self->name);
|
|
free (self->mode);
|
|
free (self->topic);
|
|
// Owner has to make sure we have no users by now
|
|
hard_assert (!self->users);
|
|
str_vector_free (&self->names_buf);
|
|
free (self);
|
|
}
|
|
|
|
REF_COUNTABLE_METHODS (channel)
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum buffer_line_flags
|
|
{
|
|
BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this
|
|
};
|
|
|
|
enum buffer_line_type
|
|
{
|
|
BUFFER_LINE_PRIVMSG, ///< PRIVMSG
|
|
BUFFER_LINE_ACTION, ///< PRIVMSG ACTION
|
|
BUFFER_LINE_NOTICE, ///< NOTICE
|
|
BUFFER_LINE_JOIN, ///< JOIN
|
|
BUFFER_LINE_PART, ///< PART
|
|
BUFFER_LINE_KICK, ///< KICK
|
|
BUFFER_LINE_NICK, ///< NICK
|
|
BUFFER_LINE_TOPIC, ///< TOPIC
|
|
BUFFER_LINE_QUIT, ///< QUIT
|
|
BUFFER_LINE_STATUS, ///< Whatever status messages
|
|
BUFFER_LINE_ERROR ///< Whatever error messages
|
|
};
|
|
|
|
struct buffer_line_args
|
|
{
|
|
char *who; ///< Name of the origin or NULL (user)
|
|
char *object; ///< Object of action
|
|
char *text; ///< Text of message
|
|
char *reason; ///< Reason for PART, KICK, QUIT
|
|
};
|
|
|
|
struct buffer_line
|
|
{
|
|
LIST_HEADER (struct buffer_line)
|
|
|
|
// We use the "type" and "flags" mostly just as formatting hints
|
|
|
|
enum buffer_line_type type; ///< Type of the event
|
|
int flags; ///< Flags
|
|
|
|
time_t when; ///< Time of the event
|
|
struct buffer_line_args args; ///< Arguments
|
|
};
|
|
|
|
struct buffer_line *
|
|
buffer_line_new (void)
|
|
{
|
|
struct buffer_line *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
buffer_line_destroy (struct buffer_line *self)
|
|
{
|
|
free (self->args.who);
|
|
free (self->args.object);
|
|
free (self->args.text);
|
|
free (self->args.reason);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum buffer_type
|
|
{
|
|
BUFFER_GLOBAL, ///< Global information
|
|
BUFFER_SERVER, ///< Server-related messages
|
|
BUFFER_CHANNEL, ///< Channels
|
|
BUFFER_PM ///< Private messages (query)
|
|
};
|
|
|
|
struct buffer
|
|
{
|
|
LIST_HEADER (struct buffer)
|
|
|
|
enum buffer_type type; ///< Type of the buffer
|
|
char *name; ///< The name of the buffer
|
|
|
|
// Readline state:
|
|
|
|
HISTORY_STATE *history; ///< Saved history state
|
|
char *saved_line; ///< Saved line
|
|
int saved_point; ///< Saved position in line
|
|
int saved_mark; ///< Saved mark
|
|
|
|
// Buffer contents:
|
|
|
|
struct buffer_line *lines; ///< All lines in this buffer
|
|
struct buffer_line *lines_tail; ///< The tail of buffer lines
|
|
unsigned lines_count; ///< How many lines we have
|
|
|
|
unsigned unseen_messages_count; ///< # messages since last visited
|
|
|
|
// Origin information:
|
|
|
|
struct user *user; ///< Reference to user
|
|
struct channel *channel; ///< Reference to channel
|
|
|
|
// TODO: eventually a reference to the server for server buffers
|
|
};
|
|
|
|
static struct buffer *
|
|
buffer_new (void)
|
|
{
|
|
struct buffer *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
buffer_destroy (struct buffer *self)
|
|
{
|
|
free (self->name);
|
|
// Can't really free "history" here
|
|
free (self->saved_line);
|
|
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
|
|
buffer_line_destroy (iter);
|
|
if (self->user)
|
|
user_unref (self->user);
|
|
if (self->channel)
|
|
channel_unref (self->channel);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
enum color_mode
|
|
{
|
|
COLOR_AUTO, ///< Autodetect if colours are available
|
|
COLOR_ALWAYS, ///< Always use coloured output
|
|
COLOR_NEVER ///< Never use coloured output
|
|
};
|
|
|
|
struct app_context
|
|
{
|
|
// Configuration:
|
|
|
|
struct str_map config; ///< User configuration
|
|
enum color_mode color_mode; ///< Colour output mode
|
|
bool reconnect; ///< Whether to reconnect on conn. fail.
|
|
unsigned long reconnect_delay; ///< Reconnect delay in seconds
|
|
bool isolate_buffers; ///< Isolate global/server buffers
|
|
|
|
// Server connection:
|
|
|
|
int irc_fd; ///< Socket FD of the server
|
|
struct str read_buffer; ///< Input yet to be processed
|
|
struct poller_fd irc_event; ///< IRC FD event
|
|
bool irc_ready; ///< Whether we may send messages now
|
|
|
|
SSL_CTX *ssl_ctx; ///< SSL context
|
|
SSL *ssl; ///< SSL connection
|
|
|
|
// TODO: an output queue to prevent excess floods (this will be needed
|
|
// especially for away status polling)
|
|
|
|
// XXX: there can be buffers for non-existent users
|
|
// TODO: initialize key_strxfrm according to server properties;
|
|
// note that collisions may arise on reconnecting
|
|
// TODO: when disconnected, get rid of all users everywhere;
|
|
// maybe also broadcast all buffers about the disconnection event
|
|
// TODO: when getting connected again, rejoin all current channels
|
|
|
|
struct str_map irc_users; ///< IRC user data
|
|
struct str_map irc_channels; ///< IRC channel data
|
|
struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers
|
|
|
|
struct user *irc_user; ///< Our own user
|
|
char *irc_user_mode; ///< Our current user mode
|
|
char *irc_user_host; ///< Our current user@host
|
|
|
|
// Events:
|
|
|
|
struct poller_fd tty_event; ///< Terminal input event
|
|
struct poller_fd signal_event; ///< Signal FD event
|
|
|
|
struct poller_timer ping_tmr; ///< We should send a ping
|
|
struct poller_timer timeout_tmr; ///< Connection seems to be dead
|
|
struct poller_timer reconnect_tmr; ///< We should reconnect now
|
|
|
|
struct poller poller; ///< Manages polled descriptors
|
|
bool quitting; ///< User requested quitting
|
|
bool polling; ///< The event loop is running
|
|
|
|
// Buffers:
|
|
|
|
struct buffer *buffers; ///< All our buffers in order
|
|
struct buffer *buffers_tail; ///< The tail of our buffers
|
|
|
|
// XXX: when we go multiserver, there will be collisions
|
|
// TODO: make buffer names unique like weechat does
|
|
struct str_map buffers_by_name; ///< Excludes GLOBAL and SERVER
|
|
|
|
struct buffer *global_buffer; ///< The global buffer
|
|
struct buffer *server_buffer; ///< The server buffer
|
|
struct buffer *current_buffer; ///< The current buffer
|
|
|
|
// TODO: So that we always output proper date change messages
|
|
time_t last_displayed_msg_time; ///< Time of last displayed message
|
|
|
|
// Terminal:
|
|
|
|
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
|
|
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
|
|
iconv_t latin1_to_utf8; ///< ISO Latin 1 to UTF-8
|
|
|
|
int lines; ///< Current terminal height
|
|
int columns; ///< Current ternimal width
|
|
|
|
char *readline_prompt; ///< The prompt we use for readline
|
|
bool readline_prompt_shown; ///< Whether the prompt is shown now
|
|
}
|
|
*g_ctx;
|
|
|
|
static void on_irc_ping_timeout (void *user_data);
|
|
static void on_irc_timeout (void *user_data);
|
|
static void on_irc_reconnect_timeout (void *user_data);
|
|
|
|
static void
|
|
app_context_init (struct app_context *self)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
|
|
str_map_init (&self->config);
|
|
self->config.free = free;
|
|
load_config_defaults (&self->config, g_config_table);
|
|
|
|
self->irc_fd = -1;
|
|
str_init (&self->read_buffer);
|
|
self->irc_ready = false;
|
|
|
|
str_map_init (&self->irc_users);
|
|
self->irc_users.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->irc_channels);
|
|
self->irc_channels.key_xfrm = irc_strxfrm;
|
|
str_map_init (&self->irc_buffer_map);
|
|
self->irc_buffer_map.key_xfrm = irc_strxfrm;
|
|
|
|
poller_init (&self->poller);
|
|
|
|
str_map_init (&self->buffers_by_name);
|
|
self->buffers_by_name.key_xfrm = irc_strxfrm;
|
|
|
|
self->last_displayed_msg_time = time (NULL);
|
|
|
|
char *encoding = nl_langinfo (CODESET);
|
|
#ifdef __linux__
|
|
encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
|
|
#else // ! __linux__
|
|
encoding = xstrdup (encoding);
|
|
#endif // ! __linux__
|
|
|
|
if ((self->term_from_utf8 =
|
|
iconv_open (encoding, "UTF-8")) == (iconv_t) -1
|
|
|| (self->latin1_to_utf8 =
|
|
iconv_open ("UTF-8", "ISO-8859-1")) == (iconv_t) -1
|
|
|| (self->term_to_utf8 =
|
|
iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1)
|
|
exit_fatal ("creating the UTF-8 conversion object failed: %s",
|
|
strerror (errno));
|
|
|
|
free (encoding);
|
|
}
|
|
|
|
static void
|
|
app_context_free (struct app_context *self)
|
|
{
|
|
str_map_free (&self->config);
|
|
str_free (&self->read_buffer);
|
|
|
|
if (self->irc_fd != -1)
|
|
{
|
|
xclose (self->irc_fd);
|
|
poller_fd_reset (&self->irc_event);
|
|
}
|
|
if (self->ssl)
|
|
SSL_free (self->ssl);
|
|
if (self->ssl_ctx)
|
|
SSL_CTX_free (self->ssl_ctx);
|
|
|
|
if (self->irc_user)
|
|
user_unref (self->irc_user);
|
|
free (self->irc_user_mode);
|
|
free (self->irc_user_host);
|
|
|
|
// FIXME: this doesn't free the history state
|
|
LIST_FOR_EACH (struct buffer, iter, self->buffers)
|
|
buffer_destroy (iter);
|
|
str_map_free (&self->buffers_by_name);
|
|
|
|
str_map_free (&self->irc_users);
|
|
str_map_free (&self->irc_channels);
|
|
str_map_free (&self->irc_buffer_map);
|
|
|
|
poller_free (&self->poller);
|
|
|
|
iconv_close (self->latin1_to_utf8);
|
|
iconv_close (self->term_from_utf8);
|
|
iconv_close (self->term_to_utf8);
|
|
|
|
free (self->readline_prompt);
|
|
}
|
|
|
|
static void refresh_prompt (struct app_context *ctx);
|
|
static char *irc_cut_nickname (const char *prefix);
|
|
static const char *irc_find_userhost (const char *prefix);
|
|
|
|
// --- Attributed output -------------------------------------------------------
|
|
|
|
static struct
|
|
{
|
|
bool initialized; ///< Terminal is available
|
|
bool stdout_is_tty; ///< `stdout' is a terminal
|
|
bool stderr_is_tty; ///< `stderr' is a terminal
|
|
|
|
char *color_set_fg[8]; ///< Codes to set the foreground colour
|
|
char *color_set_bg[8]; ///< Codes to set the background colour
|
|
}
|
|
g_terminal;
|
|
|
|
static bool
|
|
init_terminal (void)
|
|
{
|
|
int tty_fd = -1;
|
|
if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
|
|
tty_fd = STDERR_FILENO;
|
|
if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
|
|
tty_fd = STDOUT_FILENO;
|
|
|
|
int err;
|
|
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
|
|
return false;
|
|
|
|
// Make sure all terminal features used by us are supported
|
|
if (!set_a_foreground || !set_a_background
|
|
|| !enter_bold_mode || !exit_attribute_mode)
|
|
{
|
|
del_curterm (cur_term);
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
|
|
{
|
|
g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
|
|
i, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
|
|
i, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
}
|
|
|
|
return g_terminal.initialized = true;
|
|
}
|
|
|
|
static void
|
|
free_terminal (void)
|
|
{
|
|
if (!g_terminal.initialized)
|
|
return;
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
|
|
{
|
|
free (g_terminal.color_set_fg[i]);
|
|
free (g_terminal.color_set_bg[i]);
|
|
}
|
|
del_curterm (cur_term);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct app_readline_state
|
|
{
|
|
char *saved_line;
|
|
int saved_point;
|
|
int saved_mark;
|
|
};
|
|
|
|
static void
|
|
app_readline_hide (struct app_readline_state *state)
|
|
{
|
|
state->saved_point = rl_point;
|
|
state->saved_mark = rl_mark;
|
|
state->saved_line = rl_copy_text (0, rl_end);
|
|
rl_set_prompt ("");
|
|
rl_replace_line ("", 0);
|
|
rl_redisplay ();
|
|
}
|
|
|
|
static void
|
|
app_readline_restore (struct app_readline_state *state, const char *prompt)
|
|
{
|
|
rl_set_prompt (prompt);
|
|
rl_replace_line (state->saved_line, 0);
|
|
rl_point = state->saved_point;
|
|
rl_mark = state->saved_mark;
|
|
rl_redisplay ();
|
|
free (state->saved_line);
|
|
}
|
|
|
|
static void
|
|
app_readline_erase_to_bol (const char *prompt)
|
|
{
|
|
rl_set_prompt ("");
|
|
rl_replace_line ("", 0);
|
|
rl_point = rl_mark = 0;
|
|
rl_redisplay ();
|
|
rl_set_prompt (prompt);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
typedef int (*terminal_printer_fn) (int);
|
|
|
|
static int
|
|
putchar_stderr (int c)
|
|
{
|
|
return fputc (c, stderr);
|
|
}
|
|
|
|
static terminal_printer_fn
|
|
get_attribute_printer (FILE *stream)
|
|
{
|
|
if (stream == stdout && g_terminal.stdout_is_tty)
|
|
return putchar;
|
|
if (stream == stderr && g_terminal.stderr_is_tty)
|
|
return putchar_stderr;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
vprint_attributed (struct app_context *ctx,
|
|
FILE *stream, const char *attribute, const char *fmt, va_list ap)
|
|
{
|
|
terminal_printer_fn printer = get_attribute_printer (stream);
|
|
if (!attribute)
|
|
printer = NULL;
|
|
|
|
if (printer)
|
|
{
|
|
const char *value = str_map_find (&ctx->config, attribute);
|
|
tputs (value, 1, printer);
|
|
}
|
|
|
|
vfprintf (stream, fmt, ap);
|
|
|
|
if (printer)
|
|
{
|
|
const char *value = str_map_find (&ctx->config, ATTR_RESET);
|
|
tputs (value, 1, printer);
|
|
}
|
|
}
|
|
|
|
static void
|
|
print_attributed (struct app_context *ctx,
|
|
FILE *stream, const char *attribute, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start (ap, fmt);
|
|
vprint_attributed (ctx, stream, attribute, fmt, ap);
|
|
va_end (ap);
|
|
}
|
|
|
|
static void
|
|
log_message_attributed (void *user_data, const char *quote, const char *fmt,
|
|
va_list ap)
|
|
{
|
|
FILE *stream = stderr;
|
|
|
|
struct app_readline_state state;
|
|
if (g_ctx->readline_prompt_shown)
|
|
app_readline_hide (&state);
|
|
|
|
print_attributed (g_ctx, stream, user_data, "%s", quote);
|
|
vprint_attributed (g_ctx, stream, user_data, fmt, ap);
|
|
fputs ("\n", stream);
|
|
|
|
if (g_ctx->readline_prompt_shown)
|
|
app_readline_restore (&state, g_ctx->readline_prompt);
|
|
}
|
|
|
|
static void
|
|
init_colors (struct app_context *ctx)
|
|
{
|
|
bool have_ti = init_terminal ();
|
|
|
|
// Use escape sequences from terminfo if possible, and SGR as a fallback
|
|
#define INIT_ATTR(id, ti, vt100) \
|
|
str_map_set (&ctx->config, (id), xstrdup (have_ti ? (ti) : (vt100)));
|
|
|
|
INIT_ATTR (ATTR_PROMPT, enter_bold_mode, "\x1b[1m");
|
|
INIT_ATTR (ATTR_RESET, exit_attribute_mode, "\x1b[0m");
|
|
INIT_ATTR (ATTR_WARNING, g_terminal.color_set_fg[3], "\x1b[33m");
|
|
INIT_ATTR (ATTR_ERROR, g_terminal.color_set_fg[1], "\x1b[31m");
|
|
|
|
INIT_ATTR (ATTR_EXTERNAL, g_terminal.color_set_fg[7], "\x1b[37m");
|
|
INIT_ATTR (ATTR_TIMESTAMP, g_terminal.color_set_fg[7], "\x1b[37m");
|
|
INIT_ATTR (ATTR_ACTION, g_terminal.color_set_fg[1], "\x1b[31m");
|
|
INIT_ATTR (ATTR_JOIN, g_terminal.color_set_fg[2], "\x1b[32m");
|
|
INIT_ATTR (ATTR_PART, g_terminal.color_set_fg[1], "\x1b[31m");
|
|
|
|
#undef INIT_ATTR
|
|
|
|
switch (ctx->color_mode)
|
|
{
|
|
case COLOR_ALWAYS:
|
|
g_terminal.stdout_is_tty = true;
|
|
g_terminal.stderr_is_tty = true;
|
|
break;
|
|
case COLOR_AUTO:
|
|
if (!g_terminal.initialized)
|
|
{
|
|
case COLOR_NEVER:
|
|
g_terminal.stdout_is_tty = false;
|
|
g_terminal.stderr_is_tty = false;
|
|
}
|
|
}
|
|
|
|
g_log_message_real = log_message_attributed;
|
|
}
|
|
|
|
// --- Signals -----------------------------------------------------------------
|
|
|
|
static int g_signal_pipe[2]; ///< A pipe used to signal... signals
|
|
|
|
/// Program termination has been requested by a signal
|
|
static volatile sig_atomic_t g_termination_requested;
|
|
/// The window has changed in size
|
|
static volatile sig_atomic_t g_winch_received;
|
|
|
|
static void
|
|
sigterm_handler (int signum)
|
|
{
|
|
(void) signum;
|
|
|
|
g_termination_requested = true;
|
|
|
|
int original_errno = errno;
|
|
if (write (g_signal_pipe[1], "t", 1) == -1)
|
|
soft_assert (errno == EAGAIN);
|
|
errno = original_errno;
|
|
}
|
|
|
|
static void
|
|
sigwinch_handler (int signum)
|
|
{
|
|
(void) signum;
|
|
|
|
g_winch_received = true;
|
|
|
|
int original_errno = errno;
|
|
if (write (g_signal_pipe[1], "w", 1) == -1)
|
|
soft_assert (errno == EAGAIN);
|
|
errno = original_errno;
|
|
}
|
|
|
|
static void
|
|
setup_signal_handlers (void)
|
|
{
|
|
if (pipe (g_signal_pipe) == -1)
|
|
exit_fatal ("%s: %s", "pipe", strerror (errno));
|
|
|
|
set_cloexec (g_signal_pipe[0]);
|
|
set_cloexec (g_signal_pipe[1]);
|
|
|
|
// So that the pipe cannot overflow; it would make write() block within
|
|
// the signal handler, which is something we really don't want to happen.
|
|
// The same holds true for read().
|
|
set_blocking (g_signal_pipe[0], false);
|
|
set_blocking (g_signal_pipe[1], false);
|
|
|
|
signal (SIGPIPE, SIG_IGN);
|
|
|
|
struct sigaction sa;
|
|
sa.sa_flags = SA_RESTART;
|
|
sa.sa_handler = sigwinch_handler;
|
|
sigemptyset (&sa.sa_mask);
|
|
|
|
if (sigaction (SIGWINCH, &sa, NULL) == -1)
|
|
exit_fatal ("sigaction: %s", strerror (errno));
|
|
|
|
sa.sa_handler = sigterm_handler;
|
|
if (sigaction (SIGINT, &sa, NULL) == -1
|
|
|| sigaction (SIGTERM, &sa, NULL) == -1)
|
|
exit_fatal ("sigaction: %s", strerror (errno));
|
|
}
|
|
|
|
// --- Output formatter --------------------------------------------------------
|
|
|
|
// This complicated piece of code makes attributed text formatting simple.
|
|
// We use a printf-inspired syntax to push attributes and text to the object,
|
|
// then flush it either to a terminal, or a log file with formatting stripped.
|
|
//
|
|
// Format strings use a #-quoted notation, to differentiate from printf:
|
|
// #s inserts a string
|
|
// #d inserts a signed integer; also supports the #<N> and #0<N> notation
|
|
//
|
|
// #a inserts named attributes (auto-resets)
|
|
// #r resets terminal attributes
|
|
// #c sets foreground color
|
|
// #C sets background color
|
|
|
|
enum formatter_item_type
|
|
{
|
|
FORMATTER_ITEM_TEXT, ///< Text
|
|
FORMATTER_ITEM_ATTR, ///< Named formatting attributes
|
|
FORMATTER_ITEM_FG_COLOR, ///< Foreground color
|
|
FORMATTER_ITEM_BG_COLOR ///< Background color
|
|
};
|
|
|
|
struct formatter_item
|
|
{
|
|
LIST_HEADER (struct formatter_item)
|
|
|
|
enum formatter_item_type type; ///< Type of this item
|
|
int color; ///< Color
|
|
char *data; ///< Either text or an attribute string
|
|
};
|
|
|
|
static struct formatter_item *
|
|
formatter_item_new (void)
|
|
{
|
|
struct formatter_item *self = xcalloc (1, sizeof *self);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
formatter_item_destroy (struct formatter_item *self)
|
|
{
|
|
free (self->data);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct formatter
|
|
{
|
|
struct app_context *ctx; ///< Application context
|
|
bool ignore_new_attributes; ///< Whether to ignore new attributes
|
|
|
|
struct formatter_item *items; ///< Items
|
|
struct formatter_item *items_tail; ///< Tail of items
|
|
};
|
|
|
|
static void
|
|
formatter_init (struct formatter *self, struct app_context *ctx)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
self->ctx = ctx;
|
|
}
|
|
|
|
static void
|
|
formatter_free (struct formatter *self)
|
|
{
|
|
LIST_FOR_EACH (struct formatter_item, iter, self->items)
|
|
formatter_item_destroy (iter);
|
|
}
|
|
|
|
static struct formatter_item *
|
|
formatter_add_blank (struct formatter *self)
|
|
{
|
|
struct formatter_item *item = formatter_item_new ();
|
|
LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
|
|
return item;
|
|
}
|
|
|
|
static void
|
|
formatter_add_text (struct formatter *self, const char *text)
|
|
{
|
|
struct formatter_item *item = formatter_add_blank (self);
|
|
item->type = FORMATTER_ITEM_TEXT;
|
|
item->data = xstrdup (text);
|
|
}
|
|
|
|
static void
|
|
formatter_add_reset (struct formatter *self)
|
|
{
|
|
if (self->ignore_new_attributes)
|
|
return;
|
|
|
|
struct formatter_item *item = formatter_add_blank (self);
|
|
item->type = FORMATTER_ITEM_ATTR;
|
|
item->data = NULL;
|
|
}
|
|
|
|
static void
|
|
formatter_add_attr (struct formatter *self, const char *attr_name)
|
|
{
|
|
if (self->ignore_new_attributes)
|
|
return;
|
|
|
|
struct formatter_item *item = formatter_add_blank (self);
|
|
item->type = FORMATTER_ITEM_ATTR;
|
|
item->data = xstrdup (str_map_find (&self->ctx->config, attr_name));
|
|
}
|
|
|
|
static void
|
|
formatter_add_fg_color (struct formatter *self, int color)
|
|
{
|
|
if (self->ignore_new_attributes)
|
|
return;
|
|
|
|
struct formatter_item *item = formatter_add_blank (self);
|
|
item->type = FORMATTER_ITEM_FG_COLOR;
|
|
item->color = color;
|
|
}
|
|
|
|
static void
|
|
formatter_add_bg_color (struct formatter *self, int color)
|
|
{
|
|
if (self->ignore_new_attributes)
|
|
return;
|
|
|
|
struct formatter_item *item = formatter_add_blank (self);
|
|
item->type = FORMATTER_ITEM_BG_COLOR;
|
|
item->color = color;
|
|
}
|
|
|
|
static const char *
|
|
formatter_parse_field (struct formatter *self,
|
|
const char *field, struct str *buf, va_list *ap)
|
|
{
|
|
size_t width = 0;
|
|
bool zero_padded = false;
|
|
int c;
|
|
|
|
restart:
|
|
switch ((c = *field++))
|
|
{
|
|
char *s;
|
|
|
|
// We can push boring text content to the caller's buffer
|
|
// and let it flush the buffer only when it's actually needed
|
|
case 's':
|
|
s = va_arg (*ap, char *);
|
|
for (size_t len = strlen (s); len < width; len++)
|
|
str_append_c (buf, ' ');
|
|
str_append (buf, s);
|
|
break;
|
|
case 'd':
|
|
s = xstrdup_printf ("%d", va_arg (*ap, int));
|
|
for (size_t len = strlen (s); len < width; len++)
|
|
str_append_c (buf, " 0"[zero_padded]);
|
|
str_append (buf, s);
|
|
free (s);
|
|
break;
|
|
|
|
case 'a':
|
|
formatter_add_attr (self, va_arg (*ap, const char *));
|
|
break;
|
|
case 'c':
|
|
formatter_add_fg_color (self, va_arg (*ap, int));
|
|
break;
|
|
case 'C':
|
|
formatter_add_bg_color (self, va_arg (*ap, int));
|
|
break;
|
|
case 'r':
|
|
formatter_add_reset (self);
|
|
break;
|
|
|
|
default:
|
|
if (c == '0' && !zero_padded)
|
|
zero_padded = true;
|
|
else if (isdigit_ascii (c))
|
|
width = width * 10 + (c - '0');
|
|
else if (c)
|
|
hard_assert (!"unexpected format specifier");
|
|
else
|
|
hard_assert (!"unexpected end of format string");
|
|
goto restart;
|
|
}
|
|
return field;
|
|
}
|
|
|
|
static void
|
|
formatter_add (struct formatter *self, const char *format, ...)
|
|
{
|
|
struct str buf;
|
|
str_init (&buf);
|
|
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
|
|
while (*format)
|
|
{
|
|
if (*format != '#' || *++format == '#')
|
|
{
|
|
str_append_c (&buf, *format++);
|
|
continue;
|
|
}
|
|
if (buf.len)
|
|
{
|
|
formatter_add_text (self, buf.str);
|
|
str_reset (&buf);
|
|
}
|
|
|
|
format = formatter_parse_field (self, format, &buf, &ap);
|
|
}
|
|
|
|
if (buf.len)
|
|
formatter_add_text (self, buf.str);
|
|
|
|
str_free (&buf);
|
|
va_end (ap);
|
|
}
|
|
|
|
static void
|
|
formatter_flush (struct formatter *self, FILE *stream)
|
|
{
|
|
terminal_printer_fn printer = get_attribute_printer (stream);
|
|
|
|
const char *attr_reset = str_map_find (&self->ctx->config, ATTR_RESET);
|
|
if (printer)
|
|
tputs (attr_reset, 1, printer);
|
|
|
|
bool is_attributed = false;
|
|
bool is_tty = isatty (fileno (stream));
|
|
LIST_FOR_EACH (struct formatter_item, iter, self->items)
|
|
{
|
|
switch (iter->type)
|
|
{
|
|
case FORMATTER_ITEM_TEXT:
|
|
if (is_tty)
|
|
{
|
|
char *term = iconv_xstrdup
|
|
(self->ctx->term_from_utf8, iter->data, -1, NULL);
|
|
fputs (term, stream);
|
|
free (term);
|
|
}
|
|
else
|
|
fputs (iter->data, stream);
|
|
break;
|
|
case FORMATTER_ITEM_ATTR:
|
|
if (!printer)
|
|
continue;
|
|
|
|
if (is_attributed)
|
|
{
|
|
tputs (attr_reset, 1, printer);
|
|
is_attributed = false;
|
|
}
|
|
if (iter->data)
|
|
{
|
|
tputs (iter->data, 1, printer);
|
|
is_attributed = true;
|
|
}
|
|
break;
|
|
case FORMATTER_ITEM_FG_COLOR:
|
|
if (!printer)
|
|
continue;
|
|
|
|
tputs (g_terminal.color_set_fg[iter->color], 1, printer);
|
|
is_attributed = true;
|
|
break;
|
|
case FORMATTER_ITEM_BG_COLOR:
|
|
if (!printer)
|
|
continue;
|
|
|
|
tputs (g_terminal.color_set_bg[iter->color], 1, printer);
|
|
is_attributed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (is_attributed)
|
|
tputs (attr_reset, 1, printer);
|
|
}
|
|
|
|
// --- Buffers -----------------------------------------------------------------
|
|
|
|
static void
|
|
buffer_update_time (struct app_context *ctx, time_t now)
|
|
{
|
|
struct tm last, current;
|
|
if (!localtime_r (&ctx->last_displayed_msg_time, &last)
|
|
|| !localtime_r (&now, ¤t))
|
|
{
|
|
// Strange but nonfatal
|
|
print_error ("%s: %s", "localtime_r", strerror (errno));
|
|
return;
|
|
}
|
|
|
|
ctx->last_displayed_msg_time = now;
|
|
if (last.tm_year == current.tm_year
|
|
&& last.tm_mon == current.tm_mon
|
|
&& last.tm_mday == current.tm_mday)
|
|
return;
|
|
|
|
char buf[32] = "";
|
|
if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t)))
|
|
print_status ("%s", buf);
|
|
// Else the buffer was too small, which is pretty weird
|
|
}
|
|
|
|
static void
|
|
buffer_line_display (struct app_context *ctx,
|
|
struct buffer_line *line, bool is_external)
|
|
{
|
|
// Normal timestamps don't include the date, this way the user won't be
|
|
// confused as to when an event has happened
|
|
buffer_update_time (ctx, line->when);
|
|
|
|
struct buffer_line_args *a = &line->args;
|
|
|
|
char *nick = NULL;
|
|
const char *userhost = NULL;
|
|
int nick_color = -1;
|
|
int object_color = -1;
|
|
|
|
if (a->who)
|
|
{
|
|
nick = irc_cut_nickname (a->who);
|
|
userhost = irc_find_userhost (a->who);
|
|
nick_color = str_map_hash (nick, strlen (nick)) % 8;
|
|
}
|
|
if (a->object)
|
|
object_color = str_map_hash (a->object, strlen (a->object)) % 8;
|
|
|
|
struct formatter f;
|
|
formatter_init (&f, ctx);
|
|
|
|
struct tm current;
|
|
if (!localtime_r (&line->when, ¤t))
|
|
print_error ("%s: %s", "localtime_r", strerror (errno));
|
|
else
|
|
formatter_add (&f, "#a#02d:#02d:#02d#r ",
|
|
ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec);
|
|
|
|
// Ignore all formatting for messages coming from other buffers, that is
|
|
// either from the global or server buffer. Instead print them in grey.
|
|
if (is_external)
|
|
{
|
|
formatter_add (&f, "#a", ATTR_EXTERNAL);
|
|
f.ignore_new_attributes = true;
|
|
}
|
|
|
|
// TODO: try to decode as much as possible using mIRC formatting;
|
|
// could either add a #m format specifier, or write a separate function
|
|
// to translate the formatting into formatter API calls
|
|
|
|
switch (line->type)
|
|
{
|
|
case BUFFER_LINE_PRIVMSG:
|
|
formatter_add (&f, "<#c#s#r> #s", nick_color, nick, a->text);
|
|
break;
|
|
case BUFFER_LINE_ACTION:
|
|
formatter_add (&f, " #a*#r ", ATTR_ACTION);
|
|
formatter_add (&f, "#c#s#r #s", nick_color, nick, a->text);
|
|
break;
|
|
case BUFFER_LINE_NOTICE:
|
|
formatter_add (&f, " - ");
|
|
formatter_add (&f, "#s(#c#s#r): #s",
|
|
"Notice", nick_color, nick, a->text);
|
|
break;
|
|
case BUFFER_LINE_JOIN:
|
|
formatter_add (&f, "#a-->#r ", ATTR_JOIN);
|
|
formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
|
|
nick_color, nick, userhost,
|
|
ATTR_JOIN, "has joined", a->object);
|
|
break;
|
|
case BUFFER_LINE_PART:
|
|
formatter_add (&f, "#a<--#r ", ATTR_PART);
|
|
formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
|
|
nick_color, nick, userhost,
|
|
ATTR_PART, "has left", a->object);
|
|
if (a->reason)
|
|
formatter_add (&f, " (#s)", a->reason);
|
|
break;
|
|
case BUFFER_LINE_KICK:
|
|
formatter_add (&f, "#a<--#r ", ATTR_PART);
|
|
formatter_add (&f, "#c#s#r (#s) #a#s#r #c#s#r",
|
|
nick_color, nick, userhost,
|
|
ATTR_PART, "has kicked", object_color, a->object);
|
|
if (a->reason)
|
|
formatter_add (&f, " (#s)", a->reason);
|
|
break;
|
|
case BUFFER_LINE_NICK:
|
|
formatter_add (&f, " - ");
|
|
if (a->who)
|
|
formatter_add (&f, "#c#s#r #s #c#s#r",
|
|
nick_color, nick,
|
|
"is now known as", object_color, a->object);
|
|
else
|
|
formatter_add (&f, "#s #s",
|
|
"You are now known as", a->object);
|
|
break;
|
|
case BUFFER_LINE_TOPIC:
|
|
formatter_add (&f, " - ");
|
|
formatter_add (&f, "#c#s#r #s \"#s\"",
|
|
nick_color, nick,
|
|
"has changed the topic to", a->text);
|
|
break;
|
|
case BUFFER_LINE_QUIT:
|
|
formatter_add (&f, "#a<--#r ", ATTR_PART);
|
|
formatter_add (&f, "#c#s#r (%s) #a#s#r",
|
|
nick_color, nick, userhost,
|
|
ATTR_PART, "has quit");
|
|
if (a->reason)
|
|
formatter_add (&f, " (#s)", a->reason);
|
|
break;
|
|
case BUFFER_LINE_STATUS:
|
|
formatter_add (&f, " - ");
|
|
formatter_add (&f, "#s", a->text);
|
|
break;
|
|
case BUFFER_LINE_ERROR:
|
|
formatter_add (&f, "#a=!=#r ", ATTR_ERROR);
|
|
formatter_add (&f, "#s", a->text);
|
|
}
|
|
|
|
free (nick);
|
|
|
|
struct app_readline_state state;
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_hide (&state);
|
|
|
|
// TODO: write the line to a log file; note that the global and server
|
|
// buffers musn't collide with filenames
|
|
|
|
formatter_add (&f, "\n");
|
|
formatter_flush (&f, stdout);
|
|
formatter_free (&f);
|
|
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_restore (&state, ctx->readline_prompt);
|
|
}
|
|
|
|
static void
|
|
buffer_send_internal (struct app_context *ctx, struct buffer *buffer,
|
|
enum buffer_line_type type, int flags,
|
|
struct buffer_line_args a)
|
|
{
|
|
struct buffer_line *line = buffer_line_new ();
|
|
line->type = type;
|
|
line->flags = flags;
|
|
line->when = time (NULL);
|
|
line->args = a;
|
|
|
|
LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
|
|
buffer->lines_count++;
|
|
|
|
if (buffer == ctx->current_buffer)
|
|
buffer_line_display (ctx, line, false);
|
|
else if (!ctx->isolate_buffers &&
|
|
(buffer == ctx->global_buffer || buffer == ctx->server_buffer))
|
|
buffer_line_display (ctx, line, true);
|
|
else
|
|
{
|
|
buffer->unseen_messages_count++;
|
|
refresh_prompt (ctx);
|
|
}
|
|
}
|
|
|
|
#define buffer_send(ctx, buffer, type, flags, ...) \
|
|
buffer_send_internal ((ctx), (buffer), (type), (flags), \
|
|
(struct buffer_line_args) { __VA_ARGS__ })
|
|
|
|
#define buffer_send_status(ctx, buffer, ...) \
|
|
buffer_send (ctx, buffer, BUFFER_LINE_STATUS, 0, \
|
|
.text = xstrdup_printf (__VA_ARGS__))
|
|
#define buffer_send_error(ctx, buffer, ...) \
|
|
buffer_send (ctx, buffer, BUFFER_LINE_ERROR, 0, \
|
|
.text = xstrdup_printf (__VA_ARGS__))
|
|
|
|
static struct buffer *
|
|
buffer_by_name (struct app_context *ctx, const char *name)
|
|
{
|
|
return str_map_find (&ctx->buffers_by_name, name);
|
|
}
|
|
|
|
static void
|
|
buffer_add (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
hard_assert (!buffer_by_name (ctx, buffer->name));
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
|
|
// In theory this can't cause changes in the prompt
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_remove (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
hard_assert (buffer != ctx->current_buffer);
|
|
|
|
// TODO: part from the channel if needed
|
|
|
|
// rl_clear_history, being the only way I know of to get rid of the complete
|
|
// history including attached data, is a pretty recent addition. *sigh*
|
|
#if RL_READLINE_VERSION >= 0x0603
|
|
if (buffer->history)
|
|
{
|
|
// See buffer_activate() for why we need to do this BS
|
|
rl_free_undo_list ();
|
|
|
|
// This is probably the only way we can free the history fully
|
|
HISTORY_STATE *state = history_get_history_state ();
|
|
|
|
history_set_history_state (buffer->history);
|
|
free (buffer->history);
|
|
rl_clear_history ();
|
|
|
|
history_set_history_state (state);
|
|
free (state);
|
|
}
|
|
#endif // RL_READLINE_VERSION
|
|
|
|
// And make sure to unlink the buffer from "irc_buffer_map"
|
|
if (buffer->channel)
|
|
str_map_set (&ctx->irc_buffer_map, buffer->channel->name, NULL);
|
|
if (buffer->user)
|
|
str_map_set (&ctx->irc_buffer_map, buffer->user->nickname, NULL);
|
|
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
buffer_destroy (buffer);
|
|
|
|
// It's not a good idea to remove these buffers, but it's even a worse
|
|
// one to leave the pointers point to invalid memory
|
|
if (buffer == ctx->global_buffer)
|
|
ctx->global_buffer = NULL;
|
|
if (buffer == ctx->server_buffer)
|
|
ctx->server_buffer = NULL;
|
|
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_activate (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
if (ctx->current_buffer == buffer)
|
|
return;
|
|
|
|
print_status ("%s", buffer->name);
|
|
|
|
// That is, minus the buffer switch line and the readline prompt
|
|
int to_display = MAX (10, ctx->lines - 2);
|
|
struct buffer_line *line = buffer->lines_tail;
|
|
while (line && line->prev && --to_display > 0)
|
|
line = line->prev;
|
|
|
|
// Once we've found where we want to start with the backlog, print it
|
|
for (; line; line = line->next)
|
|
buffer_line_display (ctx, line, false);
|
|
buffer->unseen_messages_count = 0;
|
|
|
|
// 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.
|
|
|
|
// 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 (ctx->current_buffer)
|
|
{
|
|
ctx->current_buffer->history = history_get_history_state ();
|
|
ctx->current_buffer->saved_line = rl_copy_text (0, rl_end);
|
|
ctx->current_buffer->saved_point = rl_point;
|
|
ctx->current_buffer->saved_mark = rl_mark;
|
|
}
|
|
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
|
|
|
|
// Restore the target buffer's history
|
|
if (buffer->history)
|
|
{
|
|
// history_get_history_state() just allocates a new HISTORY_STATE
|
|
// and fills it with its current internal data. We don't need that
|
|
// shell anymore after reviving it.
|
|
history_set_history_state (buffer->history);
|
|
free (buffer->history);
|
|
buffer->history = NULL;
|
|
}
|
|
else
|
|
{
|
|
// This should get us a clean history while keeping the flags.
|
|
// Note that we've either saved the previous history entries, or we've
|
|
// cleared them altogether, so there should be nothing to leak.
|
|
HISTORY_STATE *state = history_get_history_state ();
|
|
state->offset = state->length = state->size = 0;
|
|
history_set_history_state (state);
|
|
free (state);
|
|
}
|
|
|
|
// Try to restore the target buffer's readline state
|
|
if (buffer->saved_line)
|
|
{
|
|
rl_replace_line (buffer->saved_line, 0);
|
|
rl_point = buffer->saved_point;
|
|
rl_mark = buffer->saved_mark;
|
|
free (buffer->saved_line);
|
|
buffer->saved_line = 0;
|
|
|
|
if (ctx->readline_prompt_shown)
|
|
rl_redisplay ();
|
|
}
|
|
|
|
// Now at last we can switch the pointers
|
|
ctx->current_buffer = buffer;
|
|
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
buffer_merge (struct app_context *ctx,
|
|
struct buffer *buffer, struct buffer *merged)
|
|
{
|
|
// TODO: try to merge the buffers as best as we can
|
|
}
|
|
|
|
static void
|
|
buffer_rename (struct app_context *ctx,
|
|
struct buffer *buffer, const char *new_name)
|
|
{
|
|
hard_assert (buffer->type == BUFFER_PM);
|
|
|
|
struct buffer *collision =
|
|
str_map_find (&ctx->irc_buffer_map, new_name);
|
|
if (collision)
|
|
{
|
|
// TODO: use full weechat-style buffer names
|
|
// to prevent name collisions with the global buffer
|
|
hard_assert (collision->type == BUFFER_PM);
|
|
|
|
// When there's a collision, there's not much else we can do
|
|
// other than somehow trying to merge them
|
|
buffer_merge (ctx, collision, buffer);
|
|
// TODO: log a status message about the merge
|
|
if (ctx->current_buffer == buffer)
|
|
buffer_activate (ctx, collision);
|
|
buffer_remove (ctx, buffer);
|
|
}
|
|
else
|
|
{
|
|
// Otherwise we just rename the buffer and that's it
|
|
str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
|
|
str_map_set (&ctx->buffers_by_name, new_name, buffer);
|
|
|
|
free (buffer->name);
|
|
buffer->name = xstrdup (new_name);
|
|
|
|
// We might have renamed the current buffer
|
|
refresh_prompt (ctx);
|
|
}
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_at_index (struct app_context *ctx, int n)
|
|
{
|
|
int i = 0;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
if (++i == n)
|
|
return iter;
|
|
return NULL;
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_next (struct app_context *ctx, int count)
|
|
{
|
|
struct buffer *new_buffer = ctx->current_buffer;
|
|
while (count-- > 0)
|
|
if (!(new_buffer = new_buffer->next))
|
|
new_buffer = ctx->buffers;
|
|
return new_buffer;
|
|
}
|
|
|
|
static struct buffer *
|
|
buffer_previous (struct app_context *ctx, int count)
|
|
{
|
|
struct buffer *new_buffer = ctx->current_buffer;
|
|
while (count-- > 0)
|
|
if (!(new_buffer = new_buffer->prev))
|
|
new_buffer = ctx->buffers_tail;
|
|
return new_buffer;
|
|
}
|
|
|
|
static bool
|
|
buffer_goto (struct app_context *ctx, int n)
|
|
{
|
|
struct buffer *buffer = buffer_at_index (ctx, n);
|
|
if (!buffer)
|
|
return false;
|
|
|
|
buffer_activate (ctx, buffer);
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
buffer_get_index (struct app_context *ctx, struct buffer *buffer)
|
|
{
|
|
int index = 1;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
{
|
|
if (iter == buffer)
|
|
return index;
|
|
index++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
init_buffers (struct app_context *ctx)
|
|
{
|
|
// At the moment we have only two global everpresent buffers
|
|
struct buffer *global = ctx->global_buffer = buffer_new ();
|
|
struct buffer *server = ctx->server_buffer = buffer_new ();
|
|
|
|
global->type = BUFFER_GLOBAL;
|
|
global->name = xstrdup (PROGRAM_NAME);
|
|
|
|
server->type = BUFFER_SERVER;
|
|
server->name = xstrdup (str_map_find (&ctx->config, "irc_host"));
|
|
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global);
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server);
|
|
}
|
|
|
|
// --- Users, channels ---------------------------------------------------------
|
|
|
|
static void
|
|
irc_user_on_destroy (void *object, void *user_data)
|
|
{
|
|
struct user *user = object;
|
|
struct app_context *ctx = user_data;
|
|
str_map_set (&ctx->irc_users, user->nickname, NULL);
|
|
}
|
|
|
|
static struct user *
|
|
irc_make_user (struct app_context *ctx, char *nickname)
|
|
{
|
|
hard_assert (!str_map_find (&ctx->irc_users, nickname));
|
|
|
|
struct user *user = user_new ();
|
|
user->on_destroy = irc_user_on_destroy;
|
|
user->user_data = ctx;
|
|
user->nickname = nickname;
|
|
str_map_set (&ctx->irc_users, user->nickname, user);
|
|
return user;
|
|
}
|
|
|
|
static struct buffer *
|
|
irc_get_or_make_user_buffer (struct app_context *ctx, const char *nickname)
|
|
{
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, nickname);
|
|
if (buffer)
|
|
return buffer;
|
|
|
|
struct user *user = str_map_find (&ctx->irc_users, nickname);
|
|
if (!user)
|
|
user = irc_make_user (ctx, xstrdup (nickname));
|
|
else
|
|
user = user_ref (user);
|
|
|
|
// Open a new buffer for the user
|
|
buffer = buffer_new ();
|
|
buffer->type = BUFFER_PM;
|
|
buffer->name = xstrdup (nickname);
|
|
buffer->user = user;
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
str_map_set (&ctx->irc_buffer_map, user->nickname, buffer);
|
|
return buffer;
|
|
}
|
|
|
|
static void
|
|
irc_channel_unlink_user
|
|
(struct channel *channel, struct channel_user *channel_user)
|
|
{
|
|
// First destroy the user's weak references to the channel
|
|
struct user *user = channel_user->user;
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
if (iter->channel == channel)
|
|
{
|
|
LIST_UNLINK (user->channels, iter);
|
|
user_channel_destroy (iter);
|
|
}
|
|
|
|
// Then just unlink the user from the channel
|
|
LIST_UNLINK (channel->users, channel_user);
|
|
channel_user_destroy (channel_user);
|
|
}
|
|
|
|
static void
|
|
irc_channel_on_destroy (void *object, void *user_data)
|
|
{
|
|
struct channel *channel = object;
|
|
struct app_context *ctx = user_data;
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
irc_channel_unlink_user (channel, iter);
|
|
str_map_set (&ctx->irc_channels, channel->name, NULL);
|
|
}
|
|
|
|
static struct channel *
|
|
irc_make_channel (struct app_context *ctx, char *name)
|
|
{
|
|
hard_assert (!str_map_find (&ctx->irc_channels, name));
|
|
|
|
struct channel *channel = channel_new ();
|
|
channel->on_destroy = irc_channel_on_destroy;
|
|
channel->user_data = ctx;
|
|
channel->name = name;
|
|
channel->mode = xstrdup ("");
|
|
channel->topic = NULL;
|
|
str_map_set (&ctx->irc_channels, channel->name, channel);
|
|
return channel;
|
|
}
|
|
|
|
static void
|
|
irc_remove_user_from_channel (struct user *user, struct channel *channel)
|
|
{
|
|
LIST_FOR_EACH (struct channel_user, iter, channel->users)
|
|
if (iter->user == user)
|
|
irc_channel_unlink_user (channel, iter);
|
|
}
|
|
|
|
// --- Supporting code ---------------------------------------------------------
|
|
|
|
static char *
|
|
irc_cut_nickname (const char *prefix)
|
|
{
|
|
return xstrndup (prefix, strcspn (prefix, "!@"));
|
|
}
|
|
|
|
static const char *
|
|
irc_find_userhost (const char *prefix)
|
|
{
|
|
const char *p = strchr (prefix, '!');
|
|
return p ? p + 1 : NULL;
|
|
}
|
|
|
|
static bool
|
|
irc_is_this_us (struct app_context *ctx, const char *prefix)
|
|
{
|
|
char *nick = irc_cut_nickname (prefix);
|
|
bool result = !irc_strcmp (nick, ctx->irc_user->nickname);
|
|
free (nick);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
irc_is_channel (struct app_context *ctx, const char *ident)
|
|
{
|
|
(void) ctx; // TODO: parse prefixes from server features
|
|
|
|
return *ident && !!strchr ("#&+!", *ident);
|
|
}
|
|
|
|
static void
|
|
irc_shutdown (struct app_context *ctx)
|
|
{
|
|
// TODO: set a timer after which we cut the connection?
|
|
// Generally non-critical
|
|
if (ctx->ssl)
|
|
soft_assert (SSL_shutdown (ctx->ssl) != -1);
|
|
else
|
|
soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0);
|
|
}
|
|
|
|
static void
|
|
try_finish_quit (struct app_context *ctx)
|
|
{
|
|
if (ctx->quitting && ctx->irc_fd == -1)
|
|
ctx->polling = false;
|
|
}
|
|
|
|
static void
|
|
initiate_quit (struct app_context *ctx)
|
|
{
|
|
// First get rid of readline
|
|
if (ctx->readline_prompt_shown)
|
|
{
|
|
app_readline_erase_to_bol (ctx->readline_prompt);
|
|
ctx->readline_prompt_shown = false;
|
|
}
|
|
|
|
// This is okay as long as we're not called from within readline
|
|
rl_callback_handler_remove ();
|
|
|
|
// Initiate a connection close
|
|
buffer_send_status (ctx, ctx->global_buffer, "Shutting down");
|
|
if (ctx->irc_fd != -1)
|
|
// XXX: when we go async, we'll have to flush output buffers first
|
|
irc_shutdown (ctx);
|
|
|
|
ctx->quitting = true;
|
|
try_finish_quit (ctx);
|
|
}
|
|
|
|
// As of 2015, everything should be in UTF-8. And if it's not, we'll decode it
|
|
// as ISO Latin 1. This function should not be called on the whole message.
|
|
static char *
|
|
irc_to_utf8 (struct app_context *ctx, const char *text)
|
|
{
|
|
size_t len = strlen (text) + 1;
|
|
if (utf8_validate (text, len))
|
|
return xstrdup (text);
|
|
return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL);
|
|
}
|
|
|
|
// This function is used to output debugging IRC traffic to the terminal.
|
|
// It's far from ideal, as any non-UTF-8 text degrades the entire line to
|
|
// ISO Latin 1. But it should work good enough most of the time.
|
|
static char *
|
|
irc_to_term (struct app_context *ctx, const char *text)
|
|
{
|
|
char *utf8 = irc_to_utf8 (ctx, text);
|
|
char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
|
|
free (utf8);
|
|
return term;
|
|
}
|
|
|
|
static bool irc_send (struct app_context *ctx,
|
|
const char *format, ...) ATTRIBUTE_PRINTF (2, 3);
|
|
|
|
static bool
|
|
irc_send (struct app_context *ctx, const char *format, ...)
|
|
{
|
|
if (!soft_assert (ctx->irc_fd != -1))
|
|
{
|
|
print_debug ("tried sending a message to a dead server connection");
|
|
return false;
|
|
}
|
|
|
|
va_list ap;
|
|
va_start (ap, format);
|
|
struct str str;
|
|
str_init (&str);
|
|
str_append_vprintf (&str, format, ap);
|
|
va_end (ap);
|
|
|
|
if (g_debug_mode)
|
|
{
|
|
struct app_readline_state state;
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_hide (&state);
|
|
|
|
char *term = irc_to_term (ctx, str.str);
|
|
fprintf (stderr, "[IRC] <== \"%s\"\n", term);
|
|
free (term);
|
|
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_restore (&state, ctx->readline_prompt);
|
|
}
|
|
str_append (&str, "\r\n");
|
|
|
|
bool result = true;
|
|
if (ctx->ssl)
|
|
{
|
|
// TODO: call SSL_get_error() to detect if a clean shutdown has occured
|
|
if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len)
|
|
{
|
|
LOG_FUNC_FAILURE ("SSL_write",
|
|
ERR_error_string (ERR_get_error (), NULL));
|
|
result = false;
|
|
}
|
|
}
|
|
else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len)
|
|
{
|
|
LOG_LIBC_FAILURE ("write");
|
|
result = false;
|
|
}
|
|
|
|
str_free (&str);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
irc_get_boolean_from_config
|
|
(struct app_context *ctx, const char *name, bool *value, struct error **e)
|
|
{
|
|
const char *str = str_map_find (&ctx->config, name);
|
|
hard_assert (str != NULL);
|
|
|
|
if (set_boolean_if_valid (value, str))
|
|
return true;
|
|
|
|
error_set (e, "invalid configuration value for `%s'", name);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
irc_initialize_ssl_ctx (struct app_context *ctx, struct error **e)
|
|
{
|
|
// XXX: maybe we should call SSL_CTX_set_options() for some workarounds
|
|
|
|
bool verify;
|
|
if (!irc_get_boolean_from_config (ctx, "ssl_verify", &verify, e))
|
|
return false;
|
|
|
|
if (!verify)
|
|
SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL);
|
|
|
|
const char *ca_file = str_map_find (&ctx->config, "ca_file");
|
|
const char *ca_path = str_map_find (&ctx->config, "ca_path");
|
|
|
|
struct error *error = NULL;
|
|
if (ca_file || ca_path)
|
|
{
|
|
if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path))
|
|
return true;
|
|
|
|
error_set (&error, "%s: %s",
|
|
"Failed to set locations for the CA certificate bundle",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
goto ca_error;
|
|
}
|
|
|
|
if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx))
|
|
{
|
|
error_set (&error, "%s: %s",
|
|
"Couldn't load the default CA certificate bundle",
|
|
ERR_reason_error_string (ERR_get_error ()));
|
|
goto ca_error;
|
|
}
|
|
return true;
|
|
|
|
ca_error:
|
|
if (verify)
|
|
{
|
|
error_propagate (e, error);
|
|
return false;
|
|
}
|
|
|
|
// Only inform the user if we're not actually verifying
|
|
buffer_send_error (ctx, ctx->server_buffer, "%s", error->message);
|
|
error_free (error);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
irc_initialize_ssl (struct app_context *ctx, struct error **e)
|
|
{
|
|
const char *error_info = NULL;
|
|
ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
|
|
if (!ctx->ssl_ctx)
|
|
goto error_ssl_1;
|
|
if (!irc_initialize_ssl_ctx (ctx, e))
|
|
goto error_ssl_2;
|
|
|
|
ctx->ssl = SSL_new (ctx->ssl_ctx);
|
|
if (!ctx->ssl)
|
|
goto error_ssl_2;
|
|
|
|
const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert");
|
|
if (ssl_cert)
|
|
{
|
|
char *path = resolve_config_filename (ssl_cert);
|
|
if (!path)
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "Cannot open file", ssl_cert);
|
|
// XXX: perhaps we should read the file ourselves for better messages
|
|
else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)
|
|
|| !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM))
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "Setting the SSL client certificate failed",
|
|
ERR_error_string (ERR_get_error (), NULL));
|
|
free (path);
|
|
}
|
|
|
|
SSL_set_connect_state (ctx->ssl);
|
|
if (!SSL_set_fd (ctx->ssl, ctx->irc_fd))
|
|
goto error_ssl_3;
|
|
// Avoid SSL_write() returning SSL_ERROR_WANT_READ
|
|
SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY);
|
|
|
|
switch (xssl_get_error (ctx->ssl, SSL_connect (ctx->ssl), &error_info))
|
|
{
|
|
case SSL_ERROR_NONE:
|
|
return true;
|
|
case SSL_ERROR_ZERO_RETURN:
|
|
error_info = "server closed the connection";
|
|
default:
|
|
break;
|
|
}
|
|
|
|
error_ssl_3:
|
|
SSL_free (ctx->ssl);
|
|
ctx->ssl = NULL;
|
|
error_ssl_2:
|
|
SSL_CTX_free (ctx->ssl_ctx);
|
|
ctx->ssl_ctx = NULL;
|
|
error_ssl_1:
|
|
// XXX: these error strings are really nasty; also there could be
|
|
// multiple errors on the OpenSSL stack.
|
|
if (!error_info)
|
|
error_info = ERR_error_string (ERR_get_error (), NULL);
|
|
error_set (e, "%s: %s", "could not initialize SSL", error_info);
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
irc_establish_connection (struct app_context *ctx,
|
|
const char *host, const char *port, struct error **e)
|
|
{
|
|
struct addrinfo gai_hints, *gai_result, *gai_iter;
|
|
memset (&gai_hints, 0, sizeof gai_hints);
|
|
gai_hints.ai_socktype = SOCK_STREAM;
|
|
|
|
int err = getaddrinfo (host, port, &gai_hints, &gai_result);
|
|
if (err)
|
|
{
|
|
error_set (e, "%s: %s: %s",
|
|
"connection failed", "getaddrinfo", gai_strerror (err));
|
|
return false;
|
|
}
|
|
|
|
int sockfd;
|
|
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
|
|
{
|
|
sockfd = socket (gai_iter->ai_family,
|
|
gai_iter->ai_socktype, gai_iter->ai_protocol);
|
|
if (sockfd == -1)
|
|
continue;
|
|
set_cloexec (sockfd);
|
|
|
|
int yes = 1;
|
|
soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
|
|
&yes, sizeof yes) != -1);
|
|
|
|
const char *real_host = host;
|
|
|
|
// Let's try to resolve the address back into a real hostname;
|
|
// we don't really need this, so we can let it quietly fail
|
|
char buf[NI_MAXHOST];
|
|
err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
|
|
buf, sizeof buf, NULL, 0, NI_NUMERICHOST);
|
|
if (err)
|
|
LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err));
|
|
else
|
|
real_host = buf;
|
|
|
|
char *address = format_host_port_pair (real_host, port);
|
|
buffer_send_status (ctx, ctx->server_buffer,
|
|
"Connecting to %s...", address);
|
|
free (address);
|
|
|
|
if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
|
|
break;
|
|
|
|
xclose (sockfd);
|
|
}
|
|
|
|
freeaddrinfo (gai_result);
|
|
|
|
if (!gai_iter)
|
|
{
|
|
error_set (e, "connection failed");
|
|
return false;
|
|
}
|
|
|
|
ctx->irc_fd = sockfd;
|
|
return true;
|
|
}
|
|
|
|
// --- More readline funky stuff -----------------------------------------------
|
|
|
|
static char *
|
|
make_unseen_prefix (struct app_context *ctx)
|
|
{
|
|
struct str active_buffers;
|
|
str_init (&active_buffers);
|
|
|
|
size_t i = 0;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
{
|
|
i++;
|
|
if (!iter->unseen_messages_count)
|
|
continue;
|
|
|
|
if (active_buffers.len)
|
|
str_append_c (&active_buffers, ',');
|
|
str_append_printf (&active_buffers, "%zu", i);
|
|
}
|
|
|
|
if (active_buffers.len)
|
|
return str_steal (&active_buffers);
|
|
|
|
str_free (&active_buffers);
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
make_prompt (struct app_context *ctx, struct str *output)
|
|
{
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
if (!soft_assert (buffer))
|
|
return;
|
|
|
|
str_append_c (output, '[');
|
|
|
|
char *unseen_prefix = make_unseen_prefix (ctx);
|
|
if (unseen_prefix)
|
|
str_append_printf (output, "(%s) ", unseen_prefix);
|
|
free (unseen_prefix);
|
|
|
|
str_append_printf (output, "%d:%s",
|
|
buffer_get_index (ctx, buffer), buffer->name);
|
|
if (buffer->type == BUFFER_CHANNEL && *buffer->channel->mode)
|
|
str_append_printf (output, "(%s)", buffer->channel->mode);
|
|
|
|
if (buffer != ctx->global_buffer)
|
|
{
|
|
str_append_c (output, ' ');
|
|
if (ctx->irc_fd == -1)
|
|
str_append (output, "(disconnected)");
|
|
else
|
|
{
|
|
str_append (output, ctx->irc_user->nickname);
|
|
if (*ctx->irc_user_mode)
|
|
str_append_printf (output, "(%s)", ctx->irc_user_mode);
|
|
}
|
|
}
|
|
|
|
str_append_c (output, ']');
|
|
}
|
|
|
|
static void
|
|
refresh_prompt (struct app_context *ctx)
|
|
{
|
|
bool have_attributes = !!get_attribute_printer (stdout);
|
|
|
|
struct str prompt;
|
|
str_init (&prompt);
|
|
make_prompt (ctx, &prompt);
|
|
str_append_c (&prompt, ' ');
|
|
|
|
// After building the new prompt, replace the old one
|
|
free (ctx->readline_prompt);
|
|
|
|
if (!have_attributes)
|
|
ctx->readline_prompt = xstrdup (prompt.str);
|
|
else
|
|
{
|
|
// XXX: to be completely correct, we should use tputs, but we cannot
|
|
const char *prompt_attrs = str_map_find (&ctx->config, ATTR_PROMPT);
|
|
const char *reset_attrs = str_map_find (&ctx->config, ATTR_RESET);
|
|
ctx->readline_prompt = xstrdup_printf ("%c%s%c%s%c%s%c",
|
|
RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE,
|
|
prompt.str,
|
|
RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE);
|
|
}
|
|
str_free (&prompt);
|
|
|
|
// First reset the prompt to work around a bug in readline
|
|
rl_set_prompt ("");
|
|
if (ctx->readline_prompt_shown)
|
|
rl_redisplay ();
|
|
|
|
rl_set_prompt (ctx->readline_prompt);
|
|
if (ctx->readline_prompt_shown)
|
|
rl_redisplay ();
|
|
}
|
|
|
|
static int
|
|
on_readline_goto_buffer (int count, int key)
|
|
{
|
|
(void) count;
|
|
|
|
int n = UNMETA (key) - '0';
|
|
if (n < 0 || n > 9)
|
|
return 0;
|
|
|
|
struct app_context *ctx = g_ctx;
|
|
if (!buffer_goto (ctx, n == 0 ? 10 : n))
|
|
rl_ding ();
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
on_readline_previous_buffer (int count, int key)
|
|
{
|
|
(void) key;
|
|
|
|
struct app_context *ctx = g_ctx;
|
|
if (ctx->current_buffer)
|
|
buffer_activate (ctx, buffer_previous (ctx, count));
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
on_readline_next_buffer (int count, int key)
|
|
{
|
|
(void) key;
|
|
|
|
struct app_context *ctx = g_ctx;
|
|
if (ctx->current_buffer)
|
|
buffer_activate (ctx, buffer_next (ctx, count));
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
on_readline_return (int count, int key)
|
|
{
|
|
(void) count;
|
|
(void) key;
|
|
|
|
struct app_context *ctx = g_ctx;
|
|
|
|
// Let readline pass the line to our input handler
|
|
rl_done = 1;
|
|
|
|
// Save readline state
|
|
int saved_point = rl_point;
|
|
int saved_mark = rl_mark;
|
|
char *saved_line = rl_copy_text (0, rl_end);
|
|
|
|
// Erase the entire line from screen
|
|
rl_set_prompt ("");
|
|
rl_replace_line ("", 0);
|
|
rl_redisplay ();
|
|
ctx->readline_prompt_shown = false;
|
|
|
|
// Restore readline state
|
|
rl_set_prompt (ctx->readline_prompt);
|
|
rl_replace_line (saved_line, 0);
|
|
rl_point = saved_point;
|
|
rl_mark = saved_mark;
|
|
free (saved_line);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
app_readline_bind_meta (char key, rl_command_func_t cb)
|
|
{
|
|
// One of these is going to work
|
|
char keyseq[] = { '\\', 'e', key, 0 };
|
|
rl_bind_key (META (key), cb);
|
|
rl_bind_keyseq (keyseq, cb);
|
|
}
|
|
|
|
static int
|
|
init_readline (void)
|
|
{
|
|
// XXX: maybe use rl_make_bare_keymap() and start from there;
|
|
// our dear user could potentionally rig things up in a way that might
|
|
// result in some funny unspecified behaviour
|
|
|
|
rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1);
|
|
rl_add_defun ("next-buffer", on_readline_next_buffer, -1);
|
|
|
|
// Redefine M-0 through M-9 to switch buffers
|
|
for (int i = 0; i <= 9; i++)
|
|
app_readline_bind_meta ('0' + i, on_readline_goto_buffer);
|
|
|
|
rl_bind_keyseq ("\\C-p", rl_named_function ("previous-buffer"));
|
|
rl_bind_keyseq ("\\C-n", rl_named_function ("next-buffer"));
|
|
app_readline_bind_meta ('p', rl_named_function ("previous-history"));
|
|
app_readline_bind_meta ('n', rl_named_function ("next-history"));
|
|
|
|
// We need to hide the prompt first
|
|
rl_bind_key (RETURN, on_readline_return);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// --- CTCP decoding -----------------------------------------------------------
|
|
|
|
#define CTCP_M_QUOTE '\020'
|
|
#define CTCP_X_DELIM '\001'
|
|
#define CTCP_X_QUOTE '\\'
|
|
|
|
struct ctcp_chunk
|
|
{
|
|
LIST_HEADER (struct ctcp_chunk)
|
|
|
|
bool is_extended; ///< Is this a tagged extended message?
|
|
struct str tag; ///< The tag, if any
|
|
struct str text; ///< Message contents
|
|
};
|
|
|
|
static struct ctcp_chunk *
|
|
ctcp_chunk_new (void)
|
|
{
|
|
struct ctcp_chunk *self = xcalloc (1, sizeof *self);
|
|
str_init (&self->tag);
|
|
str_init (&self->text);
|
|
return self;
|
|
}
|
|
|
|
static void
|
|
ctcp_chunk_destroy (struct ctcp_chunk *self)
|
|
{
|
|
str_free (&self->tag);
|
|
str_free (&self->text);
|
|
free (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
ctcp_low_level_decode (const char *message, struct str *output)
|
|
{
|
|
bool escape = false;
|
|
for (const char *p = message; *p; p++)
|
|
{
|
|
if (escape)
|
|
{
|
|
switch (*p)
|
|
{
|
|
case '0': str_append_c (output, '\0'); break;
|
|
case 'r': str_append_c (output, '\r'); break;
|
|
case 'n': str_append_c (output, '\n'); break;
|
|
default: str_append_c (output, *p);
|
|
}
|
|
escape = false;
|
|
}
|
|
else if (*p == CTCP_M_QUOTE)
|
|
escape = true;
|
|
else
|
|
str_append_c (output, *p);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ctcp_intra_decode (const char *chunk, size_t len, struct str *output)
|
|
{
|
|
bool escape = false;
|
|
for (size_t i = 0; i < len; i++)
|
|
{
|
|
char c = chunk[i];
|
|
if (escape)
|
|
{
|
|
if (c == 'a')
|
|
str_append_c (output, CTCP_X_DELIM);
|
|
else
|
|
str_append_c (output, c);
|
|
escape = false;
|
|
}
|
|
else if (c == CTCP_X_QUOTE)
|
|
escape = true;
|
|
else
|
|
str_append_c (output, c);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ctcp_parse_tagged (const char *chunk, size_t len, struct ctcp_chunk *output)
|
|
{
|
|
// We may search for the space before doing the higher level decoding,
|
|
// as it doesn't concern space characters at all
|
|
size_t tag_end = len;
|
|
for (size_t i = 0; i < len; i++)
|
|
if (chunk[i] == ' ')
|
|
{
|
|
tag_end = i;
|
|
break;
|
|
}
|
|
|
|
output->is_extended = true;
|
|
ctcp_intra_decode (chunk, tag_end, &output->tag);
|
|
if (tag_end++ != len)
|
|
ctcp_intra_decode (chunk + tag_end, len - tag_end, &output->text);
|
|
}
|
|
|
|
static struct ctcp_chunk *
|
|
ctcp_parse (const char *message)
|
|
{
|
|
struct str m;
|
|
str_init (&m);
|
|
ctcp_low_level_decode (message, &m);
|
|
|
|
struct ctcp_chunk *result = NULL, *result_tail = NULL;
|
|
|
|
size_t start = 0;
|
|
bool in_ctcp = false;
|
|
for (size_t i = 0; i < m.len; i++)
|
|
{
|
|
char c = m.str[i];
|
|
if (c != CTCP_X_DELIM)
|
|
continue;
|
|
|
|
// Remember the current state
|
|
size_t my_start = start;
|
|
bool my_is_ctcp = in_ctcp;
|
|
|
|
start = i + 1;
|
|
in_ctcp = !in_ctcp;
|
|
|
|
// Skip empty chunks
|
|
if (my_start == i)
|
|
continue;
|
|
|
|
struct ctcp_chunk *chunk = ctcp_chunk_new ();
|
|
if (my_is_ctcp)
|
|
ctcp_parse_tagged (m.str + my_start, i - my_start, chunk);
|
|
else
|
|
ctcp_intra_decode (m.str + my_start, i - my_start, &chunk->text);
|
|
LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
|
|
}
|
|
|
|
// Finish the last text part. We ignore unended tagged chunks.
|
|
if (!in_ctcp && start != m.len)
|
|
{
|
|
struct ctcp_chunk *chunk = ctcp_chunk_new ();
|
|
ctcp_intra_decode (m.str + start, m.len - start, &chunk->text);
|
|
LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
|
|
}
|
|
|
|
str_free (&m);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
ctcp_destroy (struct ctcp_chunk *list)
|
|
{
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, list)
|
|
ctcp_chunk_destroy (iter);
|
|
}
|
|
|
|
// --- Input handling ----------------------------------------------------------
|
|
|
|
// TODO: we will need a proper mode parser; to be shared with kike
|
|
// TODO: we alse definitely need to parse server capability messages
|
|
|
|
static void
|
|
irc_handle_join (struct app_context *ctx, 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 (ctx, channel_name))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&ctx->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// We've joined a new channel
|
|
if (!channel && irc_is_this_us (ctx, msg->prefix))
|
|
{
|
|
buffer = buffer_new ();
|
|
buffer->type = BUFFER_CHANNEL;
|
|
buffer->name = xstrdup (channel_name);
|
|
buffer->channel = channel =
|
|
irc_make_channel (ctx, xstrdup (channel_name));
|
|
LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
|
|
str_map_set (&ctx->irc_buffer_map, channel->name, buffer);
|
|
|
|
buffer_activate (ctx, buffer);
|
|
}
|
|
|
|
// This is weird, ignoring
|
|
if (!channel)
|
|
return;
|
|
|
|
// Get or make a user object
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&ctx->irc_users, nickname);
|
|
if (!user)
|
|
user = irc_make_user (ctx, nickname);
|
|
else
|
|
{
|
|
user = user_ref (user);
|
|
free (nickname);
|
|
}
|
|
|
|
// Link the user with the channel
|
|
struct user_channel *user_channel = user_channel_new ();
|
|
user_channel->channel = channel;
|
|
LIST_PREPEND (user->channels, user_channel);
|
|
|
|
struct channel_user *channel_user = channel_user_new ();
|
|
channel_user->user = user;
|
|
channel_user->modes = xstrdup ("");
|
|
LIST_PREPEND (channel->users, channel_user);
|
|
|
|
// Finally log the message
|
|
if (buffer)
|
|
{
|
|
buffer_send (ctx, buffer, BUFFER_LINE_JOIN, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.object = irc_to_utf8 (ctx, channel_name));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_kick (struct app_context *ctx, 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 (ctx, channel_name)
|
|
|| irc_is_channel (ctx, target))
|
|
return;
|
|
|
|
const char *message = "";
|
|
if (msg->params.len > 2)
|
|
message = msg->params.vector[2];
|
|
|
|
struct user *user = str_map_find (&ctx->irc_users, target);
|
|
struct channel *channel = str_map_find (&ctx->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be is weird for this to be false
|
|
if (user && channel)
|
|
irc_remove_user_from_channel (user, channel);
|
|
|
|
if (buffer)
|
|
{
|
|
buffer_send (ctx, buffer, BUFFER_LINE_KICK, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.object = irc_to_utf8 (ctx, target),
|
|
.reason = irc_to_utf8 (ctx, message));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_mode (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
// TODO: parse the mode change and apply it
|
|
// TODO: log a message
|
|
}
|
|
|
|
static void
|
|
irc_handle_nick (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 1)
|
|
return;
|
|
|
|
const char *new_nickname = msg->params.vector[0];
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&ctx->irc_users, nickname);
|
|
free (nickname);
|
|
if (!user)
|
|
return;
|
|
|
|
// What the fuck
|
|
// TODO: probably log a message and force a reconnect
|
|
if (str_map_find (&ctx->irc_users, new_nickname))
|
|
return;
|
|
|
|
// Log a message in any PM buffer and rename it;
|
|
// we may even have one for ourselves
|
|
struct buffer *pm_buffer =
|
|
str_map_find (&ctx->irc_buffer_map, user->nickname);
|
|
if (pm_buffer)
|
|
{
|
|
str_map_set (&ctx->irc_buffer_map, new_nickname, pm_buffer);
|
|
str_map_set (&ctx->irc_buffer_map, user->nickname, NULL);
|
|
|
|
buffer_send (ctx, pm_buffer, BUFFER_LINE_NICK, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.object = irc_to_utf8 (ctx, new_nickname));
|
|
// TODO: use a full weechat-style buffer name here
|
|
buffer_rename (ctx, pm_buffer, new_nickname);
|
|
}
|
|
|
|
if (irc_is_this_us (ctx, msg->prefix))
|
|
{
|
|
// Log a message in all open buffers on this server
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &ctx->irc_buffer_map);
|
|
struct buffer *buffer;
|
|
while ((buffer = str_map_iter_next (&iter)))
|
|
{
|
|
// We've already done that
|
|
if (buffer == pm_buffer)
|
|
continue;
|
|
|
|
buffer_send (ctx, buffer, BUFFER_LINE_NICK, 0,
|
|
.object = irc_to_utf8 (ctx, new_nickname));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Log a message in all channels the user is in
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
{
|
|
struct buffer *buffer =
|
|
str_map_find (&ctx->irc_buffer_map, iter->channel->name);
|
|
hard_assert (buffer != NULL);
|
|
buffer_send (ctx, buffer, BUFFER_LINE_NICK, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.object = irc_to_utf8 (ctx, new_nickname));
|
|
}
|
|
}
|
|
|
|
// Finally rename the user
|
|
str_map_set (&ctx->irc_users, new_nickname, user_ref (user));
|
|
str_map_set (&ctx->irc_users, user->nickname, NULL);
|
|
|
|
free (user->nickname);
|
|
user->nickname = xstrdup (new_nickname);
|
|
|
|
// We might have renamed ourselves
|
|
refresh_prompt (ctx);
|
|
}
|
|
|
|
static void
|
|
irc_handle_notice (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
// TODO: log a message
|
|
}
|
|
|
|
static void
|
|
irc_handle_part (struct app_context *ctx, 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 (ctx, channel_name))
|
|
return;
|
|
|
|
const char *message = "";
|
|
if (msg->params.len > 1)
|
|
message = msg->params.vector[1];
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&ctx->irc_users, nickname);
|
|
free (nickname);
|
|
|
|
struct channel *channel = str_map_find (&ctx->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be is weird for this to be false
|
|
if (user && channel)
|
|
irc_remove_user_from_channel (user, channel);
|
|
|
|
if (buffer)
|
|
{
|
|
buffer_send (ctx, buffer, BUFFER_LINE_PART, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.object = irc_to_utf8 (ctx, channel_name),
|
|
.reason = irc_to_utf8 (ctx, message));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_ping (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
if (msg->params.len)
|
|
irc_send (ctx, "PONG :%s", msg->params.vector[0]);
|
|
else
|
|
irc_send (ctx, "PONG");
|
|
}
|
|
|
|
static char *
|
|
ctime_now (char buf[26])
|
|
{
|
|
struct tm tm_;
|
|
time_t now = time (NULL);
|
|
if (!asctime_r (localtime_r (&now, &tm_), buf))
|
|
return NULL;
|
|
|
|
// Annoying thing
|
|
*strchr (buf, '\n') = '\0';
|
|
return buf;
|
|
}
|
|
|
|
static void
|
|
irc_handle_ctcp_request (struct app_context *ctx,
|
|
const struct irc_message *msg, struct ctcp_chunk *chunk)
|
|
{
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
char *nickname_utf8 = irc_to_utf8 (ctx, nickname);
|
|
char *tag_utf8 = irc_to_utf8 (ctx, chunk->tag.str);
|
|
|
|
buffer_send_status (ctx, ctx->server_buffer,
|
|
"CTCP requested by %s: %s", nickname_utf8, tag_utf8);
|
|
|
|
const char *target = msg->params.vector[0];
|
|
const char *recipient = nickname;
|
|
if (irc_is_channel (ctx, target))
|
|
recipient = target;
|
|
|
|
// TODO: log a "CTCP reply to <recipient>: <whatever>
|
|
// Probably abstract the irc_send call to something like
|
|
// irc_send_ctcp_reply (ctx, recipient, fmt, ...)
|
|
|
|
if (!strcmp (chunk->tag.str, "CLIENTINFO"))
|
|
irc_send (ctx, "NOTICE %s :\x01" "CLIENTINFO %s %s %s %s\x01",
|
|
recipient, "PING", "VERSION", "TIME", "CLIENTINFO");
|
|
else if (!strcmp (chunk->tag.str, "PING"))
|
|
irc_send (ctx, "NOTICE %s :\x01" "PING %s\x01",
|
|
recipient, chunk->text.str);
|
|
else if (!strcmp (chunk->tag.str, "VERSION"))
|
|
{
|
|
struct utsname info;
|
|
if (uname (&info))
|
|
LOG_LIBC_FAILURE ("uname");
|
|
else
|
|
irc_send (ctx, "NOTICE %s :\x01" "VERSION %s %s on %s %s\x01",
|
|
recipient, PROGRAM_NAME, PROGRAM_VERSION,
|
|
info.sysname, info.machine);
|
|
}
|
|
else if (!strcmp (chunk->tag.str, "TIME"))
|
|
{
|
|
char buf[26];
|
|
if (!ctime_now (buf))
|
|
LOG_LIBC_FAILURE ("asctime_r");
|
|
else
|
|
irc_send (ctx, "NOTICE %s :\x01" "TIME %s\x01", recipient, buf);
|
|
}
|
|
|
|
free (nickname);
|
|
free (nickname_utf8);
|
|
free (tag_utf8);
|
|
}
|
|
|
|
static struct buffer *
|
|
irc_get_buffer_for_message (struct app_context *ctx,
|
|
const struct irc_message *msg, const char *target)
|
|
{
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, target);
|
|
if (irc_is_channel (ctx, target))
|
|
{
|
|
struct channel *channel = str_map_find (&ctx->irc_channels, target);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// This is weird
|
|
if (!channel)
|
|
return NULL;
|
|
}
|
|
else if (!buffer)
|
|
{
|
|
// Implying that the target is us
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
buffer = irc_get_or_make_user_buffer (ctx, nickname);
|
|
free (nickname);
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
static void
|
|
irc_handle_privmsg_text (struct app_context *ctx,
|
|
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 (ctx, msg, target);
|
|
|
|
if (buffer)
|
|
{
|
|
// TODO: highlights
|
|
enum buffer_line_type type = is_action
|
|
? BUFFER_LINE_ACTION
|
|
: BUFFER_LINE_PRIVMSG;
|
|
buffer_send (ctx, buffer, type, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.text = irc_to_utf8 (ctx, text->str));
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_privmsg (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
// This ignores empty messages which we should never receive anyway
|
|
struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
|
|
LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
|
|
if (!iter->is_extended)
|
|
irc_handle_privmsg_text (ctx, msg, &iter->text, false);
|
|
else if (!strcmp (iter->tag.str, "ACTION"))
|
|
irc_handle_privmsg_text (ctx, msg, &iter->text, true);
|
|
else
|
|
irc_handle_ctcp_request (ctx, msg, iter);
|
|
ctcp_destroy (chunks);
|
|
}
|
|
|
|
static void
|
|
irc_handle_quit (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix)
|
|
return;
|
|
|
|
// What the fuck
|
|
if (irc_is_this_us (ctx, msg->prefix))
|
|
return;
|
|
|
|
char *nickname = irc_cut_nickname (msg->prefix);
|
|
struct user *user = str_map_find (&ctx->irc_users, nickname);
|
|
free (nickname);
|
|
if (!user)
|
|
return;
|
|
|
|
const char *message = "";
|
|
if (msg->params.len > 0)
|
|
message = msg->params.vector[0];
|
|
|
|
// Log a message in any PM buffer
|
|
struct buffer *buffer =
|
|
str_map_find (&ctx->irc_buffer_map, user->nickname);
|
|
if (buffer)
|
|
{
|
|
buffer_send (ctx, buffer, BUFFER_LINE_QUIT, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.reason = irc_to_utf8 (ctx, message));
|
|
|
|
// TODO: set some kind of a flag in the buffer and when the user
|
|
// reappers on a channel (JOIN), log a "is back online" message.
|
|
// Also set this flag when we receive a "no such nick" numeric
|
|
// and reset it when we send something to the buffer.
|
|
}
|
|
|
|
// Log a message in all channels the user is in
|
|
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
|
{
|
|
buffer = str_map_find (&ctx->irc_buffer_map, iter->channel->name);
|
|
if (buffer)
|
|
buffer_send (ctx, buffer, BUFFER_LINE_QUIT, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.reason = irc_to_utf8 (ctx, message));
|
|
|
|
// This destroys "iter" which doesn't matter to us
|
|
irc_remove_user_from_channel (user, iter->channel);
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_handle_topic (struct app_context *ctx, const struct irc_message *msg)
|
|
{
|
|
if (!msg->prefix || msg->params.len < 2)
|
|
return;
|
|
|
|
const char *channel_name = msg->params.vector[0];
|
|
const char *topic = msg->params.vector[1];
|
|
if (!irc_is_channel (ctx, channel_name))
|
|
return;
|
|
|
|
struct channel *channel = str_map_find (&ctx->irc_channels, channel_name);
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, channel_name);
|
|
hard_assert ((channel && buffer) ||
|
|
(channel && !buffer) || (!channel && !buffer));
|
|
|
|
// It would be is weird for this to be false
|
|
if (channel)
|
|
{
|
|
free (channel->topic);
|
|
channel->topic = xstrdup (topic);
|
|
}
|
|
|
|
if (buffer)
|
|
{
|
|
buffer_send (ctx, buffer, BUFFER_LINE_TOPIC, 0,
|
|
.who = irc_to_utf8 (ctx, msg->prefix),
|
|
.text = irc_to_utf8 (ctx, topic));
|
|
}
|
|
}
|
|
|
|
static struct irc_handler
|
|
{
|
|
char *name;
|
|
void (*handler) (struct app_context *ctx, const struct irc_message *msg);
|
|
}
|
|
g_irc_handlers[] =
|
|
{
|
|
// This list needs to stay sorted
|
|
{ "JOIN", irc_handle_join },
|
|
{ "KICK", irc_handle_kick },
|
|
{ "MODE", irc_handle_mode },
|
|
{ "NICK", irc_handle_nick },
|
|
{ "NOTICE", irc_handle_notice },
|
|
{ "PART", irc_handle_part },
|
|
{ "PING", irc_handle_ping },
|
|
{ "PRIVMSG", irc_handle_privmsg },
|
|
{ "QUIT", irc_handle_quit },
|
|
{ "TOPIC", irc_handle_topic },
|
|
};
|
|
|
|
static int
|
|
irc_handler_cmp_by_name (const void *a, const void *b)
|
|
{
|
|
const struct irc_handler *first = a;
|
|
const struct irc_handler *second = b;
|
|
return strcasecmp_ascii (first->name, second->name);
|
|
}
|
|
|
|
static bool
|
|
irc_try_parse_word_for_userhost (struct app_context *ctx, const char *word)
|
|
{
|
|
regex_t re;
|
|
int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
|
|
if (!soft_assert (!err))
|
|
return false;
|
|
|
|
regmatch_t matches[2];
|
|
bool result = false;
|
|
if (!regexec (&re, word, 2, matches, 0))
|
|
{
|
|
free (ctx->irc_user_host);
|
|
ctx->irc_user_host = xstrndup (word + matches[1].rm_so,
|
|
matches[1].rm_eo - matches[1].rm_so);
|
|
result = true;
|
|
}
|
|
regfree (&re);
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
irc_try_parse_welcome_for_userhost (struct app_context *ctx, const char *m)
|
|
{
|
|
struct str_vector v;
|
|
str_vector_init (&v);
|
|
split_str_ignore_empty (m, ' ', &v);
|
|
for (size_t i = 0; i < v.len; i++)
|
|
if (irc_try_parse_word_for_userhost (ctx, v.vector[i]))
|
|
break;
|
|
str_vector_free (&v);
|
|
}
|
|
|
|
static void
|
|
irc_process_numeric (struct app_context *ctx,
|
|
const struct irc_message *msg, unsigned long numeric)
|
|
{
|
|
// Numerics typically have human-readable information
|
|
// TODO: try to output certain replies in more specific buffers
|
|
|
|
// Get rid of the first parameter, if there's any at all,
|
|
// as it contains our nickname and is of no practical use to the user
|
|
struct str_vector copy;
|
|
str_vector_init (©);
|
|
str_vector_add_vector (©, msg->params.vector + !!msg->params.len);
|
|
|
|
// Join the parameter vector back, recode it to our internal encoding
|
|
// and send it to the server buffer
|
|
char *reconstructed = join_str_vector (©, ' ');
|
|
str_vector_free (©);
|
|
buffer_send (ctx, ctx->server_buffer, BUFFER_LINE_STATUS, 0,
|
|
.text = irc_to_utf8 (ctx, reconstructed));
|
|
free (reconstructed);
|
|
|
|
switch (numeric)
|
|
{
|
|
case IRC_RPL_WELCOME:
|
|
// We still issue a USERHOST anyway as this is in general unreliable
|
|
if (msg->params.len == 2)
|
|
irc_try_parse_welcome_for_userhost (ctx, msg->params.vector[1]);
|
|
break;
|
|
case IRC_RPL_ISUPPORT:
|
|
// TODO: parse this, mainly PREFIX; see
|
|
// http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
|
|
break;
|
|
case IRC_RPL_NAMREPLY:
|
|
// TODO: find the channel and if found, push nicks to names_buf
|
|
break;
|
|
case IRC_RPL_ENDOFNAMES:
|
|
// TODO: find the channel and if found, overwrite users;
|
|
// however take care to combine channel user modes
|
|
break;
|
|
case IRC_ERR_NICKNAMEINUSE:
|
|
// TODO: if not connected yet (irc_ready), use a different nick;
|
|
// either use a number suffix, or accept commas in "nickname" config
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
irc_process_message (const struct irc_message *msg,
|
|
const char *raw, void *user_data)
|
|
{
|
|
struct app_context *ctx = user_data;
|
|
|
|
if (g_debug_mode)
|
|
{
|
|
struct app_readline_state state;
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_hide (&state);
|
|
|
|
char *term = irc_to_term (ctx, raw);
|
|
fprintf (stderr, "[IRC] ==> \"%s\"\n", term);
|
|
free (term);
|
|
|
|
if (ctx->readline_prompt_shown)
|
|
app_readline_restore (&state, ctx->readline_prompt);
|
|
}
|
|
|
|
// XXX: or is the 001 numeric enough? For what?
|
|
if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE")
|
|
|| !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD
|
|
|| !strcasecmp (msg->command, "422"))) // ERR_NOMOTD
|
|
{
|
|
// XXX: should we really print this?
|
|
buffer_send_status (ctx, ctx->server_buffer, "Successfully connected");
|
|
ctx->irc_ready = true;
|
|
refresh_prompt (ctx);
|
|
|
|
// TODO: parse any response and store the result for us in app_context;
|
|
// this enables proper message splitting on output;
|
|
// we can also use WHOIS if it's not supported (optional by RFC 2812)
|
|
irc_send (ctx, "USERHOST %s", ctx->irc_user->nickname);
|
|
|
|
const char *autojoin = str_map_find (&ctx->config, "autojoin");
|
|
if (autojoin)
|
|
irc_send (ctx, "JOIN :%s", autojoin);
|
|
}
|
|
|
|
struct irc_handler key = { .name = msg->command };
|
|
struct irc_handler *handler = bsearch (&key, g_irc_handlers,
|
|
N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name);
|
|
if (handler)
|
|
handler->handler (ctx, msg);
|
|
|
|
unsigned long numeric;
|
|
if (xstrtoul (&numeric, msg->command, 10))
|
|
irc_process_numeric (ctx, msg, numeric);
|
|
}
|
|
|
|
// --- Message autosplitting magic ---------------------------------------------
|
|
|
|
// This is the most basic acceptable algorithm; something like ICU with proper
|
|
// locale specification would be needed to make it work better.
|
|
|
|
static size_t
|
|
wrap_text_for_single_line (const char *text, size_t text_len,
|
|
size_t line_len, struct str *output)
|
|
{
|
|
int eaten = 0;
|
|
|
|
// First try going word by word
|
|
const char *word_start;
|
|
const char *word_end = text + strcspn (text, " ");
|
|
size_t word_len = word_end - text;
|
|
while (line_len && word_len <= line_len)
|
|
{
|
|
if (word_len)
|
|
{
|
|
str_append_data (output, text, word_len);
|
|
|
|
text += word_len;
|
|
eaten += word_len;
|
|
line_len -= word_len;
|
|
}
|
|
|
|
// Find the next word's end
|
|
word_start = text + strspn (text, " ");
|
|
word_end = word_start + strcspn (word_start, " ");
|
|
word_len = word_end - text;
|
|
}
|
|
|
|
if (eaten)
|
|
// Discard whitespace between words if split
|
|
return eaten + (word_start - text);
|
|
|
|
// And if that doesn't help, cut the longest valid block of characters
|
|
while (true)
|
|
{
|
|
const char *next = utf8_next (text, text_len - eaten);
|
|
hard_assert (next);
|
|
|
|
size_t char_len = next - text;
|
|
if (char_len > line_len)
|
|
break;
|
|
|
|
str_append_data (output, text, char_len);
|
|
|
|
text += char_len;
|
|
eaten += char_len;
|
|
line_len -= char_len;
|
|
}
|
|
return eaten;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
wrap_message (const char *message,
|
|
int line_max, struct str_vector *output, struct error **e)
|
|
{
|
|
if (line_max <= 0)
|
|
goto error;
|
|
|
|
for (size_t message_left = strlen (message); message_left; )
|
|
{
|
|
struct str m;
|
|
str_init (&m);
|
|
|
|
size_t eaten = wrap_text_for_single_line (message,
|
|
MIN ((size_t) line_max, message_left), message_left, &m);
|
|
if (!eaten)
|
|
{
|
|
str_free (&m);
|
|
goto error;
|
|
}
|
|
|
|
str_vector_add_owned (output, str_steal (&m));
|
|
message += eaten;
|
|
message_left -= eaten;
|
|
}
|
|
return true;
|
|
|
|
error:
|
|
// Well, that's just weird
|
|
error_set (e,
|
|
"Message splitting was unsuccessful as there was "
|
|
"too little room for UTF-8 characters");
|
|
return false;
|
|
}
|
|
|
|
/// Automatically splits messages that arrive at other clients with our prefix
|
|
/// so that they don't arrive cut off by the server
|
|
static bool
|
|
irc_autosplit_message (struct app_context *ctx, const char *message,
|
|
int fixed_part, struct str_vector *output, struct error **e)
|
|
{
|
|
// :<nick>!<user>@<host> <fixed-part><message>
|
|
int space_in_one_message = 0;
|
|
if (ctx->irc_user_host)
|
|
space_in_one_message = 510
|
|
- 1 - (int) strlen (ctx->irc_user->nickname)
|
|
- 1 - (int) strlen (ctx->irc_user_host)
|
|
- 1 - fixed_part;
|
|
|
|
// However we don't always have the full info for message splitting
|
|
if (!space_in_one_message)
|
|
str_vector_add (output, message);
|
|
else if (!wrap_message (message, space_in_one_message, output, e))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
struct send_autosplit_args;
|
|
|
|
typedef void (*send_autosplit_logger_fn) (struct app_context *ctx,
|
|
struct send_autosplit_args *args, struct buffer *buffer, const char *line);
|
|
|
|
struct send_autosplit_args
|
|
{
|
|
const char *command; ///< E.g. PRIVMSG or NOTICE
|
|
const char *target; ///< User or channel
|
|
const char *message; ///< A message to be autosplit
|
|
send_autosplit_logger_fn logger; ///< Logger for all resulting lines
|
|
const char *prefix; ///< E.g. "\x01ACTION"
|
|
const char *suffix; ///< E.g. "\x01"
|
|
};
|
|
|
|
static void
|
|
send_autosplit_message (struct app_context *ctx, struct send_autosplit_args a)
|
|
{
|
|
struct buffer *buffer = str_map_find (&ctx->irc_buffer_map, a.target);
|
|
int fixed_part = strlen (a.command) + 1 + strlen (a.target) + 1 + 1
|
|
+ strlen (a.prefix) + strlen (a.suffix);
|
|
|
|
struct str_vector lines;
|
|
str_vector_init (&lines);
|
|
struct error *e = NULL;
|
|
if (!irc_autosplit_message (ctx, a.message, fixed_part, &lines, &e))
|
|
{
|
|
buffer_send_error (ctx,
|
|
buffer ? buffer : ctx->server_buffer, "%s", e->message);
|
|
error_free (e);
|
|
goto end;
|
|
}
|
|
|
|
for (size_t i = 0; i < lines.len; i++)
|
|
{
|
|
irc_send (ctx, "%s %s :%s%s%s", a.command, a.target,
|
|
a.prefix, lines.vector[i], a.suffix);
|
|
a.logger (ctx, &a, buffer, lines.vector[i]);
|
|
}
|
|
end:
|
|
str_vector_free (&lines);
|
|
}
|
|
|
|
static void
|
|
log_outcoming_action (struct app_context *ctx,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
(void) a;
|
|
|
|
if (buffer)
|
|
buffer_send (ctx, buffer, BUFFER_LINE_ACTION, 0,
|
|
.who = irc_to_utf8 (ctx, ctx->irc_user->nickname),
|
|
.text = irc_to_utf8 (ctx, line));
|
|
|
|
// This can only be sent from a user or channel buffer
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_ACTION(ctx, target, message) \
|
|
send_autosplit_message ((ctx), (struct send_autosplit_args) \
|
|
{ "PRIVMSG", (target), (message), log_outcoming_action, \
|
|
"\x01" "ACTION ", "\x01" })
|
|
|
|
static void
|
|
log_outcoming_privmsg (struct app_context *ctx,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
if (buffer)
|
|
buffer_send (ctx, buffer, BUFFER_LINE_PRIVMSG, 0,
|
|
.who = irc_to_utf8 (ctx, ctx->irc_user->nickname),
|
|
.text = irc_to_utf8 (ctx, line));
|
|
else
|
|
// TODO: fix logging and encoding
|
|
buffer_send (ctx, ctx->server_buffer, BUFFER_LINE_STATUS, 0,
|
|
.text = xstrdup_printf ("MSG(%s): %s", a->target, line));
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_PRIVMSG(ctx, target, message) \
|
|
send_autosplit_message ((ctx), (struct send_autosplit_args) \
|
|
{ "PRIVMSG", (target), (message), log_outcoming_privmsg, "", "" })
|
|
|
|
static void
|
|
log_outcoming_notice (struct app_context *ctx,
|
|
struct send_autosplit_args *a, struct buffer *buffer, const char *line)
|
|
{
|
|
if (buffer)
|
|
buffer_send (ctx, buffer, BUFFER_LINE_NOTICE, 0,
|
|
.who = irc_to_utf8 (ctx, ctx->irc_user->nickname),
|
|
.text = irc_to_utf8 (ctx, line));
|
|
else
|
|
// TODO: fix logging and encoding
|
|
buffer_send (ctx, ctx->server_buffer, BUFFER_LINE_STATUS, 0,
|
|
.text = xstrdup_printf ("Notice -> %s: %s", a->target, line));
|
|
}
|
|
|
|
#define SEND_AUTOSPLIT_NOTICE(ctx, target, message) \
|
|
send_autosplit_message ((ctx), (struct send_autosplit_args) \
|
|
{ "NOTICE", (target), (message), log_outcoming_notice, "", "" })
|
|
|
|
// --- User input handling -----------------------------------------------------
|
|
|
|
static bool handle_command_help (struct app_context *, char *);
|
|
|
|
/// Cuts the longest non-whitespace portion of text and advances the pointer
|
|
static char *
|
|
cut_word (char **s)
|
|
{
|
|
char *start = *s;
|
|
size_t word_len = strcspn (*s, " \t");
|
|
char *end = start + word_len;
|
|
*s = end + strspn (end, " \t");
|
|
*end = '\0';
|
|
return start;
|
|
}
|
|
|
|
static bool
|
|
try_handle_buffer_goto (struct app_context *ctx, const char *word)
|
|
{
|
|
unsigned long n;
|
|
if (!xstrtoul (&n, word, 10))
|
|
return false;
|
|
|
|
if (n > INT_MAX || !buffer_goto (ctx, n))
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "no such buffer", word);
|
|
return true;
|
|
}
|
|
|
|
static struct buffer *
|
|
try_decode_buffer (struct app_context *ctx, const char *word)
|
|
{
|
|
unsigned long n;
|
|
struct buffer *buffer = NULL;
|
|
if (xstrtoul (&n, word, 10) && n <= INT_MAX)
|
|
buffer = buffer_at_index (ctx, n);
|
|
if (!buffer)
|
|
buffer = buffer_by_name (ctx, word);
|
|
// TODO: decode the global and server buffers, partial matches
|
|
return buffer;
|
|
}
|
|
|
|
static bool
|
|
server_command_check (struct app_context *ctx, const char *action)
|
|
{
|
|
if (ctx->current_buffer->type == BUFFER_GLOBAL)
|
|
buffer_send_error (ctx, ctx->current_buffer,
|
|
"Can't do this from a global buffer (%s)", action);
|
|
else if (ctx->irc_fd == -1)
|
|
buffer_send_error (ctx, ctx->server_buffer, "Not connected");
|
|
else
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
show_buffers_list (struct app_context *ctx)
|
|
{
|
|
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
|
|
buffer_send_status (ctx, ctx->global_buffer, "Buffers list:");
|
|
|
|
int i = 1;
|
|
LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
|
|
buffer_send_status (ctx, ctx->global_buffer,
|
|
" [%d] %s", i++, iter->name);
|
|
}
|
|
|
|
static void
|
|
handle_buffer_close (struct app_context *ctx, char *arguments)
|
|
{
|
|
struct buffer *buffer = NULL;
|
|
const char *which = NULL;
|
|
if (!*arguments)
|
|
buffer = ctx->current_buffer;
|
|
else
|
|
buffer = try_decode_buffer (ctx, (which = cut_word (&arguments)));
|
|
|
|
if (!buffer)
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "No such buffer", which);
|
|
else if (buffer == ctx->global_buffer)
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"Can't close the global buffer");
|
|
else if (buffer == ctx->server_buffer)
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"Can't close the server buffer");
|
|
else
|
|
{
|
|
if (buffer == ctx->current_buffer)
|
|
buffer_activate (ctx, buffer_next (ctx, 1));
|
|
buffer_remove (ctx, buffer);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
handle_command_buffer (struct app_context *ctx, char *arguments)
|
|
{
|
|
char *action = cut_word (&arguments);
|
|
if (try_handle_buffer_goto (ctx, action))
|
|
return true;
|
|
|
|
// XXX: also build a prefix map?
|
|
// TODO: some subcommand to print N last lines from the buffer
|
|
if (!strcasecmp_ascii (action, "list"))
|
|
show_buffers_list (ctx);
|
|
else if (!strcasecmp_ascii (action, "clear"))
|
|
{
|
|
// TODO
|
|
}
|
|
else if (!strcasecmp_ascii (action, "move"))
|
|
{
|
|
// TODO: unlink the buffer and link it back at index;
|
|
// we will probably need to extend liberty for this
|
|
}
|
|
else if (!strcasecmp_ascii (action, "close"))
|
|
handle_buffer_close (ctx, arguments);
|
|
else
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_msg (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "send messages"))
|
|
return true;
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&arguments);
|
|
if (!*arguments)
|
|
buffer_send_error (ctx, ctx->server_buffer, "No text to send");
|
|
else
|
|
SEND_AUTOSPLIT_PRIVMSG (ctx, target, arguments);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_query (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "send messages"))
|
|
return true;
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&arguments);
|
|
if (irc_is_channel (ctx, target))
|
|
buffer_send_error (ctx, ctx->server_buffer, "Cannot query a channel");
|
|
else if (!*arguments)
|
|
buffer_send_error (ctx, ctx->server_buffer, "No text to send");
|
|
else
|
|
{
|
|
buffer_activate (ctx, irc_get_or_make_user_buffer (ctx, target));
|
|
SEND_AUTOSPLIT_PRIVMSG (ctx, target, arguments);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_notice (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "send messages"))
|
|
return true;
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&arguments);
|
|
if (!*arguments)
|
|
buffer_send_error (ctx, ctx->server_buffer, "No text to send");
|
|
else
|
|
SEND_AUTOSPLIT_NOTICE (ctx, target, arguments);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_ctcp (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "send messages"))
|
|
return true;
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
char *target = cut_word (&arguments);
|
|
if (!*arguments)
|
|
return false;
|
|
|
|
char *tag = cut_word (&arguments);
|
|
for (char *p = tag; *p; p++)
|
|
*p = toupper_ascii (*p);
|
|
|
|
if (*arguments)
|
|
irc_send (ctx, "PRIVMSG %s :\x01%s %s\x01", target, tag, arguments);
|
|
else
|
|
irc_send (ctx, "PRIVMSG %s :\x01%s\x01", target, tag);
|
|
|
|
buffer_send_status (ctx, ctx->server_buffer,
|
|
"CTCP query to %s: %s", target, tag);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_me (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "send messages"))
|
|
return true;
|
|
|
|
if (ctx->current_buffer->type == BUFFER_CHANNEL)
|
|
SEND_AUTOSPLIT_ACTION (ctx,
|
|
ctx->current_buffer->channel->name, arguments);
|
|
else if (ctx->current_buffer->type == BUFFER_PM)
|
|
SEND_AUTOSPLIT_ACTION (ctx,
|
|
ctx->current_buffer->user->nickname, arguments);
|
|
else
|
|
buffer_send_error (ctx, ctx->server_buffer,
|
|
"Can't do this from a server buffer (%s)",
|
|
"send CTCP actions");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_quit (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (ctx->irc_fd != -1)
|
|
{
|
|
if (*arguments)
|
|
irc_send (ctx, "QUIT :%s", arguments);
|
|
else
|
|
irc_send (ctx, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
|
|
}
|
|
initiate_quit (ctx);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_join (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "join"))
|
|
return true;
|
|
|
|
if (*arguments)
|
|
// TODO: check if the arguments are in the form of
|
|
// "channel(,channel)* key(,key)*"
|
|
irc_send (ctx, "JOIN %s", arguments);
|
|
else
|
|
{
|
|
if (ctx->current_buffer->type != BUFFER_CHANNEL)
|
|
buffer_send_error (ctx, ctx->current_buffer,
|
|
"%s: %s", "Can't join",
|
|
"no argument given and this buffer is not a channel");
|
|
// TODO: have a better way of checking if we're on the channel
|
|
else if (ctx->current_buffer->channel->users)
|
|
buffer_send_error (ctx, ctx->current_buffer,
|
|
"%s: %s", "Can't join",
|
|
"you already are on the channel");
|
|
else
|
|
// TODO: send the key if known
|
|
irc_send (ctx, "JOIN %s", ctx->current_buffer->channel->name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_part (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "part"))
|
|
return true;
|
|
|
|
if (*arguments)
|
|
// TODO: check if the arguments are in the form of "channel(,channel)*"
|
|
// TODO: make sure to send the reason as one argument
|
|
irc_send (ctx, "PART %s", arguments);
|
|
else
|
|
{
|
|
if (ctx->current_buffer->type != BUFFER_CHANNEL)
|
|
buffer_send_error (ctx, ctx->current_buffer,
|
|
"%s: %s", "Can't part",
|
|
"no argument given and this buffer is not a channel");
|
|
// TODO: have a better way of checking if we're on the channel
|
|
else if (!ctx->current_buffer->channel->users)
|
|
buffer_send_error (ctx, ctx->current_buffer,
|
|
"%s: %s", "Can't join", "you're not on the channel");
|
|
else
|
|
irc_send (ctx, "PART %s", ctx->current_buffer->channel->name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_list (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "list channels"))
|
|
return true;
|
|
|
|
if (*arguments)
|
|
irc_send (ctx, "LIST %s", arguments);
|
|
else
|
|
irc_send (ctx, "LIST");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_command_quote (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!server_command_check (ctx, "quote"))
|
|
return true;
|
|
|
|
irc_send (ctx, "%s", arguments);
|
|
return true;
|
|
}
|
|
|
|
static struct command_handler
|
|
{
|
|
const char *name;
|
|
bool (*handler) (struct app_context *ctx, char *arguments);
|
|
const char *description;
|
|
const char *usage;
|
|
}
|
|
g_command_handlers[] =
|
|
{
|
|
{ "help", handle_command_help, "Show help",
|
|
"[command]" },
|
|
{ "quit", handle_command_quit, "Quit the program",
|
|
"[message]" },
|
|
{ "buffer", handle_command_buffer, "Manage buffers",
|
|
"list | clear | move | { close [<number> | <name>] } | <number>" },
|
|
|
|
{ "msg", handle_command_msg, "Send message to a nick or channel",
|
|
"<target> <message>" },
|
|
{ "query", handle_command_query, "Send a private message to a nick",
|
|
"<nick> <message>" },
|
|
{ "notice", handle_command_notice, "Send notice to a nick or channel",
|
|
"<target> <message>" },
|
|
{ "ctcp", handle_command_ctcp, "Send a CTCP query",
|
|
"<target> <tag>" },
|
|
{ "me", handle_command_me, "Send a CTCP action",
|
|
"<message>" },
|
|
|
|
{ "join", handle_command_join, "Join channels",
|
|
"[<channel>[,<channel>...]]" },
|
|
{ "part", handle_command_part, "Leave channels",
|
|
"[<channel>[,<channel>...]]" },
|
|
#if 0
|
|
{ "cycle", NULL, "", "" },
|
|
|
|
{ "mode", NULL, "", "" },
|
|
{ "topic", NULL, "", "" },
|
|
{ "kick", NULL, "", "" },
|
|
{ "kickban", NULL, "", "" },
|
|
{ "ban", NULL, "", "" },
|
|
{ "invite", NULL, "", "" },
|
|
#endif
|
|
|
|
{ "list", handle_command_list, "List channels and their topic",
|
|
"[<channel>[,<channel>...]] [server]" },
|
|
#if 0
|
|
{ "names", NULL, "", "" },
|
|
{ "who", NULL, "", "" },
|
|
{ "whois", NULL, "", "" },
|
|
|
|
{ "motd", NULL, "", "" },
|
|
{ "away", NULL, "", "" },
|
|
#endif
|
|
{ "quote", handle_command_quote, "Send a raw command to the server",
|
|
"command" },
|
|
};
|
|
|
|
static bool
|
|
handle_command_help (struct app_context *ctx, char *arguments)
|
|
{
|
|
if (!*arguments)
|
|
{
|
|
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
|
|
buffer_send_status (ctx, ctx->global_buffer, "Commands:");
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
buffer_send_status (ctx, ctx->global_buffer, " %s: %s",
|
|
handler->name, handler->description);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
char *command = cut_word (&arguments);
|
|
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
|
{
|
|
struct command_handler *handler = &g_command_handlers[i];
|
|
if (!strcasecmp_ascii (command, handler->name))
|
|
{
|
|
buffer_send_status (ctx, ctx->global_buffer, "%s", "");
|
|
buffer_send_status (ctx, ctx->global_buffer, "%s: %s",
|
|
handler->name, handler->description);
|
|
buffer_send_status (ctx, ctx->global_buffer, " Arguments: %s",
|
|
handler->usage);
|
|
return true;
|
|
}
|
|
}
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "No such command", command);
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
command_handler_cmp_by_length (const void *a, const void *b)
|
|
{
|
|
const struct command_handler *first = a;
|
|
const struct command_handler *second = b;
|
|
return strlen (first->name) - strlen (second->name);
|
|
}
|
|
|
|
static void
|
|
init_partial_matching_user_command_map (struct str_map *partial)
|
|
{
|
|
// Trivially create a partial matching map
|
|
str_map_init (partial);
|
|
partial->key_xfrm = tolower_ascii_strxfrm;
|
|
|
|
// We process them from the longest to the shortest one,
|
|
// so that common prefixes favor shorter entries
|
|
struct command_handler *by_length[N_ELEMENTS (g_command_handlers)];
|
|
for (size_t i = 0; i < N_ELEMENTS (by_length); i++)
|
|
by_length[i] = &g_command_handlers[i];
|
|
qsort (by_length, N_ELEMENTS (by_length), sizeof *by_length,
|
|
command_handler_cmp_by_length);
|
|
|
|
for (size_t i = N_ELEMENTS (by_length); i--; )
|
|
{
|
|
char *copy = xstrdup (by_length[i]->name);
|
|
for (size_t part = strlen (copy); part; part--)
|
|
{
|
|
copy[part] = '\0';
|
|
str_map_set (partial, copy, by_length[i]);
|
|
}
|
|
free (copy);
|
|
}
|
|
}
|
|
|
|
static void
|
|
process_user_command (struct app_context *ctx, char *command)
|
|
{
|
|
static bool initialized = false;
|
|
static struct str_map partial;
|
|
if (!initialized)
|
|
{
|
|
init_partial_matching_user_command_map (&partial);
|
|
initialized = true;
|
|
}
|
|
|
|
char *name = cut_word (&command);
|
|
if (try_handle_buffer_goto (ctx, name))
|
|
return;
|
|
|
|
struct command_handler *handler = str_map_find (&partial, name);
|
|
if (!handler)
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: %s", "No such command", name);
|
|
else if (!handler->handler (ctx, command))
|
|
buffer_send_error (ctx, ctx->global_buffer,
|
|
"%s: /%s %s", "Usage", handler->name, handler->usage);
|
|
}
|
|
|
|
static void
|
|
send_message_to_target (struct app_context *ctx,
|
|
const char *target, char *message, struct buffer *buffer)
|
|
{
|
|
if (ctx->irc_fd == -1)
|
|
{
|
|
buffer_send_error (ctx, buffer, "Not connected");
|
|
return;
|
|
}
|
|
|
|
SEND_AUTOSPLIT_PRIVMSG (ctx, target, message);
|
|
}
|
|
|
|
static void
|
|
send_message_to_current_buffer (struct app_context *ctx, char *message)
|
|
{
|
|
struct buffer *buffer = ctx->current_buffer;
|
|
hard_assert (buffer != NULL);
|
|
|
|
switch (buffer->type)
|
|
{
|
|
case BUFFER_GLOBAL:
|
|
case BUFFER_SERVER:
|
|
buffer_send_error (ctx, buffer, "This buffer is not a channel");
|
|
break;
|
|
case BUFFER_CHANNEL:
|
|
send_message_to_target (ctx, buffer->channel->name, message, buffer);
|
|
break;
|
|
case BUFFER_PM:
|
|
send_message_to_target (ctx, buffer->user->nickname, message, buffer);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
process_input (struct app_context *ctx, char *user_input)
|
|
{
|
|
char *input;
|
|
size_t len;
|
|
|
|
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
|
|
print_error ("character conversion failed for `%s'", "user input");
|
|
else if (input[0] != '/')
|
|
send_message_to_current_buffer (ctx, input);
|
|
else if (input[1] == '/')
|
|
send_message_to_current_buffer (ctx, input + 1);
|
|
else
|
|
process_user_command (ctx, input + 1);
|
|
|
|
free (input);
|
|
}
|
|
|
|
// --- Supporting code (continued) ---------------------------------------------
|
|
|
|
enum irc_read_result
|
|
{
|
|
IRC_READ_OK, ///< Some data were read successfully
|
|
IRC_READ_EOF, ///< The server has closed connection
|
|
IRC_READ_AGAIN, ///< No more data at the moment
|
|
IRC_READ_ERROR ///< General connection failure
|
|
};
|
|
|
|
static enum irc_read_result
|
|
irc_fill_read_buffer_ssl (struct app_context *ctx, struct str *buf)
|
|
{
|
|
int n_read;
|
|
start:
|
|
n_read = SSL_read (ctx->ssl, buf->str + buf->len,
|
|
buf->alloc - buf->len - 1 /* null byte */);
|
|
|
|
const char *error_info = NULL;
|
|
switch (xssl_get_error (ctx->ssl, n_read, &error_info))
|
|
{
|
|
case SSL_ERROR_NONE:
|
|
buf->str[buf->len += n_read] = '\0';
|
|
return IRC_READ_OK;
|
|
case SSL_ERROR_ZERO_RETURN:
|
|
return IRC_READ_EOF;
|
|
case SSL_ERROR_WANT_READ:
|
|
return IRC_READ_AGAIN;
|
|
case SSL_ERROR_WANT_WRITE:
|
|
{
|
|
// Let it finish the handshake as we don't poll for writability;
|
|
// any errors are to be collected by SSL_read() in the next iteration
|
|
struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT };
|
|
soft_assert (poll (&pfd, 1, 0) > 0);
|
|
goto start;
|
|
}
|
|
case XSSL_ERROR_TRY_AGAIN:
|
|
goto start;
|
|
default:
|
|
LOG_FUNC_FAILURE ("SSL_read", error_info);
|
|
return IRC_READ_ERROR;
|
|
}
|
|
}
|
|
|
|
static enum irc_read_result
|
|
irc_fill_read_buffer (struct app_context *ctx, struct str *buf)
|
|
{
|
|
ssize_t n_read;
|
|
start:
|
|
n_read = recv (ctx->irc_fd, buf->str + buf->len,
|
|
buf->alloc - buf->len - 1 /* null byte */, 0);
|
|
|
|
if (n_read > 0)
|
|
{
|
|
buf->str[buf->len += n_read] = '\0';
|
|
return IRC_READ_OK;
|
|
}
|
|
if (n_read == 0)
|
|
return IRC_READ_EOF;
|
|
|
|
if (errno == EAGAIN)
|
|
return IRC_READ_AGAIN;
|
|
if (errno == EINTR)
|
|
goto start;
|
|
|
|
LOG_LIBC_FAILURE ("recv");
|
|
return IRC_READ_ERROR;
|
|
}
|
|
|
|
static bool irc_connect (struct app_context *, struct error **);
|
|
static void irc_queue_reconnect (struct app_context *);
|
|
|
|
static void
|
|
irc_cancel_timers (struct app_context *ctx)
|
|
{
|
|
poller_timer_reset (&ctx->timeout_tmr);
|
|
poller_timer_reset (&ctx->ping_tmr);
|
|
poller_timer_reset (&ctx->reconnect_tmr);
|
|
}
|
|
|
|
static void
|
|
on_irc_reconnect_timeout (void *user_data)
|
|
{
|
|
struct app_context *ctx = user_data;
|
|
|
|
struct error *e = NULL;
|
|
if (irc_connect (ctx, &e))
|
|
return;
|
|
|
|
buffer_send_error (ctx, ctx->server_buffer, "%s", e->message);
|
|
error_free (e);
|
|
irc_queue_reconnect (ctx);
|
|
}
|
|
|
|
static void
|
|
irc_queue_reconnect (struct app_context *ctx)
|
|
{
|
|
// TODO: exponentional backoff
|
|
hard_assert (ctx->irc_fd == -1);
|
|
buffer_send_status (ctx, ctx->server_buffer,
|
|
"Trying to reconnect in %ld seconds...", ctx->reconnect_delay);
|
|
poller_timer_set (&ctx->reconnect_tmr, ctx->reconnect_delay * 1000);
|
|
}
|
|
|
|
static void
|
|
on_irc_disconnected (struct app_context *ctx)
|
|
{
|
|
// Get rid of the dead socket and related things
|
|
if (ctx->ssl)
|
|
{
|
|
SSL_free (ctx->ssl);
|
|
ctx->ssl = NULL;
|
|
SSL_CTX_free (ctx->ssl_ctx);
|
|
ctx->ssl_ctx = NULL;
|
|
}
|
|
|
|
xclose (ctx->irc_fd);
|
|
ctx->irc_fd = -1;
|
|
ctx->irc_ready = false;
|
|
|
|
user_unref (ctx->irc_user);
|
|
ctx->irc_user = NULL;
|
|
|
|
free (ctx->irc_user_mode);
|
|
ctx->irc_user_mode = NULL;
|
|
free (ctx->irc_user_host);
|
|
ctx->irc_user_host = NULL;
|
|
|
|
ctx->irc_event.closed = true;
|
|
poller_fd_reset (&ctx->irc_event);
|
|
|
|
// All of our timers have lost their meaning now
|
|
irc_cancel_timers (ctx);
|
|
|
|
if (ctx->quitting)
|
|
try_finish_quit (ctx);
|
|
else if (!ctx->reconnect)
|
|
// XXX: not sure if we want this in a client
|
|
initiate_quit (ctx);
|
|
else
|
|
irc_queue_reconnect (ctx);
|
|
}
|
|
|
|
static void
|
|
on_irc_ping_timeout (void *user_data)
|
|
{
|
|
struct app_context *ctx = user_data;
|
|
buffer_send_error (ctx, ctx->server_buffer, "Connection timeout");
|
|
on_irc_disconnected (ctx);
|
|
}
|
|
|
|
static void
|
|
on_irc_timeout (void *user_data)
|
|
{
|
|
// Provoke a response from the server
|
|
struct app_context *ctx = user_data;
|
|
irc_send (ctx, "PING :%s",
|
|
(char *) str_map_find (&ctx->config, "nickname"));
|
|
}
|
|
|
|
static void
|
|
irc_reset_connection_timeouts (struct app_context *ctx)
|
|
{
|
|
irc_cancel_timers (ctx);
|
|
poller_timer_set (&ctx->timeout_tmr, 3 * 60 * 1000);
|
|
poller_timer_set (&ctx->ping_tmr, (3 * 60 + 30) * 1000);
|
|
}
|
|
|
|
static void
|
|
on_irc_readable (const struct pollfd *fd, struct app_context *ctx)
|
|
{
|
|
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
|
|
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
|
|
|
|
(void) set_blocking (ctx->irc_fd, false);
|
|
|
|
struct str *buf = &ctx->read_buffer;
|
|
enum irc_read_result (*fill_buffer)(struct app_context *, struct str *)
|
|
= ctx->ssl
|
|
? irc_fill_read_buffer_ssl
|
|
: irc_fill_read_buffer;
|
|
bool disconnected = false;
|
|
while (true)
|
|
{
|
|
str_ensure_space (buf, 512);
|
|
switch (fill_buffer (ctx, buf))
|
|
{
|
|
case IRC_READ_AGAIN:
|
|
goto end;
|
|
case IRC_READ_ERROR:
|
|
buffer_send_error (ctx, ctx->server_buffer,
|
|
"Reading from the IRC server failed");
|
|
disconnected = true;
|
|
goto end;
|
|
case IRC_READ_EOF:
|
|
buffer_send_error (ctx, ctx->server_buffer,
|
|
"The IRC server closed the connection");
|
|
disconnected = true;
|
|
goto end;
|
|
case IRC_READ_OK:
|
|
break;
|
|
}
|
|
|
|
if (buf->len >= (1 << 20))
|
|
{
|
|
buffer_send_error (ctx, ctx->server_buffer,
|
|
"The IRC server seems to spew out data frantically");
|
|
irc_shutdown (ctx);
|
|
goto end;
|
|
}
|
|
}
|
|
end:
|
|
(void) set_blocking (ctx->irc_fd, true);
|
|
irc_process_buffer (buf, irc_process_message, ctx);
|
|
|
|
if (disconnected)
|
|
on_irc_disconnected (ctx);
|
|
else
|
|
irc_reset_connection_timeouts (ctx);
|
|
}
|
|
|
|
static bool
|
|
irc_connect (struct app_context *ctx, struct error **e)
|
|
{
|
|
const char *irc_host = str_map_find (&ctx->config, "irc_host");
|
|
const char *irc_port = str_map_find (&ctx->config, "irc_port");
|
|
|
|
const char *socks_host = str_map_find (&ctx->config, "socks_host");
|
|
const char *socks_port = str_map_find (&ctx->config, "socks_port");
|
|
const char *socks_username = str_map_find (&ctx->config, "socks_username");
|
|
const char *socks_password = str_map_find (&ctx->config, "socks_password");
|
|
|
|
const char *nickname = str_map_find (&ctx->config, "nickname");
|
|
const char *username = str_map_find (&ctx->config, "username");
|
|
const char *realname = str_map_find (&ctx->config, "realname");
|
|
|
|
// We have a default value for these
|
|
hard_assert (irc_port && socks_port);
|
|
|
|
// These are filled automatically if needed
|
|
hard_assert (nickname && username && realname);
|
|
|
|
// TODO: again, get rid of `struct error' in here. The question is: how
|
|
// do we tell our caller that he should not try to reconnect?
|
|
bool use_ssl;
|
|
if (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e))
|
|
return false;
|
|
|
|
if (socks_host)
|
|
{
|
|
char *address = format_host_port_pair (irc_host, irc_port);
|
|
char *socks_address = format_host_port_pair (socks_host, socks_port);
|
|
buffer_send_status (ctx, ctx->server_buffer,
|
|
"Connecting to %s via %s...", address, socks_address);
|
|
free (socks_address);
|
|
free (address);
|
|
|
|
struct error *error = NULL;
|
|
int fd = socks_connect (socks_host, socks_port, irc_host, irc_port,
|
|
socks_username, socks_password, &error);
|
|
if (fd == -1)
|
|
{
|
|
error_set (e, "%s: %s", "SOCKS connection failed", error->message);
|
|
error_free (error);
|
|
return false;
|
|
}
|
|
ctx->irc_fd = fd;
|
|
}
|
|
else if (!irc_establish_connection (ctx, irc_host, irc_port, e))
|
|
return false;
|
|
|
|
if (use_ssl && !irc_initialize_ssl (ctx, e))
|
|
{
|
|
xclose (ctx->irc_fd);
|
|
ctx->irc_fd = -1;
|
|
return false;
|
|
}
|
|
buffer_send_status (ctx, ctx->server_buffer, "Connection established");
|
|
|
|
poller_fd_init (&ctx->irc_event, &ctx->poller, ctx->irc_fd);
|
|
ctx->irc_event.dispatcher = (poller_fd_fn) on_irc_readable;
|
|
ctx->irc_event.user_data = ctx;
|
|
|
|
poller_fd_set (&ctx->irc_event, POLLIN);
|
|
irc_reset_connection_timeouts (ctx);
|
|
|
|
irc_send (ctx, "NICK %s", nickname);
|
|
irc_send (ctx, "USER %s 8 * :%s", username, realname);
|
|
|
|
// XXX: maybe we should wait for the first message from the server
|
|
ctx->irc_user = irc_make_user (ctx, xstrdup (nickname));
|
|
ctx->irc_user_mode = xstrdup ("");
|
|
ctx->irc_user_host = NULL;
|
|
return true;
|
|
}
|
|
|
|
// --- I/O event handlers ------------------------------------------------------
|
|
|
|
static void
|
|
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
|
|
{
|
|
char dummy;
|
|
(void) read (fd->fd, &dummy, 1);
|
|
|
|
if (g_termination_requested && !ctx->quitting)
|
|
{
|
|
// There may be a timer set to reconnect to the server
|
|
irc_cancel_timers (ctx);
|
|
|
|
if (ctx->irc_fd != -1)
|
|
irc_send (ctx, "QUIT :Terminated by signal");
|
|
initiate_quit (ctx);
|
|
}
|
|
|
|
if (g_winch_received)
|
|
{
|
|
// 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 ();
|
|
rl_get_screen_size (&ctx->lines, &ctx->columns);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
|
|
{
|
|
(void) ctx;
|
|
|
|
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
|
|
print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
|
|
|
|
rl_callback_read_char ();
|
|
}
|
|
|
|
static void
|
|
on_readline_input (char *line)
|
|
{
|
|
if (line)
|
|
{
|
|
if (*line)
|
|
add_history (line);
|
|
|
|
process_input (g_ctx, line);
|
|
free (line);
|
|
}
|
|
else
|
|
{
|
|
app_readline_erase_to_bol (g_ctx->readline_prompt);
|
|
rl_ding ();
|
|
}
|
|
|
|
// initiate_quit() disables readline; we just wait then
|
|
if (!g_ctx->quitting)
|
|
g_ctx->readline_prompt_shown = true;
|
|
}
|
|
|
|
// --- Configuration loading ---------------------------------------------------
|
|
|
|
static bool
|
|
read_hexa_escape (const char **cursor, struct str *output)
|
|
{
|
|
int i;
|
|
char c, code = 0;
|
|
|
|
for (i = 0; i < 2; i++)
|
|
{
|
|
c = tolower (*(*cursor));
|
|
if (c >= '0' && c <= '9')
|
|
code = (code << 4) | (c - '0');
|
|
else if (c >= 'a' && c <= 'f')
|
|
code = (code << 4) | (c - 'a' + 10);
|
|
else
|
|
break;
|
|
|
|
(*cursor)++;
|
|
}
|
|
|
|
if (!i)
|
|
return false;
|
|
|
|
str_append_c (output, code);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
read_octal_escape (const char **cursor, struct str *output)
|
|
{
|
|
int i;
|
|
char c, code = 0;
|
|
|
|
for (i = 0; i < 3; i++)
|
|
{
|
|
c = *(*cursor);
|
|
if (c < '0' || c > '7')
|
|
break;
|
|
|
|
code = (code << 3) | (c - '0');
|
|
(*cursor)++;
|
|
}
|
|
|
|
if (!i)
|
|
return false;
|
|
|
|
str_append_c (output, code);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
read_string_escape_sequence (const char **cursor,
|
|
struct str *output, struct error **e)
|
|
{
|
|
int c;
|
|
switch ((c = *(*cursor)++))
|
|
{
|
|
case '?': str_append_c (output, '?'); break;
|
|
case '"': str_append_c (output, '"'); break;
|
|
case '\\': str_append_c (output, '\\'); break;
|
|
case 'a': str_append_c (output, '\a'); break;
|
|
case 'b': str_append_c (output, '\b'); break;
|
|
case 'f': str_append_c (output, '\f'); break;
|
|
case 'n': str_append_c (output, '\n'); break;
|
|
case 'r': str_append_c (output, '\r'); break;
|
|
case 't': str_append_c (output, '\t'); break;
|
|
case 'v': str_append_c (output, '\v'); break;
|
|
|
|
case 'e':
|
|
case 'E':
|
|
str_append_c (output, '\x1b');
|
|
break;
|
|
|
|
case 'x':
|
|
case 'X':
|
|
if (!read_hexa_escape (cursor, output))
|
|
FAIL ("invalid hexadecimal escape");
|
|
break;
|
|
|
|
case '\0':
|
|
FAIL ("premature end of escape sequence");
|
|
|
|
default:
|
|
(*cursor)--;
|
|
if (!read_octal_escape (cursor, output))
|
|
FAIL ("unknown escape sequence");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
unescape_string (const char *s, struct str *output, struct error **e)
|
|
{
|
|
int c;
|
|
while ((c = *s++))
|
|
{
|
|
if (c != '\\')
|
|
str_append_c (output, c);
|
|
else if (!read_string_escape_sequence (&s, output, e))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
autofill_user_info (struct app_context *ctx, struct error **e)
|
|
{
|
|
const char *nickname = str_map_find (&ctx->config, "nickname");
|
|
const char *username = str_map_find (&ctx->config, "username");
|
|
const char *realname = str_map_find (&ctx->config, "realname");
|
|
|
|
if (nickname && username && realname)
|
|
return true;
|
|
|
|
// Read POSIX user info and fill the configuration if needed
|
|
struct passwd *pwd = getpwuid (geteuid ());
|
|
if (!pwd)
|
|
FAIL ("cannot retrieve user information: %s", strerror (errno));
|
|
|
|
if (!nickname)
|
|
str_map_set (&ctx->config, "nickname", xstrdup (pwd->pw_name));
|
|
if (!username)
|
|
str_map_set (&ctx->config, "username", xstrdup (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';
|
|
|
|
str_map_set (&ctx->config, "realname", xstrdup (gecos));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
unescape_config (struct str_map *input, struct str_map *output, struct error **e)
|
|
{
|
|
struct error *error = NULL;
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, input);
|
|
while (str_map_iter_next (&iter))
|
|
{
|
|
struct str value;
|
|
str_init (&value);
|
|
if (!unescape_string (iter.link->data, &value, &error))
|
|
{
|
|
error_set (e, "error reading configuration: %s: %s",
|
|
iter.link->key, error->message);
|
|
error_free (error);
|
|
return false;
|
|
}
|
|
|
|
str_map_set (output, iter.link->key, str_steal (&value));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
load_config (struct app_context *ctx, struct error **e)
|
|
{
|
|
// TODO: employ a better configuration file format, so that we don't have
|
|
// to do this convoluted post-processing anymore.
|
|
|
|
struct str_map map;
|
|
str_map_init (&map);
|
|
map.free = free;
|
|
|
|
bool success = read_config_file (&map, e) &&
|
|
unescape_config (&map, &ctx->config, e) &&
|
|
autofill_user_info (ctx, e);
|
|
str_map_free (&map);
|
|
if (!success)
|
|
return false;
|
|
|
|
const char *irc_host = str_map_find (&ctx->config, "irc_host");
|
|
if (!irc_host)
|
|
{
|
|
error_set (e, "no hostname specified in configuration");
|
|
return false;
|
|
}
|
|
|
|
if (!irc_get_boolean_from_config (ctx,
|
|
"reconnect", &ctx->reconnect, e)
|
|
|| !irc_get_boolean_from_config (ctx,
|
|
"isolate_buffers", &ctx->isolate_buffers, e))
|
|
return false;
|
|
|
|
const char *delay_str = str_map_find (&ctx->config, "reconnect_delay");
|
|
hard_assert (delay_str != NULL); // We have a default value for this
|
|
if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10))
|
|
{
|
|
error_set (e, "invalid configuration value for `%s'",
|
|
"reconnect_delay");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- Main program ------------------------------------------------------------
|
|
|
|
static void
|
|
init_poller_events (struct app_context *ctx)
|
|
{
|
|
poller_timer_init (&ctx->timeout_tmr, &ctx->poller);
|
|
ctx->timeout_tmr.dispatcher = on_irc_timeout;
|
|
ctx->timeout_tmr.user_data = ctx;
|
|
|
|
poller_timer_init (&ctx->ping_tmr, &ctx->poller);
|
|
ctx->ping_tmr.dispatcher = on_irc_ping_timeout;
|
|
ctx->ping_tmr.user_data = ctx;
|
|
|
|
poller_timer_init (&ctx->reconnect_tmr, &ctx->poller);
|
|
ctx->reconnect_tmr.dispatcher = on_irc_reconnect_timeout;
|
|
ctx->reconnect_tmr.user_data = ctx;
|
|
|
|
poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]);
|
|
ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
|
|
ctx->signal_event.user_data = ctx;
|
|
poller_fd_set (&ctx->signal_event, POLLIN);
|
|
|
|
poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO);
|
|
ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
|
|
ctx->tty_event.user_data = &ctx;
|
|
poller_fd_set (&ctx->tty_event, POLLIN);
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
// We include a generated file from kike including this array we don't use;
|
|
// let's just keep it there and silence the compiler warning instead
|
|
(void) g_default_replies;
|
|
|
|
static const struct opt opts[] =
|
|
{
|
|
{ 'd', "debug", NULL, 0, "run in debug mode" },
|
|
{ 'h', "help", NULL, 0, "display this help and exit" },
|
|
{ 'V', "version", NULL, 0, "output version information and exit" },
|
|
{ 'w', "write-default-cfg", "FILENAME",
|
|
OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
|
|
"write a default configuration file and exit" },
|
|
{ 0, NULL, NULL, 0, NULL }
|
|
};
|
|
|
|
struct opt_handler oh;
|
|
opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental IRC client.");
|
|
|
|
int c;
|
|
while ((c = opt_handler_get (&oh)) != -1)
|
|
switch (c)
|
|
{
|
|
case 'd':
|
|
g_debug_mode = true;
|
|
break;
|
|
case 'h':
|
|
opt_handler_usage (&oh, stdout);
|
|
exit (EXIT_SUCCESS);
|
|
case 'V':
|
|
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
|
|
exit (EXIT_SUCCESS);
|
|
case 'w':
|
|
call_write_default_config (optarg, g_config_table);
|
|
exit (EXIT_SUCCESS);
|
|
default:
|
|
print_error ("wrong options");
|
|
opt_handler_usage (&oh, stderr);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
opt_handler_free (&oh);
|
|
|
|
print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");
|
|
|
|
// We only need to convert to and from the terminal encoding
|
|
setlocale (LC_CTYPE, "");
|
|
|
|
struct app_context ctx;
|
|
app_context_init (&ctx);
|
|
g_ctx = &ctx;
|
|
|
|
SSL_library_init ();
|
|
atexit (EVP_cleanup);
|
|
SSL_load_error_strings ();
|
|
atexit (ERR_free_strings);
|
|
|
|
using_history ();
|
|
// This can cause memory leaks, or maybe even a segfault. Funny, eh?
|
|
stifle_history (HISTORY_LIMIT);
|
|
|
|
setup_signal_handlers ();
|
|
|
|
struct error *e = NULL;
|
|
if (!load_config (&ctx, &e))
|
|
{
|
|
print_error ("%s", e->message);
|
|
error_free (e);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
init_colors (&ctx);
|
|
init_poller_events (&ctx);
|
|
init_buffers (&ctx);
|
|
ctx.current_buffer = ctx.server_buffer;
|
|
refresh_prompt (&ctx);
|
|
|
|
// TODO: connect asynchronously (first step towards multiple servers)
|
|
if (!irc_connect (&ctx, &e))
|
|
{
|
|
buffer_send_error (&ctx, ctx.server_buffer, "%s", e->message);
|
|
error_free (e);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
rl_startup_hook = init_readline;
|
|
rl_catch_sigwinch = false;
|
|
rl_callback_handler_install (ctx.readline_prompt, on_readline_input);
|
|
rl_get_screen_size (&ctx.lines, &ctx.columns);
|
|
ctx.readline_prompt_shown = true;
|
|
|
|
ctx.polling = true;
|
|
while (ctx.polling)
|
|
poller_run (&ctx.poller);
|
|
|
|
app_context_free (&ctx);
|
|
free_terminal ();
|
|
return EXIT_SUCCESS;
|
|
}
|