File Coverage

blib/lib/Net/CyanChat.pm
Criterion Covered Total %
statement 12 207 5.8
branch 0 68 0.0
condition 0 13 0.0
subroutine 4 28 14.2
pod 22 23 95.6
total 38 339 11.2


line stmt bran cond sub pod time code
1             package Net::CyanChat;
2              
3             #------------------------------------------------------------------------------#
4             # Net::CyanChat - Perl interface for connecting to Cyan Worlds' chat room. #
5             #------------------------------------------------------------------------------#
6             # POD documentation is at the very end of this source code. #
7             #------------------------------------------------------------------------------#
8              
9 1     1   77711 use strict;
  1         4  
  1         52  
10 1     1   560 use warnings;
  1         3  
  1         46  
11 1     1   3136 use IO::Socket;
  1         47319  
  1         6  
12 1     1   17181 use IO::Select;
  1         3399  
  1         5947  
13              
14             our $VERSION = '0.06';
15              
16             sub new {
17 0     0 1   my $proto = shift;
18 0   0       my $class = ref($proto) || $proto;
19              
20 0           my $self = {
21             host => 'cho.cyan.com', # Default CC Host
22             port => 1812, # Default CC Port (1813=debugging)
23             debug => 0, # Debug Mode
24             proto => 1, # Use Protocol 1 (not 0)
25             sock => undef, # Socket Object
26             select => undef, # Select Object
27             pinged => 0, # Last Ping Time
28             refresh => 60, # Ping Rate = 60 Seconds
29             nickname => '', # Our Nickname
30             handlers => {}, # Handlers
31             connected => 0, # Are We Connected?
32             accepted => 0, # Logged in?
33             who => {}, # Who List
34             special => {}, # Special who List
35             ignored => {}, # Ignored List
36             nicks => {}, # Nickname Lookup Table
37             @_,
38             };
39              
40             # Protocol support numbers: 0 and 1.
41 0 0 0       if ($self->{proto} < 0 || $self->{proto} > 1) {
42 0           die "Unsupported protocol version: must be 0 or 1!";
43             }
44              
45 0           bless ($self,$class);
46 0           return $self;
47             }
48              
49             sub version {
50 0     0 1   my ($self) = @_;
51 0           return $VERSION;
52             }
53              
54             sub debug {
55 0     0 1   my ($self,$msg) = @_;
56              
57 0 0         return unless $self->{debug} == 1;
58 0           print "Net::CyanChat::debug // $msg\n";
59             }
60              
61             sub send {
62 0     0 1   my ($self,$data) = @_;
63              
64             # Send the data.
65 0 0         if (defined $self->{sock}) {
66 0           $self->debug (">>> $data\n");
67              
68             # Send true CrLf
69 0 0         $self->{sock}->send ("$data\x0d\x0a") or do {
70             # We've been disconnected!
71 0           $self->{sock}->close();
72 0           $self->{sock} = undef;
73 0           $self->{select} = undef;
74 0           $self->{connected} = 0;
75 0           $self->{nick} = '';
76 0           $self->{pinged} = 0;
77 0           $self->{who} = {};
78 0           $self->{nicks} = {};
79 0           $self->_event ('Disconnected');
80             };
81             }
82             else {
83 0           warn "Could not send \"$data\" to CyanChat: connection not established!";
84             }
85             }
86              
87             sub setHandler {
88 0     0 1   my ($self,$event,$code) = @_;
89              
90             # Set this handler.
91 0           $self->{handlers}->{$event} = $code;
92             }
93              
94             sub connect {
95 0     0 1   my ($self) = @_;
96              
97             # Connect to CyanChat.
98 0           $self->{sock} = new IO::Socket::INET (
99             PeerAddr => $self->{host},
100             PeerPort => $self->{port},
101             Proto => 'tcp',
102             );
103              
104             # Error?
105 0 0         if (!defined $self->{sock}) {
106 0           $self->_event ('Error', "00|Connection Error", "Net::CyanChat Connection Error: $!");
107 0           return undef;
108             }
109              
110             # Create a select object.
111 0           $self->{select} = IO::Select->new ($self->{sock});
112 0           return 1;
113             }
114              
115             sub start {
116 0     0 1   my ($self) = @_;
117              
118 0           while (1) {
119 0 0         $self->do_one_loop or last;
120             }
121              
122 0           return undef;
123             }
124              
125             sub login {
126 0     0 1   my ($self,$nick) = @_;
127              
128             # Remove newline characters.
129 0           $nick =~ s/[\x0d\x0a]//ig;
130              
131 0 0         if (length $nick > 0) {
132             # Sign in.
133 0           $self->send ("10|$nick");
134 0           $self->{nickname} = $nick;
135 0           return 1;
136             }
137              
138 0           return undef;
139             }
140              
141             sub logout {
142 0     0 1   my ($self) = @_;
143              
144 0 0         return undef unless length $self->{nickname} > 0;
145 0           $self->{nickname} = '';
146 0           $self->{accepted} = 0;
147 0           $self->send ("15");
148 0           return 1;
149             }
150              
151             sub sendMessage {
152 0     0 1   my ($self,$msg) = @_;
153              
154             # Remove newline characters.
155 0           $msg =~ s/[\x0d\x0a]//ig;
156              
157             # Send the message.
158 0 0         return undef unless length $msg > 0;
159 0           $self->send ("30|^1$msg");
160 0           return 1;
161             }
162              
163             sub sendPrivate {
164 0     0 1   my ($self,$to,$msg) = @_;
165              
166             # Remove newline characters.
167 0           $to =~ s/[\x0d\x0a]//ig;
168 0           $msg =~ s/[\x0d\x0a]//ig;
169              
170 0 0 0       return undef unless (length $to > 0 && length $msg > 0);
171             # Get the user's full nick.
172 0           my $nick = $self->{nicks}->{$to};
173              
174             # Send this user a message.
175 0           $self->send ("20|$nick|^1$msg");
176 0           return 1;
177             }
178              
179             sub getBuddies {
180 0     0 1   my ($self) = @_;
181              
182             # Return the buddylist.
183 0           my $buddies = {};
184 0           foreach my $key (keys %{$self->{who}}) {
  0            
185 0           $buddies->{who}->{$key} = $self->{who}->{$key};
186             }
187 0           foreach my $key (keys %{$self->{special}}) {
  0            
188 0           $buddies->{special}->{$key} = $self->{special}->{$key};
189             }
190 0           return $buddies;
191             }
192              
193             sub getUsername {
194 0     0 1   my ($self,$who) = @_;
195              
196             # Return this user's full name.
197 0   0       return $self->{nicks}->{who}->{$who} || $self->{nicks}->{special}->{$who} || undef;
198             }
199             sub getFullName {
200 0     0 0   my ($self,$who) = @_;
201              
202             # Alias for getUsername.
203 0           return $self->getUsername ($who);
204             }
205              
206             sub getAddress {
207 0     0 1   my ($self,$who) = @_;
208              
209             # Return this user's address.
210 0   0       return $self->{who}->{$who} || (
211             exists $self->{special}->{$who} ? "Cyan Worlds" : undef
212             ) || undef;
213             }
214              
215             sub protocol {
216 0     0 1   my ($self) = @_;
217 0           return $self->{proto};
218             }
219              
220             sub nick {
221 0     0 1   my ($self) = @_;
222              
223 0           return $self->{nickname};
224             }
225              
226             sub ignore {
227 0     0 1   my ($self,$who) = @_;
228              
229             # Remove newline characters.
230 0           $who =~ s/[\x0d\x0a]//ig;
231              
232             # Ignore this user.
233 0 0         return undef unless length $who > 0;
234 0           $self->{ignored}->{$who} = 1;
235 0           $self->send ("70|$who");
236 0           return 1;
237             }
238             sub unignore {
239 0     0 1   my ($self,$who) = @_;
240              
241             # Remove newline characters.
242 0           $who =~ s/[\x0d\x0a]//ig;
243              
244             # Unignore this user.
245 0 0         return undef unless length $who > 0;
246 0           delete $self->{ignored}->{$who};
247 0           $self->send ("70|$who");
248 0           return 1;
249             }
250              
251             sub authenticate {
252 0     0 1   my ($self,$password) = @_;
253              
254             # Remove newline characters.
255 0           $password =~ s/[\x0d\x0a]//ig;
256              
257             # Authenticate with a CC password.
258 0           $self->send ("50|$password");
259 0           return 1;
260             }
261              
262             sub promote {
263 0     0 1   my ($self,$user) = @_;
264              
265             # Remove newline characters.
266 0           $user =~ s/[\x0d\x0a]//ig;
267              
268             # Promote this user to Special Guest.
269 0           $self->send ("60|$user|4");
270 0           return 1;
271             }
272              
273             sub demote {
274 0     0 1   my ($self,$user) = @_;
275              
276             # Remove newline characters.
277 0           $user =~ s/[\x0d\x0a]//ig;
278              
279             # Demote this user.
280 0           $self->send ("60|$user|0");
281 0           return 1;
282             }
283              
284             sub _event {
285 0     0     my ($self,$event,@data) = @_;
286              
287 0 0         return unless exists $self->{handlers}->{$event};
288 0           &{$self->{handlers}->{$event}} ($self,@data);
  0            
289             }
290              
291             sub do_one_loop {
292 0     0 1   my ($self) = @_;
293              
294             # Time to ping again?
295 0 0         if ($self->{pinged} > 0) {
296             # If connected...
297 0 0         if ($self->{connected} == 1) {
298             # If logged in...
299 0 0         if ($self->{accepted} == 1) {
300             # If refresh time has passed...
301 0 0         if (time() - $self->{pinged} >= $self->{refresh}) {
302             # To ping, send a private message to nobody.
303 0           $self->send ("20||^1ping");
304 0           $self->{pinged} = time();
305             }
306             }
307             }
308             }
309              
310 0 0         return undef unless defined $self->{select};
311              
312             # Loop with the server.
313 0           my @ready = $self->{select}->can_read(.01);
314 0 0         return 1 unless(@ready);
315              
316 0           foreach my $socket (@ready) {
317 0           my $resp;
318 0           $self->{sock}->recv ($resp,2048,0);
319 0           my @in = split(/\n/, $resp);
320              
321             # The server has sent us a message!
322 0           foreach my $said (@in) {
323 0           $said =~ s/[\x0d\x0a]//ig;
324 0           my ($command,@args) = split(/\|/, $said);
325              
326 0           $self->debug("<<< $said\n");
327              
328             # Go through the commands.
329 0 0         if ($command == 10) {
    0          
    0          
    0          
    0          
    0          
    0          
330             # 10 = Name is invalid.
331 0           $self->_event ('Error', 10, "Your name is invalid.");
332             }
333             elsif ($command == 11) {
334             # 11 = Name accepted.
335 0           $self->{accepted} = 1;
336 0           $self->_event ('Name_Accepted');
337             }
338             elsif ($command == 21) {
339             # 21 = Private Message
340 0           my $type = 0;
341 0           my $fullNick = $args[0];
342 0           my ($level) = $args[0] =~ /^(\d)/;
343 0           $type = $args[1] =~ /^\^(\d)/;
344 0           $args[0] =~ s/^(\d)//ig;
345 0           $args[1] =~ s/^\^(\d)//ig;
346              
347             # Get the sender's nick and address.
348 0           my ($nick,$addr) = split(/\,/, $args[0], 2);
349              
350             # Skip ignored users.
351 0 0         next if exists $self->{ignored}->{$nick};
352              
353 0           shift (@args);
354 0           my $text = join ('|',@args);
355              
356             # Call the event.
357 0           $self->_event ('Private', {
358             nick => $nick,
359             username => $fullNick,
360             level => $level,
361             address => $addr,
362             message => $text,
363             });
364             }
365             elsif ($command == 31) {
366             # 31 = Public Message.
367 0           my $type = 1;
368 0           my $fullNick = $args[0];
369 0           my ($level) = $args[0] =~ /^(\d)/;
370 0           ($type) = $args[1] =~ /^\^(\d)/;
371 0           $args[0] =~ s/^(\d)//i;
372 0           $args[1] =~ s/^\^(\d)//i;
373              
374             # Get the sender's nick and address.
375 0           my ($nick,$addr) = split(/\,/, $args[0], 2);
376              
377             # Skip ignored users.
378 0 0         next if exists $self->{ignored}->{$nick};
379              
380             # Chop off spaces.
381 0           $args[1] =~ s/^\s//ig;
382              
383             # Shift off data.
384 0           shift (@args); # nickname
385 0           my $text = join ('|',@args);
386              
387             # User has entered the room.
388 0 0         if ($type == 2) {
    0          
389             # Call the event.
390 0           $self->_event ('Chat_Buddy_In', {
391             nick => $nick,
392             username => $fullNick,
393             level => $level,
394             address => $addr,
395             message => $text,
396             });
397             }
398             elsif ($type == 3) {
399             # Call the event.
400 0           $self->_event ('Chat_Buddy_Out', {
401             nick => $nick,
402             username => $fullNick,
403             level => $level,
404             address => $addr,
405             message => $text,
406             });
407             }
408             else {
409             # Normal message.
410 0           $self->_event ('Message', {
411             nick => $nick,
412             username => $fullNick,
413             level => $level,
414             address => $addr,
415             message => $text,
416             });
417             }
418             }
419             elsif ($command == 35) {
420             # 35 = Who List Update.
421              
422             # Keep track of all the FullNick's we found.
423 0           my %this = ();
424              
425             # Keep running arrays of users for the WhoList event.
426 0           my @list = ();
427              
428             # Go through each item received.
429 0           foreach my $user (@args) {
430 0           my ($nick,$addr) = split(/\,/, $user, 2);
431 0           my $fullNick = $nick;
432              
433             # Get data about this user.
434 0           my ($level) = $nick =~ /^(\d)/;
435 0           $nick =~ s/^(\d)//i;
436              
437             # User is online.
438 0 0         if ($level == 0) {
439             # Add user to the normal users list.
440 0           $self->{who}->{$nick} = $addr;
441 0           $self->{nicks}->{who}->{$nick} = $fullNick;
442             }
443             else {
444             # Add them to the Cyan & Guests list.
445 0           $self->{special}->{$nick} = $addr;
446 0           $self->{nicks}->{special}->{$nick} = $fullNick;
447             }
448              
449 0           push (@list, {
450             nick => $nick,
451             level => $level,
452             address => $addr,
453             username => $fullNick,
454             });
455 0           $this{$fullNick} = 1;
456             }
457              
458             # New event: WhoList = sends the entire Who List at once.
459 0           $self->_event ('WhoList', @list);
460              
461             # See if anybody should be dropped.
462 0           foreach my $who (keys %{$self->{who}}) {
  0            
463 0           my $fullNick = $self->{nicks}->{who}->{$who};
464 0 0         if (!exists $this{$fullNick}) {
465             # Buddy's gone.
466 0           delete $self->{who}->{$who};
467             }
468             }
469 0           foreach my $who (keys %{$self->{special}}) {
  0            
470 0           my $fullNick = $self->{nicks}->{special}->{$who};
471 0 0         if (!exists $this{$fullNick}) {
472             # Buddy's gone.
473 0           delete $self->{special}->{$who};
474             }
475             }
476              
477             # If we haven't been connected, now is the time to authenticate.
478 0 0         if ($self->{connected} == 0) {
479 0           $self->{connected} = 1;
480              
481             # Send event 40 to the server (40 = client ready).
482 0           $self->send ("40|$self->{proto}");
483              
484             # The server is ready for us now.
485 0           $self->_event ('Connected');
486              
487             # Start the pinging process.
488 0           $self->{pinged} = time();
489             }
490             }
491             elsif ($command == 40) {
492             # 40 = Server welcome message (the "pong" of 40 from the client).
493 0           $args[0] =~ s/^1//i;
494 0           $self->_event ('Welcome', $args[0]);
495             }
496             elsif ($command == 70) {
497             # 70 = Ignored/Unignored a user.
498 0           my $user = $args[0];
499 0           $self->_event ('Ignored', $user);
500             }
501             else {
502 0           $self->debug ("Unknown event code from server: $command|"
503             . join ('|', @args) );
504             }
505             }
506             }
507              
508 0           return 1;
509             }
510              
511             1;
512             __END__