ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/Net-IRC3/samples/jsonclient
Revision: 1.8
Committed: Tue Jan 16 19:39:17 2007 UTC (19 years, 8 months ago) by elmex
Branch: MAIN
Changes since 1.7: +255 -55 lines
Log Message:
Net::IRC3:
        - fixed case handling with channels
        - added functionality to change the nick automatically
          when it is already taken when registering an IRC connection.
          (Net::IRC3::Client::Connection)
        - added reply number <=> reply name mapping to Net::IRC3::Util
          accessible through rfc_code_to_name
        - added error event to Net::IRC3::Client::Connection
json chat framework:
        - client history
        - nick listing more correct
        - improved completion (added nick completion)
        - further improvement of the protocol
        - finally got the id handling correct
        - added logging to jsonsrv
        - many other changes i forgot.

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