Add slacktopic.pl script to irssi.

This commit is contained in:
Darren 'Tadgy' Austin 2021-08-31 19:26:52 +01:00
commit b743d6f364
3 changed files with 431 additions and 0 deletions

View file

@ -0,0 +1,429 @@
#!/usr/bin/perl
# slacktopic.pl, by B. Watson <yalhcru@gmail.com>.
# Licensed under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details.
# This is an irssi script that updates the ##slackware topic any time
# there's a new update in the Slackware ChangeLog. Place the script in
# your ~/.irssi/scripts/autorun/ dir.
# At script startup, and again every $update_frequency seconds, we
# check for updates like so:
# - Exec a curl process to get the first part of the ChangeLog
# and extract the new date from it.
# - Extract the old date from the current /topic.
# - If the /topic has an old date, and if it's different from the new
# date, retrieve the full ChangeLog entry (up to the first "+----..." line),
# check for the string "(* Security fix *)" or similar. If nothing
# found, it's not a security update, so don't update the topic.
# - If it needs updating, update the /topic (really, get ChanServ to do # it).
# Only the date (inside []) is changed, all the other stuff is left as-is.
# Assumptions made:
# - Pat won't be updating the ChangeLog several times in the same
# minute or so. If he did, we might get confused about whether
# or not an update is a security fix update.
# - Client is set to autojoin ##slackware, or else the user will
# always manually join it. Script doesn't do anything until
# this happens.
# - At some point we'll successfully log in to services. If the
# script tries to check for updates before that happens, it
# won't hurt anything, but the topic won't get updated either.
# - We have enough ChanServ access on the channel to set the topic via
# ChanServ's topic command (flag +t in access list). Again, no
# harm done, but no topic updates either.
# Notes:
# - This script would possibly work for other FreeNode or Libera channels
# that track a ChangeLog and update the /topic when there's
# a change. You'd want to at least change @update_channels and
# $update_cmd. If you're not on FreeNode/Libera, more surgery will be
# required (if there's a way to change the /topic by talking to a
# 'services' bot, it should be possible).
# - Please don't try to talk me into using LWP and one of the Date::
# modules in place of executing curl and date. Slackware doesn't ship
# them, plus they're huge and I don't want to keep them loaded in
# irssi all the time.
# References:
# https://raw.githubusercontent.com/irssi/irssi/master/docs/perl.txt
# http://wiki.foonetic.net/wiki/ChanServ_Commands
use warnings;
use strict;
# I really wish I could just say 'use Irssi ":all"' here.
use Irssi qw/
channel_find
command
command_bind
servers
signal_add_last
timeout_add
timeout_add_once
timeout_remove
window_find_name
/;
our $VERSION = "0.2";
our %IRSSI = (
authors => 'B. Watson',
contact => 'yalhcru@gmail.com or Urchlay on libera.chat ##slackware',
name => 'slacktopic',
description => 'Updates ##slackware /topic whenever there\'s a ' .
'security update in the Slackware ChangeLog.',
license => 'WTFPL',
url => 'https://slackware.uk/~urchlay/repos/misc-scripts',
);
### Configurables.
# TODO: make some or all of these config variables into irssi
# settings? Probably overkill, this script is niche-market (probably
# nobody but the author will ever run it...)
# Print verbose debugging messages in local irssi window?
our $DEBUG = 0;
# For testing, fake the date. 0 = use the real ChangeLog date. If you
# set this, it *must* match /\d\d\d\d-\d\d-\d\d/.
#our $FAKE = '9999-99-99';
our $FAKE = 0;
# Slackware ChangeLog URL. Version number is hardcoded here. Notice it's
# the plain http URL, not https (which doesn't even exist). Actually,
# ftp would also work, but then you got the whole passive vs. active
# firewall mess.
# 20190920 bkw: old URL quit working:
#our $changelog_url =
#"http://ftp.slackware.com/pub/slackware/slackware64-14.2/ChangeLog.txt";
our $changelog_url =
"ftp://ftp.osuosl.org/pub/slackware/slackware-14.2/ChangeLog.txt";
# Max time curl will spend trying to do its thing. It'll give up after
# this many seconds, if it can't download the ChangeLog.
our $cmd_timeout = 60;
# $update_cmd will write its output here. It'll be in YYYY-MM-DD form,
# which is just what's needed for the /topic. I prefer to keep this
# in ~/.irssi, but it might be better in /tmp (especially if /tmp is
# a tmpfs).
our $cmd_outfile = "$ENV{HOME}/.irssi/slack_update.txt";
# We /exec this to get the first line of the ChangeLog. So long as Pat
# follows his standard conventions, bytes 0-28 are the first line of
# the ChangeLog. If you *really* wanted to, you could use wget instead
# of curl, but it doesn't have the --range option... All the business
# with rm and mv is to (try to) avoid ever reading the file when it's
# only partially written.
our $curl_args = "--silent --range 0-28 --max-time $cmd_timeout";
our $update_cmd = "rm -f $cmd_outfile ; " .
"curl $curl_args $changelog_url |" .
"date -u -f- '+%F' > $cmd_outfile.new ; " .
"mv $cmd_outfile.new $cmd_outfile";
if($FAKE) { $update_cmd = "echo '$FAKE' > $cmd_outfile"; }
# What channel(s) /topic are we updating?
our @update_channels = (
"##slackware", "#slackware.uk"
);
# What server are @update_channels supposed to be on? This is paranoid
# maybe, AFAIK no other network uses the ## like freenode does, so
# the channel name ##slackware should be enough to identify it. But,
# ehhh, a little paranoia goes a long way...
# 20210602 bkw: now there's libera.chat, which can be thought of as a
# fork of freenode.
our $server_regex = qr/\.libera\.chat$/;
# Seconds between update checks. Every check executes $update_script, which
# talks to ftp.slackware.com, so be polite here.
our $update_frequency = 600;
### End of configurables.
### Bookkeeping stuffs.
our $timeout_tag;
our $child_proc;
our $log_window;
### Functions.
# Print a message to the status window, if there is one. Otherwise print
# it to whatever the active window happens to be. Use this or one of
# (err|debug|log)msg for all output, don't use regular print or warn.
sub echo {
if($log_window) {
$log_window->print($_) for @_;
} else {
command("/echo $_") for @_;
}
}
sub errmsg {
my (undef, $file, $line) = caller;
echo("$file:$line: $_") for @_;
}
sub debugmsg {
goto &errmsg if $DEBUG;
}
sub logmsg {
echo("$IRSSI{name}: $_") for @_;
}
# Called once at script load.
sub init {
$log_window = window_find_name("(msgs)"); # should be status window
if($log_window) {
logmsg("Logging to status window");
} else {
logmsg("Logging to active window");
}
debugmsg("init() called");
# This gets called any time an /exec finished.
signal_add_last("exec remove", "finish_update");
# Command for manual update checks (without argument), or
# forcing the date (with an argument).
command_bind("slacktopic", "start_update");
# Check once at script load.
initial_update();
if($update_frequency < 60) {
# Typo protection. Ugh.
errmsg("You didn't really mean to set \$update_frequency to " .
"$update_frequency seconds, did you? Not starting timer. " .
"Fix the script and reload it.");
} else {
# Also, automatically run it on a timer. 3rd argument unused here.
timeout_add($update_frequency * 1000, "start_update", 0);
}
}
# Return a list of the @update_channels we're actually joined to, or
# undef (false) if none.
sub get_channels {
my @result;
my $s;
for(servers()) {
$s = $_, last if($_->{address} =~ $server_regex);
}
if(!defined($s)) {
errmsg("not connected to any server matching $server_regex");
return;
}
for(@update_channels) {
my $chan = $s->channel_find($_);
if(!$chan) {
errmsg("not joined to $_ on " . $s->{address});
next;
}
push @result, $chan;
}
return @result;
}
# First update might need to be delayed. Usually we're being autoloaded at
# irssi startup, and we might get called before autojoining the channel,
# and/or before being logged in to services. Hard-coded 10 sec here. If
# the IRC server or your ISP is being slow, the first update still might
# fail. Oh well.
sub initial_update {
if(get_channels()) {
start_update();
} else {
timeout_add_once(10 * 1000, "start_update", 0);
}
}
# Start the update process.
sub start_update {
my $force_date = shift || 0;
debugmsg("start_update() called, force_date==$force_date");
if($force_date) {
if($force_date !~ /^\d\d\d\d-\d\d-\d\d$/) {
errmsg("Invalid date '$force_date'");
} else {
set_topic_date($_, $force_date, 1) for get_channels();
}
} else {
# Don't do anything if we're not joined to the channel already.
exec_update() if get_channels();
}
}
# Called when an /exec finishes.
sub finish_update {
debugmsg("finish_update() called");
my ($proc, $status) = @_;
# We get called for *every* /exec. Make sure we only respond to
# the right one.
## debugmsg("$proc->{name}: $status");
return unless $proc->{name} eq 'slacktopic_update';
if(defined($timeout_tag)) {
timeout_remove($timeout_tag);
undef $timeout_tag;
undef $child_proc;
}
# ChanServ would let us change the topic even if we weren't in
# the channel, but let's not do that. For one thing, it's a PITA
# to retrieve the old topic, if we're not in the channel.
# No debugmsg here, get_channels() already did it.
my @chans = get_channels();
return unless @chans;
# Get the date of the last update.
my $new_date;
open my $fh, "<$cmd_outfile" or do {
errmsg("$cmd_outfile not found, update command failed");
return;
};
chomp($new_date = <$fh>);
close $fh;
$new_date ||= "";
# This should never happen, but...
if($new_date !~ /^\d\d\d\d-\d\d-\d\d$/) {
errmsg("$cmd_outfile content isn't a valid date: '$new_date'");
return;
}
for(@chans) {
set_topic_date($_, $new_date, 0);
}
}
sub set_topic_date {
my ($chan, $new_date, $force) = @_;
# Get old topic, replace the date with the new one.
debugmsg("set_topic_date() called, \$new_date is: $new_date");
my $t = $chan->{topic};
unless($t =~ s,\[\d\d\d\d-\d\d-\d\d\],[$new_date],) {
errmsg("topic doesn't contain [yyyy-mm-dd] date, fix it manually");
return;
}
# Don't do anything if the topic's already correct.
if($t eq $chan->{topic}) {
debugmsg("topic already correct, not doing anything");
return;
}
# Make sure this is a security fix update.
if(!$force) {
return unless is_security_update($new_date);
}
# Ask ChanServ to change the topic for us. We don't need +o in
# the channel, so long as we're logged in to services and have +t.
logmsg("ChangeLog updated [$new_date], asking ChanServ to update topic");
$chan->{server}->send_raw("ChanServ topic " . $chan->{name} . " $t");
}
# Called if the child process times out ($cmd_timeout + 2 sec).
sub update_timed_out {
errmsg("child process timed out, killing it");
undef $timeout_tag;
if(defined($child_proc) && defined($child_proc->{pid})) {
kill 'KILL', $child_proc->{pid};
}
undef $child_proc;
}
# Spawn $update_cmd. It'll either complete (in which case finish_update()
# gets called) or time out (in which case, update_timed_out()).
sub exec_update {
debugmsg("exec_update() called");
if($timeout_tag) {
errmsg("Timeout still active, not spawning new process");
return;
}
$child_proc = command("/exec - -name slacktopic_update $update_cmd");
$timeout_tag = timeout_add_once(
1000 * ($cmd_timeout + 2),
'update_timed_out',
0); # last arg is unused
}
# Without caching the last result, every non-security update would result
# in us wasting bandwidth rechecking the ChangeLog every $update_frequency
# sec.
our $sup_last_date = "";
our $sup_last_result;
# Return true if the first ChangeLog entry is a security update. Unlike
# the regular check-for-update that happens periodically, this one blocks
# for up to $cmd_timeout seconds. The updates only happen every few days,
# I don't see this as a real problem that needs extra complexity to solve.
# Notice we don't check the $date argument against the date read from
# the file.
sub is_security_update {
my $date = shift;
debugmsg("is_security_update($date) called");
if($date eq $sup_last_date) {
debugmsg("already checked & got '$sup_last_result' for $date");
return $sup_last_result;
}
$sup_last_date = $date;
debugmsg("getting start of ChangeLog");
my $result = 0;
my $lines = 0;
# Too bad we couldn't have kept the TCP connection open
# from the previous run of curl.
open my $pipe,
"curl --silent --no-buffer --max-time $cmd_timeout $changelog_url|";
# Read lines until we hit "(* Security fix *)" or the separator that
# ends the entry. Unfortunately, even with --no-buffer, we get a lot
# more data than we need (no harm done, just wastes a bit of bandwidth).
while(<$pipe>) {
$lines++;
# Allow Pat some typos (case insensitive, variable spacing).
if(/\(\*\s*security\s+fix\s*\*\)/i) {
$result = 1;
last;
} elsif(/^\+-+\+/) { # Separator between entries.
last;
}
}
my $curl_exit = (close $pipe) >> 8;
# 23 is "error writing output" (from curl man page), this is expected
# when we close the read pipe. If we get any other error, it's worth
# complaining about.
# Also, exit status 0 (success) isn't worth griping about.
if(($curl_exit != 23) && ($curl_exit != 0)) {
errmsg("curl exited with unexpected status: $curl_exit");
}
debugmsg("read $lines lines from ChangeLog, returning $result");
$sup_last_result = $result;
return $result;
}
### main()
init();