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

# Content
1 #!/opt/perl/bin/perl
2 use strict;
3 use utf8;
4 use URI;
5 use JSONConnection;
6 use JSON::Syck;
7 use POSIX qw(strftime);
8 use CursesChatMainwindow;
9
10 our $id_seq = 0;
11
12 my $HOST = 'localhost';
13 my $PORT = 1236;
14
15 our $temporary_timer;
16
17 our @highlight;
18 our $CFG;
19 our $js;
20 our %completion = map { $_ => 1 } qw{
21 /goto
22 /kill
23 /colors
24 /clear_temporaries
25 /buffers
26 /reload
27 /remote_reload
28 /reconnect
29 /pop
30 };
31
32 our %buffer_nicks;
33
34 our %on_reply;
35
36 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 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 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 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 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 sub write_chatline {
89 my ($buffer, $id, $time, $msg, $is_echo, $type, $msg_scope, $highlight) = @_;
90 my ($host, $nick) = get_id_parts ($id);
91
92 my $nick_color =
93 $is_echo
94 ? 0
95 : (
96 $msg_scope eq 'private'
97 ? 4
98 : ($highlight ? 70 : 7)
99 );
100 my $ts = '[' . POSIX::strftime ("%T", localtime ($time)) . ']';
101
102 my ($adel, $bdel) = ('<', '>');
103 if ($type eq 'notice') {
104 ($adel, $bdel) = ('{', '}');
105 }
106
107 my (@chatline) = (("p".(length ($ts) + 7 + length ($nick) + length ($host))), 0, $ts, 0, " [$host]", 0, " $adel", $nick_color, "$nick", 0, "$bdel ", 0, $msg);
108
109 printline ($buffer, \@chatline);
110 }
111
112 sub write_infoline {
113 my ($buffer, $time, $msg) = @_;
114 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 my (@chatline) = (("p".length $ts), 0, $ts, 0, " ", 3, $msg);
122 printline ($buffer, \@chatline);
123 }
124
125 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
139 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 sub get_first_highlight {
147 return $highlight[0];
148 }
149
150 sub maybe_pop_highlight {
151 my ($buffer) = @_;
152 @highlight = grep { $buffer ne $_ } @highlight;
153 printline (msgline => [map { (103, $_, 0, ' ') } @highlight]);
154 }
155
156 sub push_highlight {
157 my ($id) = @_;
158
159 unless (grep { $_ eq $id } (@highlight, current_buffer ())) {
160 push @highlight, $id;
161 printline (msgline => [map { (103, $_, 0, ' ') } @highlight]);
162 }
163 }
164
165 sub change_buffer {
166 my ($new_buf) = @_;
167 maybe_pop_highlight ($new_buf);
168 select_buffer ($new_buf);
169
170 my $add_info = "";
171 if ($new_buf =~ /^jsirc:/) {
172 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 }
177
178 sub get_id_parts {
179 my ($id) = @_;
180 my $uri = URI->new ($id);
181 my $a = $uri->authority;
182 my $p = ($uri->path_segments ())[1];
183 ($a, $p)
184 }
185
186 sub prettyfy_id {
187 my ($id) = @_;
188 my ($a, $p) = get_id_parts ($id);
189 "$a/$p"
190 }
191
192 sub protocolize_id {
193 my ($pretty_id) = @_;
194 if ($pretty_id =~ m/^([^\/]+)\/?(.*)$/) {
195 my ($auth, $path) = ($1, $2);
196 my $uri = URI->new;
197 $uri->scheme ('jsirc');
198 $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 load_cfg ();
217
218 my $c = AnyEvent->condvar;
219
220 #timer ();
221
222 $js =
223 JSONClientConnection->new (
224 packet_cb => sub {
225 my ($js, $data) = @_;
226 require JSON::Syck;
227
228 if ($data->{type} eq 'message' or $data->{type} eq 'notice') {
229 my ($src_id) = ($data->{src});
230
231 my $highlight = 0;
232 if ($data->{msg_scope} eq 'private') {
233 push_highlight ($src_id);
234 } else {
235 for (@{$::CFG->{highlight}}) {
236 if ($data->{message} =~ m/$_/) {
237 push_highlight ($src_id);
238 $highlight = 1;
239 last;
240 }
241 }
242 }
243
244 write_chatline ($src_id, $data->{from}->{id}, $data->{timestamp}, $data->{message}, $data->{is_echo}, $data->{type}, $data->{msg_scope}, $highlight);
245
246 } elsif ($data->{type} eq 'subid') {
247 my $src_id = $data->{src};
248 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 "sub id add: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
257
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 "sub id remove: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
265 } 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 "sub id list: " . join (', ', map { prettyfy_id ($_) } sort @{$data->{ids}}));
282
283
284 } elsif ($it eq 'special') {
285 write_infoline ($src_id, $data->{timestamp}, "[$data->{message}]");
286 }
287
288 } elsif ($data->{type} eq 'info') {
289 write_infoline (undef, $data->{timestamp} => $data->{message});
290 do_reply ($data->{id}, info => $data);
291
292 } elsif ($data->{type} eq 'error') {
293 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 }
299
300 printline (debug => [0, JSON::Syck::Dump ($data)]);
301 },
302 disconnect_cb => sub {
303 write_errorline (undef, undef, "Lost connection to jsonsrv: $_[0]");
304 }
305 );
306
307 CursesChatMainwindow::init;
308
309 change_buffer ('status');
310 connect_json;
311
312 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 my ($word, $idx, @line) = @_;
335
336 # TODO: make completion cycling with $first_compl
337 #d# printline (undef, [0, "[$idx] [$word]"]);
338
339 my @found;
340
341 if ($idx == 0) {
342 for (keys %{$buffer_nicks{current_buffer ()} || {}}) {
343 my $n = $_ . ":";
344 if ($n =~ /^\Q$word\E/) {
345 push @found, $n;
346 }
347 }
348
349 for (keys %completion) {
350 if (/^\Q$word\E/) {
351 push @found, $_;
352 }
353 }
354
355 } else {
356 my %buffers = map { $_ => 1 } list_buffers;
357
358 for (keys %{$buffer_nicks{current_buffer ()} || {}}) {
359 if (/^\Q$word\E/) {
360 push @found, $_;
361 }
362 }
363
364 for (keys %buffers) {
365 if (m/^jsirc:/) {
366 my $pi = prettyfy_id ($_);
367 if ($pi =~ /^\Q$word\E/) {
368 push @found, $pi;
369 }
370 } else {
371 if (/^\Q$word\E/) {
372 push @found, $_;
373 }
374 }
375 }
376 }
377
378 return "$found[0] " if @found == 1;
379
380 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 return find_common_prefix (@found);
398 }
399
400 return $word;
401 });
402
403 CursesChatMainwindow::register_input_cb (sub {
404 my ($input, $escape) = @_;
405
406 unless (defined $input) {
407 if (exists $CFG->{buffers}->{$escape}) {
408 change_buffer (my $buf = $CFG->{buffers}->{$escape} || 'status');
409 } else {
410 change_buffer ('status');
411 }
412 return;
413 }
414
415 if ($input =~ m/^\/goto\s*(\S+)/) {
416 my $buf = $1;
417 if ($buf =~ m/^[^\/:]+\/\S*$/) {
418 change_buffer (protocolize_id ($buf));
419 } else {
420 change_buffer ("$buf");
421 }
422
423 } 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 print_temporary_line (undef, [0, "buffers:"]);
431 for (sort { $a cmp $b } (list_buffers ())) {
432 if (/^jsirc:/) {
433 print_temporary_line (undef, ['p12', 0, sprintf "- (%6s) | %s", 'jsirc', prettyfy_id ($_)]);
434 } else {
435 print_temporary_line (undef, ['p12', 0, sprintf "- (%6s) | %s", 'other', $_]);
436 }
437 }
438
439 } elsif ($input =~ m/^\/colors/) {
440 CursesChatMainwindow::print_colors;
441
442 } elsif ($input =~ m/^\/clear_temporaries/) {
443 clear_buffer_temporaries (current_buffer ());
444
445 } elsif ($input =~ m/^\/reconnect\s*(\S*)\s*(\S*)/) {
446 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
463 $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 } elsif ($input =~ m/^\/reload/) {
475 load_cfg ();
476
477 } elsif ($input =~ m/^\/remote_reload/) {
478 $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
493 } else {
494 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
500 my $msg = {
501 type => 'message',
502 dest => $dest_id,
503 message => $input,
504 id => $id_seq
505 };
506
507 $js->send_data ($msg);
508 printline (debug => [0, JSON::Syck::Dump ($msg)]);
509
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 }
518 });
519
520 $c->wait;
521
522 CursesChatMainwindow::end;