Discord in my IRC client_

June 29, 2021 @13:45

I don't understand the desire to shove everything into a web browser. Other than the fact that it is how tech startups extract money from venture capitalists, spy on your user base, and lock out interoperability I don't see why a web browser is a better place to implement most things and yet, here we are. I have resisted participating in the new real time chat services because they don't really offer much over IRC, however I finally gave in and joined a group of friends on Discord.

irssi in tmux

You may be shocked to hear that I don't use their bloated Electron client, instead I connect with my trusty IRC client (irssi). The nuts and bolts of that is simple, I use rdircd to connect to Discord and bridge it into my IRC client, but that's not really all that interesting. I found a couple situations where I needed some additional functionality. Now I'm sure installing the Discord app would have provided a solution but you may imagine that I didn't want to deal with that.

Notifications

I run my IRC client in a tmux session on a server. This server is in a colocation facility and is up all the time (it is the same server hosting this website) if I am looking at it or not. Sometimes I'm away and people ping me on IRC. Usually if it is critical they will also SMS or e-mail me so I don't particularly care; however, with Discord there seems to be an expectation that it is like SMS in that the recipient will get notified immediately. To bridge this behavior I wrote an irssi plugin that e-mails me if I am inactive and directed messages come in. It is polite, in that it will queue messages if it has recently e-mailed me and has filters so I can ignore words/senders/channels if need be.

# tomail.pl (c) 2012 - 2021 Matthew J. Ernisse <matt@going-flying.com>
# All Rights Reserved
#
# Send a mail for incoming message events.
#
# The script will send a message to a configured e-mail account
# after a delay has elapsed.  The delay is set per-channel, or
# per-query session and is also gated by a global that watches
# the last time the connected user has uttered something.
#
# Since I am nearly never set myself /away, this seemed like the
# most logical way to solve this particular problem.
#
# (inspired by hilightwin.pl by Timo Sirainen, and mailhilights.pl
#  by Fernando 'Bucciarati' Vezzosi)
#
# Redistribution and use in source and binary forms,
# with or without modification, are permitted provided
# that the following conditions are met:
#
#     * Redistributions of source code must retain the
#       above copyright notice, this list of conditions
#       and the following disclaimer.
#     * Redistributions in binary form must reproduce
#       the above copyright notice, this list of conditions
#       and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# For A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
use Irssi qw(print signal_add settings_add_str settings_add_int
    settings_get_str settings_get_int);
use POSIX qw(strftime);
use IPC::Open2;

use vars qw($VERSION %IRSSI);

%IRSSI = (
    authors     => "Matthew Ernisse",
    contact     => 'matt@going-flying.com',
    name        => "tomail",
    description => "Send mail for incoming messages",
    license     => "BSD"
);
$VERSION = "1.0";

my $active = 0;
my $last_message = 0;
my %queue;

sub count_messages {
    my $cnt = 0;
    foreach my $key (keys %queue) {
        $cnt += scalar split("\n", $queue{$key});
    }
    return $cnt;
}

sub send_mail {
    my $command = settings_get_str('tomail_command');
    my $count = count_messages;
    my $from = settings_get_str('tomail_from');
    my $to = settings_get_str('tomail_to');
    my $subj = settings_get_str('tomail_subject_prefix');
    $subj = "$subj $count new messages received";

    my $body = "";
    foreach my $src (keys %queue) {
        $body = $body . "\n" . $src . ":\n";
        $body = $body . "--------------------------------------";
        $body = $body . "--------------------------------------";
        $body = $body . "\n" . $queue{$src} . "\n\n";

        delete $queue{$src};
    }

    open MAIL, "| " . $command;
    print MAIL "From: " . $from . "\n";
    print MAIL "To: " . $to . "\n";
    print MAIL "Subject: " . $subj . "\n\n";
    print MAIL $body . "\n";
    close MAIL;

    if ( $? != 0 ) {
        print "Error sending mail: $command returned $?";
        return 1;
    }
    print "[tomail] Mail sent with $count messages.";
    $last_message = time;
}

# This runs every time a server event happens (most often a PONG) so
# it works as a periodic timer.  If we are idle, have not sent a message
# in the last 5 minutes and we have messages in the queue, send away.
sub sig_event {
    my $delay = settings_get_int('tomail_delay_min') * 60;

    return if (time - $active < $delay);
    return if (time - $last_message < 600);
    return if (count_messages eq 0);

    send_mail;
}

