The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Net::Snarl;

use warnings;
use strict;

use IO::Socket;
use Carp;

=head1 NAME

Net::Snarl - Snarl network protocol

=head1 VERSION

Version 0.05

=cut

our $VERSION = '0.05';

use constant SNARL_PORT           => 9887;
use constant SNARL_PROTO_VERSION  => '1.1';

=head1 SYNOPSIS

  use Net::Snarl;
  
  # connect to localhost and register Net::Snarl application
  my $snarl = Net::Snarl->new('Net::Snarl');
  $snarl->add_class('Test'); # add Test notification class
  $snarl->notify('Test', 'Hello', 'World', 5); # show hello world for 5 seconds

=head1 DESCRIPTION

A simple interface to send Snarl notifications across the network.  Snarl must 
be running on the target machine.  
    
=cut

sub _send {
  my ($self, %param) = @_;
  
  my $data = 'type=SNP#?version=' . Net::Snarl::SNARL_PROTO_VERSION . '#?' . 
    join('#?', map "$_=$param{$_}", keys %param);
    
  $self->{socket}->print("$data\x0d\x0a");
  $self->_recv;  
}

sub _recv {
  my ($self) = @_;
  
  my $data = $self->{socket}->getline();
  chomp $data;
  
  my ($header, $version, $code, $desc, @rest) = split /\//, $data;
  
  die "Unexpected response: $data" unless $header eq 'SNP';
  
  # hackishly disregard responses above 300
  if ($code >= 300) {
    push @{$self->{queue}}, [$code, $desc, @rest];
    return $self->_recv;
  }
  
  return $code, $desc, @rest;
}
    
=head1 INTERFACE

=head2 register($application, $host, $port)

Connects to Snarl and register an application.  Host defaults to localhost and 
port defaults to C<Net::Snarl::SNARL_PORT>.

=cut

sub register {
  my ($class, $application, $host, $port) = @_;
  
  croak 'Cannot call register as an instance method' if ref $class;
  croak 'Application name required' unless $application;
  
  my $socket = IO::Socket::INET->new(
    PeerAddr  => $host || 'localhost',
    PeerPort  => $port || Net::Snarl::SNARL_PORT,
    Proto     => 'tcp',
  ) or die "Unable to create socket: $!";
  
  my $self = bless { socket => $socket, application => $application }, $class;
  
  my ($result, $text) = $self->_send(
    action => 'register', 
    app => $application,
  );
  
  die "Unable to register: $text" if $result;
  
  return $self;
}

=head2 add_class($class, $title)

Registers a notification class with your application.  Title is the optional 
friendly name for the class.

=cut

sub add_class {
  my ($self, $class, $title) = @_;
  
  croak 'Cannot call add_class as a class method' unless ref $self;
  croak 'Class name required' unless $class;
  
  my ($result, $text) = $self->_send(
    action  => 'add_class', 
    app     => $self->{application},
    class   => $class,
    title   => $title || $class,
  );
  
  die "Unable to add class: $text" if $result;
}

=head2 notify($class, $title, $text, $timeout, $icon)

Displays a notification of the specified class.  Timeout defaults to 0 (sticky)
and icon defaults to nothing.

=cut

sub notify {
  my ($self, $class, $title, $text, $timeout, $icon) = @_;
  
  croak 'Cannot call notify as a class method' unless ref $self;
  croak 'Class name required' unless $class;
  croak 'Title required' unless $title;
  croak 'Text required' unless $text;
  
  my ($result, $rtext) = $self->_send(
    action  => 'notification',
    app     => $self->{application},
    class   => $class,
    title   => $title,
    text    => $text,
    timeout => $timeout || 0,
    icon    => $icon || '',
  );
  
  die "Unable to send notification: $rtext" if $result;
}

sub DESTROY {
  my ($self) = @_;
  
  $self->_send(
    action  => 'unregister',
    app     => $self->{application},
  );
}

=head1 BUGS

Please report any bugs or feature requests to C<bug-net-snarl at rt.cpan.org>, 
or through the web interface at 
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-Snarl>.  I will be 
notified, and then you'll automatically be notified of progress on your bug as 
I make changes.

=head1 TODO

Later versions of Snarl report interactions with the notifications back to the
socket.  Currently these are stored in a private queue.  Eventually, I will 
expose an interface for triggering callbacks on these events but that will
most likely require threading so I'm a little reluctant to implement it.

=head1 AUTHOR

Alan Berndt, C<< <alan@eatabrick.org> >>

=head1 LICENSE AND COPYRIGHT

Copyright 2010 Alan Berndt.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.


=cut

1; # End of Net::Snarl