1# INSTALLATION
2# [&bitlbee] set typing_notice true
3# <@root> typing_notice = `true'
4# AND
5# /statusbar window add typing_notice
6#
7# SETTINGS
8# [bitlbee]
9# bitlbee_send_typing = ON
10#   -> send typing messages to buddies
11# bitlbee_typing_allwin = OFF
12#   -> show typing notifications in all windows
13#
14#
15# Changelog:
16#
17# 2016-01-01 (version 1.7.2)
18# * Fix crash in Irssi during disconnect
19#
20# 2010-08-09 (version 1.7.1)
21# * Multiple control channels supported by checking chanmodes
22#
23# 2010-07-27 (version 1.7)
24# * Using new server detection for latest BitlBee support
25#
26# 2010-07-26 (version 1.6.3)
27# * Removed checking if nicks exists in &bitlbee channel, this because BitlBee
28#   can be used without control channel from this date
29#
30# 2007-03-03 (version 1.6.2)
31# * Fix: timers weren't deleted correctly. This resulted in huge mem usage.
32#
33# 2006-11-02 (version 1.6.1)
34# * Sending typing works again.
35#
36# 2006-10-27 (version 1.6)
37# * 'channel sync' re-implemented.
38# * bitlbee_send_typing was a string setting, It's a boolean now, like it should.
39#
40# 2006-10-24 (version 1.5)
41# * Sending notices to online users only. ( removed this again at 2010-07-26, see above )
42# * Using the new get_channel function;
43#
44# 2005-12-15 (version 1.42):
45# * Fixed small bug with typing notices disappearing under certain circumstances
46#   in channels
47# * Fixed bug that caused outgoing notifications not to work
48# * root cares not about our typing status.
49#
50# 2005-12-04 (version 1.41):
51# * Implemented stale states in statusbar (shows "(stale)" for OSCAR connections)
52# * Introduced bitlbee_typing_allwin (default OFF). Set this to ON to make
53#   typing notifications visible in all windows.
54#
55# 2005-12-03 (version 1.4):
56# * Major code cleanups and rewrites for bitlbee 1.0 with the updated typing
57#   scheme. TYPING 0, TYPING 1, and TYPING 2 are now supported from the server.
58# * Stale states (where user has typed in text but has stopped typing) are now
59#   recognized.
60# * Bug where user thinks you are still typing if you close the window after
61#   typing something and then erasing it quickly.. fixed.
62# * If a user signs off while they are still typing, the notification is removed
63# This update by Matt "f0rked" Sparks
64#
65# 2005-08-26:
66# Some fixes for AIM, Thanks to Dracula.
67#
68# 2005-08-16:
69# AIM supported, for sending notices, using CTCP TYPING 0. (Use the AIM patch from Hanji http://get.bitlbee.org/patches/)
70#
71# 2004-10-31:
72# Sends typing notice to the bitlbee server when typing a message in irssi. bitlbee > 0.92
73#
74# 2004-06-11:
75# shows [typing: ] in &bitlbee with multiple users.
76#
77use strict;
78use Irssi::TextUI;
79use Data::Dumper;
80
81use vars qw($VERSION %IRSSI);
82
83$VERSION = '1.7.3';
84%IRSSI = (
85	authors	 	=> 'Tijmen "timing" Ruizendaal, Matt "f0rked" Sparks',
86	contact		=> 'tijmen.ruizendaal@gmail.com, root@f0rked.com',
87	name		=> 'BitlBee_typing_notice',
88	description	=> '1. Adds an item to the status bar wich shows [typing] when someone is typing a message on the supported IM-networks	2. Sends typing notices to the supported IM networks (the other way arround). (For bitlbee 3.0+)',
89	sbitems 	=> 'typing_notice',
90	license	 	=> 'GPLv2',
91	url		=> 'http://the-timing.nl/stuff/irssi-bitlbee, http://f0rked.com',
92);
93
94my %bitlbee_tag; # server object
95my %control_channels; # mostly: &bitlbee, &facebook etc.
96init();
97
98sub init { # if script is loaded after connect
99	my @servers = Irssi::servers();
100	foreach my $server(@servers) {
101		if( $server->isupport('NETWORK') eq 'BitlBee' ){
102			my $T = $server->{tag};
103			$bitlbee_tag{$T} = 1;
104			my @channels = $server->channels();
105			foreach my $channel(@channels) {
106				if( $channel->{mode} =~ /C/ ){
107					push @{ $control_channels{$T} }, $channel->{name} unless (grep $_ eq $channel->{name}, @{ $control_channels{$T} // [] });
108				}
109			}
110		}
111	}
112}
113# if connect after script is loaded
114Irssi::signal_add_last('event 005' => sub {
115	my( $server ) = @_;
116	if( $server->isupport('NETWORK') eq 'BitlBee' ){
117		$bitlbee_tag{ $server->{tag} } = 1;
118	}
119});
120# if new control channel is synced after script is loaded
121Irssi::signal_add_last('channel sync' => sub {
122	my( $channel ) = @_;
123	my $T = $channel->{server}->{tag};
124	if( $channel->{mode} =~ /C/ && $bitlbee_tag{$T} ){
125		push @{ $control_channels{$T} }, $channel->{name} unless (grep $_ eq $channel->{name}, @{ $control_channels{$T} // [] });
126	}
127});
128
129# How often to check if we are typing, or on msn,
130# how long to keep the typing notice up, or check
131# if the other user is still typing...
132my $KEEP_TYPING_TIMEOUT = 1;
133my $STOP_TYPING_TIMEOUT = 7;
134
135my %timer_tag;
136
137my %typing;
138my %tag;
139my $line;
140my %out_typing;
141my $lastkey;
142my $keylog_active = 1;
143my $command_char = Irssi::settings_get_str('cmdchars'); # mostly: /
144my $to_char = Irssi::settings_get_str("completion_char"); # mostly: :
145
146sub event_ctcp_msg {
147	my ($server, $msg, $from, $address) = @_;
148	my $tag = $server->{tag};
149	return unless $bitlbee_tag{ $tag };
150	if ( my($type) = $msg =~ "TYPING ([0-9])" ){
151		Irssi::signal_stop();
152		if( $type == 0 ){
153			unset_typing($tag, $from);
154		} elsif( $type == 1 ){
155			my $k = "$tag/$from";
156			$typing{$k}=1;
157			if( $address !~ /\@login\.oscar\.aol\.com/ and $address !~ /\@YAHOO/ and $address !~ /\@login\.icq\.com/ ){
158				Irssi::timeout_remove($tag{$k});
159				delete($tag{$k});
160				$tag{$k}=Irssi::timeout_add_once($STOP_TYPING_TIMEOUT*1000,"unset_typing",[$tag,$from]);
161			}
162			redraw($tag, $from);
163		} elsif( $type == 2 ){
164			stale_typing($tag, $from);
165		}
166	}
167}
168
169sub unset_typing {
170	my($tag,$from,$no_redraw)=@_;
171	my $k = "$tag/$from";
172	delete $typing{$k} if $typing{$k};
173	Irssi::timeout_remove($tag{$k});
174	delete($tag{$k});
175	redraw($tag, $from) if !$no_redraw;
176}
177
178sub stale_typing {
179	my($tag,$from)=@_;
180	my $k = "$tag/$from";
181	$typing{$k}=2;
182	redraw($tag, $from);
183}
184
185sub redraw {
186	my($tag,$from)=@_;
187	my $window = Irssi::active_win();
188	my $name = $window->get_active_name();
189
190	# only redraw if current window equals to the typing person, is a control channel or if allwin is set
191	if( $from eq $name || (grep $_ eq $name, @{ $control_channels{$tag} // [] }) || Irssi::settings_get_bool("bitlbee_typing_allwin") ){
192		Irssi::statusbar_items_redraw('typing_notice');
193	}
194}
195
196sub event_msg {
197	my ($server,$data,$from,$address,$target) = @_;
198	my $tag = $server->{tag};
199	return unless $bitlbee_tag{ $tag };
200	my $channel=Irssi::active_win()->get_active_name();
201	unset_typing $tag, $from, "no redraw";
202	unset_typing $tag, $channel;
203}
204
205sub event_quit {
206	my $server = shift;
207	my $tag = $server->{tag};
208	return unless $bitlbee_tag{ $tag };
209	my $nick = shift;
210	unset_typing $tag, $nick;
211}
212
213sub typing_notice {
214	my ($item, $get_size_only) = @_;
215	my $window = Irssi::active_win();
216	my $tag = $window->{active}{server}{tag} // "";
217	my $channel = $window->get_active_name();
218	my $k = "$tag/$channel";
219
220	if (exists($typing{$k})) {
221		my $append=$typing{$k}==2 ? " (stale)" : "";
222		$item->default_handler($get_size_only, "{sb typing$append}", 0, 1);
223	} else {
224		$item->default_handler($get_size_only, "", 0, 1);
225		Irssi::timeout_remove($tag{$k});
226		delete($tag{$k});
227	}
228	# we check for correct windows again, because the statusbar item is redrawn after window change too.
229	if( (grep $_ eq $channel, @{ $control_channels{$tag} // [] }) || Irssi::settings_get_bool("bitlbee_typing_allwin")) {
230		foreach my $key (sort keys(%typing)) {
231			$line .= " ".$key;
232			if ($typing{$key}==2) { $line .= " (stale)"; }
233		}
234		if ($line ne "") {
235			$item->default_handler($get_size_only, "{sb typing:$line}", 0, 1);
236			$line = "";
237		}
238	}
239}
240
241sub window_change {
242	Irssi::statusbar_items_redraw('typing_notice');
243	my $win = !Irssi::active_win() ? undef : Irssi::active_win()->{active};
244	if (ref $win && defined $win->{server}->{tag} && $bitlbee_tag{ $win->{server}->{tag} }) {
245		if (!$keylog_active) {
246			$keylog_active = 1;
247			Irssi::signal_add_last('gui key pressed', 'key_pressed');
248		}
249	} else {
250		if ($keylog_active) {
251			$keylog_active = 0;
252			Irssi::signal_remove('gui key pressed', 'key_pressed');
253		}
254	}
255}
256
257sub key_pressed {
258	return if !Irssi::settings_get_bool("bitlbee_send_typing");
259	my $key = shift;
260	if ($key != 9 && $key != 10 && $key != 13 && $lastkey != 27 && $key != 27
261	   && $lastkey != 91 && $key != 126 && $key != 127)
262	{
263		my $window = Irssi::active_win();
264		my $nick = $window->get_active_name();
265		my $tag = $window->{active}{server}{tag};
266		if (defined $tag && $bitlbee_tag{ $tag } && $nick ne "(status)" && $nick ne "root") {
267			if( grep $_ eq $nick, @{ $control_channels{$tag} // [] } ){ # send typing if in control channel
268				my $input = Irssi::parse_special("\$L");
269				my ($first_word) = split(/ /,$input);
270				if ($input !~ /^$command_char.*/ && $first_word =~ s/$to_char$//){
271					send_typing($tag, $first_word);
272				}
273			} else { # or any other channels / query
274				my $input = Irssi::parse_special("\$L");
275				if ($input !~ /^$command_char.*/ && length($input) > 0){
276					send_typing($tag, $nick);
277				}
278			}
279		}
280	}
281	$lastkey = $key;
282}
283
284sub delete_server {
285	my $tag = shift;
286	delete $bitlbee_tag{$tag};
287	delete $control_channels{$tag};
288	undef;
289}
290
291sub out_empty {
292	my ($a) = @_;
293	my($nick,$tag)=@{$a};
294	my $k = "$tag/$nick";
295	delete($out_typing{$k});
296	Irssi::timeout_remove($timer_tag{$k});
297	delete($timer_tag{$k});
298	if (my $bitlbee_server = Irssi::server_find_tag($tag)) {
299		$bitlbee_server->command("^CTCP $nick TYPING 0");
300	} else {
301		delete_server($tag);
302	}
303}
304
305sub send_typing {
306	my ($tag, $nick) = @_;
307	my $k = "$tag/$nick";
308	if (!exists($out_typing{$k}) || time - $out_typing{$k} > $KEEP_TYPING_TIMEOUT) {
309		if (my $bitlbee_server = Irssi::server_find_tag($tag)) {
310			$bitlbee_server->command("^CTCP $nick TYPING 1");
311		} else {
312			delete_server($tag);
313		}
314		$out_typing{$k} = time;
315		### Reset 'stop-typing' timer
316		Irssi::timeout_remove($timer_tag{$k});
317		delete($timer_tag{$k});
318
319		### create new timer
320		$timer_tag{$k} = Irssi::timeout_add_once($STOP_TYPING_TIMEOUT*1000, 'out_empty', ["$nick", $tag]);
321	}
322}
323
324#README: Delete the old bitlbee_send_typing string from ~/.irssi/config. A boolean is better.
325
326sub db_typing {
327	local $Data::Dumper::Sortkeys = 1;
328	print "Detected channels: ";
329	print Dumper(%control_channels);
330	print "Detected server tag: ".join ", ", sort keys %bitlbee_tag;
331	print "Tag: ".Dumper(%tag);
332	print "Timer Tag: ".Dumper(%timer_tag);
333	print "Typing: ".Dumper(%typing);
334	print "Out Typing: ".Dumper(%out_typing);
335}
336
337Irssi::command_bind('db_typing','db_typing');
338
339Irssi::settings_add_bool("bitlbee","bitlbee_send_typing",1);
340Irssi::settings_add_bool("bitlbee","bitlbee_typing_allwin",0);
341
342Irssi::signal_add("ctcp msg", "event_ctcp_msg");
343Irssi::signal_add("message private", "event_msg");
344Irssi::signal_add("message public", "event_msg");
345Irssi::signal_add("message quit", "event_quit");
346Irssi::signal_add_last('window changed', 'window_change');
347Irssi::signal_add_last('gui key pressed', 'key_pressed');
348Irssi::statusbar_item_register('typing_notice', undef, 'typing_notice');
349Irssi::statusbars_recreate_items();
350