37 Commits

Author SHA1 Message Date
051c43a072 NEWS: fix a garbled up entry
Try not to commit, push and tag releases tired.
2021-07-08 05:17:13 +02:00
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
2336340ad8 Bump version, update NEWS 2020-10-31 23:50:32 +01:00
8f5dec0456 degesch: buffer creation cleanup 2020-10-31 23:44:18 +01:00
3dc6ee9a5b degesch: sanitize IRC nicknames/channel names
Don't trust the IRCd to have them in a subset of UTF-8.
2020-10-31 23:25:08 +01:00
821ce04915 degesch: implement autocompletion for /set
It was super annoying to just slightly modify strings and
string arrays, now you can have existing values filled in.

complete_word() looks a bit cleaner now as well.
2020-10-31 23:18:31 +01:00
2fe3b95ecd README.adoc: improve backlog helper invocation
When fancy-prompt.lua is enabled, tho prompt is two-lined
and a simple PageUp would skip one line of content.

It works slightly better than it should: when there's under
a page of content to scroll, there is no shift at all.
2020-10-31 20:00:23 +01:00
32c99c9d66 kike: avoid crash with a wildcard address
A most unfortunate 06d3b3b regression, mostly stemming from
forgetting why the `break` was in place and not documenting it.
2020-10-31 17:34:32 +01:00
cd7133e173 README.adoc: minor documentation update 2020-10-31 16:06:13 +01:00
b4ed52015a degesch: mark some issues for later resolution 2020-10-31 16:06:12 +01:00
271689da99 fancy-prompt.lua: allow non-ASCII buffer names
It may theoretically bite us in the ass with non-UTF-8-compliant
IRC servers, and certainly with double-width characters.
2020-10-31 16:05:15 +01:00
38c23d0d38 degesch: fix fancy-prompt.lua with libedit
Partly by unifying the interface for prompt hooks to match GNU Readline.
2020-10-31 16:04:30 +01:00
439af8884c degesch: make PageUp actually scroll a page up
Now that the input to the backlog helper is wrapped the same way
as what we display.  There's a slight issue always triggered by
fancy-prompt.lua where a multiline prompt/command line makes less(1)
go too high up but it's nothing too important.
2020-10-31 16:00:55 +01:00
8ccf38ad76 Minor rebranding
There's nothing experimental about this project anymore.  It's stable.

Maybe we should add a photo of Hitler or something.
2020-10-31 13:42:56 +01:00
47a4c8beca CMakeLists.txt: clean up OpenBSD support
A few things might have changed.
2020-10-29 15:27:09 +01:00
15 changed files with 597 additions and 204 deletions

View File

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

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.

35
NEWS
View File

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

View File

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

View File

@@ -6,7 +6,7 @@ degesch(1)
Name
----
degesch - an experimental IRC client
degesch - terminal-based IRC client
Synopsis
--------

587
degesch.c
View File

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

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

@@ -6,7 +6,7 @@ kike(1)
Name
----
kike - an experimental IRC daemon
kike - IRC daemon
Synopsis
--------

33
kike.c
View File

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

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

View File

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

View File

@@ -6,7 +6,7 @@ zyklonb(1)
Name
----
zyklonb - an experimental IRC bot
zyklonb - modular IRC bot
Synopsis
--------

View File

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