429 lines
13 KiB
Perl
429 lines
13 KiB
Perl
#!/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-15.0/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();
|