Add slacktopic.pl script to irssi.
This commit is contained in:
parent
dbded93c95
commit
b743d6f364
3 changed files with 431 additions and 0 deletions
429
.irssi/scripts/slacktopic.pl
Normal file
429
.irssi/scripts/slacktopic.pl
Normal 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();
|
Loading…
Add table
Add a link
Reference in a new issue