IRC

Notificações do irssi no macOS: menções, ligação perdida e falta de autenticação

Como receber notificações nativas no macOS quando o irssi, a correr num servidor remoto dentro de um screen, te menciona, perde a ligação ou fica por autenticar. Inclui a definição que faz o highlight apanhar o nick em qualquer posição da frase.

Um irssi que corre num servidor remoto, dentro de um screen, não consegue mostrar notificações no teu ecrã. Quando alguém te menciona ou a ligação cai, só dás por isso ao abrir o terminal.

Este tutorial monta, no macOS, notificações nativas para três situações: uma menção ao teu nick, a perda de ligação ao servidor IRC e uma ligação restabelecida sem autenticação no NickServ. Não usa serviços externos, o que evita enviar o texto das conversas para terceiros.

Requisitos: irssi com suporte Perl no servidor, acesso ssh por chave a partir do Mac, e macOS.

Como funciona

irssi (servidor)  --escreve-->  ~/.irssi/fnotify
                                      |
Mac: ssh + tail -F  <------lê---------+
        |
        +--> osascript --> notificação nativa do macOS

Um script Perl no irssi escreve uma linha por aviso num ficheiro. No Mac, um agente launchd mantém um ssh ... tail -F aberto sobre esse ficheiro e transforma cada linha numa notificação. A limitação é que só recebes avisos com o Mac ligado e com rede: o que acontece com ele desligado perde-se.

O script do irssi

Cria ~/.irssi/scripts/fnotify.pl no servidor:

# fnotify.pl - escreve avisos (highlights, ligacao perdida, autenticacao em falta)
# num ficheiro, para serem lidos por outro processo (ex.: tail -F por ssh).
#
# Formato, uma linha por aviso, campos separados por TAB:
#   <tipo> TAB <titulo> TAB <texto>
# tipos: hilight | disconnect | auth

use strict;
use warnings;
use Irssi;
use vars qw($VERSION %IRSSI);

$VERSION = '0.1';
%IRSSI = (
    name        => 'fnotify',
    description => 'Escreve highlights, desligacoes e falhas de autenticacao num ficheiro',
    license     => 'GPLv2',
);

my %last;          # "tipo|servidor" => time do ultimo aviso (evita duplicados)
my $MAX_SIZE = 100_000;

sub clean {
    my ($s, $max) = @_;
    $s = '' unless defined $s;
    $s =~ s/[\x00-\x1f\x7f]+/ /g;  # tabs, newlines e controlos nunca saem do script
    $s =~ s/^\s+|\s+$//g;
    $s = substr($s, 0, $max - 3) . '...' if length($s) > $max;
    return $s;
}

sub notify {
    my ($type, $title, $text, $dedupe_key, $window) = @_;
    $window = 60 unless defined $window;
    if (defined $dedupe_key) {
        my $k = "$type|$dedupe_key";
        return if $last{$k} && time() - $last{$k} < $window;
        $last{$k} = time();
    }
    my $file = Irssi::settings_get_str('fnotify_file');
    $file =~ s{^~}{$ENV{HOME}};
    # ficheiro pequeno: corta quando passa o limite
    if (-s $file && -s $file > $MAX_SIZE) {
        if (open(my $t, '>', $file)) { close($t); }
    }
    my $old = umask(0077);
    if (open(my $fh, '>>', $file)) {
        print $fh join("\t", $type, clean($title, 80), clean($text, 300)), "\n";
        close($fh);
    } else {
        Irssi::print("fnotify: nao consegui escrever em $file: $!");
    }
    umask($old);
}

# 1. highlight: o que o irssi marca como HILIGHT (o teu nick e palavras em /hilight)
Irssi::signal_add('print text', sub {
    my ($dest, $text, $stripped) = @_;
    my $level = $dest->{level};
    return unless $level & Irssi::MSGLEVEL_HILIGHT();
    return if $level & Irssi::MSGLEVEL_NOHILIGHT();
    my $where = defined $dest->{target} && length $dest->{target} ? $dest->{target} : 'irssi';
    notify('hilight', $where, $stripped, "$where|$stripped", 2);
});

