1#
2# usage: /sync-check [channel (servers)|-stop]
3#   examples:
4#		/sync-check *.de
5#		/sync-check
6#		/sync-check #irssi
7#		/sync-check poznan.irc.pl
8#		/sync-check #irssi poznan.irc.pl *.de
9#		/sync-check -stop
10# usage: /SET synccheck_show_all_errors [On/Off]
11#
12
13use strict;
14use Irssi 20020313 ();
15
16use vars qw($VERSION %IRSSI);
17$VERSION = "0.4.9.1";
18%IRSSI = (
19	authors		=> 'Marcin Rozycki',
20	contact		=> 'derwan@irssi.pl',
21	name		=> 'sync-check',
22	description	=> 'Script checking channel synchronization. Usage: /sync-check [channel (servers)|-stop]',
23	license		=> 'GNU GPL v2',
24	url		=> 'http://derwan.irssi.pl',
25	changed		=> 'Fri Aug  9 23:00:00 CEST 2002'
26);
27
28my $synccheck = undef;
29
30sub _print ($$$)
31{
32	my ($server, $level, $msg) = @_;
33	if (defined $synccheck and $server and my $win = $server->channel_find($synccheck->{name})) {
34		$win->print($msg, $level);
35	}
36}
37
38sub _endof
39{
40	%$synccheck = (), undef $synccheck if (defined $synccheck);
41	Irssi::print(shift) if (@_);
42}
43
44sub _new ($)
45{
46	_endof; my $server = shift;
47	return 0 unless ($server and $server->{type} eq 'SERVER' and $server->{connected});
48
49	$synccheck = {};
50	$synccheck->{time} = time;
51	$synccheck->{server} = $server->{address};
52	$synccheck->{tag} = $server->{tag};
53	$synccheck->{_error} = 0;
54	$synccheck->{_tested} = 0;
55	$synccheck->{_info} = 0;
56
57	return $synccheck;
58}
59
60sub _setchan ($)
61{
62	if (defined $synccheck) {
63		$synccheck->{name} = shift;
64		$synccheck->{channel} = lc($synccheck->{name});
65	}
66}
67
68sub _addlink ($)
69{
70	my $link = shift;
71	if (defined $synccheck and $link and $link ne $synccheck->{server}) {
72		push (@{$synccheck->{links}}, $link);
73	}
74}
75
76sub _register
77{
78	my $server = shift; my $nick = lc(shift); my $sig = shift;
79	%{$synccheck->{names}->{$server}->{$nick}} = (
80		NULL		=> 1,
81		op		=> 0,
82		voice		=> 0,
83		$sig		=> 1,
84	) if (defined $synccheck);
85}
86
87sub _isregister ($$)
88{
89	my $server = shift; my $nick = lc(shift);
90	return ((defined $synccheck and defined $synccheck->{names}->{$server}->{$nick}->{NULL}) ? 1 : 0);
91}
92
93sub _isop ($$)
94{
95	my $server = shift; my $nick = lc(shift);
96	return ((_isregister($server, $nick) and $synccheck->{names}->{$server}->{$nick}->{op}) ? 1 : 0);
97}
98
99sub _isvoice ($$)
100{
101	my $server = shift; my $nick = lc(shift);
102	return ((_isregister($server, $nick) and $synccheck->{names}->{$server}->{$nick}->{voice}) ? 1 : 0);
103}
104
105sub _rec2mod ($)
106{
107	my $hash = shift;
108	my $mod = ($hash->{voice}) ? '+' : undef; $mod .= ($hash->{op}) ? '@' : undef;
109	return $mod;
110}
111
112sub _reg2mod ($$)
113{
114	my $server = shift; my $nick = lc(shift); my $mod = undef;
115	if (_isregister($server, $nick)) {
116		$mod .= ($synccheck->{names}->{$server}->{$nick}->{voice}) ? '+' : ($synccheck->{names}->{$server}->{$nick}->{op}) ? '@' : '';
117	}
118	return $mod;
119}
120
121sub _errorregister ($$) {
122	my ($nick, $sig) = @_; my $retval = 1;
123	unless (Irssi::settings_get_bool("synccheck_show_all_errors")) {
124		$retval = ($synccheck->{registered_errors}->{$nick}->{$sig}) ? 0 : 1;
125	}
126	$synccheck->{registered_errors}->{$nick}->{$sig}++;
127	return $retval;
128}
129
130sub _adderror ($$$$)
131{
132	my ($nick, $sig, $server, $error) = @_;
133	if (_errorregister $nick, $sig) {
134		push @{$synccheck->{errors}->{$server}}, $error;
135	}
136}
137
138sub _flusherrors ($$)
139{
140	my ($local, $remote) = @_;
141	if ($#{$synccheck->{errors}->{$remote}} >= 0) {
142		_print($local, MSGLEVEL_CLIENTCRAP, "(error in synchronization will be shown only once for the first server where the error exists, but it can exists on more servers; if you want to show all errors use /set synccheck_show_all_errors On)")
143			if (!Irssi::settings_get_bool("synccheck_show_all_errors") and !$synccheck->{_info}++);
144		_print($local, MSGLEVEL_CLIENTCRAP, "%RPossible channel %n%_$synccheck->{name}%_%R desynced%n%_ $synccheck->{server} <-> $remote%_:");
145		for (@{$synccheck->{errors}->{$remote}})
146		{
147			_print($local, MSGLEVEL_CLIENTCRAP, "%_".sprintf("%03d", ++$synccheck->{_error}).".%_ $_");
148		}
149		delete $synccheck->{errors}->{$remote};
150	}
151}
152
153sub _addnames
154{
155	my $remote = shift; return unless ($remote and defined $synccheck);
156	for (@_)
157	{
158		/^\@/ and substr($_, 0, 1) = "", _register($remote, $_, "op"), next;
159		/^\+/ and substr($_, 0, 1) = "", _register($remote, $_, "voice"), next;
160		_register($remote, $_, "NULL");
161	}
162}
163
164sub _numlinks
165{
166	return ((defined $synccheck and defined $synccheck->{links}) ? scalar(@{$synccheck->{links}}) : 0);
167}
168
169sub tdiff ($)
170{
171	my $end = time(); my $start = shift;
172	return (($start and $start =~ /^\d+$/ and $start <= $end) ? ($end - $start) : 0);
173}
174
175sub _synccheck ($)
176{
177	my $local = shift; my $remote = ${$synccheck->{links}}[$synccheck->{_tested}++];
178
179	_endof("End of sync-check (canceled)"), return
180		if (!$synccheck or !$local->channel_find($synccheck->{name}));
181
182	unless ($remote) {
183		_print($local, MSGLEVEL_CLIENTCRAP, "%_Sync-check%_ in $synccheck->{name} ($synccheck->{tag}) %_finished in ".tdiff($synccheck->{time})." secs%_");
184		_endof; return;
185	}
186
187	_print($local, MSGLEVEL_CLIENTCRAP, "%K->%n checking $synccheck->{name}: $synccheck->{server} %_<-> $remote%_ %K[%n$synccheck->{_tested}/"._numlinks."%K]%n");
188
189	$local->redirect_event("names", 0, '', 1, undef, {
190		'event 353'	=> 'redir names line',
191		'event 366'	=> 'redir names done',
192		'event 402',	=> 'redir names split',
193		''		=> 'event empty' });
194
195	$local->send_raw("NAMES $synccheck->{channel} :$remote");
196}
197
198sub _test
199{
200	my ($local, $remote) = @_;
201
202	unless (_isregister $remote, $local->{nick}) {
203		_adderror($local->{nick}, "notexsit", $remote, "%_you\'re%_ not in channel $synccheck->{name} on $remote");
204		_flusherrors($local, $remote);
205		delete $synccheck->{names}->{$remote};
206		return;
207	}
208
209	my $channel = $local->channel_find($synccheck->{name});
210	_endof, return unless $channel;
211
212	my %orig = (); map($orig{lc($_->{nick})} = $_, $channel->nicks());
213
214	foreach my $nick (keys %{$synccheck->{names}->{$remote}})
215	{
216		if (!$orig{$nick}) {
217			_adderror($nick, "notexist", $remote, "%_*notexist%_($synccheck->{server}) %_!= "._reg2mod($remote, $nick)."$nick%_($remote)");
218			$orig{$nick} = 0; next;
219		}
220
221		my $op = _isop $remote, $nick; my $voice = _isvoice $remote, $nick;
222		if ($orig{$nick}->{op} != $op) {
223			my $mod1 = _rec2mod($orig{$nick}); my $mod2 = _reg2mod($remote, $nick);
224			_adderror($nick, "op", $remote, "%_$mod1%_$nick($synccheck->{server}) %_!= $mod2%_$nick($remote)");
225
226		} elsif (!$op and $orig{$nick}->{voice} != $voice) {
227			my $mod1 = _rec2mod($orig{$nick}); my $mod2 = _reg2mod($remote, $nick);
228			_adderror($nick, "voice", $remote, "%_$mod1%_$nick($synccheck->{server}) %_!= $mod2%_$nick($remote)");
229		}
230		$orig{$nick} = 0;
231	}
232	delete $synccheck->{names}->{$remote};
233
234	foreach my $nick (keys %orig)
235	{
236		next unless $orig{$nick};
237		_adderror($nick, "notexist", $remote, _rec2mod($orig{$nick})."%_$nick%_($synccheck->{server}) %_!= *notexist%_($remote)");
238	}
239
240	_flusherrors($local, $remote);
241	_synccheck $local;
242}
243
244Irssi::command_bind 'sync-check' => sub
245{
246	my $usage = "/%_sync-check%_ [%_channel%_ (%_servers%_)|%_-stop%_]";
247
248	unless ($_[1] and $_[1]->{type} eq 'SERVER' and $_[1]->{connected}) {
249		Irssi::print("Not connected to server");
250		return;
251	}
252
253	if (defined $synccheck) {
254		if ($_[0] !~ /^-stop/) {
255			Irssi::print("Sync-check already running " . tdiff($synccheck->{time}) . " secs ago for channel %_$synccheck->{name}%_, wait...");
256		} else {
257			_endof("%_Stopping%_ sync-checker for channel %_$synccheck->{name}%_ in $synccheck->{tag}")
258		}
259		return;
260	}
261
262	return unless _new($_[1]);
263
264	foreach (split / +/, $_[0])
265	{
266		/^-yes/i and $synccheck->{_yes} = 1, next;
267		/^-stop$/ and _endof("Not running any sync-checker"), return;
268		/^-/ and _endof("Unknown argument: %_$_%_, usage: $usage"), return;
269		if ($_[1]->ischannel($_)) { _setchan $_; } else { _addlink $_; }
270	}
271
272	if ($synccheck->{channel} and !$_[1]->channel_find($synccheck->{channel})) {
273		_endof("You\'re not in channel %_$synccheck->{name}%_"); return;
274	} elsif (!$synccheck->{channel}) {
275		if ($_[2] and $_[2]->{type} eq 'CHANNEL') {
276			_setchan $_[2]->{name};
277		} else {
278			_endof("Not joined to any channel"); return;
279		}
280	}
281
282	if (!_numlinks) {
283		_endof("Doing this is not a good idea. Add -YES option to command if you really mean it"), return unless ($synccheck->{_yes});
284
285		_print($_[1], MSGLEVEL_CLIENTCRAP, "Checking for %_links%_ from %_$synccheck->{server}%_ in %_$synccheck->{tag}%_, wait...");
286		$_[1]->redirect_event('links', 0, '', 1, undef, {
287			'event 364'	=> 'redir links line',
288			'event 365'	=> 'redir links done',
289			''		=> 'event empty' });
290		$_[1]->send_raw('LINKS :*');
291
292	} else {
293		if (_numlinks) {
294			_print($_[1], MSGLEVEL_CLIENTCRAP, "%_Checking channel $synccheck->{name} synchronization%_ in: $synccheck->{server} %_<->%_ @{$synccheck->{links}}. This will take a while..");
295			_synccheck $_[1];
296		}
297	}
298};
299
300Irssi::Irc::Server::redirect_register(
301	"links", 0, 0,
302	{ "event 364" => 1, },
303	{ "event 402" => 1, "event 263" => 1, "event 365" => 1, },
304	undef,
305);
306
307Irssi::Irc::Server::redirect_register(
308	"names", 0, 0,
309	{ "event 353" => 1, },
310	{ "event 366" => 1,
311	  "event 402" => 1, },
312	undef,
313);
314
315Irssi::signal_add 'redir links line' => sub {
316	$_[1] =~ /(.*) (.*) (.*) :(.*)/;
317	_addlink $2;
318};
319
320Irssi::signal_add 'redir links done' => sub {
321	if (_numlinks) {
322		_print($_[0], MSGLEVEL_CLIENTCRAP, "%_Checking channel $synccheck->{name} synchronization%_ in: $synccheck->{server} %_<->%_ @{$synccheck->{links}}. This will take a while..");
323		_synccheck $_[0];
324	}
325};
326
327Irssi::signal_add 'redir names line' => sub {
328	$_[1] =~ /(.*) (.*) :(.*)/;
329	_addnames($_[2], split(" ", $3)) if (defined $synccheck and lc($2) eq $synccheck->{channel});
330};
331
332Irssi::signal_add 'redir names done' => sub
333{
334	$_[1] =~ /(.*) (.*) :(.*)/;
335	_test($_[0], $_[2]) if (defined $synccheck and lc($2) eq $synccheck->{channel});;
336};
337
338Irssi::signal_add 'redir names split' => sub
339{
340	$_[1] =~ /(.*) (.*) :(.*)/;
341	_print($_[0], MSGLEVEL_CLIENTCRAP, "%K->%n%_ $2%_: cannot find link (".lc($3)."), skipping");
342	_synccheck $_[0];
343};
344
345Irssi::settings_add_bool('misc', 'synccheck_show_all_errors', 0);
346
347