ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/Net-IRC3/samples/jsonsrv
Revision: 1.7
Committed: Tue Jan 16 19:39:17 2007 UTC (19 years, 8 months ago) by elmex
Branch: MAIN
Changes since 1.6: +195 -26 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.7 #!/opt/perl/bin/perl
2 elmex 1.1 use strict;
3 elmex 1.5 use URI;
4 elmex 1.1 use AnyEvent::Impl::Perl;
5 elmex 1.7 use IO::Handle;
6 elmex 1.1 use JSON::Syck;
7     use JSONConnection;
8     use Net::IRC3::Client::Connection;
9 elmex 1.7 use POSIX qw/strftime/;
10 elmex 1.1 $Net::IRC3::Client::Connection::DEBUG = 1;
11    
12     our $CFG;
13 elmex 1.5 our %ALIASES;
14 elmex 1.1 our %CONNS;
15    
16 elmex 1.7 our %LOGS;
17    
18 elmex 1.5 our $JS;
19    
20 elmex 1.7 sub log_line {
21     my ($server, $src, $line) = @_;
22     my $logdir = $CFG->{log_dir} || "$ENV{HOME}/.jsonirc_logs/";
23     my $ts = POSIX::strftime "%F %T %Z", localtime (time);
24    
25     eval {
26     unless (-e $logdir) {
27     mkdir $logdir or die "Couldn't make directory '$logdir': $!";
28     }
29     my $logfile = "$logdir/${server}" . ($src ne "" ? "_$src" : "");
30     unless ($LOGS{$logfile}) {
31     open my $logfh, ">>", "$logfile"
32     or die "Couldn't open '$logfile': $!";
33     $LOGS{$logfile} = $logfh;
34     $logfh->autoflush (1);
35     $LOGS{$logfile}->print ("---- $ts ---- starting log ----\n");
36     }
37     $LOGS{$logfile}->print ("$ts: $line\n");
38     };
39     if ($@) {
40     error_reply (undef, undef, logging_error => "Couldn't log for $server $src: $@");
41     }
42     }
43    
44 elmex 1.1 sub load_cfg {
45     $CFG ||= {};
46     return unless -e "$ENV{HOME}/.jsonircrc";
47     open CFGH, "<", "$ENV{HOME}/.jsonircrc" or die "Couldn't open ~/.jsonircrc: $!";
48     $::CFG = JSON::Syck::Load (do { local $/; <CFGH> });
49     }
50    
51     sub save_cfg {
52     open CFGH, ">", "$ENV{HOME}/.jsonircrc" or die "Couldn't open for writing ~/.jsonircrc: $!";
53     print CFGH (JSON::Syck::Dump ($::CFG));
54     close CFGH;
55     }
56    
57 elmex 1.3 sub to_dest_id {
58 elmex 1.5 my ($irccon, $targ) = @_;
59 elmex 1.7 $targ ||= '';
60 elmex 1.5 $irccon = $ALIASES{$irccon} || $irccon;
61     my $uri = new URI;
62     $uri->scheme ("jsirc"); $uri->authority ($irccon); $uri->path ($targ);
63     "$uri"
64 elmex 1.3 }
65 elmex 1.5
66     sub unalias {
67     my ($id) = @_;
68     for (keys %ALIASES) {
69     if ($id eq $ALIASES{$_}) {
70     return $_;
71     }
72 elmex 1.3 }
73     }
74    
75 elmex 1.5 sub lookup_connection {
76     my ($id) = @_;
77     my $alias = unalias ($id);
78     return $CONNS{$alias || $id}
79     }
80 elmex 1.3
81 elmex 1.5 sub from_dest_id {
82     my ($dest_id) = @_;
83     my $uri = URI->new ($dest_id);
84     my $path = ($uri->path_segments ())[1];
85     return (lookup_connection ($uri->authority), unalias ($uri->authority) || $uri->authority, $path);
86     }
87 elmex 1.1
88 elmex 1.5 sub connect_irc {
89     my ($host, $port, $alias) = @_;
90     my $irccon = "$host:$port";
91     my $pc = $CONNS{$irccon} = Net::IRC3::Client::Connection->new;
92     $ALIASES{$irccon} = $alias if defined $alias;
93 elmex 1.1
94     $pc->reg_cb (
95 elmex 1.6 channel_add => sub {
96     my ($pc, $chan, @nicks) = @_;
97 elmex 1.7 $chan = lc $chan;
98 elmex 1.6 my $dest = to_dest_id ($irccon, $chan);
99     my @ids = map { to_dest_id ($irccon, $_) } @nicks;
100     $JS->broadcast ({
101     src => $dest,
102 elmex 1.7 type => 'subid',
103     command => 'add',
104 elmex 1.6 ids => \@ids,
105     timestamp => time (),
106     });
107 elmex 1.7 log_line ($irccon, $chan, "$chan add: @nicks");
108 elmex 1.6 1;
109     },
110     channel_remove => sub {
111     my ($pc, $chan, @nicks) = @_;
112 elmex 1.7 $chan = lc $chan;
113 elmex 1.6 my $dest = to_dest_id ($irccon, $chan);
114     my @ids = map { to_dest_id ($irccon, $_) } @nicks;
115     $JS->broadcast ({
116     src => $dest,
117 elmex 1.7 type => 'subid',
118     command => 'remove',
119 elmex 1.6 ids => \@ids,
120     timestamp => time (),
121     });
122 elmex 1.7 log_line ($irccon, $chan, "$chan remove: @nicks");
123     1;
124     },
125     channel_change => sub {
126     my ($pc, $chan, $old_nick, $new_nick) = @_;
127     $chan = lc $chan;
128     my $dest = to_dest_id ($irccon, $chan);
129     $JS->broadcast ({
130     src => $dest,
131     type => 'subid',
132     command => 'change',
133     old_id => to_dest_id ($irccon, $old_nick),
134     new_id => to_dest_id ($irccon, $new_nick),
135     timestamp => time (),
136     });
137     log_line ($irccon, $chan, "$chan nick change: $old_nick => $new_nick");
138 elmex 1.6 1;
139     },
140 elmex 1.1 publicmsg => sub {
141     my ($pc, $chan, $msg) = @_;
142 elmex 1.7 $chan = lc $chan;
143 elmex 1.5 my ($nick) = Net::IRC3::Util::split_prefix ($msg->{prefix});
144     my $dest = to_dest_id ($irccon, $chan);
145     my $nickdest = to_dest_id ($irccon, $nick);
146    
147     $JS->broadcast ({
148 elmex 1.3 src => $dest,
149     type => "message",
150 elmex 1.7 msg_type => (uc ($msg->{command}) eq 'NOTICE' ? "public_notice" : "public"),
151 elmex 1.1 message => $msg->{trailing},
152 elmex 1.2 timestamp => time (),
153 elmex 1.5 to => { nick => $pc->nick, id => to_dest_id ($irccon, $pc->nick) },
154 elmex 1.4 from => { nick => $nick, id => $nickdest },
155 elmex 1.1 });
156 elmex 1.7 log_line ($irccon, $chan, (uc ($msg->{command}) eq 'NOTICE' ? "{$nick}" : "<$nick>") . " $msg->{trailing}");
157 elmex 1.1 1;
158     },
159     privatemsg => sub {
160     my ($pc, $dsgnick, $msg) = @_;
161     my ($nick) = Net::IRC3::Util::split_prefix ($msg->{prefix});
162 elmex 1.5 my $dest = to_dest_id ($irccon, $nick);
163    
164     $JS->broadcast ({
165 elmex 1.3 src => $dest,
166     type => "message",
167 elmex 1.7 msg_type => (uc ($msg->{command}) eq 'NOTICE' ? "private_notice" : "private"),
168 elmex 1.1 message => $msg->{trailing},
169 elmex 1.2 timestamp => time (),
170 elmex 1.5 to => { nick => $pc->nick, id => to_dest_id ($irccon, $pc->nick) },
171 elmex 1.4 from => { nick => $nick, id => $dest },
172 elmex 1.1 });
173 elmex 1.7 log_line ($irccon, $nick, (uc ($msg->{command}) eq 'NOTICE' ? "{$nick}" : "<$nick>") . " $msg->{trailing}");
174    
175     1;
176     },
177     error => sub {
178     my ($pc, $code, $message, @params) = @_;
179     my $name = Net::IRC3::Util::rfc_code_to_name ($code);
180     error_reply (undef, undef, irc_error => "$name: $message", to_dest_id ($irccon));
181     log_line ($irccon, "", "$name: $message");
182 elmex 1.1 1;
183     },
184 elmex 1.5 connect => sub {
185 elmex 1.7 info_reply (undef, connected =>
186 elmex 1.5 "Connected to $host:$port "
187 elmex 1.7 . (defined $alias ? "(aka $alias)" : ""),
188     to_dest_id ($irccon)
189 elmex 1.5 );
190 elmex 1.7 log_line ($irccon, "", "connected");
191     1;
192 elmex 1.5 },
193     disconnect => sub {
194 elmex 1.7 error_reply (undef, undef, disconnected =>
195     "Lost connection to $host:$port "
196     . (defined $alias ? "(aka $alias)" : ""),
197     to_dest_id ($irccon)
198 elmex 1.5 );
199 elmex 1.7 log_line ($irccon, "", "disconnected");
200 elmex 1.5 delete $CONNS{"$host:$port"};
201     delete $ALIASES{"$host:$port"};
202 elmex 1.7 1;
203 elmex 1.5 }
204 elmex 1.1 );
205    
206 elmex 1.5 eval {
207     $pc->connect ($host, $port);
208     };
209     if ($@) {
210 elmex 1.7 error_reply (undef, undef, connection_error =>
211 elmex 1.5 "Couldn't connect to $host:$port "
212     . (defined $alias ? "(aka $alias)" : "")
213 elmex 1.7 . ": $@",
214     to_dest_id ($irccon)
215 elmex 1.5 );
216 elmex 1.7 log_line ($irccon, "", "couldn't connect to $host:$port: $@");
217 elmex 1.5 delete $CONNS{"$host:$port"};
218     delete $ALIASES{"$host:$port"};
219     return;
220     }
221 elmex 1.7 my ($nick, $user, $real) = @{
222     $CFG->{userinfo}->{$alias || "$host:$port"}
223     || $CFG->{default_userinfo}
224     || []
225     };
226     $nick or die "No nickname in configuration given";
227     $user ||= $nick;
228     $real ||= $nick;
229     $pc->register ($nick, $user, $real);
230 elmex 1.1
231 elmex 1.5 for (@{$CFG->{channels}->{$alias || "$host:$port"}}) {
232     $pc->send_srv (JOIN => undef => $_);
233 elmex 1.1 }
234     }
235    
236 elmex 1.5 sub update_connections {
237     for (map { /^(\S+):(\d+)/ ? [$1, $2, $_] : [] } keys %CONNS) {
238     my ($h, $p) = @$_;
239     unless (
240     grep {
241     ($_->{host} eq $h) && (($_->{port} || 6667) == $p) && ($_->{connect})
242     } @{$CFG->{servers}})
243     {
244     $CONNS{$_->[2]}->disconnect;
245     }
246     }
247    
248     for my $con (@{$CFG->{servers}}) {
249     my ($host, $port) = ($con->{host}, $con->{port} || 6667);
250    
251     if ($con->{connect} and not $CONNS{"$host:$port"}) {
252 elmex 1.7 eval {
253     connect_irc ($host, $port, $con->{alias});
254     };
255     if ($@) {
256     error_reply (undef, undef,
257     connect_irc => "Couldn't connect to IRC server '$host:$port': $@",
258     to_dest_id ("$host:$port"));
259     log_line ("$host:$port", "", "couldn't connect to $host:$port: $@");
260     }
261 elmex 1.5 }
262     }
263     }
264    
265     sub info_reply {
266 elmex 1.7 my ($lid, $type, $string, $infoid) = @_;
267    
268     my @source = defined $infoid ? (info_id => $infoid) : ();
269    
270 elmex 1.5 if (defined $lid) {
271 elmex 1.7 $JS->send_data ($lid, { type => 'info', info_type => $type, message => $string, @source });
272 elmex 1.5 } else {
273 elmex 1.7 $JS->broadcast ({ type => 'info', info_type => $type, message => $string, @source });
274 elmex 1.5 }
275     }
276    
277     sub error_reply {
278 elmex 1.7 my ($lid, $srcpkt, $type, $string, $errid) = @_;
279    
280     my @source = defined $errid ? (error_id => $errid) : ();
281    
282     my @id = (defined $srcpkt and defined $srcpkt->{id}) ? (id => $srcpkt->{id}) : ();
283    
284 elmex 1.5 if (defined $lid) {
285 elmex 1.7 $JS->send_data ($lid, {
286     timestamp => time,
287     type => 'error',
288     error_type => $type,
289     message => $string,
290     @source,
291     @id
292     });
293 elmex 1.5 } else {
294 elmex 1.7 $JS->broadcast ({
295     timestamp => time,
296     type => 'error',
297     error_type => $type,
298     message => $string,
299     @source,
300     @id
301     });
302     }
303     }
304    
305     sub reply {
306     my ($lid, $srcpkt, $type, @reply) = @_;
307    
308     my @id = (defined $srcpkt and defined $srcpkt->{id}) ? (id => $srcpkt->{id}) : ();
309    
310     if (defined $lid) {
311     $JS->send_data ($lid, {
312     timestamp => time,
313     type => 'reply',
314     reply_type => $type,
315     @reply,
316     @id
317     });
318     } else {
319     $JS->broadcast ({
320     timestamp => time,
321     type => 'reply',
322     reply_type => $type,
323     @reply,
324     @id
325     });
326 elmex 1.5 }
327     }
328    
329     load_cfg;
330    
331     my $c = AnyEvent->condvar;
332    
333     $JS =
334     JSONConnection->new (
335     packet_cb => sub {
336     my ($JS, $lid, $data) = @_;
337    
338     if ($data->{type} eq 'message') {
339     my ($con, $irccon, $dest) = from_dest_id ($data->{dest});
340     unless (defined $con) {
341 elmex 1.7 error_reply ($lid, $data, no_connection => "No connection to ID.", $data->{dest});
342 elmex 1.5 return
343     }
344     $con->send_srv (PRIVMSG => $data->{message} => $dest);
345    
346 elmex 1.7 $JS->broadcast ({
347     src => $data->{dest},
348     type => "message",
349     msg_type => $data->{msg_type},
350     message => $data->{message},
351     is_echo => 1,
352     timestamp => time,
353     to => { nick => $dest, id => $data->{dest} },
354     from => { nick => $con->nick, id => to_dest_id ($irccon, $con->nick) },
355     });
356    
357     log_line ($irccon, $dest, "<".$con->nick."> $data->{message}");
358     reply ($lid, $data, 'sent_message');
359    
360 elmex 1.5 } elsif ($data->{type} eq 'command') {
361     if ($data->{command} eq 'reload') {
362     load_cfg;
363     update_connections;
364 elmex 1.7 reply ($lid, $data, 'reloaded');
365    
366 elmex 1.5 } elsif ($data->{command} eq 'list_connections') {
367     } elsif ($data->{command} eq 'list_ids') {
368     }
369 elmex 1.7 } elsif ($data->{type} eq 'raw') {
370     my $raw = $data->{message};
371     my ($con, $irccon, $dest) = from_dest_id ($data->{dest});
372     unless (defined $con) {
373     error_reply ($lid, $data, no_connection => "No connection to ID.", $data->{dest});
374     return
375     }
376     $con->send_raw ($raw);
377     reply ($lid, $data, 'sent_raw_message');
378    
379 elmex 1.5 } else {
380 elmex 1.7 error_reply ($lid, $data, bad_packet => "Did not understand this packet");
381 elmex 1.5 }
382     1
383     },
384     connect_cb => sub {
385     my ($JS, $lid) = @_;
386     $JS->send_data ($lid, { type => "hello" });
387 elmex 1.7 for my $irccon (keys %CONNS) {
388     for my $chan (keys %{$CONNS{$irccon}->channel_list}) {
389     my $dest = to_dest_id ($irccon, lc $chan);
390     $JS->send_data ($lid, {
391     src => $dest,
392     type => 'subid',
393     command => 'list',
394     ids => [
395     map {
396     to_dest_id ($irccon, $_)
397     } keys %{$CONNS{$irccon}->channel_list ()->{$chan}}
398     ],
399     timestamp => time (),
400     });
401     }
402     }
403 elmex 1.5 });
404    
405     update_connections;
406    
407     $JS->start_listener;
408 elmex 1.1
409     $c->wait;