ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/Net-IRC3/samples/jsonclient
Revision: 1.10
Committed: Wed Jan 17 09:29:41 2007 UTC (19 years, 8 months ago) by elmex
Branch: MAIN
Changes since 1.9: +68 -28 lines
Log Message:
changed completion to timeout the displayed partial-completion helpers.
added 'temporary' lines, which can be cleared from the buffers.
improved completion of many strings.
changed uri scheme again to jsirc, as jsirc is a special kind of IRC.
the client handles generally the 'json chat protocol' which works with any
kind of uris.

File Contents

# User Rev Content
1 elmex 1.8 #!/opt/perl/bin/perl
2 elmex 1.1 use strict;
3 elmex 1.5 use utf8;
4 elmex 1.6 use URI;
5 elmex 1.1 use JSONConnection;
6 elmex 1.3 use JSON::Syck;
7 elmex 1.2 use POSIX qw(strftime);
8     use CursesChatMainwindow;
9 elmex 1.1
10 elmex 1.8 our $id_seq = 0;
11    
12     my $HOST = 'localhost';
13     my $PORT = 1236;
14    
15 elmex 1.10 our $temporary_timer;
16    
17 elmex 1.6 our @highlight;
18 elmex 1.3 our $CFG;
19 elmex 1.4 our $js;
20     our %completion = map { $_ => 1 } qw{
21 elmex 1.8 /goto
22     /kill
23 elmex 1.6 /colors
24 elmex 1.10 /clear_temporaries
25 elmex 1.8 /buffers
26 elmex 1.10 /reload
27     /remote_reload
28     /reconnect
29 elmex 1.8 /pop
30 elmex 1.4 };
31    
32 elmex 1.8 our %buffer_nicks;
33    
34     our %on_reply;
35    
36 elmex 1.4 my $d = 0;
37     my $wt;
38     sub timer {
39     $wt = AnyEvent->timer (after => 3, cb => sub {
40     printline (status => [0, "TEST $d"]); $d++;
41     timer ();
42     });
43     }
44    
45 elmex 1.10 sub start_temporary_killer {
46     my ($buffer, $timeout) = @_;
47     $::temporary_timer = AnyEvent->timer (after => $timeout || 4, cb => sub {
48     clear_buffer_temporaries ($buffer);
49     $temporary_timer = undef;
50     });
51     }
52    
53 elmex 1.6 sub search_buffer {
54     my ($id) = @_;
55    
56     for (keys %{$::CFG->{buffers}}) {
57     if ($::CFG->{buffers}->{$_} eq $id) {
58     return $_;
59     }
60     }
61    
62     return undef
63     }
64    
65 elmex 1.8 sub on_reply {
66     my ($id, $cb) = @_;
67     push @{$on_reply{$id}}, $cb;
68     }
69    
70     sub do_reply {
71     my ($id, $status, $data) = @_;
72     for my $cb (@{$on_reply{$id} || []}) {
73     $cb->($status, $data);
74     }
75     delete $on_reply{$id};
76     }
77    
78 elmex 1.10 sub print_temporary_line {
79     my ($buffer, $line, $timeout) = @_;
80     $buffer = current_buffer () unless defined $buffer;
81     unshift @$line, 't';
82     printline ($buffer, $line);
83     if ($timeout) {
84     start_temporary_killer ($buffer);
85     }
86     }
87    
88 elmex 1.4 sub write_chatline {
89 elmex 1.9 my ($buffer, $id, $time, $msg, $is_echo, $type, $msg_scope, $highlight) = @_;
90 elmex 1.8 my ($host, $nick) = get_id_parts ($id);
91    
92     my $nick_color =
93     $is_echo
94 elmex 1.10 ? 0
95 elmex 1.8 : (
96 elmex 1.9 $msg_scope eq 'private'
97 elmex 1.8 ? 4
98 elmex 1.10 : ($highlight ? 70 : 7)
99 elmex 1.8 );
100 elmex 1.6 my $ts = '[' . POSIX::strftime ("%T", localtime ($time)) . ']';
101 elmex 1.8
102     my ($adel, $bdel) = ('<', '>');
103 elmex 1.9 if ($type eq 'notice') {
104 elmex 1.8 ($adel, $bdel) = ('{', '}');
105     }
106    
107 elmex 1.10 my (@chatline) = (("p".(length ($ts) + 7 + length ($nick) + length ($host))), 0, $ts, 0, " [$host]", 0, " $adel", $nick_color, "$nick", 0, "$bdel ", 0, $msg);
108 elmex 1.8
109 elmex 1.4 printline ($buffer, \@chatline);
110     }
111 elmex 1.3
112 elmex 1.7 sub write_infoline {
113     my ($buffer, $time, $msg) = @_;
114 elmex 1.8 unless (defined $buffer) {
115     write_infoline (status => $time, $msg);
116     write_infoline (current_buffer () => $time, $msg) if current_buffer () ne 'status';
117     return;
118     }
119    
120     my $ts = '[' . POSIX::strftime ("%T", localtime ($time || time ())) . ']';
121 elmex 1.7 my (@chatline) = (("p".length $ts), 0, $ts, 0, " ", 3, $msg);
122     printline ($buffer, \@chatline);
123     }
124    
125 elmex 1.8 sub write_errorline {
126     my ($buffer, $time, $msg) = @_;
127     unless (defined $buffer) {
128     write_errorline (status => $time, $msg);
129     write_errorline (current_buffer () => $time, $msg) if current_buffer () ne 'status';
130     return;
131     }
132    
133     my $ts = '[' . POSIX::strftime ("%T", localtime ($time || time ())) . ']';
134     my (@chatline) = (("p".length $ts), 0, $ts, 0, " ", 68, "ERROR: ", 4, $msg);
135     printline ($buffer, \@chatline);
136     }
137    
138 elmex 1.7
139 elmex 1.3 sub load_cfg {
140     $CFG ||= {};
141     return unless -e "$ENV{HOME}/.jsonircclrc";
142     open CFGH, "<", "$ENV{HOME}/.jsonircclrc" or die "Couldn't open ~/.jsonircclrc: $!";
143     $::CFG = JSON::Syck::Load (do { local $/; <CFGH> });
144     }
145    
146 elmex 1.8 sub get_first_highlight {
147 elmex 1.10 return $highlight[0];
148 elmex 1.8 }
149    
150 elmex 1.6 sub maybe_pop_highlight {
151     my ($buffer) = @_;
152 elmex 1.10 @highlight = grep { $buffer ne $_ } @highlight;
153     printline (msgline => [map { (103, $_, 0, ' ') } @highlight]);
154 elmex 1.6 }
155    
156     sub push_highlight {
157     my ($id) = @_;
158    
159 elmex 1.10 unless (grep { $_ eq $id } (@highlight, current_buffer ())) {
160     push @highlight, $id;
161     printline (msgline => [map { (103, $_, 0, ' ') } @highlight]);
162 elmex 1.6 }
163     }
164    
165     sub change_buffer {
166     my ($new_buf) = @_;
167     maybe_pop_highlight ($new_buf);
168     select_buffer ($new_buf);
169 elmex 1.8
170     my $add_info = "";
171 elmex 1.10 if ($new_buf =~ /^jsirc:/) {
172 elmex 1.8 my $u = URI->new ($new_buf);
173     $add_info = sprintf " {%s} %s@%s", $u->scheme, ($u->path_segments ())[1], $u->authority;
174     }
175     printline (statusline => ['f', 56, "$new_buf |$add_info"]);
176 elmex 1.6 }
177    
178 elmex 1.8 sub get_id_parts {
179 elmex 1.6 my ($id) = @_;
180     my $uri = URI->new ($id);
181     my $a = $uri->authority;
182     my $p = ($uri->path_segments ())[1];
183 elmex 1.8 ($a, $p)
184     }
185    
186     sub prettyfy_id {
187     my ($id) = @_;
188     my ($a, $p) = get_id_parts ($id);
189 elmex 1.6 "$a/$p"
190     }
191    
192 elmex 1.8 sub protocolize_id {
193     my ($pretty_id) = @_;
194     if ($pretty_id =~ m/^([^\/]+)\/?(.*)$/) {
195     my ($auth, $path) = ($1, $2);
196     my $uri = URI->new;
197 elmex 1.10 $uri->scheme ('jsirc');
198 elmex 1.8 $uri->authority ($auth);
199     $uri->path ($path);
200     return "$uri";
201     }
202     undef
203     }
204    
205     sub connect_json {
206     eval {
207     $js->connect ($1 || $HOST, $2 || $PORT);
208     };
209     if ($@) {
210     write_errorline (undef, undef, "Error on connect to jsonsrv at $HOST:$PORT: $@");
211     } else {
212     write_infoline (undef, undef, "Connected to jsonsrv at $HOST:$PORT.");
213     }
214     }
215    
216 elmex 1.3 load_cfg ();
217    
218 elmex 1.2 my $c = AnyEvent->condvar;
219 elmex 1.1
220 elmex 1.2 #timer ();
221 elmex 1.1
222     $js =
223 elmex 1.6 JSONClientConnection->new (
224     packet_cb => sub {
225     my ($js, $data) = @_;
226     require JSON::Syck;
227    
228 elmex 1.9 if ($data->{type} eq 'message' or $data->{type} eq 'notice') {
229 elmex 1.6 my ($src_id) = ($data->{src});
230    
231 elmex 1.8 my $highlight = 0;
232 elmex 1.9 if ($data->{msg_scope} eq 'private') {
233 elmex 1.8 push_highlight ($src_id);
234 elmex 1.6 } else {
235     for (@{$::CFG->{highlight}}) {
236     if ($data->{message} =~ m/$_/) {
237 elmex 1.8 push_highlight ($src_id);
238     $highlight = 1;
239     last;
240 elmex 1.6 }
241     }
242     }
243    
244 elmex 1.9 write_chatline ($src_id, $data->{from}->{id}, $data->{timestamp}, $data->{message}, $data->{is_echo}, $data->{type}, $data->{msg_scope}, $highlight);
245 elmex 1.7
246 elmex 1.8 } elsif ($data->{type} eq 'subid') {
247 elmex 1.7 my $src_id = $data->{src};
248 elmex 1.8 my $it = $data->{command};
249    
250     if ($it eq 'add') {
251     for (@{$data->{ids}}) {
252     my ($h, $n) = get_id_parts ($_);
253     $buffer_nicks{$src_id}->{$n} = 1;
254     }
255     write_infoline ($src_id, $data->{timestamp},
256 elmex 1.10 "sub id add: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
257 elmex 1.8
258     } elsif ($it eq 'remove') {
259     for (@{$data->{ids}}) {
260     my ($h, $n) = get_id_parts ($_);
261     delete $buffer_nicks{$src_id}->{$n};
262     }
263     write_infoline ($src_id, $data->{timestamp},
264 elmex 1.10 "sub id remove: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
265 elmex 1.8 } elsif ($it eq 'change') {
266     my ($oh, $on) = get_id_parts ($data->{old_id});
267     delete $buffer_nicks{$src_id}->{$on};
268     my ($h, $n) = get_id_parts ($data->{new_id});
269     $buffer_nicks{$src_id}->{$n} = 1;
270    
271     write_infoline ($src_id, $data->{timestamp},
272     "sub id change: " . prettyfy_id ($data->{old_id}) . " => " . prettyfy_id ($data->{new_id}));
273    
274     } elsif ($it eq 'list') {
275     $buffer_nicks{$src_id} = {};
276     for (@{$data->{ids}}) {
277     my ($h, $n) = get_id_parts ($_);
278     $buffer_nicks{$src_id}->{$n} = 1
279     }
280     write_infoline ($src_id, $data->{timestamp},
281 elmex 1.10 "sub id list: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
282 elmex 1.7
283    
284     } elsif ($it eq 'special') {
285 elmex 1.8 write_infoline ($src_id, $data->{timestamp}, "[$data->{message}]");
286 elmex 1.7 }
287    
288 elmex 1.6 } elsif ($data->{type} eq 'info') {
289 elmex 1.8 write_infoline (undef, $data->{timestamp} => $data->{message});
290     do_reply ($data->{id}, info => $data);
291    
292 elmex 1.6 } elsif ($data->{type} eq 'error') {
293 elmex 1.8 do_reply ($data->{id}, 'error' => $data);
294     write_errorline (undef, $data->{timestamp}, "$data->{error_type}: $data->{message}");
295    
296     } elsif ($data->{type} eq 'reply') {
297     do_reply ($data->{id}, 'reply' => $data);
298 elmex 1.6 }
299    
300     printline (debug => [0, JSON::Syck::Dump ($data)]);
301     },
302     disconnect_cb => sub {
303 elmex 1.8 write_errorline (undef, undef, "Lost connection to jsonsrv: $_[0]");
304 elmex 1.2 }
305 elmex 1.6 );
306 elmex 1.1
307 elmex 1.2 CursesChatMainwindow::init;
308 elmex 1.1
309 elmex 1.8 change_buffer ('status');
310     connect_json;
311 elmex 1.2
312 elmex 1.4 sub find_common_prefix {
313     my (@words) = @_;
314     @words = sort { length ($a) <=> length ($b) } @words;
315     my $shortest = $words[0];
316    
317     while ($shortest ne '') {
318     my $no_match = 0;
319     for (@words) {
320     unless (/^\Q$shortest\E/) {
321     $no_match = 1;
322     last;
323     }
324     }
325    
326     return $shortest if not $no_match;
327     substr $shortest, -1, 1, '';
328     }
329    
330     return '';
331     }
332    
333     CursesChatMainwindow::register_complete_cb (sub {
334 elmex 1.10 my ($word, $idx, @line) = @_;
335 elmex 1.4
336     # TODO: make completion cycling with $first_compl
337 elmex 1.10 #d# printline (undef, [0, "[$idx] [$word]"]);
338 elmex 1.4
339     my @found;
340 elmex 1.6
341 elmex 1.4 if ($idx == 0) {
342 elmex 1.8 for (keys %{$buffer_nicks{current_buffer ()} || {}}) {
343     my $n = $_ . ":";
344     if ($n =~ /^\Q$word\E/) {
345     push @found, $n;
346     }
347     }
348    
349 elmex 1.4 for (keys %completion) {
350     if (/^\Q$word\E/) {
351     push @found, $_;
352     }
353     }
354 elmex 1.6
355 elmex 1.4 } else {
356     my %buffers = map { $_ => 1 } list_buffers;
357 elmex 1.6
358 elmex 1.8 for (keys %{$buffer_nicks{current_buffer ()} || {}}) {
359 elmex 1.10 if (/^\Q$word\E/) {
360     push @found, $_;
361 elmex 1.8 }
362     }
363    
364     for (keys %buffers) {
365 elmex 1.10 if (m/^jsirc:/) {
366 elmex 1.9 my $pi = prettyfy_id ($_);
367     if ($pi =~ /^\Q$word\E/) {
368     push @found, $pi;
369     }
370     } else {
371 elmex 1.8 if (/^\Q$word\E/) {
372     push @found, $_;
373     }
374 elmex 1.4 }
375     }
376     }
377 elmex 1.6
378 elmex 1.4 return "$found[0] " if @found == 1;
379 elmex 1.6
380 elmex 1.10 if (@found > 7) {
381     my $longest;
382     for (@found) { $longest = length $_ > $longest ? length $_ : $longest }
383    
384     my %first_chars;
385     my $len = 1;
386     while ((keys %first_chars) <= 1 && $len < $longest) {
387     %first_chars = ();
388     for (@found) { $first_chars{(substr $_, 0, $len)} = 1 }
389     $len++;
390     }
391    
392     print_temporary_line (undef, [t => 7, "$word: ", map { (3, "$_-", 0, ', ') } sort keys %first_chars], 1);
393     return find_common_prefix (keys %first_chars);
394    
395     } elsif (@found) {
396     print_temporary_line (undef, [t => 7, "$word: ", map { (3, $_, 0, ', ') } @found], 1);
397 elmex 1.4 return find_common_prefix (@found);
398     }
399    
400     return $word;
401     });
402    
403 elmex 1.2 CursesChatMainwindow::register_input_cb (sub {
404 elmex 1.3 my ($input, $escape) = @_;
405    
406     unless (defined $input) {
407     if (exists $CFG->{buffers}->{$escape}) {
408 elmex 1.6 change_buffer (my $buf = $CFG->{buffers}->{$escape} || 'status');
409 elmex 1.2 } else {
410 elmex 1.6 change_buffer ('status');
411 elmex 1.1 }
412 elmex 1.3 return;
413     }
414    
415 elmex 1.8 if ($input =~ m/^\/goto\s*(\S+)/) {
416     my $buf = $1;
417 elmex 1.10 if ($buf =~ m/^[^\/:]+\/\S*$/) {
418 elmex 1.8 change_buffer (protocolize_id ($buf));
419     } else {
420     change_buffer ("$buf");
421     }
422 elmex 1.6
423 elmex 1.8 } elsif ($input =~ m/^\/kill\s*(\S+)/) {
424     change_buffer ('status') if "$1" eq current_buffer ();
425     if (clear_buffer ("$1") or "$1" eq current_buffer ()) {
426     write_infoline (undef, undef, "Killed buffer '$1'.");
427     }
428    
429     } elsif ($input =~ m/^\/buffers/) {
430 elmex 1.10 print_temporary_line (undef, [0, "buffers:"]);
431 elmex 1.9 for (sort { $a cmp $b } (list_buffers ())) {
432 elmex 1.10 if (/^jsirc:/) {
433     print_temporary_line (undef, ['p12', 0, sprintf "- (%6s) | %s", 'jsirc', prettyfy_id ($_)]);
434 elmex 1.9 } else {
435 elmex 1.10 print_temporary_line (undef, ['p12', 0, sprintf "- (%6s) | %s", 'other', $_]);
436 elmex 1.9 }
437 elmex 1.4 }
438 elmex 1.6
439 elmex 1.2 } elsif ($input =~ m/^\/colors/) {
440     CursesChatMainwindow::print_colors;
441 elmex 1.6
442 elmex 1.10 } elsif ($input =~ m/^\/clear_temporaries/) {
443     clear_buffer_temporaries (current_buffer ());
444    
445 elmex 1.6 } elsif ($input =~ m/^\/reconnect\s*(\S*)\s*(\S*)/) {
446 elmex 1.8 connect_json;
447    
448     } elsif ($input =~ m/^\/raw\s+(.*)$/) {
449     my $raw = $1;
450     my $dest_id = "" . current_buffer ();
451     unless ($dest_id =~ m/^[^:]+:/) {
452     write_errorline ($dest_id, undef, "Can't send regular messages in this buffer: '$dest_id'.");
453     return;
454     }
455    
456     my $msg = {
457     type => 'raw',
458     dest => $dest_id,
459     message => $raw,
460     id => $id_seq
461     };
462 elmex 1.6
463 elmex 1.8 $js->send_data ($msg);
464    
465     on_reply ($id_seq => sub {
466     if ($_[0] eq 'error') {
467     write_errorline ($dest_id, undef, "Couldn't send raw message: '$raw': $_[1]->{message}");
468     } else {
469     write_infoline ($dest_id, undef, "Sent raw message: '$raw'.");
470     }
471     });
472    
473     $id_seq++;
474 elmex 1.6 } elsif ($input =~ m/^\/reload/) {
475     load_cfg ();
476    
477     } elsif ($input =~ m/^\/remote_reload/) {
478 elmex 1.8 $js->send_data ({ type => 'command', command => 'reload', id => $id_seq });
479     on_reply ($id_seq => sub { write_infoline (undef, undef, "reloaded jsonsrv") });
480     $id_seq++;
481    
482     } elsif ($input =~ m/^\/pop/) {
483     my $buf = get_first_highlight ();
484     if (my $buf = get_first_highlight ()) {
485     change_buffer ($buf);
486     } else {
487     write_errorline (current_buffer (), undef, "There are no highlights to pop.");
488     }
489    
490     } elsif ($input =~ m/^\//) {
491     write_errorline (current_buffer (), undef, "Not a recognized command: '$input'");
492 elmex 1.6
493 elmex 1.2 } else {
494 elmex 1.8 my $dest_id = "" . current_buffer ();
495     unless ($dest_id =~ m/^[^:]+:/) {
496     write_errorline ($dest_id, undef, "Can't send regular messages in this buffer: '$dest_id'.");
497     return;
498     }
499 elmex 1.6
500 elmex 1.4 my $msg = {
501 elmex 1.3 type => 'message',
502 elmex 1.4 dest => $dest_id,
503 elmex 1.8 message => $input,
504     id => $id_seq
505 elmex 1.4 };
506 elmex 1.8
507 elmex 1.4 $js->send_data ($msg);
508     printline (debug => [0, JSON::Syck::Dump ($msg)]);
509 elmex 1.8
510     on_reply ($id_seq => sub {
511     if ($_[0] eq 'error') {
512     write_errorline ($dest_id, time, "Couldn't send message: '$input': $_[1]->{message}");
513     }
514     });
515    
516     $id_seq++;
517 elmex 1.1 }
518 elmex 1.2 });
519 elmex 1.1
520     $c->wait;
521    
522 elmex 1.2 CursesChatMainwindow::end;