23 Commits

Author SHA1 Message Date
0fe0b56280 Bump version, update NEWS 2021-07-08 05:09:30 +02:00
f0281cf028 test-nick-colors: fix and streamline
A recent addition of an N_ELEMENTS macro invocation broke it.
2021-06-25 06:35:00 +02:00
da5dd4eb91 degesch: make /ban and /unban respect EXTBAN 2021-06-17 12:21:48 +02:00
10cb6651c0 degesch: expand/analyze a few TODO comments 2021-06-16 22:10:25 +02:00
7f28dcd1ef degesch: make "/help /command" work
Works for aliases as well.  Resolves a TODO entry.
2021-06-16 21:57:47 +02:00
61c52d793c degesch: fix a GCC compiler warning 2021-06-15 07:11:35 +02:00
b4dd0052ff degesch: pick colours based on relative luminance
Replaces the inaccurate Rec. 709 luma we used to use before.

This is the first feature here that requires libm, which doesn't
seem to be a particularly great sacrifice.

Moreover, I've rectified that the input isn't linear in sRGB,
and then was even normalized wrong for the luma formula.
2021-06-15 07:09:23 +02:00
e3c47c33fa degesch: implement -=/+= for multiple values
It didn't make sense to have these unimplemented,
though perhaps += shouldn't enforce a set.

Sadly, autocomplete is fairly difficult for -= of multiple items.
2021-06-14 09:06:38 +02:00
80c1e8f8eb degesch: make /deop and /devoice default to self
It's pretty annoying to type `/mode -o <user>`, for little reason.
2021-06-03 00:12:22 +02:00
c5f49ab1e6 censor.lua: strip colours, configurable formatting
Colour parsing code taken from prime.lua, and modified to strip.
2021-06-03 00:12:22 +02:00
6f62b9c0c7 degesch: make CHGHOST update our own userhost info
I've almost forgotten that we use this for message spliting.
2021-05-30 08:23:23 +02:00
c1d69e3630 degesch: add support for IRCv3 chghost
This is somewhat similar to a nick change.
2021-05-30 08:06:38 +02:00
c75ef167f2 degesch: document the SASL EXTERNAL support
So far it's only been mentioned in the NEWS file,
which is definitely not sufficient.

It would be good to move this kind of stuff out from README.adoc.
2021-05-29 06:38:33 +02:00
ddffc71abe degesch: factor out irc_try_finish_cap_negotiation()
Too much repeated, non-obvious code.
2021-05-28 04:59:21 +02:00
5a0b2d1c57 degesch: add trivial SASL EXTERNAL support
Just set `tls_cert`, and add `sasl` to `capabilities`.
2021-05-28 04:59:20 +02:00
bb451a5050 degesch: support CAP DEL, request cap-notify
It doesn't require much effort to cancel capabilities, plus with
the newer version we get the respective notification anyway.
2021-05-28 04:59:20 +02:00
61f15ead8a degesch: don't CAP REQ when already registered
The list may later be requested manually, which shouldn't have
an unexpected side-effect.
2021-05-28 04:59:20 +02:00
17f430043a degesch: IRCv3.2 capability negotiation
We can receive and display capability values now.
2021-05-28 04:59:20 +02:00
735096d76d degesch: add a /squery command for IRCnet 2021-05-28 04:06:27 +02:00
1ba59e6ee0 degesch: fix back-parsing outgoing CAP REQ
The bug has apparently been there since the beginning.
2021-05-28 04:04:44 +02:00
f9ba682c0e degesch: reset away-notify on disconnect
Forgotten to do it when adding the support for it.
2021-05-28 04:04:23 +02:00
8e8ffe2c73 degesch: don't switch to channels while typing
We might just always set the highlighted bit on,
it would be consistent with PMs.
2021-04-10 05:11:46 +02:00
d05c85833d degesch: make a second SIGINT force-quit
Also fixed the possibility of eating a sequence of signals
as we reset the indicators /after/ we took action,
which creates a time window for races.
2020-11-01 15:33:16 +01:00
9 changed files with 423 additions and 130 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0)
project (uirc3 VERSION 1.1.0 LANGUAGES C)
project (uirc3 VERSION 1.2.0 LANGUAGES C)
# Options
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
@@ -61,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}})

