Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0fe0b56280
|
|||
|
f0281cf028
|
|||
|
da5dd4eb91
|
|||
|
10cb6651c0
|
|||
|
7f28dcd1ef
|
|||
|
61c52d793c
|
|||
|
b4dd0052ff
|
|||
|
e3c47c33fa
|
|||
|
80c1e8f8eb
|
|||
|
c5f49ab1e6
|
|||
|
6f62b9c0c7
|
|||
|
c1d69e3630
|
|||
|
c75ef167f2
|
|||
|
ddffc71abe
|
|||
|
5a0b2d1c57
|
|||
|
bb451a5050
|
|||
|
61f15ead8a
|
|||
|
17f430043a
|
|||
|
735096d76d
|
|||
|
1ba59e6ee0
|
|||
|
f9ba682c0e
|
|||
|
8e8ffe2c73
|
|||
|
d05c85833d
|
|||
|
2336340ad8
|
|||
|
8f5dec0456
|
|||
|
3dc6ee9a5b
|
|||
|
821ce04915
|
|||
|
2fe3b95ecd
|
|||
|
32c99c9d66
|
|||
|
cd7133e173
|
|||
|
b4ed52015a
|
|||
|
271689da99
|
|||
|
38c23d0d38
|
|||
|
439af8884c
|
|||
|
8ccf38ad76
|
|||
|
47a4c8beca
|
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required (VERSION 3.0)
|
||||
project (uirc3 VERSION 1.0.0 LANGUAGES C)
|
||||
project (uirc3 VERSION 1.2.0 LANGUAGES C)
|
||||
|
||||
# Options
|
||||
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
|
||||
@@ -54,8 +54,6 @@ include_directories (${libssl_INCLUDE_DIRS})
|
||||
link_directories (${libssl_LIBRARY_DIRS})
|
||||
|
||||
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
|
||||
include_directories (/usr/local/include)
|
||||
link_directories (/usr/local/lib)
|
||||
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
|
||||
# our POSIX version macros make it undefined
|
||||
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
|
||||
@@ -63,7 +61,8 @@ endif ()
|
||||
|
||||
# -lrt is only for glibc < 2.17
|
||||
# -liconv may or may not be a part of libc
|
||||
foreach (extra iconv rt)
|
||||
# -lm may or may not be a part of libc
|
||||
foreach (extra iconv rt m)
|
||||
find_library (extra_lib_${extra} ${extra})
|
||||
if (extra_lib_${extra})
|
||||
list (APPEND project_libraries ${extra_lib_${extra}})
|
||||
@@ -115,7 +114,7 @@ if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)
|
||||
elseif (WANT_READLINE)
|
||||
# OpenBSD's default readline is too old
|
||||
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
|
||||
include_directories (/usr/local/include/ereadline)
|
||||
include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
|
||||
list (APPEND degesch_libraries ereadline)
|
||||
else ()
|
||||
list (APPEND degesch_libraries readline)
|
||||
@@ -218,7 +217,7 @@ foreach (page ${project_MAN_PAGES})
|
||||
endforeach ()
|
||||
|
||||
# CPack
|
||||
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Experimental IRC client, daemon and bot")
|
||||
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Unethical IRC client, daemon and bot")
|
||||
set (CPACK_PACKAGE_VERSION "${project_version_safe}")
|
||||
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
|
||||
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
Copyright (c) 2014 - 2021, Přemysl Eric Janouch <p@janouch.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
35
NEWS
35
NEWS
@@ -1,3 +1,38 @@
|
||||
1.2.0 (2021-07-08) "There Are Other Countries As Well"
|
||||
|
||||
* degesch: added a /squery command for IRCnet
|
||||
|
||||
* degesch: added trivial support for SASL EXTERNAL, enabled by adding "sasl"
|
||||
to the respective server's "capabilities" list
|
||||
|
||||
* degesch: now supporting IRCv3.2 capability negotiation, including CAP DEL
|
||||
|
||||
* degesch: added support for IRCv3 chghost
|
||||
|
||||
* degesch: /deop and /devoice without arguments will use the client's user
|
||||
|
||||
* degesch: /set +=/-= now treats its argument as a string array
|
||||
|
||||
* degesch: made "/help /command" work the same way as "/help command" does
|
||||
|
||||
* degesch: /ban and /unban don't mangle extended bans anymore
|
||||
|
||||
* degesch: joining new channels no longer switches to their buffer
|
||||
automatically open them if the input buffer isn't empty
|
||||
|
||||
* censor.lua: now stripping colours from censored messages;
|
||||
their attributes are also configurable rather than always black on black
|
||||
|
||||
|
||||
1.1.0 (2020-10-31) "What Do You Mean By 'This Isn't Germany'?"
|
||||
|
||||
* degesch: made fancy-prompt.lua work with libedit
|
||||
|
||||
* kike: fixed a regression with an unspecified "bind_host"
|
||||
|
||||
* Miscellaneous minor improvements
|
||||
|
||||
|
||||
1.0.0 (2020-10-29) "We're Finally There!"
|
||||
|
||||
* Coming with real manual pages instead of help2man-generated stubs
|
||||
|
||||
19
README.adoc
19
README.adoc
@@ -3,8 +3,8 @@ uirc3
|
||||
:compact-option:
|
||||
|
||||
The [line-through]#unethical# edgy IRC trinity. This project consists of an
|
||||
experimental IRC client, daemon, and bot. It's all you're ever going to need
|
||||
for chatting, as long as you can make do with minimalist software.
|
||||
IRC client, daemon, and bot. It's all you're ever going to need for chatting,
|
||||
as long as you can make do with minimalist software.
|
||||
|
||||
All of them have these potentially interesting properties:
|
||||
|
||||
@@ -103,6 +103,7 @@ Or you can try telling CMake to make a package for you. For Debian it is:
|
||||
Usage
|
||||
-----
|
||||
'degesch' has in-program configuration. Just run it and read the instructions.
|
||||
Consult its link:degesch.adoc[man page] for details about the interface.
|
||||
|
||||
For the rest you might want to generate a configuration file:
|
||||
|
||||
@@ -124,7 +125,11 @@ as a `forking` type systemd user service.
|
||||
|
||||
Client Certificates
|
||||
-------------------
|
||||
'kike' uses SHA1 fingerprints of TLS client certificates to authenticate users.
|
||||
'degesch' will use the SASL EXTERNAL method to authenticate using the TLS
|
||||
client certificate specified by the respective server's `tls_cert` option
|
||||
if you add `sasl` to the `capabilities` option and the server supports this.
|
||||
|
||||
'kike' uses SHA-1 fingerprints of TLS client certificates to authenticate users.
|
||||
To get the fingerprint from a certificate file in the required form, use:
|
||||
|
||||
$ openssl x509 -in public.pem -outform DER | sha1sum
|
||||
@@ -152,13 +157,13 @@ Beware that you can easily break the program if you're not careful.
|
||||
How do I make degesch look like the screenshot?
|
||||
-----------------------------------------------
|
||||
First of all, you must build it with Lua support. With the defaults, degesch
|
||||
doesn't look very fancy because some things are rather hackish, and I also don't
|
||||
want to depend on UTF-8 or 256color terminals in the code. In addition to that,
|
||||
I appear to be one of the few people who use black on white terminals.
|
||||
doesn't look too fancy because I don't want to depend on Lua or 256-colour
|
||||
terminals. In addition to that, I appear to be one of the few people who use
|
||||
black on white terminals.
|
||||
|
||||
/set behaviour.date_change_line = "%a %e %b %Y"
|
||||
/set behaviour.plugin_autoload += "fancy-prompt.lua"
|
||||
/set behaviour.backlog_helper = "LESSSECURE=1 less -R +Gb -Ps'Backlog ?ltlines %lt-%lb?L/%L. .?e(END):?pB%pB\\%..'"
|
||||
/set behaviour.backlog_helper = "LESSSECURE=1 less -R +Gb1d -Ps'Backlog ?ltlines %lt-%lb?L/%L. .?e(END):?pB%pB\\%..'"
|
||||
/set attributes.userhost = "\x1b[38;5;109m"
|
||||
/set attributes.join = "\x1b[38;5;108m"
|
||||
/set attributes.part = "\x1b[38;5;138m"
|
||||
|
||||
@@ -6,7 +6,7 @@ degesch(1)
|
||||
|
||||
Name
|
||||
----
|
||||
degesch - an experimental IRC client
|
||||
degesch - terminal-based IRC client
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
|
||||
587
degesch.c
587
degesch.c
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* degesch.c: the experimental IRC client
|
||||
* degesch.c: a terminal-based IRC client
|
||||
*
|
||||
* Copyright (c) 2015 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
* Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted.
|
||||
@@ -51,6 +51,7 @@ enum
|
||||
#include "common.c"
|
||||
#include "kike-replies.c"
|
||||
|
||||
#include <math.h>
|
||||
#include <langinfo.h>
|
||||
#include <locale.h>
|
||||
#include <pwd.h>
|
||||
@@ -1591,12 +1592,14 @@ static struct ispect_field g_buffer_ispect[] =
|
||||
};
|
||||
|
||||
static struct buffer *
|
||||
buffer_new (struct input *input)
|
||||
buffer_new (struct input *input, enum buffer_type type, char *name)
|
||||
{
|
||||
struct buffer *self = xcalloc (1, sizeof *self);
|
||||
self->ref_count = 1;
|
||||
self->input = input;
|
||||
self->input_data = CALL (input, buffer_new);
|
||||
self->type = type;
|
||||
self->name = name;
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -1716,8 +1719,10 @@ struct server
|
||||
char *irc_user_host; ///< Our current user@host
|
||||
bool autoaway_active; ///< Autoaway is currently active
|
||||
|
||||
struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS
|
||||
bool cap_echo_message; ///< Whether the server echoes messages
|
||||
bool cap_away_notify; ///< Whether we get AWAY notifications
|
||||
bool cap_sasl; ///< Whether SASL is available
|
||||
|
||||
// Server-specific information (from RPL_ISUPPORT):
|
||||
|
||||
@@ -1728,6 +1733,9 @@ struct server
|
||||
char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
|
||||
char *irc_statusmsg; ///< Prefixes for channel targets
|
||||
|
||||
char irc_extban_prefix; ///< EXTBAN prefix or \0
|
||||
char *irc_extban_types; ///< EXTBAN types
|
||||
|
||||
char *irc_chanmodes_list; ///< Channel modes for lists
|
||||
char *irc_chanmodes_param_always; ///< Channel modes with mandatory param
|
||||
char *irc_chanmodes_param_when_set; ///< Channel modes with param when set
|
||||
@@ -1776,6 +1784,9 @@ server_init_specifics (struct server *self)
|
||||
self->irc_idchan_prefixes = xstrdup ("");
|
||||
self->irc_statusmsg = xstrdup ("");
|
||||
|
||||
self->irc_extban_prefix = 0;
|
||||
self->irc_extban_types = xstrdup ("");
|
||||
|
||||
self->irc_chanmodes_list = xstrdup ("b");
|
||||
self->irc_chanmodes_param_always = xstrdup ("k");
|
||||
self->irc_chanmodes_param_when_set = xstrdup ("l");
|
||||
@@ -1794,6 +1805,8 @@ server_free_specifics (struct server *self)
|
||||
free (self->irc_idchan_prefixes);
|
||||
free (self->irc_statusmsg);
|
||||
|
||||
free (self->irc_extban_types);
|
||||
|
||||
free (self->irc_chanmodes_list);
|
||||
free (self->irc_chanmodes_param_always);
|
||||
free (self->irc_chanmodes_param_when_set);
|
||||
@@ -1839,6 +1852,7 @@ server_new (struct poller *poller)
|
||||
|
||||
self->irc_user_mode = str_make ();
|
||||
|
||||
self->cap_ls_buf = strv_make ();
|
||||
server_init_specifics (self);
|
||||
return self;
|
||||
}
|
||||
@@ -1885,6 +1899,7 @@ server_destroy (struct server *self)
|
||||
str_free (&self->irc_user_mode);
|
||||
free (self->irc_user_host);
|
||||
|
||||
strv_free (&self->cap_ls_buf);
|
||||
server_free_specifics (self);
|
||||
free (self);
|
||||
}
|
||||
@@ -2102,17 +2117,23 @@ filter_color_cube_for_acceptable_nick_colors (size_t *len)
|
||||
// This is a pure function and we don't use threads, static storage is fine
|
||||
static int table[6 * 6 * 6];
|
||||
size_t len_counter = 0;
|
||||
for (int x = 0; x < 6 * 6 * 6; x++)
|
||||
for (int x = 0; x < (int) N_ELEMENTS (table); x++)
|
||||
{
|
||||
// FIXME this isn't exactly right, the values aren't linear
|
||||
int r = x / 36;
|
||||
int g = (x / 6) % 6;
|
||||
int b = (x % 6);
|
||||
|
||||
// Use the luma value of colours within the cube to filter colours that
|
||||
// look okay-ish on terminals with both black and white backgrounds
|
||||
double luma = 0.2126 * r / 6. + 0.7152 * g / 6. + 0.0722 * b / 6.;
|
||||
if (luma >= .3 && luma <= .5)
|
||||
// The first step is 95/255, the rest are 40/255,
|
||||
// as an approximation we can double the first step
|
||||
double linear_R = pow ((r + !!r) / 6., 2.2);
|
||||
double linear_G = pow ((g + !!g) / 6., 2.2);
|
||||
double linear_B = pow ((b + !!b) / 6., 2.2);
|
||||
|
||||
// Use the relative luminance of colours within the cube to filter
|
||||
// colours that look okay-ish on terminals with both black and white
|
||||
// backgrounds (use the test-nick-colors script to calibrate)
|
||||
double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
|
||||
if (Y >= .25 && Y <= .4)
|
||||
table[len_counter++] = 16 + x;
|
||||
}
|
||||
*len = len_counter;
|
||||
@@ -2335,7 +2356,7 @@ static struct config_schema g_config_server[] =
|
||||
.type = CONFIG_ITEM_STRING_ARRAY,
|
||||
.validate = config_validate_nonjunk_string,
|
||||
.default_ = "\"multi-prefix,invite-notify,server-time,echo-message,"
|
||||
"message-tags,away-notify\"" },
|
||||
"message-tags,away-notify,cap-notify,chghost\"" },
|
||||
|
||||
{ .name = "tls",
|
||||
.comment = "Whether to use TLS",
|
||||
@@ -2458,7 +2479,7 @@ static struct config_schema g_config_behaviour[] =
|
||||
{ .name = "backlog_helper",
|
||||
.comment = "Shell command to display a buffer's history",
|
||||
.type = CONFIG_ITEM_STRING,
|
||||
.default_ = "\"LESSSECURE=1 less -M -R +G\"" },
|
||||
.default_ = "\"LESSSECURE=1 less -M -R +Gb\"" },
|
||||
{ .name = "backlog_helper_strip_formatting",
|
||||
.comment = "Strip formatting from backlog helper input",
|
||||
.type = CONFIG_ITEM_BOOLEAN,
|
||||
@@ -3026,6 +3047,20 @@ irc_skip_statusmsg (struct server *s, const char *target)
|
||||
return target + (*target && strchr (s->irc_statusmsg, *target));
|
||||
}
|
||||
|
||||
static bool
|
||||
irc_is_extban (struct server *s, const char *target)
|
||||
{
|
||||
// Some servers have a prefix, and some support negation
|
||||
if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix)
|
||||
return false;
|
||||
if (*target == '~')
|
||||
target++;
|
||||
|
||||
// XXX: we don't know if it's supposed to have an argument, or not
|
||||
return *target && strchr (s->irc_extban_types, *target++)
|
||||
&& strchr (":\0", *target);
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it
|
||||
@@ -4029,6 +4064,13 @@ log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
|
||||
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
||||
"#n is now known as #n", (old), (new_))
|
||||
|
||||
#define log_chghost_self(s, buffer, new_) \
|
||||
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
||||
"You are now #N", (new_))
|
||||
#define log_chghost(s, buffer, old, new_) \
|
||||
log_server ((s), (buffer), BUFFER_LINE_STATUS | BUFFER_LINE_UNIMPORTANT, \
|
||||
"#N is now #N", (old), (new_))
|
||||
|
||||
#define log_outcoming_notice(s, buffer, who, text) \
|
||||
log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text))
|
||||
#define log_outcoming_privmsg(s, buffer, prefixes, who, text) \
|
||||
@@ -4081,6 +4123,13 @@ buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
|
||||
return;
|
||||
|
||||
// TODO: should we try to reopen files wrt. case mapping?
|
||||
// - Need to read the whole directory and look for matches:
|
||||
// irc_server_strcmp(buffer->s, d_name, make_log_filename())
|
||||
// remember to strip the ".log" suffix from d_name, case-sensitively.
|
||||
// - The tolower_ascii() in make_log_filename() is a perfect overlap,
|
||||
// it may stay as-is.
|
||||
// - buffer_get_log_path() will need to return a FILE *,
|
||||
// or an error that includes the below message.
|
||||
char *path = buffer_get_log_path (buffer);
|
||||
if (!(buffer->log_file = fopen (path, "ab")))
|
||||
log_global_error (ctx, "Couldn't open log file `#s': #l",
|
||||
@@ -4446,9 +4495,8 @@ buffer_remove_safe (struct app_context *ctx, struct buffer *buffer)
|
||||
static void
|
||||
init_global_buffer (struct app_context *ctx)
|
||||
{
|
||||
struct buffer *global = ctx->global_buffer = buffer_new (ctx->input);
|
||||
global->type = BUFFER_GLOBAL;
|
||||
global->name = xstrdup (PROGRAM_NAME);
|
||||
struct buffer *global = ctx->global_buffer =
|
||||
buffer_new (ctx->input, BUFFER_GLOBAL, xstrdup (PROGRAM_NAME));
|
||||
|
||||
buffer_add (ctx, global);
|
||||
buffer_activate (ctx, global);
|
||||
@@ -4456,6 +4504,19 @@ init_global_buffer (struct app_context *ctx)
|
||||
|
||||
// --- Users, channels ---------------------------------------------------------
|
||||
|
||||
static char *
|
||||
irc_make_buffer_name (struct server *s, const char *target)
|
||||
{
|
||||
if (!target)
|
||||
return xstrdup (s->name);
|
||||
|
||||
// XXX: this may be able to trigger the uniqueness assertion with non-UTF-8
|
||||
char *target_utf8 = irc_to_utf8 (target);
|
||||
char *result = xstrdup_printf ("%s.%s", s->name, target_utf8);
|
||||
free (target_utf8);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
irc_user_on_destroy (void *object, void *user_data)
|
||||
{
|
||||
@@ -4495,9 +4556,8 @@ irc_get_or_make_user_buffer (struct server *s, const char *nickname)
|
||||
struct user *user = irc_get_or_make_user (s, nickname);
|
||||
|
||||
// Open a new buffer for the user
|
||||
buffer = buffer_new (s->ctx->input);
|
||||
buffer->type = BUFFER_PM;
|
||||
buffer->name = xstrdup_printf ("%s.%s", s->name, nickname);
|
||||
buffer = buffer_new (s->ctx->input,
|
||||
BUFFER_PM, irc_make_buffer_name (s, nickname));
|
||||
buffer->server = s;
|
||||
buffer->user = user;
|
||||
str_map_set (&s->irc_buffer_map, user->nickname, buffer);
|
||||
@@ -4943,7 +5003,10 @@ irc_destroy_state (struct server *s)
|
||||
str_reset (&s->irc_user_mode);
|
||||
cstr_set (&s->irc_user_host, NULL);
|
||||
|
||||
strv_reset (&s->cap_ls_buf);
|
||||
s->cap_away_notify = false;
|
||||
s->cap_echo_message = false;
|
||||
s->cap_sasl = false;
|
||||
|
||||
// Need to call this before server_init_specifics()
|
||||
irc_set_casemapping (s, irc_tolower, irc_strxfrm);
|
||||
@@ -5008,14 +5071,17 @@ irc_initiate_disconnect (struct server *s, const char *reason)
|
||||
}
|
||||
|
||||
static void
|
||||
initiate_quit (struct app_context *ctx, const char *message)
|
||||
request_quit (struct app_context *ctx, const char *message)
|
||||
{
|
||||
if (!ctx->quitting)
|
||||
{
|
||||
log_global_status (ctx, "Shutting down");
|
||||
ctx->quitting = true;
|
||||
|
||||
// Hide the user interface
|
||||
// Disable the user interface
|
||||
CALL (ctx->input, hide);
|
||||
}
|
||||
|
||||
// Initiate a connection close
|
||||
struct str_map_iter iter = str_map_iter_make (&ctx->servers);
|
||||
struct server *s;
|
||||
while ((s = str_map_iter_next (&iter)))
|
||||
@@ -5029,7 +5095,6 @@ initiate_quit (struct app_context *ctx, const char *message)
|
||||
irc_destroy_connector (s);
|
||||
}
|
||||
|
||||
ctx->quitting = true;
|
||||
try_finish_quit (ctx);
|
||||
}
|
||||
|
||||
@@ -5676,9 +5741,9 @@ irc_register (struct server *s)
|
||||
const char *realname = get_config_string (s->config, "realname");
|
||||
hard_assert (username && realname);
|
||||
|
||||
// Start IRCv3.1 capability negotiation;
|
||||
// Start IRCv3 capability negotiation, with up to 3.2 features;
|
||||
// at worst the server will ignore this or send a harmless error message
|
||||
irc_send (s, "CAP LS");
|
||||
irc_send (s, "CAP LS 302");
|
||||
|
||||
const char *password = get_config_string (s->config, "password");
|
||||
if (password)
|
||||
@@ -6028,6 +6093,14 @@ make_prompt (struct app_context *ctx, struct str *output)
|
||||
static void
|
||||
input_maybe_set_prompt (struct input *self, char *new_prompt)
|
||||
{
|
||||
// Fix libedit's expectations to see a non-control character following
|
||||
// the end mark (see prompt.c and literal.c) by cleaning this up
|
||||
for (char *p = new_prompt; *p; )
|
||||
if (p[0] == INPUT_END_IGNORE && p[1] == INPUT_START_IGNORE)
|
||||
memmove (p, p + 2, strlen (p + 2) + 1);
|
||||
else
|
||||
p++;
|
||||
|
||||
// Redisplay can be an expensive operation
|
||||
const char *prompt = CALL (self, get_prompt);
|
||||
if (prompt && !strcmp (new_prompt, prompt))
|
||||
@@ -6055,6 +6128,12 @@ on_refresh_prompt (struct app_context *ctx)
|
||||
prompt.str[--prompt.len] = 0;
|
||||
attributed_suffix = " ";
|
||||
}
|
||||
|
||||
// Also enable a uniform interface for prompt hooks by assuming it uses
|
||||
// GNU Readline escapes: turn this into libedit's almost-flip-flop
|
||||
for (size_t i = 0; i < prompt.len; i++)
|
||||
if (prompt.str[i] == '\x01' || prompt.str[i] == '\x02')
|
||||
prompt.str[i] = INPUT_START_IGNORE /* == INPUT_END_IGNORE */;
|
||||
#endif // HAVE_EDITLINE
|
||||
|
||||
char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
|
||||
@@ -6371,11 +6450,11 @@ irc_handle_mode_user (struct server *s, char **params)
|
||||
static void
|
||||
irc_handle_sent_cap (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
if (msg->params.len < 2)
|
||||
if (msg->params.len < 1)
|
||||
return;
|
||||
|
||||
const char *subcommand = msg->params.vector[1];
|
||||
const char *args = (msg->params.len > 2) ? msg->params.vector[2] : "";
|
||||
const char *subcommand = msg->params.vector[0];
|
||||
const char *args = (msg->params.len > 1) ? msg->params.vector[1] : "";
|
||||
if (!strcasecmp_ascii (subcommand, "REQ"))
|
||||
log_server_status (s, s->buffer,
|
||||
"#s: #S", "Capabilities requested", args);
|
||||
@@ -6492,6 +6571,20 @@ irc_process_sent_message (const struct irc_message *msg, struct server *s)
|
||||
|
||||
// --- Input handling ----------------------------------------------------------
|
||||
|
||||
static void
|
||||
irc_handle_authenticate (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
if (msg->params.len < 1)
|
||||
return;
|
||||
|
||||
// Empty challenge -> empty response for e.g. SASL EXTERNAL,
|
||||
// abort anything else as it doesn't make much sense to let the user do it
|
||||
if (!strcmp (msg->params.vector[0], "+"))
|
||||
irc_send (s, "AUTHENTICATE +");
|
||||
else
|
||||
irc_send (s, "AUTHENTICATE *");
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_away (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
@@ -6507,6 +6600,58 @@ irc_handle_away (struct server *s, const struct irc_message *msg)
|
||||
user->away = !!msg->params.len;
|
||||
}
|
||||
|
||||
static void
|
||||
irc_process_cap_ls (struct server *s)
|
||||
{
|
||||
log_server_status (s, s->buffer,
|
||||
"#s: #&S", "Capabilities supported", strv_join (&s->cap_ls_buf, " "));
|
||||
|
||||
struct strv chosen = strv_make ();
|
||||
struct strv use = strv_make ();
|
||||
|
||||
cstr_split (get_config_string (s->config, "capabilities"), ",", true, &use);
|
||||
|
||||
// Filter server capabilities for ones we can make use of
|
||||
for (size_t i = 0; i < s->cap_ls_buf.len; i++)
|
||||
{
|
||||
const char *cap = s->cap_ls_buf.vector[i];
|
||||
size_t cap_name_len = strcspn (cap, "=");
|
||||
for (size_t k = 0; k < use.len; k++)
|
||||
if (!strncasecmp_ascii (use.vector[k], cap, cap_name_len))
|
||||
strv_append_owned (&chosen, xstrndup (cap, cap_name_len));
|
||||
}
|
||||
strv_reset (&s->cap_ls_buf);
|
||||
|
||||
char *chosen_str = strv_join (&chosen, " ");
|
||||
strv_free (&chosen);
|
||||
strv_free (&use);
|
||||
|
||||
// XXX: with IRCv3.2, this may end up being too long for one message,
|
||||
// and we need to be careful with CAP END. One probably has to count
|
||||
// the number of sent CAP REQ vs the number of received CAP ACK/NAK.
|
||||
if (s->state == IRC_CONNECTED)
|
||||
irc_send (s, "CAP REQ :%s", chosen_str);
|
||||
|
||||
free (chosen_str);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_toggle_cap (struct server *s, const char *cap, bool active)
|
||||
{
|
||||
if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active;
|
||||
if (!strcasecmp_ascii (cap, "away-notify")) s->cap_away_notify = active;
|
||||
if (!strcasecmp_ascii (cap, "sasl")) s->cap_sasl = active;
|
||||
}
|
||||
|
||||
static void
|
||||
irc_try_finish_cap_negotiation (struct server *s)
|
||||
{
|
||||
// It does not make sense to do this post-registration, although it would
|
||||
// not hurt either, as the server must ignore it in that case
|
||||
if (s->state == IRC_CONNECTED)
|
||||
irc_send (s, "CAP END");
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_cap (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
@@ -6532,50 +6677,89 @@ irc_handle_cap (struct server *s, const struct irc_message *msg)
|
||||
active = false;
|
||||
cap++;
|
||||
}
|
||||
if (!strcasecmp_ascii (cap, "echo-message"))
|
||||
s->cap_echo_message = active;
|
||||
if (!strcasecmp_ascii (cap, "away-notify"))
|
||||
s->cap_away_notify = active;
|
||||
irc_toggle_cap (s, cap, active);
|
||||
}
|
||||
irc_send (s, "CAP END");
|
||||
if (s->cap_sasl && s->transport == &g_transport_tls)
|
||||
irc_send (s, "AUTHENTICATE EXTERNAL");
|
||||
else
|
||||
irc_try_finish_cap_negotiation (s);
|
||||
}
|
||||
else if (!strcasecmp_ascii (subcommand, "NAK"))
|
||||
{
|
||||
log_server_error (s, s->buffer,
|
||||
"#s: #S", "Capabilities not acknowledged", args);
|
||||
irc_send (s, "CAP END");
|
||||
irc_try_finish_cap_negotiation (s);
|
||||
}
|
||||
else if (!strcasecmp_ascii (subcommand, "DEL"))
|
||||
{
|
||||
log_server_error (s, s->buffer,
|
||||
"#s: #S", "Capabilities deleted", args);
|
||||
for (size_t i = 0; i < v.len; i++)
|
||||
irc_toggle_cap (s, v.vector[i], false);
|
||||
}
|
||||
else if (!strcasecmp_ascii (subcommand, "LS"))
|
||||
{
|
||||
log_server_status (s, s->buffer,
|
||||
"#s: #S", "Capabilities supported", args);
|
||||
|
||||
struct strv chosen = strv_make ();
|
||||
struct strv use = strv_make ();
|
||||
|
||||
cstr_split (get_config_string (s->config, "capabilities"),
|
||||
",", true, &use);
|
||||
|
||||
// Filter server capabilities for ones we can make use of
|
||||
for (size_t i = 0; i < v.len; i++)
|
||||
if (msg->params.len > 3 && !strcmp (args, "*"))
|
||||
cstr_split (msg->params.vector[3], " ", true, &s->cap_ls_buf);
|
||||
else
|
||||
{
|
||||
const char *cap = v.vector[i];
|
||||
for (size_t k = 0; k < use.len; k++)
|
||||
if (!strcasecmp_ascii (use.vector[k], cap))
|
||||
strv_append (&chosen, cap);
|
||||
strv_append_vector (&s->cap_ls_buf, v.vector);
|
||||
irc_process_cap_ls (s);
|
||||
}
|
||||
|
||||
char *chosen_str = strv_join (&chosen, " ");
|
||||
strv_free (&chosen);
|
||||
strv_free (&use);
|
||||
|
||||
irc_send (s, "CAP REQ :%s", chosen_str);
|
||||
free (chosen_str);
|
||||
}
|
||||
|
||||
strv_free (&v);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_chghost (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
if (!msg->prefix || msg->params.len < 2)
|
||||
return;
|
||||
|
||||
char *nickname = irc_cut_nickname (msg->prefix);
|
||||
struct user *user = str_map_find (&s->irc_users, nickname);
|
||||
free (nickname);
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
char *new_prefix = xstrdup_printf ("%s!%s@%s", user->nickname,
|
||||
msg->params.vector[0], msg->params.vector[1]);
|
||||
|
||||
if (irc_is_this_us (s, msg->prefix))
|
||||
{
|
||||
cstr_set (&s->irc_user_host, xstrdup_printf ("%s@%s",
|
||||
msg->params.vector[0], msg->params.vector[1]));
|
||||
|
||||
log_chghost_self (s, s->buffer, new_prefix);
|
||||
|
||||
// Log a message in all open buffers on this server
|
||||
struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
|
||||
struct buffer *buffer;
|
||||
while ((buffer = str_map_iter_next (&iter)))
|
||||
log_chghost_self (s, buffer, new_prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log a message in any PM buffer
|
||||
struct buffer *buffer =
|
||||
str_map_find (&s->irc_buffer_map, user->nickname);
|
||||
if (buffer)
|
||||
log_chghost (s, buffer, msg->prefix, new_prefix);
|
||||
|
||||
// Log a message in all channels the user is in
|
||||
LIST_FOR_EACH (struct user_channel, iter, user->channels)
|
||||
{
|
||||
buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
|
||||
hard_assert (buffer != NULL);
|
||||
log_chghost (s, buffer, msg->prefix, new_prefix);
|
||||
}
|
||||
}
|
||||
|
||||
free (new_prefix);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_error (struct server *s, const struct irc_message *msg)
|
||||
{
|
||||
@@ -6628,16 +6812,21 @@ irc_handle_join (struct server *s, const struct irc_message *msg)
|
||||
if (!irc_is_this_us (s, msg->prefix))
|
||||
return;
|
||||
|
||||
buffer = buffer_new (s->ctx->input);
|
||||
buffer->type = BUFFER_CHANNEL;
|
||||
buffer->name = xstrdup_printf ("%s.%s", s->name, channel_name);
|
||||
buffer = buffer_new (s->ctx->input,
|
||||
BUFFER_CHANNEL, irc_make_buffer_name (s, channel_name));
|
||||
buffer->server = s;
|
||||
buffer->channel = channel =
|
||||
irc_make_channel (s, xstrdup (channel_name));
|
||||
str_map_set (&s->irc_buffer_map, channel->name, buffer);
|
||||
|
||||
buffer_add (s->ctx, buffer);
|
||||
|
||||
char *input = CALL (s->ctx->input, get_line);
|
||||
if (!*input)
|
||||
buffer_activate (s->ctx, buffer);
|
||||
else
|
||||
buffer->highlighted = true;
|
||||
free (input);
|
||||
}
|
||||
|
||||
if (irc_is_this_us (s, msg->prefix))
|
||||
@@ -7209,8 +7398,10 @@ irc_handle_topic (struct server *s, const struct irc_message *msg)
|
||||
static struct irc_handler g_irc_handlers[] =
|
||||
{
|
||||
// This list needs to stay sorted
|
||||
{ "AUTHENTICATE", irc_handle_authenticate },
|
||||
{ "AWAY", irc_handle_away },
|
||||
{ "CAP", irc_handle_cap },
|
||||
{ "CHGHOST", irc_handle_chghost },
|
||||
{ "ERROR", irc_handle_error },
|
||||
{ "INVITE", irc_handle_invite },
|
||||
{ "JOIN", irc_handle_join },
|
||||
@@ -7757,6 +7948,16 @@ irc_handle_isupport_statusmsg (struct server *s, char *value)
|
||||
cstr_set (&s->irc_statusmsg, xstrdup (value));
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_isupport_extban (struct server *s, char *value)
|
||||
{
|
||||
s->irc_extban_prefix = 0;
|
||||
if (*value && *value != ',')
|
||||
s->irc_extban_prefix = *value++;
|
||||
|
||||
cstr_set (&s->irc_extban_types, xstrdup (*value == ',' ? ++value : ""));
|
||||
}
|
||||
|
||||
static void
|
||||
irc_handle_isupport_chanmodes (struct server *s, char *value)
|
||||
{
|
||||
@@ -7813,6 +8014,7 @@ dispatch_isupport (struct server *s, const char *name, char *value)
|
||||
MATCH ("CHANTYPES", irc_handle_isupport_chantypes);
|
||||
MATCH ("IDCHAN", irc_handle_isupport_idchan);
|
||||
MATCH ("STATUSMSG", irc_handle_isupport_statusmsg);
|
||||
MATCH ("EXTBAN", irc_handle_isupport_extban);
|
||||
MATCH ("CHANMODES", irc_handle_isupport_chanmodes);
|
||||
MATCH ("MODES", irc_handle_isupport_modes);
|
||||
|
||||
@@ -7905,6 +8107,15 @@ irc_process_numeric (struct server *s,
|
||||
if (irc_handle_rpl_endofwho (s, msg)) buffer = NULL;
|
||||
break;
|
||||
|
||||
case IRC_ERR_NICKLOCKED:
|
||||
case IRC_RPL_SASLSUCCESS:
|
||||
case IRC_ERR_SASLFAIL:
|
||||
case IRC_ERR_SASLTOOLONG:
|
||||
case IRC_ERR_SASLABORTED:
|
||||
case IRC_ERR_SASLALREADY:
|
||||
irc_try_finish_cap_negotiation (s);
|
||||
break;
|
||||
|
||||
case IRC_RPL_LIST:
|
||||
|
||||
case IRC_ERR_UNKNOWNCOMMAND:
|
||||
@@ -7961,8 +8172,8 @@ irc_process_message (const struct irc_message *msg, struct server *s)
|
||||
irc_sanitize_cut_off_utf8 (&msg->params.vector[msg->params.len - 1]);
|
||||
|
||||
// TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec())
|
||||
// -> change all calls to log_{server,nick,outcoming,ctcp}*() to take
|
||||
// an extra argument specifying time
|
||||
// -> change all calls to log_{server,nick,chghost,outcoming,ctcp}*()
|
||||
// to take an extra numeric argument specifying time
|
||||
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);
|
||||
@@ -8299,9 +8510,8 @@ server_add (struct app_context *ctx,
|
||||
s->config = subtree;
|
||||
|
||||
// Add a buffer and activate it
|
||||
struct buffer *buffer = s->buffer = buffer_new (ctx->input);
|
||||
buffer->type = BUFFER_SERVER;
|
||||
buffer->name = xstrdup (s->name);
|
||||
struct buffer *buffer = s->buffer = buffer_new (ctx->input,
|
||||
BUFFER_SERVER, irc_make_buffer_name (s, NULL));
|
||||
buffer->server = s;
|
||||
|
||||
buffer_add (ctx, buffer);
|
||||
@@ -10890,45 +11100,38 @@ handle_command_buffer (struct handler_args *a)
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
replace_string_array
|
||||
(struct config_item *item, struct strv *array, struct error **e)
|
||||
{
|
||||
char *changed = strv_join (array, ",");
|
||||
struct str tmp = { .str = changed, .len = strlen (changed) };
|
||||
bool result = config_item_set_from (item,
|
||||
config_item_string_array (&tmp), e);
|
||||
free (changed);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
handle_command_set_add
|
||||
(struct config_item *item, const char *value, struct error **e)
|
||||
(struct strv *items, const struct strv *values, struct error **e)
|
||||
{
|
||||
struct strv items = strv_make ();
|
||||
if (item->type != CONFIG_ITEM_NULL)
|
||||
cstr_split (item->value.string.str, ",", false, &items);
|
||||
if (items.len == 1 && !*items.vector[0])
|
||||
strv_reset (&items);
|
||||
|
||||
// FIXME: handle multiple items properly
|
||||
bool result = false;
|
||||
if (strv_find (&items, value) != -1)
|
||||
error_set (e, "already present in the array: %s", value);
|
||||
else
|
||||
for (size_t i = 0; i < values->len; i++)
|
||||
{
|
||||
strv_append (&items, value);
|
||||
result = replace_string_array (item, &items, e);
|
||||
const char *value = values->vector[i];
|
||||
if (strv_find (items, values->vector[i]) != -1)
|
||||
return error_set (e, "already present in the array: %s", value);
|
||||
strv_append (items, value);
|
||||
}
|
||||
|
||||
strv_free (&items);
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
handle_command_set_remove
|
||||
(struct config_item *item, const char *value, struct error **e)
|
||||
(struct strv *items, const struct strv *values, struct error **e)
|
||||
{
|
||||
for (size_t i = 0; i < values->len; i++)
|
||||
{
|
||||
const char *value = values->vector[i];
|
||||
ssize_t i = strv_find (items, value);
|
||||
if (i == -1)
|
||||
return error_set (e, "not present in the array: %s", value);
|
||||
strv_remove (items, i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
handle_command_set_modify
|
||||
(struct config_item *item, const char *value, bool add, struct error **e)
|
||||
{
|
||||
struct strv items = strv_make ();
|
||||
if (item->type != CONFIG_ITEM_NULL)
|
||||
@@ -10936,18 +11139,23 @@ handle_command_set_remove
|
||||
if (items.len == 1 && !*items.vector[0])
|
||||
strv_reset (&items);
|
||||
|
||||
// FIXME: handle multiple items properly
|
||||
bool result = false;
|
||||
ssize_t i = strv_find (&items, value);
|
||||
if (i == -1)
|
||||
error_set (e, "not present in the array: %s", value);
|
||||
else
|
||||
struct strv values = strv_make ();
|
||||
cstr_split (value, ",", false, &values);
|
||||
bool result = add
|
||||
? handle_command_set_add (&items, &values, e)
|
||||
: handle_command_set_remove (&items, &values, e);
|
||||
|
||||
if (result)
|
||||
{
|
||||
strv_remove (&items, i);
|
||||
result = replace_string_array (item, &items, e);
|
||||
char *changed = strv_join (&items, ",");
|
||||
struct str tmp = { .str = changed, .len = strlen (changed) };
|
||||
result = config_item_set_from (item,
|
||||
config_item_string_array (&tmp), e);
|
||||
free (changed);
|
||||
}
|
||||
|
||||
strv_free (&items);
|
||||
strv_free (&values);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -10966,10 +11174,8 @@ handle_command_set_assign_item (struct app_context *ctx,
|
||||
config_item_set_from (item, config_item_clone (new_), &e);
|
||||
else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY)
|
||||
error_set (&e, "not a string array");
|
||||
else if (add)
|
||||
handle_command_set_add (item, new_->value.string.str, &e);
|
||||
else if (remove)
|
||||
handle_command_set_remove (item, new_->value.string.str, &e);
|
||||
else
|
||||
handle_command_set_modify (item, new_->value.string.str, add, &e);
|
||||
|
||||
if (e)
|
||||
{
|
||||
@@ -11215,6 +11421,20 @@ handle_command_notice (struct handler_args *a)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
handle_command_squery (struct handler_args *a)
|
||||
{
|
||||
if (!*a->arguments)
|
||||
return false;
|
||||
|
||||
char *target = cut_word (&a->arguments);
|
||||
if (!*a->arguments)
|
||||
log_server_error (a->s, a->s->buffer, "No text to send");
|
||||
else
|
||||
irc_send (a->s, "SQUERY %s :%s", target, a->arguments);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
handle_command_ctcp (struct handler_args *a)
|
||||
{
|
||||
@@ -11254,7 +11474,7 @@ handle_command_me (struct handler_args *a)
|
||||
static bool
|
||||
handle_command_quit (struct handler_args *a)
|
||||
{
|
||||
initiate_quit (a->ctx, *a->arguments ? a->arguments : NULL);
|
||||
request_quit (a->ctx, *a->arguments ? a->arguments : NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11379,7 +11599,8 @@ handle_command_topic (struct handler_args *a)
|
||||
if (*a->arguments)
|
||||
// FIXME: there's no way to start the topic with whitespace
|
||||
// FIXME: there's no way to unset the topic;
|
||||
// we could adopt the Tcl style of "-switches" with "--" sentinels
|
||||
// we could adopt the Tcl style of "-switches" with "--" sentinels,
|
||||
// or we could accept "strings" in the config format
|
||||
irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments);
|
||||
else
|
||||
irc_send (a->s, "TOPIC %s", a->channel_name);
|
||||
@@ -11458,8 +11679,7 @@ mass_channel_mode_mask_list
|
||||
for (size_t i = 0; i < v.len; i++)
|
||||
{
|
||||
char *target = v.vector[i];
|
||||
// TODO: support EXTBAN and leave those alone, too
|
||||
if (strpbrk (target, "!@*?"))
|
||||
if (strpbrk (target, "!@*?") || irc_is_extban (a->s, target))
|
||||
continue;
|
||||
|
||||
v.vector[i] = xstrdup_printf ("%s!*@*", target);
|
||||
@@ -11743,11 +11963,17 @@ static bool
|
||||
handle_command_channel_mode
|
||||
(struct handler_args *a, bool adding, char mode_char)
|
||||
{
|
||||
if (!*a->arguments)
|
||||
const char *targets = a->arguments;
|
||||
if (!*targets)
|
||||
{
|
||||
if (adding)
|
||||
return false;
|
||||
|
||||
targets = a->s->irc_user->nickname;
|
||||
}
|
||||
|
||||
struct strv v = strv_make ();
|
||||
cstr_split (a->arguments, " ", true, &v);
|
||||
cstr_split (targets, " ", true, &v);
|
||||
mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
|
||||
strv_free (&v);
|
||||
return true;
|
||||
@@ -11830,6 +12056,9 @@ g_command_handlers[] =
|
||||
{ "notice", "Send notice to a nick or channel",
|
||||
"<target> <message>",
|
||||
handle_command_notice, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
||||
{ "squery", "Send a message to a service",
|
||||
"<service> <message>",
|
||||
handle_command_squery, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
||||
{ "ctcp", "Send a CTCP query",
|
||||
"<target> <tag>",
|
||||
handle_command_ctcp, HANDLER_SERVER | HANDLER_NEEDS_REG },
|
||||
@@ -11851,13 +12080,13 @@ g_command_handlers[] =
|
||||
"<nick>...",
|
||||
handle_command_op, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
||||
{ "deop", "Remove channel operator status",
|
||||
"<nick>...",
|
||||
"[<nick>...]",
|
||||
handle_command_deop, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
||||
{ "voice", "Give voice",
|
||||
"<nick>...",
|
||||
handle_command_voice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
||||
{ "devoice", "Remove voice",
|
||||
"<nick>...",
|
||||
"[<nick>...]",
|
||||
handle_command_devoice, HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
|
||||
|
||||
{ "mode", "Change mode",
|
||||
@@ -11999,8 +12228,9 @@ handle_command_help (struct handler_args *a)
|
||||
if (!*a->arguments)
|
||||
return show_command_list (ctx);
|
||||
|
||||
// TODO: we should probably also accept commands names with a leading slash
|
||||
char *command = cut_word (&a->arguments);
|
||||
const char *word = cut_word (&a->arguments);
|
||||
|
||||
const char *command = word + (*word == '/');
|
||||
for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
|
||||
{
|
||||
struct command_handler *handler = &g_command_handlers[i];
|
||||
@@ -12008,13 +12238,13 @@ handle_command_help (struct handler_args *a)
|
||||
return show_command_help (ctx, handler);
|
||||
}
|
||||
|
||||
if (try_handle_command_help_option (ctx, command))
|
||||
if (try_handle_command_help_option (ctx, word))
|
||||
return true;
|
||||
|
||||
if (str_map_find (get_aliases_config (ctx), command))
|
||||
log_global_status (ctx, "/#s is an alias", command);
|
||||
else
|
||||
log_global_error (ctx, "#s: #s", "No such command or option", command);
|
||||
log_global_error (ctx, "#s: #s", "No such command or option", word);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12089,6 +12319,12 @@ expand_alias_escape (const char *p, const char *arguments, struct str *output)
|
||||
cstr_split (arguments, " ", true, &words);
|
||||
|
||||
// TODO: eventually also add support for argument ranges
|
||||
// - Can use ${0}, ${0:}, ${:0}, ${1:-1} with strtol, dispose of $1 syntax
|
||||
// (default aliases don't use numeric arguments).
|
||||
// - Start numbering from zero, since we'd have to figure out what to do
|
||||
// in case we encounter a zero if we keep the current approach.
|
||||
// - Ignore the sequence altogether if no closing '}' can be found,
|
||||
// or if the internal format doesn't fit the above syntax.
|
||||
if (*p >= '1' && *p <= '9')
|
||||
{
|
||||
size_t offset = *p - '1';
|
||||
@@ -12341,15 +12577,12 @@ completion_locate (struct completion *self, size_t offset)
|
||||
self->location = i - 1;
|
||||
}
|
||||
|
||||
static bool
|
||||
completion_matches (struct completion *self, int word, const char *pattern)
|
||||
static char *
|
||||
completion_word (struct completion *self, int word)
|
||||
{
|
||||
hard_assert (word >= 0 && word < (int) self->words_len);
|
||||
char *text = xstrndup (self->line + self->words[word].start,
|
||||
return xstrndup (self->line + self->words[word].start,
|
||||
self->words[word].end - self->words[word].start);
|
||||
bool result = !fnmatch (pattern, text, 0);
|
||||
free (text);
|
||||
return result;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@@ -12444,6 +12677,66 @@ complete_option (struct app_context *ctx, struct completion *data,
|
||||
strv_free (&options);
|
||||
}
|
||||
|
||||
static void
|
||||
complete_set_value (struct config_item *item, const char *word,
|
||||
struct strv *output)
|
||||
{
|
||||
struct str serialized = str_make ();
|
||||
config_item_write (item, false, &serialized);
|
||||
if (!strncmp (serialized.str, word, strlen (word)))
|
||||
strv_append_owned (output, str_steal (&serialized));
|
||||
else
|
||||
str_free (&serialized);
|
||||
}
|
||||
|
||||
static void
|
||||
complete_set_value_array (struct config_item *item, const char *word,
|
||||
struct strv *output)
|
||||
{
|
||||
if (!item->schema || item->schema->type != CONFIG_ITEM_STRING_ARRAY)
|
||||
return;
|
||||
|
||||
struct strv items = strv_make ();
|
||||
cstr_split (item->value.string.str, ",", false, &items);
|
||||
for (size_t i = 0; i < items.len; i++)
|
||||
{
|
||||
struct str wrapped = str_make (), serialized = str_make ();
|
||||
str_append (&wrapped, items.vector[i]);
|
||||
config_item_write_string (&serialized, &wrapped);
|
||||
str_free (&wrapped);
|
||||
|
||||
if (!strncmp (serialized.str, word, strlen (word)))
|
||||
strv_append_owned (output, str_steal (&serialized));
|
||||
else
|
||||
str_free (&serialized);
|
||||
}
|
||||
strv_free (&items);
|
||||
}
|
||||
|
||||
static void
|
||||
complete_set (struct app_context *ctx, struct completion *data,
|
||||
const char *word, struct strv *output)
|
||||
{
|
||||
if (data->location == 1)
|
||||
{
|
||||
complete_option (ctx, data, word, output);
|
||||
return;
|
||||
}
|
||||
if (data->location != 3)
|
||||
return;
|
||||
|
||||
char *key = completion_word (data, 1);
|
||||
struct config_item *item = config_item_get (ctx->config.root, key, NULL);
|
||||
if (item)
|
||||
{
|
||||
char *op = completion_word (data, 2);
|
||||
if (!strcmp (op, "-=")) complete_set_value_array (item, word, output);
|
||||
if (!strcmp (op, "=")) complete_set_value (item, word, output);
|
||||
free (op);
|
||||
}
|
||||
free (key);
|
||||
}
|
||||
|
||||
static void
|
||||
complete_topic (struct app_context *ctx, struct completion *data,
|
||||
const char *word, struct strv *output)
|
||||
@@ -12497,33 +12790,30 @@ static char **
|
||||
complete_word (struct app_context *ctx, struct completion *data,
|
||||
const char *word)
|
||||
{
|
||||
// First figure out what exactly we need to complete
|
||||
bool try_commands = false;
|
||||
bool try_options = false;
|
||||
bool try_topic = false;
|
||||
bool try_nicknames = false;
|
||||
|
||||
if (data->location == 0 && completion_matches (data, 0, "/*"))
|
||||
try_commands = true;
|
||||
else if (data->location == 1 && completion_matches (data, 0, "/set"))
|
||||
try_options = true;
|
||||
else if (data->location == 1 && completion_matches (data, 0, "/help"))
|
||||
try_commands = try_options = true;
|
||||
else if (data->location == 1 && completion_matches (data, 0, "/topic"))
|
||||
try_topic = try_nicknames = true;
|
||||
else
|
||||
try_nicknames = true;
|
||||
char *initial = completion_word (data, 0);
|
||||
|
||||
// Start with a placeholder for the longest common prefix
|
||||
struct strv words = strv_make ();
|
||||
|
||||
// Add placeholder
|
||||
strv_append_owned (&words, NULL);
|
||||
|
||||
if (try_commands) complete_command (ctx, data, word, &words);
|
||||
if (try_options) complete_option (ctx, data, word, &words);
|
||||
if (try_topic) complete_topic (ctx, data, word, &words);
|
||||
if (try_nicknames) complete_nicknames (ctx, data, word, &words);
|
||||
if (data->location == 0 && *initial == '/')
|
||||
complete_command (ctx, data, word, &words);
|
||||
else if (data->location >= 1 && !strcmp (initial, "/set"))
|
||||
complete_set (ctx, data, word, &words);
|
||||
else if (data->location == 1 && !strcmp (initial, "/help"))
|
||||
{
|
||||
complete_command (ctx, data, word, &words);
|
||||
complete_option (ctx, data, word, &words);
|
||||
}
|
||||
else if (data->location == 1 && !strcmp (initial, "/topic"))
|
||||
{
|
||||
complete_topic (ctx, data, word, &words);
|
||||
complete_nicknames (ctx, data, word, &words);
|
||||
}
|
||||
else
|
||||
complete_nicknames (ctx, data, word, &words);
|
||||
|
||||
cstr_set (&initial, NULL);
|
||||
LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
|
||||
{
|
||||
struct completion_hook *hook = (struct completion_hook *) iter;
|
||||
@@ -13642,12 +13932,15 @@ on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
|
||||
while (try_reap_child (ctx))
|
||||
;
|
||||
|
||||
if (g_termination_requested && !ctx->quitting)
|
||||
initiate_quit (ctx, NULL);
|
||||
if (g_termination_requested)
|
||||
{
|
||||
g_termination_requested = false;
|
||||
request_quit (ctx, NULL);
|
||||
}
|
||||
if (g_winch_received)
|
||||
{
|
||||
redraw_screen (ctx);
|
||||
g_winch_received = false;
|
||||
redraw_screen (ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14064,7 +14357,7 @@ main (int argc, char *argv[])
|
||||
};
|
||||
|
||||
struct opt_handler oh =
|
||||
opt_handler_make (argc, argv, opts, NULL, "Experimental IRC client.");
|
||||
opt_handler_make (argc, argv, opts, NULL, "Terminal-based IRC client.");
|
||||
bool format_mode = false;
|
||||
|
||||
int c;
|
||||
|
||||
@@ -85,3 +85,9 @@
|
||||
482 IRC_ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
|
||||
501 IRC_ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
|
||||
502 IRC_ERR_USERSDONTMATCH ":Cannot change mode for other users"
|
||||
902 IRC_ERR_NICKLOCKED ":You must use a nick assigned to you"
|
||||
903 IRC_RPL_SASLSUCCESS ":SASL authentication successful"
|
||||
904 IRC_ERR_SASLFAIL ":SASL authentication failed"
|
||||
905 IRC_ERR_SASLTOOLONG ":SASL message too long"
|
||||
906 IRC_ERR_SASLABORTED ":SASL authentication aborted"
|
||||
907 IRC_ERR_SASLALREADY ":You have already authenticated using SASL"
|
||||
|
||||
@@ -6,7 +6,7 @@ kike(1)
|
||||
|
||||
Name
|
||||
----
|
||||
kike - an experimental IRC daemon
|
||||
kike - IRC daemon
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
|
||||
33
kike.c
33
kike.c
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* kike.c: the experimental IRC daemon
|
||||
* kike.c: an IRC daemon
|
||||
*
|
||||
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
*
|
||||
@@ -614,7 +614,8 @@ struct server_context
|
||||
{
|
||||
int *listen_fds; ///< Listening socket FD's
|
||||
struct poller_fd *listen_events; ///< New connections available
|
||||
size_t n_listen_fds; ///< Number of listening sockets
|
||||
size_t listen_len; ///< Number of listening sockets
|
||||
size_t listen_alloc; ///< How many we've allocated
|
||||
|
||||
time_t started; ///< When has the server been started
|
||||
|
||||
@@ -695,7 +696,7 @@ server_context_free (struct server_context *self)
|
||||
{
|
||||
str_map_free (&self->config);
|
||||
|
||||
for (size_t i = 0; i < self->n_listen_fds; i++)
|
||||
for (size_t i = 0; i < self->listen_len; i++)
|
||||
{
|
||||
poller_fd_reset (&self->listen_events[i]);
|
||||
xclose (self->listen_fds[i]);
|
||||
@@ -746,12 +747,12 @@ irc_initiate_quit (struct server_context *ctx)
|
||||
{
|
||||
print_status ("shutting down");
|
||||
|
||||
for (size_t i = 0; i < ctx->n_listen_fds; i++)
|
||||
for (size_t i = 0; i < ctx->listen_len; i++)
|
||||
{
|
||||
poller_fd_reset (&ctx->listen_events[i]);
|
||||
xclose (ctx->listen_fds[i]);
|
||||
}
|
||||
ctx->n_listen_fds = 0;
|
||||
ctx->listen_len = 0;
|
||||
|
||||
for (struct client *iter = ctx->clients; iter; iter = iter->next)
|
||||
if (!iter->closing_link)
|
||||
@@ -3852,16 +3853,19 @@ irc_listen_resolve (struct server_context *ctx,
|
||||
int fd;
|
||||
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
|
||||
{
|
||||
if (ctx->listen_len == ctx->listen_alloc)
|
||||
break;
|
||||
|
||||
if ((fd = irc_listen (gai_iter)) == -1)
|
||||
continue;
|
||||
set_blocking (fd, false);
|
||||
|
||||
struct poller_fd *event = &ctx->listen_events[ctx->n_listen_fds];
|
||||
struct poller_fd *event = &ctx->listen_events[ctx->listen_len];
|
||||
*event = poller_fd_make (&ctx->poller, fd);
|
||||
event->dispatcher = (poller_fd_fn) on_irc_client_available;
|
||||
event->user_data = ctx;
|
||||
|
||||
ctx->listen_fds[ctx->n_listen_fds++] = fd;
|
||||
ctx->listen_fds[ctx->listen_len++] = fd;
|
||||
poller_fd_set (event, POLLIN);
|
||||
}
|
||||
freeaddrinfo (gai_result);
|
||||
@@ -3882,13 +3886,20 @@ irc_setup_listen_fds (struct server_context *ctx, struct error **e)
|
||||
|
||||
struct strv ports = strv_make ();
|
||||
cstr_split (bind_port, ",", true, &ports);
|
||||
ctx->listen_fds = xcalloc (ports.len, sizeof *ctx->listen_fds);
|
||||
ctx->listen_events = xcalloc (ports.len, sizeof *ctx->listen_events);
|
||||
|
||||
// For C and simplicity's sake let's assume that the host will resolve
|
||||
// to at most two different addresses: IPv4 and IPv6 in case it is NULL
|
||||
ctx->listen_alloc = ports.len * 2;
|
||||
|
||||
ctx->listen_fds =
|
||||
xcalloc (ctx->listen_alloc, sizeof *ctx->listen_fds);
|
||||
ctx->listen_events =
|
||||
xcalloc (ctx->listen_alloc, sizeof *ctx->listen_events);
|
||||
for (size_t i = 0; i < ports.len; i++)
|
||||
irc_listen_resolve (ctx, bind_host, ports.vector[i], &gai_hints);
|
||||
strv_free (&ports);
|
||||
|
||||
if (!ctx->n_listen_fds)
|
||||
if (!ctx->listen_len)
|
||||
{
|
||||
error_set (e, "%s: %s",
|
||||
"network setup failed", "no ports to listen on");
|
||||
@@ -3991,7 +4002,7 @@ main (int argc, char *argv[])
|
||||
};
|
||||
|
||||
struct opt_handler oh =
|
||||
opt_handler_make (argc, argv, opts, NULL, "Experimental IRC daemon.");
|
||||
opt_handler_make (argc, argv, opts, NULL, "IRC daemon.");
|
||||
|
||||
int c;
|
||||
while ((c = opt_handler_get (&oh)) != -1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--
|
||||
-- censor.lua: black out certain users' messages
|
||||
--
|
||||
-- Copyright (c) 2016, Přemysl Eric Janouch <p@janouch.name>
|
||||
-- Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
|
||||
--
|
||||
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||
-- purpose with or without fee is hereby granted.
|
||||
@@ -38,6 +38,7 @@ local read_masks = function (v)
|
||||
end
|
||||
end
|
||||
|
||||
local quote
|
||||
degesch.setup_config {
|
||||
masks = {
|
||||
type = "string_array",
|
||||
@@ -45,13 +46,29 @@ degesch.setup_config {
|
||||
comment = "user masks (optionally \"/#channel\") to censor",
|
||||
on_change = read_masks
|
||||
},
|
||||
quote = {
|
||||
type = "string",
|
||||
default = "\"\\x0301,01\"",
|
||||
comment = "formatting prefix for censored messages",
|
||||
on_change = function (v) quote = v end
|
||||
},
|
||||
}
|
||||
|
||||
local decolor = function (text)
|
||||
local rebuilt, last = {""}, 1
|
||||
for start in text:gmatch ('()\x03') do
|
||||
table.insert (rebuilt, text:sub (last, start - 1))
|
||||
local sub = text:sub (start + 1)
|
||||
last = start + (sub:match ('^%d%d?,%d%d?()') or sub:match ('^%d?%d?()'))
|
||||
end
|
||||
return table.concat (rebuilt) .. text:sub (last)
|
||||
end
|
||||
|
||||
local censor = function (line)
|
||||
-- Taking a shortcut to avoid lengthy message reassembly
|
||||
local start, text = line:match ("^(.- PRIVMSG .- :)(.*)$")
|
||||
local ctcp, rest = text:match ("^(\x01%g+ )(.*)")
|
||||
text = ctcp and ctcp .. "\x0301,01" .. rest or "\x0301,01" .. text
|
||||
text = ctcp and ctcp .. quote .. decolor (rest) or quote .. decolor (text)
|
||||
return start .. text
|
||||
end
|
||||
|
||||
|
||||
@@ -64,12 +64,13 @@ degesch.hook_prompt (function (hook)
|
||||
local lines, cols = degesch.get_screen_size ()
|
||||
x = x .. " " .. active .. string.rep (" ", cols)
|
||||
|
||||
-- Readline seems to be broken and completely corrupts the prompt
|
||||
-- (tested on 7.0.003 Archlinux, 7.0-5 Debian buster)
|
||||
x = x:gsub("[\128-\255]", "?")
|
||||
-- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
|
||||
-- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
|
||||
--x = x:gsub("[\128-\255]", "?")
|
||||
|
||||
-- Cut off extra characters and apply formatting, including the hack.
|
||||
-- Note that this doesn't count with full-width or zero-width characters.
|
||||
-- FIXME: this doesn't count with full-width or zero-width characters.
|
||||
-- We might want to export wcwidth() above term_from_utf8 somehow.
|
||||
local overflow = utf8.offset (x, cols - 1)
|
||||
if overflow then x = x:sub (1, overflow) end
|
||||
x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
|
||||
|
||||
2
test
2
test
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/expect -f
|
||||
# Very basic end-to-end testing for Travis CI
|
||||
# Very basic end-to-end testing for CI
|
||||
|
||||
# Run the daemon to test against
|
||||
system ./kike --write-default-cfg
|
||||
|
||||
26
test-nick-colors
Executable file
26
test-nick-colors
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
# Check whether the terminal colours filtered by our algorithm are legible
|
||||
export example=$(
|
||||
tcc "-run -lm" - <<-END
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
|
||||
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
|
||||
|
||||
$(perl -0777 -ne 'print $& if /^.*?\nfilter_color(?s:.*?)^}$/m' \
|
||||
"$(dirname "$0")"/degesch.c)
|
||||
|
||||
void main () {
|
||||
size_t len = 0;
|
||||
int *table = filter_color_cube_for_acceptable_nick_colors (&len);
|
||||
for (size_t i = 0; i < len; i++)
|
||||
printf ("<@\\x1b[38;5;%dmIRCuser\\x1b[m> I'm typing!\n", table[i]);
|
||||
}
|
||||
END
|
||||
)
|
||||
|
||||
# Both should give acceptable results,
|
||||
# which results in a bad compromise that the main author himself needs
|
||||
xterm -bg black -fg white -e 'echo $example; cat' &
|
||||
xterm -bg white -fg black -e 'echo $example; cat' &
|
||||
@@ -6,7 +6,7 @@ zyklonb(1)
|
||||
|
||||
Name
|
||||
----
|
||||
zyklonb - an experimental IRC bot
|
||||
zyklonb - modular IRC bot
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* zyklonb.c: the experimental IRC bot
|
||||
* zyklonb.c: a modular IRC bot
|
||||
*
|
||||
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
*
|
||||
@@ -1983,7 +1983,7 @@ main (int argc, char *argv[])
|
||||
};
|
||||
|
||||
struct opt_handler oh =
|
||||
opt_handler_make (argc, argv, opts, NULL, "Experimental IRC bot.");
|
||||
opt_handler_make (argc, argv, opts, NULL, "Modular IRC bot.");
|
||||
|
||||
int c;
|
||||
while ((c = opt_handler_get (&oh)) != -1)
|
||||
|
||||
Reference in New Issue
Block a user