aboutsummaryrefslogtreecommitdiff
path: root/slacktopic.pl
blob: ea450f1a70297b31c358239e237efa8977b93df8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
#!/usr/bin/perl

# irssi script. updates the ##slackware topic any time there's a security
# update in the Slackware ChangeLog.

# 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 date from it.
# - Extract the date from the current /topic.
# - If the /topic has a date, and if it's different, update the
#   /topic. Only the date (inside []) is changed, all the other
#   stuff is left as-is.

# Assumptions made:
# - Every new ChangeLog entry is a security fix. This is almost
#   100% true for stable releases (which is what we track), and
#   the only other updates will be fixes for major regressions...
# - 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.

# TODO: send debug/error/status messages to window #1, including
# successful topic updates. Otherwise, I'll never know when this script
# is working (the topic gets changed by ChanServ, I can't tell if that
# was my script doing it or another op doing it manually...)

# References:
# https://raw.githubusercontent.com/irssi/irssi/master/docs/perl.txt
# http://wiki.foonetic.net/wiki/ChanServ_Commands

use warnings;
use strict;

use Irssi qw/
	command
	timeout_add
	timeout_add_once
	timeout_remove
	window_find_name
	window_find_item
	windows
	window_find_refnum
	channel_find
	command_bind
	pidwait_add
	signal_add_last
/;

our $VERSION = "0.1";
our %IRSSI = (
	authors     => 'Urchlay',
	contact     => 'Urchlay on FreeNode ##slackware',
	name        => 'slacktopic',
	description => 'Updates ##slackware /topic whenever there\'s a ' .
	               'security update in the Slackware ChangeLog.',
	license     => 'WTFPL',
	url         => 'none',
);

# 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 = 1;

# 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.
our $changelog_url =
	"http://ftp.slackware.com/pub/slackware/slackware64-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.
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.
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_channel = "##slackware";

# What server is $update_channel 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...
our $server_regex = qr/\.freenode\.(org|net)$/;

# Seconds between update checks. Every check executes $update_script, which
# talks to ftp.slackware.com, so be polite here.
our $update_frequency = 600;

# Bookkeeping stuffs.
our $timeout_tag;
our $child_proc;
our $log_window;

# 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 {
	return unless $DEBUG;
	my (undef, $file, $line) = caller;
	echo("$file:$line: $_") for @_;
}

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 updates.
	command_bind("slacktopic", "start_update");

	# Check once at script load.
	start_update();

	# Also, automatically run it on a timer. 3rd argument unused here.
	timeout_add($update_frequency * 1000, "start_update", 0);
}

# Start the update process.
sub start_update {
	debugmsg("start_update() called");

	# Don't do anything if we're not joined to the channel already.
	my $chan = channel_find($update_channel);
	if(!$chan) {
		errmsg("not joined to $update_channel");
		return;
	}

	my $server = $chan->{server}->{address};
	if($server !~ $server_regex) {
		errmsg("channel $update_channel server is wrong: " .
				 $chan->{server}->{address});
		return;
	}

	exec_update();
}

# 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;
	}

	my $chan = channel_find($update_channel);
	if(!$chan) {
		debugmsg("not joined to $update_channel");
		return;
	}

	# 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;
	}

	# Get old topic, replace the date with the new one.
	debugmsg("\$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;
	}

	# 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 $update_channel $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

	# is this really necessary? seems it isn't.
	#pidwait_add($child_proc->{pid});
}

# main()
init();