View File

@@ -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.

26
NEWS
View File

@@ -1,3 +1,29 @@
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

View File

@@ -125,6 +125,10 @@ as a `forking` type systemd user service.
Client Certificates
-------------------
'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:

461
degesch.c
View File

@@ -1,7 +1,7 @@
/*
* 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>
@@ -1718,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):
@@ -1730,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
@@ -1778,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");
@@ -1796,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);
@@ -1841,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;
}
@@ -1887,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);
}
@@ -2104,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;
@@ -2337,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",
@@ -3028,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
@@ -4031,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) \
@@ -4083,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",
@@ -4956,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);
@@ -5021,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)
{
log_global_status (ctx, "Shutting down");
if (!ctx->quitting)
{
log_global_status (ctx, "Shutting down");
ctx->quitting = true;
// Hide the user interface
CALL (ctx->input, hide);
// 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)))
@@ -5042,7 +5095,6 @@ initiate_quit (struct app_context *ctx, const char *message)
irc_destroy_connector (s);
}
ctx->quitting = true;
try_finish_quit (ctx);
}
@@ -5689,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)
@@ -6398,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);
@@ -6519,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)
{
@@ -6534,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)
{
@@ -6559,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)
{
@@ -6663,8 +6820,13 @@ irc_handle_join (struct server *s, const struct irc_message *msg)
str_map_set (&s->irc_buffer_map, channel->name, buffer);
buffer_add (s->ctx, buffer);
// XXX: this is annoying, consider only doing it a while after /join
buffer_activate (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))
@@ -7236,22 +7398,24 @@ irc_handle_topic (struct server *s, const struct irc_message *msg)
static struct irc_handler g_irc_handlers[] =
{
// This list needs to stay sorted
{ "AWAY", irc_handle_away },
{ "CAP", irc_handle_cap },
{ "ERROR", irc_handle_error },
{ "INVITE", irc_handle_invite },
{ "JOIN", irc_handle_join },
{ "KICK", irc_handle_kick },
{ "KILL", irc_handle_kill },
{ "MODE", irc_handle_mode },
{ "NICK", irc_handle_nick },
{ "NOTICE", irc_handle_notice },
{ "PART", irc_handle_part },
{ "PING", irc_handle_ping },
{ "PRIVMSG", irc_handle_privmsg },
{ "QUIT", irc_handle_quit },
{ "TAGMSG", irc_handle_tagmsg },
{ "TOPIC", irc_handle_topic },
{ "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 },
{ "KICK", irc_handle_kick },
{ "KILL", irc_handle_kill },
{ "MODE", irc_handle_mode },
{ "NICK", irc_handle_nick },
{ "NOTICE", irc_handle_notice },
{ "PART", irc_handle_part },
{ "PING", irc_handle_ping },
{ "PRIVMSG", irc_handle_privmsg },
{ "QUIT", irc_handle_quit },
{ "TAGMSG", irc_handle_tagmsg },
{ "TOPIC", irc_handle_topic },
};
static bool
@@ -7784,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)
{
@@ -7840,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);
@@ -7932,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:
@@ -7988,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);
@@ -10916,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)
@@ -10962,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;
}
@@ -10992,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)
{
@@ -11241,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)
{
@@ -11280,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;
}
@@ -11405,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);
@@ -11484,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);
@@ -11769,11 +11963,17 @@ static bool
handle_command_channel_mode
(struct handler_args *a, bool adding, char mode_char)
{
if (!*a->arguments)
return false;
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;
@@ -11856,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 },
@@ -11877,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",
@@ -12025,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];
@@ -12034,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;
}
@@ -12115,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';
@@ -13722,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);
}
}

View File

@@ -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"

View File

@@ -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

2
test
View File

@@ -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
View 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' &