App-Alice

 view release on metacpan or  search on metacpan

lib/App/Alice/HTTPD.pm  view on Meta::CPAN

package App::Alice::HTTPD;

use AnyEvent;
use AnyEvent::HTTP;

use Twiggy::Server;
use Plack::Request;
use Plack::Builder;
use Plack::Middleware::Static;
use Plack::Session::Store::File;

use IRC::Formatting::HTML qw/html_to_irc/;

use App::Alice::Stream;
use App::Alice::Commands;

use JSON;
use Encode;
use utf8;
use Any::Moose;
use Try::Tiny;

has 'app' => (
  is  => 'ro',
  isa => 'App::Alice',
  required => 1,
);

has 'httpd' => (is  => 'rw');
has 'ping_timer' => (is  => 'rw');

has 'config' => (
  is => 'ro',
  isa => 'App::Alice::Config',
  lazy => 1,
  default => sub {shift->app->config},
);

my $url_handlers = [
  [ qr{^/$}               => \&send_index ],
  [ qr{^/say/?$}          => \&handle_message ],
  [ qr{^/stream/?$}       => \&setup_stream ],
  [ qr{^/config/?$}       => \&send_config ],
  [ qr{^/prefs/?$}        => \&send_prefs ],
  [ qr{^/serverconfig/?$} => \&server_config ],
  [ qr{^/save/?$}         => \&save_config ],
  [ qr{^/tabs/?$}         => \&tab_order ],
  [ qr{^/login/?$}        => \&login ],
  [ qr{^/logout/?$}       => \&logout ],
  [ qr{^/logs/?$}         => \&send_logs ],
  [ qr{^/search/?$}       => \&send_search ],
  [ qr{^/range/?$}        => \&send_range ],
  [ qr{^/view/?$}         => \&send_index ],
  [ qr{^/get}             => \&image_proxy ],
];

sub url_handlers { return $url_handlers }

has 'streams' => (
  is => 'rw',
  auto_deref => 1,
  isa => 'ArrayRef[App::Alice::Stream]',
  default => sub {[]},
);

sub add_stream {push @{shift->streams}, @_}
sub no_streams {@{$_[0]->streams} == 0}
sub stream_count {scalar @{$_[0]->streams}}

sub BUILD {
  my $self = shift;
  my $httpd = Twiggy::Server->new(
    host => $self->config->http_address,
    port => $self->config->http_port,
  );
  $httpd->register_service(
    builder {
      if ($self->app->auth_enabled) {
        mkdir $self->config->path."/sessions"
          unless -d $self->config->path."/sessions";
        enable "Session",
          store => Plack::Session::Store::File->new(dir => $self->config->path),
          expires => "24h";
      }
      enable "Static", path => qr{^/static/}, root => $self->config->assetdir;
      sub {$self->dispatch(shift)}
    }
  );
  $self->httpd($httpd);
  $self->ping;

lib/App/Alice/HTTPD.pm  view on Meta::CPAN

  if ($self->app->auth_enabled) {
    unless ($req->path eq "/login" or $self->is_logged_in($req)) {
      my $res = $req->new_response;
      $res->redirect("/login");
      return $res->finalize;
    }
  }
  for my $handler (@{$self->url_handlers}) {
    my $re = $handler->[0];
    if ($req->path_info =~ /$re/) {
      return $handler->[1]->($self, $req);
    }
  }
  return $self->not_found($req);
}

sub is_logged_in {
  my ($self, $req) = @_;
  my $session = $req->env->{"psgix.session"};
  return $session->{is_logged_in};
}

sub login {
  my ($self, $req) = @_;
  my $res = $req->new_response;
  if (!$self->app->auth_enabled or $self->is_logged_in($req)) {
    $res->redirect("/");
    return $res->finalize;
  }
  elsif (my $user = $req->param("username")
     and my $pass = $req->param("password")) {
    if ($self->app->authenticate($user, $pass)) {
      $req->env->{"psgix.session"}->{is_logged_in} = 1;
      $res->redirect("/");
      return $res->finalize;
    }
    $res->body($self->app->render("login", "bad username or password"));
  }
  else {
    $res->body($self->app->render("login"));
  }
  $res->status(200);
  return $res->finalize;
}

sub logout {
  my ($self, $req) = @_;
  my $res = $req->new_response;
  if (!$self->app->auth_enabled) {
    $res->redirect("/");
  } else {
    $req->env->{"psgix.session"}{is_logged_in} = 0;
    $req->env->{"psgix.session.options"}{expire} = 1;
    $res->redirect("/login");
  }
  return $res->finalize;
}

sub ping {
  my $self = shift;
  $self->ping_timer(AnyEvent->timer(
    after    => 5,
    interval => 10,
    cb       => sub {
      $self->broadcast({
        type => "action",
        event => "ping",
      });
    }
  ));
}

sub shutdown {
  my $self = shift;
  $_->close for $self->streams;
  $self->streams([]);
  $self->ping_timer(undef);
  $self->httpd(undef);
}

sub image_proxy {
  my ($self, $req) = @_;
  my $url = $req->request_uri;
  $url =~ s/^\/get\///;
  return sub {
    my $respond = shift;
    http_get $url, sub {
      my ($data, $headers) = @_;
      my $res = $req->new_response($headers->{Status});
      $res->headers($headers);
      $res->body($data);
      $respond->($res->finalize);
    };
  }
}

sub broadcast {
  my ($self, @data) = @_;
  return if $self->no_streams or !@data;
  my $purge = 0;
  for my $stream ($self->streams) {
    try {
      $stream->send(@data);
    } catch {
      $stream->close;
      $purge = 1;
    };
  }
  $self->purge_disconnects if $purge;
};

sub setup_stream {
  my ($self, $req) = @_;
  $self->app->log(info => "opening new stream");
  my $min = $req->param('msgid') || 0;
  return sub {
    my $respond = shift;
    my $stream = App::Alice::Stream->new(
      queue      => [ map({$_->join_action} $self->app->windows) ],
      writer     => $respond,
      start_time => $req->param('t'),
      # android requires 4K updates to trigger loading event
      min_bytes  => $req->user_agent =~ /android/i ? 4096 : 0,
    );
    $self->add_stream($stream);
    $self->app->with_messages(sub {
      return unless @_;
      $stream->enqueue(
        map  {$_->{buffered} = 1; $_}
        grep {$_->{msgid} > $min}
        @_
      );
      $stream->send;
    });
  }
}



( run in 0.952 second using v1.01-cache-2.11-cpan-39bf76dae61 )