From b743d6f364a88cd0519fc5ddf0a149493105908b Mon Sep 17 00:00:00 2001 From: Darren 'Tadgy' Austin Date: Tue, 31 Aug 2021 19:26:52 +0100 Subject: [PATCH] Add slacktopic.pl script to irssi. --- .irssi/.gitignore | 1 + .irssi/scripts/slacktopic.pl | 429 +++++++++++++++++++++++++++++++++++ .irssi/startup | 1 + 3 files changed, 431 insertions(+) create mode 100644 .irssi/scripts/slacktopic.pl diff --git a/.irssi/.gitignore b/.irssi/.gitignore index 04204c7..e1ae9d3 100644 --- a/.irssi/.gitignore +++ b/.irssi/.gitignore @@ -1 +1,2 @@ config +slack_update.txt diff --git a/.irssi/scripts/slacktopic.pl b/.irssi/scripts/slacktopic.pl new file mode 100644 index 0000000..0b2e5af --- /dev/null +++ b/.irssi/scripts/slacktopic.pl @@ -0,0 +1,429 @@ +#!/usr/bin/perl + +# slacktopic.pl, by B. Watson . +# 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(); diff --git a/.irssi/startup b/.irssi/startup index dbeb783..ba27c84 100644 --- a/.irssi/startup +++ b/.irssi/startup @@ -1,3 +1,4 @@ /load autowhois.pl +/load slacktopic.pl /load trackbar.pl /load urlmachine2.pl