# 2. ligacao perdida
Irssi::signal_add('server disconnected', sub {
    my ($server) = @_;
    my $tag = $server->{tag} // '?';
    notify('disconnect', $tag, "Ligacao perdida a $tag", $tag);
});

# 3. ligado mas sem autenticacao: confirma o modo de utilizador passado uns segundos
Irssi::signal_add('event connected', sub {
    my ($server) = @_;
    my $tag  = $server->{tag} // '?';
    my $wait = Irssi::settings_get_int('fnotify_auth_wait');
    $wait = 30 if $wait < 5;
    Irssi::timeout_add_once($wait * 1000, sub {
        my $srv = Irssi::server_find_tag($tag) or return;
        return unless $srv->{connected};
        my $mode = Irssi::settings_get_str('fnotify_auth_mode');
        return unless length $mode;
        my $um = $srv->{usermode} // '';
        return if index($um, $mode) >= 0;
        notify('auth', $tag, "Ligado a $tag mas sem autenticar (modo +$mode em falta)", $tag);
    }, undef);
});

# 3b. o NickServ a pedir identificacao
Irssi::signal_add('message irc notice', sub {
    my ($server, $msg, $nick, $addr, $target) = @_;
    return unless defined $nick && lc($nick) eq 'nickserv';
    return unless $msg =~ /\b(identify|identif|register(ed)?|registad|password)\b/i;
    my $tag = $server->{tag} // '?';
    notify('auth', $tag, "NickServ: $msg", $tag);
});

Irssi::settings_add_str('fnotify', 'fnotify_file', '~/.irssi/fnotify');
Irssi::settings_add_str('fnotify', 'fnotify_auth_mode', 'r');
Irssi::settings_add_int('fnotify', 'fnotify_auth_wait', 30);

O bloco do highlight escuta o sinal print text e só avisa quando a linha tem o nível HILIGHT, o que quer dizer que respeita tudo o que o irssi já considera um highlight, incluindo as palavras que definires com /hilight. Para a falta de autenticação o script espera 30 segundos depois de ligar e verifica se o teu modo de utilizador tem a letra r, que na maioria das redes significa nick registado e identificado. Isto varia de rede para rede, por isso é configurável com /set fnotify_auth_mode.

Duas decisões que valem a pena explicar. O texto das mensagens vem de estranhos, por isso o script remove tabs, newlines e caracteres de controlo antes de escrever, e o ficheiro é criado com permissões 600. E o mesmo aviso do mesmo tipo e rede é suprimido durante 60 segundos, senão uma desligação seguida de três tentativas de reconexão dava três notificações.

Carrega-o e deixa-o em autorun:

/script load fnotify.pl
ln -s ../fnotify.pl ~/.irssi/scripts/autorun/fnotify.pl

O leitor no Mac

Cria ~/.local/bin/irssi-notify e dá-lhe chmod 755:

#!/bin/zsh
# Le os avisos do irssi no servidor (~/.irssi/fnotify) e mostra notificacoes no macOS.
# O texto vem do IRC (nao confiavel): e passado ao osascript como argumento, nunca interpolado.
notify() {  # $1=titulo $2=texto $3=som
  osascript -e 'on run argv' \
            -e 'display notification (item 2 of argv) with title (item 1 of argv) sound name (item 3 of argv)' \
            -e 'end run' -- "$1" "$2" "$3" >/dev/null 2>&1
}

ssh -o BatchMode=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ConnectTimeout=15 meuservidor \
    'tail -F -n0 ~/.irssi/fnotify 2>/dev/null' |
while IFS=$'\t' read -r type title text; do
  case $type in
    hilight)    notify "IRC $title" "$text" "Glass" ;;
    disconnect) notify "IRC: ligação perdida" "$text" "Basso" ;;
    auth)       notify "IRC: autenticar $title" "$text" "Sosumi" ;;
  esac
done
exit 1   # o ssh terminou: o launchd volta a arrancar

Troca meuservidor pelo alias do teu ~/.ssh/config. O ssh tem de entrar sem pedir password, por isso uma chave sem passphrase ou carregada num agente.