# Insert messages into the query hash based on some criteria:
# - Are we active?
# - Is this a message a query with me?
# - Is this message in a Discord control channel?
# - Is this message a Discord message to @everyone
# - Does this message match an ignore pattern?
sub sig_message {
    my ($server, $msg, $src, $srcmask, $target) = @_;

    my $delay = settings_get_int('tomail_delay_min') * 60;
    my $directed = 0;
    my $from = $src;
    my $ign_pattern = settings_get_str('tomail_ignore_pattern');
    my $now = strftime("%H:%M:%S", gmtime());
    my $query = 0;

    # If the message is a privmsg the target is me, otherwise it is
    # a channel.  Set the query bool and target up as needed.
    #
    # rdircd (Discord) puts direct message / private group messages
    # into #me.chat.(server_id) channels, these should be treated as
    # queries.
    if (($target eq $server->{nick}) or ($target =~ /^#me\./)) {
        $query = 1;
    } else {
        $from = $target;
    }

    # Ignore the rdircd (Discord) control/monitor channels.
    return if ($target =~ /rdircd\./);

    if (($msg =~ /$server->{nick}/i) or ($msg =~ /\@everyone/i)) {
        $directed = 1;
    }

    # If we are active, who cares.
    return if (time - $active < $delay);

    # If my nick isn't in the message and it isn't a query I don't
    # care about it.
    return unless $directed or $query;

    # return if we're ignoring this pattern.
    if ($ign_pattern ne '') {
        return if ($text =~ /$ign_pattern/);
        return if ($from =~ /$ign_pattern/);
    }

    $queue{$from} = "" unless defined $queue{$from};
    $queue{$from} = "$queue{$from}<$now> [$src]: $msg\n";
}

sub sig_keypress {
    if (count_messages gt 0) {
        print "[tomail] Clearing queue of " . count_messages .
            " messages.";
        foreach my $src (keys %queue) {
            delete $queue{$src};
        }
    }

    $active = time;
}

# setup signal handlers
signal_add('server event', 'sig_event');
signal_add('gui key pressed', 'sig_keypress');
signal_add('message public', 'sig_message');
signal_add('message private', 'sig_message');

# setup default settings
settings_add_str('tomail', 'tomail_to', "");
settings_add_str('tomail', 'tomail_from', "");
settings_add_int('tomail', 'tomail_delay_min', 5);
settings_add_str('tomail', 'tomail_ignore_pattern', "");
settings_add_str('tomail', 'tomail_subject_prefix', "[IRC]");
settings_add_str('tomail', 'tomail_command',
    "/usr/sbin/sendmail -oi -t");

The script is reasonably well documented. You can swap the sendmail binary to something like msmtp if you don't have a working mail system on the host you run irssi on.

Catching URLs

I usually just copy/paste URLs out of irssi into my web browser. This has worked for the last 20 or so years, but in part due to the fact that rdircd exposes the URLs of assets shared in Discord chats and in part due to the nature of Discord chats I found myself wanting a way to catch and share URLs easily between devices. Another irssi script forms the foundation, sending URLs up to a simple Flask app that provides a list of URLs as clickable links. The latter is left up to the reader, though if there is interest I may write about it separately, the irssi plugin is rather simple and is as follows.

# urllogger.pl (c) 2020 - 2021 Matthew J. Ernisse <matt@going-flying.com>
# All Rights Reserved
#
# Logs all urls from #channels and /msgs into my flask junkdrawer.  Based on
# urlwindow.pl by zdleaf@zinc.london.
#
# Redistribution and use in source and binary forms,
# with or without modification, are permitted provided
# that the following conditions are met:
#
#     * Redistributions of source code must retain the
#       above copyright notice, this list of conditions
#       and the following disclaimer.
#     * Redistributions in binary form must reproduce
#       the above copyright notice, this list of conditions
#       and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
use strict;
use utf8;

use Irssi;
use POSIX;
use JSON;
use LWP::UserAgent;

use vars qw($VERSION %IRSSI);

%IRSSI = (
    authors     => "mernisse",
    contact     => 'matt@going-flying.com',
    name        => 'urllogger',
    description => "Log all urls from privmsg and pubmsgs.",
    license     => "BSD",
    url         => "http://irssi.org/",
);
$VERSION = "1.0";

# These are robots, ignore them.
my @IGNORES = ('marvin', 'IdleRPG');

sub sig_message {
    my ($server, $msg, $src, $srcmask, $target) = @_;

    foreach my $nick ( @IGNORES ) {
        return if ($nick eq $src);
    }

    if ($msg =~ qr#((?:(https?|gopher|gemini|ftp)://[^\s<>"]+|www\.[-a-z0-9.]+)[^\s.,;<">\):])#) {

        my $ua = LWP::UserAgent->new;
        my $res = $ua->put(
            "https://ssl.ub3rgeek.net/mobiletools/api/v1/urls",
            "Content-Type" => "application/json; charset=utf-8",
            Content => JSON->new->encode({
                source=>$src,
                url=>"$&"}
            )
        );

        if ((! $res->is_success) && ($res->code != 409)) {
            Irssi::print("ERROR [$&]: $res->status_line");
        }
    }
}

Irssi::signal_add('message public', 'sig_message');
Irssi::signal_add('message private', 'sig_message');

The real trick here was handling UTF-8 properly so that it was encoded all the way into the SQLite database in the Flask app. You could gut all the JSON / LWP bits out and write the links as a file that you serve if you don't want a fancy webthing on the frontend, but I already had a Flask junk-drawer and adding a list of URLs to it was trivial.

Conclusion

I think all the software I write these days falls into the category of I wrote this because I don't want to use the crappy web interface. This is no different. They will probably work even if you use any of the other webchat services as long as you plumb them into irssi. Some customization is almost assuredly required.

As a side-ramble, it was nice to go write a little Perl again after so many years of being away from it. I never liked Perl back in the day but after writing a bunch of JavaScript and Ruby lately it felt comfy to go visit.

Subscribe via RSS. Send me a comment.