ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/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

# 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.6 our @highlight;
16 elmex 1.3 our $CFG;
17 elmex 1.4 our $js;
18     our %completion = map { $_ => 1 } qw{
19 elmex 1.8 /goto
20     /kill
21 elmex 1.6 /colors
22 elmex 1.8 /buffers
23     /reload /remote_reload /reconnect
24     /pop
25 elmex 1.4 };
26    
27 elmex 1.8 our %buffer_nicks;
28    
29     our %on_reply;
30    
31 elmex 1.4 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 elmex 1.6 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 elmex 1.8 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 elmex 1.4 sub write_chatline {
66 elmex 1.8 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 elmex 1.6 my $ts = '[' . POSIX::strftime ("%T", localtime ($time)) . ']';
78 elmex 1.8
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 elmex 1.4 printline ($buffer, \@chatline);
87     }
88 elmex 1.3
89 elmex 1.7 sub write_infoline {
90     my ($buffer, $time, $msg) = @_;
91 elmex 1.8 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 elmex 1.7 my (@chatline) = (("p".length $ts), 0, $ts, 0, " ", 3, $msg);
99     printline ($buffer, \@chatline);
100     }
101    
102 elmex 1.8 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 elmex 1.7
116 elmex 1.3 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 elmex 1.8 sub get_first_highlight {
124     return ($highlight[0] || [])->[0];
125     }
126    
127 elmex 1.6 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 elmex 1.8
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 elmex 1.6 }
154    
155 elmex 1.8 sub get_id_parts {
156 elmex 1.6 my ($id) = @_;
157     my $uri = URI->new ($id);
158     my $a = $uri->authority;
159     my $p = ($uri->path_segments ())[1];
160 elmex 1.8 ($a, $p)
161     }
162    
163     sub prettyfy_id {
164     my ($id) = @_;
165     my ($a, $p) = get_id_parts ($id);
166 elmex 1.6 "$a/$p"
167     }
168    
169 elmex 1.8 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 elmex 1.3 load_cfg ();
194    
195 elmex 1.2 my $c = AnyEvent->condvar;
196 elmex 1.1
197 elmex 1.2 #timer ();
198 elmex 1.1
199     $js =
200 elmex 1.6 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 elmex 1.8 my $highlight = 0;
209     if ($data->{msg_type} eq 'private' or $data->{msg_type} eq 'private_notice') {
210     push_highlight ($src_id);
211 elmex 1.6 } else {
212     for (@{$::CFG->{highlight}}) {
213     if ($data->{message} =~ m/$_/) {
214 elmex 1.8 push_highlight ($src_id);
215     $highlight = 1;
216     last;
217 elmex 1.6 }
218     }
219     }
220    
221 elmex 1.8 write_chatline ($src_id, $data->{from}->{id}, $data->{timestamp}, $data->{message}, $data->{is_echo}, $data->{msg_type}, $highlight);
222 elmex 1.7
223 elmex 1.8 } elsif ($data->{type} eq 'subid') {
224 elmex 1.7 my $src_id = $data->{src};
225 elmex 1.8 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 elmex 1.7
260    
261     } elsif ($it eq 'special') {
262 elmex 1.8 write_infoline ($src_id, $data->{timestamp}, "[$data->{message}]");
263 elmex 1.7 }
264    
265 elmex 1.6 } elsif ($data->{type} eq 'info') {
266 elmex 1.8 write_infoline (undef, $data->{timestamp} => $data->{message});
267     do_reply ($data->{id}, info => $data);
268    
269 elmex 1.6 } elsif ($data->{type} eq 'error') {
270 elmex 1.8 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 elmex 1.6 }
276    
277     printline (debug => [0, JSON::Syck::Dump ($data)]);
278     },
279     disconnect_cb => sub {
280 elmex 1.8 write_errorline (undef, undef, "Lost connection to jsonsrv: $_[0]");
281 elmex 1.2 }
282 elmex 1.6 );
283 elmex 1.1
284 elmex 1.2 CursesChatMainwindow::init;
285 elmex 1.1
286 elmex 1.8 change_buffer ('status');
287     connect_json;
288 elmex 1.2
289 elmex 1.4 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 elmex 1.8 #d# printline (undef, [0, "f[$first_compl] [$idx] [$word]"]);
315 elmex 1.4
316     my @found;
317 elmex 1.6
318 elmex 1.4 if ($idx == 0) {
319 elmex 1.8 for (keys %{$buffer_nicks{current_buffer ()} || {}}) {
320     my $n = $_ . ":";
321     if ($n =~ /^\Q$word\E/) {
322     push @found, $n;
323     }
324     }
325    
326 elmex 1.4 for (keys %completion) {
327     if (/^\Q$word\E/) {
328     push @found, $_;
329     }
330     }
331 elmex 1.6
332 elmex 1.4 } else {
333     my %buffers = map { $_ => 1 } list_buffers;
334 elmex 1.6
335 elmex 1.8 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 elmex 1.4 }
349     }
350 elmex 1.6
351 elmex 1.4 for (keys %buffers) {
352     if (/^\Q$word\E/) {
353     push @found, $_;
354     }
355     }
356     }
357 elmex 1.6
358 elmex 1.4 return "$found[0] " if @found == 1;
359 elmex 1.6
360 elmex 1.4 if (@found) {
361     printline (undef, [0, "$word: " . join ", ", @found]);
362     return find_common_prefix (@found);
363     }
364    
365     return $word;
366     });
367    
368 elmex 1.2 CursesChatMainwindow::register_input_cb (sub {
369 elmex 1.3 my ($input, $escape) = @_;
370    
371     unless (defined $input) {
372     if (exists $CFG->{buffers}->{$escape}) {
373 elmex 1.6 change_buffer (my $buf = $CFG->{buffers}->{$escape} || 'status');
374 elmex 1.2 } else {
375 elmex 1.6 change_buffer ('status');
376 elmex 1.1 }
377 elmex 1.3 return;
378     }
379    
380 elmex 1.8 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 elmex 1.6
388 elmex 1.8 } 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 elmex 1.4 printline (undef, [0, "buffers:"]);
396     for (list_buffers) {
397     printline (undef, [0, "- $_"]);
398     }
399 elmex 1.6
400 elmex 1.2 } elsif ($input =~ m/^\/colors/) {
401     CursesChatMainwindow::print_colors;
402 elmex 1.6
403     } elsif ($input =~ m/^\/reconnect\s*(\S*)\s*(\S*)/) {
404 elmex 1.8 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 elmex 1.6
421 elmex 1.8 $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 elmex 1.6 } elsif ($input =~ m/^\/reload/) {
433     load_cfg ();
434    
435     } elsif ($input =~ m/^\/remote_reload/) {
436 elmex 1.8 $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 elmex 1.6
451 elmex 1.2 } else {
452 elmex 1.8 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 elmex 1.6
458 elmex 1.4 my $msg = {
459 elmex 1.3 type => 'message',
460 elmex 1.4 dest => $dest_id,
461 elmex 1.8 message => $input,
462     id => $id_seq
463 elmex 1.4 };
464 elmex 1.8
465 elmex 1.4 $js->send_data ($msg);
466     printline (debug => [0, JSON::Syck::Dump ($msg)]);
467 elmex 1.8
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 elmex 1.1 }
476 elmex 1.2 });
477 elmex 1.1
478     $c->wait;
479    
480 elmex 1.2 CursesChatMainwindow::end;