O ponto crítico é o osascript. O texto das mensagens IRC vem de estranhos, por isso é passado como argumento (item 2 of argv) e nunca colado dentro do código AppleScript. Se o texto fosse interpolado numa string, uma mensagem bem escolhida num canal podia executar comandos no teu Mac.

Manter o leitor sempre a correr

Cria ~/Library/LaunchAgents/pt.exemplo.irssi-notify.plist, trocando o caminho pela tua pasta pessoal:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>pt.exemplo.irssi-notify</string>
  <key>ProgramArguments</key><array><string>/Users/O_TEU_UTILIZADOR/.local/bin/irssi-notify</string></array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>30</integer>
  <key>StandardErrorPath</key><string>/Users/O_TEU_UTILIZADOR/Library/Logs/irssi-notify.log</string>
</dict></plist>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/pt.exemplo.irssi-notify.plist

O ThrottleInterval de 30 segundos é deliberado. Se a rede cair, o ssh falha e o launchd tenta de novo. Sem espera, isso resultaria em dezenas de ligações falhadas por minuto, e um fail2ban no servidor pode acabar por banir o teu IP.

Para testar sem tocar no irssi, escreve uma linha de teste diretamente no ficheiro:

ssh meuservidor "printf 'hilight\t#teste\t<eu> isto é um teste\n' >> ~/.irssi/fnotify"

A notificação “IRC #teste” devia aparecer no Mac. Se não aparecer, vê as permissões em Definições do Sistema, Notificações: o osascript aparece como Script Editor.

Highlight com o nick em qualquer posição

Por omissão, o irssi só considera uma mensagem como highlight quando o teu nick aparece no início da frase: “nick, tudo bem?” conta, “oi nick” não conta. Como o fnotify só reage ao que o irssi marca como highlight, uma menção a meio da frase não gera notificação. O sintoma visível é o irssi não pintar de outra cor essas linhas.

A definição que controla este comportamento vem desligada:

/set hilight_nick_matches_everywhere ON
/save

Com ela ativa, o nick conta em qualquer posição da frase, a linha é pintada com a cor de highlight e o fnotify apanha-a.

Detetar o nick no próprio script (por exemplo no sinal message public) também funciona, mas duplica avisos sempre que o irssi marca a mesma mensagem como highlight, porque o texto dos dois caminhos difere ligeiramente e a deduplicação não os reconhece. Por isso o fnotify usa uma só fonte: o que o irssi mostra como highlight é o que gera notificação, o que também respeita /hilight e o nível NOHILIGHT.

Como testar

As mensagens que tu próprio escreves nunca são highlight, por isso escrever o teu nick num canal não serve de teste. Força um highlight a partir do próprio irssi:

/script exec Irssi::active_win->print("o_teu_nick teste do fnotify", Irssi::MSGLEVEL_HILIGHT)

Ou usa um segundo cliente com outro nick (um webchat serve). Para ver o que o irssi registou:

ssh meuservidor 'tail ~/.irssi/fnotify'

Cada mensagem tem de gerar uma só linha. Se houver duas, a deduplicação não está a reconhecer uma diferença de formato entre elas.

Limitações

A deteção de ligação perdida e a de falta de autenticação dependem da rede. A de autenticação assume que a rede marca os nicks identificados com o modo +r, e o padrão do NickServ (identify, register, password) é largo, por isso pode disparar com avisos que não são pedidos de autenticação. A deduplicação limita o ruído, mas convém ajustar o padrão à tua rede. Estes dois avisos foram verificados apenas com uma versão simulada do módulo Irssi, não com uma desligação real, por isso confirma-os da primeira vez que a ligação cair.

Também não há avisos para mensagens privadas, e nada chega ao telemóvel.

Para desfazer: launchctl bootout gui/$(id -u)/pt.exemplo.irssi-notify, apaga os dois ficheiros do Mac e, no servidor, /script unload fnotify e o link em autorun.

Próximo passo

Como o servidor só escreve num ficheiro de texto, o mesmo ficheiro serve para outros destinos. Para receber os avisos também no telemóvel, basta trocar o leitor do Mac por um que envie cada linha para um serviço de push. O script do irssi não muda.

Partilhar

Comentários 0

Ainda sem comentários — sê o primeiro.