#!/usr/bin/perl use strict; use warnings; use Apache::Htpasswd; use CGI; use Getopt::Long qw(:config auto_help); use JSON::Any; use HTML::Prototype; use HTTP::Date; use HTTP::Status; use MIME::Base64; use Pod::Usage; use POE qw(Component::Server::HTTP); use Template; use URI; use YAML; our $VERSION = '0.01'; GetOptions( '-pass=s' => \my $htpasswd_path, '-dump=s' => \my $dump_path, '-port=s' => \my $port, '-logline=s' => \my $logline, '--quiet' => \my $quiet ); pod2usage(1) unless $htpasswd_path && $dump_path; my $htpasswd; my $status = { message_id => 0, messages => [], users => {}, }; $status = YAML::LoadFile($dump_path) or die $! if -f $dump_path; $port = '' unless $port && $port =~ /^\d+$/; $logline = 50 unless $logline && $logline =~ /^\d+$/; my $server = POE::Component::Server::HTTP->new( Port => $port || 8888, Headers => { Server => "Twitterd/$VERSION", }, ContentHandler => { '/' => \&index, }, PreHandler => { '/' => \&auth, }, ); $poe_kernel->run(); exit(); 1; sub auth { my($req, $res) = @_; $res->code(RC_OK); $htpasswd = Apache::Htpasswd->new({ passwdFile => $htpasswd_path, ReadOnly => 1, }); my($user, $pass) = $req->authorization_basic; return RC_OK if $user && $htpasswd->htCheckPassword($user, $pass); $res->code(RC_UNAUTHORIZED); $res->header('WWW-Authenticate' => 'Basic realm="user login"'); $res->content('ERROR'); return RC_DENY; } sub index { my($req, $res) = @_; print sprintf "[%s] - - '%s %s'\n", time2str(time), $req->method, $req->uri; return RC_DENY unless $res->code eq RC_OK; my $query; if ($req->method eq 'GET') { $query = CGI->new($req->uri->query); } elsif ($req->method eq 'POST') { $query = CGI->new($req->content); } else { $res->code(RC_BAD_REQUEST); return RC_DENY; } $query->charset("utf-8"); my($user) = $req->authorization_basic; my $to_user; if ($req->uri->path =~ m!^/statuses/update(?:\..+)?$!) { update($req, $res, $query, $user); } elsif ($req->uri->path =~ m!^/statuses/(friends|public)_timeline(?:/(.+?))?.json$!) { friends_timeline($req, $res, $query); } elsif ($req->uri->path =~ m!^/statuses/user_timeline(?:/(.+?))?.json$!) { my $by_user = $1 || $user; user_timeline($req, $res, $query, $by_user); } elsif ($req->uri->path eq '/spinner') { show_spinner($res); } elsif (($to_user) = ($req->uri->path =~ m!^/(.+)!)) {# && print "TO: $to_user,\n" && unless ($htpasswd->fetchPass($to_user)) { $res->code(RC_NOT_FOUND); return RC_DENY; } home($req, $res, $query, $user, $to_user); } elsif ($req->uri->path eq '/') { home($req, $res, $query, $user); } else { $res->code(RC_NOT_FOUND); return RC_DENY; } return RC_OK; } sub home { my($req, $res, $query, $user, $to_user) = @_; my $tt = Template->new; my $template = home_template(); my $stash; $stash->{prototype} = HTML::Prototype->new; $stash->{user} = $user || ''; $stash->{to_user} = $to_user || ''; $tt->process(\$template, $stash, \my $out); $res->header('text/html; charset=utf-8'); $res->content($out); } sub update { my($req, $res, $query, $user) = @_; my $text = $query->param('status'); unless ($text) { return unless $status->{users}->{$user}; my @ids = sort { $b <=> $a } keys %{ $status->{users}->{$user} }; my $last_id = shift @ids; my $last_message = $status->{users}->{$user}->{$last_id}; $res->content(JSON::Any->objToJson($last_message)); return; } $status->{message_id}++; my $message = { created_at => time2str(time), text => $text, id => $status->{message_id}, user => { name => $user, description => '', location => '', url => '', screen_name => $user, id => $user, protected => '', profile_image_url => '', }, }; unshift @{ $status->{messages} }, $message; $status->{users}->{$user}->{$status->{message_id}} = $message; if (scalar(@{ $status->{messages} }) > $logline) { my $delete = pop @{ $status->{messages} }; my $time = str2time($delete->{created_at}); delete $status->{users}->{$delete->{user}}->{$delete->{id}}; } $res->content(JSON::Any->objToJson($message)); YAML::DumpFile($dump_path, $status); } sub friends_timeline { my($req, $res, $query) = @_; return $res->content(JSON::Any->objToJson($status->{messages})) unless $query->param('since'); my $since = str2time($query->param('since')); my $msgs = []; for my $msg (@{ $status->{messages} }) { next if str2time($msg->{created_at}) < $since; push @{ $msgs }, $msg; } $res->content(JSON::Any->objToJson($msgs)); } sub user_timeline { my($req, $res, $query, $user) = @_; my $msgs = []; my $since = $query->param('since') ? str2time($query->param('since')) : 0; for my $id ( sort {$b <=> $a} keys %{ $status->{users}->{$user} }) { my $msg = $status->{users}->{$user}->{$id}; next if str2time($msg->{created_at}) < $since; push @{ $msgs }, $msg; } $res->content(JSON::Any->objToJson($msgs)); } sub home_template { return <<'TEMPLATE'; [% SET title = "Twitterd" -%] [% IF to_user %][% SET title = title _ ' - ' _ to_user %][% END %] [% title %] [% prototype.define_javascript_functions %] [home] [[% user | html %]]

[% title %]

Status:

updates

TEMPLATE } sub show_spinner { my $res = shift; $res->header('image/gif'); $res->content(decode_base64(<ko@yappo.ne.jpE =head1 SEE ALSO L, L =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut