ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/Net-IRC3/samples/jsonclient
Revision: 1.11
Committed: Wed Jan 17 09:47:16 2007 UTC (19 years, 9 months ago) by elmex
Branch: MAIN
Changes since 1.10: +12 -5 lines
Log Message:
added some todos and changed timestamp format a bit.

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