view release on metacpan or search on metacpan
directory => ["t", "xt", "eg", "inc", "share", "benchmark"],
file => ['README.pod', 'README.md'],
},
meta_add => {
'meta-spec' => {
version => 2,
url => 'https://metacpan.org/pod/CPAN::Meta::Spec',
},
resources => {
bugtracker => {
web => 'https://github.com/debug-ito/busybird/issues',
},
repository => {
url => 'git://github.com/debug-ito/busybird.git',
web => 'https://github.com/debug-ito/busybird',
},
}
}
)->create_build_script();
0.12 2014-12-08
- No functional changes.
[PACKAGING]
- Now it requires Exporter 5.57, because we import the import() function instead of inheritance.
- Now it requires perl 5.8.0 (previously it was 5.10.0, but in fact I didn't use any feature in 5.10.0).
- Now it requires autovivification 0.14 for bug fix of RT#99458.
It drops dependency on EV.
0.11 2014-10-22
[ENHANCEMENTS]
- Now it renders "busybird.user_permalink" field as the link URL for the user.
This is an experimental feature for now.
0.10 2014-10-15
[ENHANCEMENTS]
- Web API: add "only_statuses" query parameter to GET /timelines/{timeline}/statues.json
- Add per-timeline config params: "acked_statuses_load_count" and "default_level_threshold".
- Now it warns you when you give unknown config parameters.
[DOCUMENTATION]
- BusyBird::SafeData is now public class. End-users are free to use it.
[PACKAGING]
bin/busybird
Build.PL
Changes
cpanfile
lib/BusyBird.pm
lib/BusyBird/Config.pm
lib/BusyBird/Filter.pm
lib/BusyBird/Filter/Twitter.pm
lib/BusyBird/Flow.pm
lib/BusyBird/Input/Generator.pm
lib/BusyBird/Log.pm
lib/BusyBird/Watcher/Aggregator.pm
MANIFEST This list of files
README.md
share/sample_config.psgi
share/www/static/bootstrap/css/bootstrap.min.css
share/www/static/bootstrap/fonts/glyphicons-halflings-regular.eot
share/www/static/bootstrap/fonts/glyphicons-halflings-regular.svg
share/www/static/bootstrap/fonts/glyphicons-halflings-regular.ttf
share/www/static/bootstrap/fonts/glyphicons-halflings-regular.woff
share/www/static/bootstrap/js/bootstrap.min.js
share/www/static/busybird.css
share/www/static/busybird.js
share/www/static/favicon_alert.ico
share/www/static/favicon_normal.ico
share/www/static/jquery.js
share/www/static/q.js
share/www/static/spin.js
share/www/static/timeline.js
share/www/static/timeline_list.js
share/www/templates/error.tx
share/www/templates/status.tx
share/www/templates/timeline.tx
"Test::MockObject" : "1.09",
"Test::MockObject::Extends" : "0",
"Test::More" : "0.98",
"Test::Warn" : "0.24"
}
}
},
"release_status" : "stable",
"resources" : {
"bugtracker" : {
"web" : "https://github.com/debug-ito/busybird/issues"
},
"repository" : {
"url" : "git://github.com/debug-ito/busybird.git",
"web" : "https://github.com/debug-ito/busybird"
}
},
"version" : "0.12"
}
Tie::IxHash: '0'
Time::HiRes: '1.9720'
Try::Tiny: '0.10'
Twiggy: '0'
URI::Escape: '0'
autovivification: '0.14'
parent: '0'
perl: v5.8.0
sort: '0'
resources:
bugtracker: https://github.com/debug-ito/busybird/issues
repository: git://github.com/debug-ito/busybird.git
version: '0.12'
BusyBird: a multi-level Web-based timeline viewer
=================================================
[](https://travis-ci.org/debug-ito/busybird)
BusyBird is a personal Web-based timeline viewer application.
You can think of it as a Twitter client, but BusyBird is more generic and focused on viewing.
BusyBird accepts data called **Statuses** from its RESTful Web API.
The received statuses are stored to one or more **Timelines** .
You can view those statuses in a timeline by a Web browser.
For more information, visit https://metacpan.org/pod/BusyBird
SCREENSHOTS
-----------
https://github.com/debug-ito/busybird/wiki/Screenshots
QUICK START
-----------
Example in Ubuntu Linux.
- Install `make` and `curl`
$ sudo apt-get install build-essential curl
- Install
$ curl -L http://cpanmin.us/ | perl - -n BusyBird
$ export PERL5LIB="$HOME/perl5/lib/perl5:$PERL5LIB"
$ export PATH="$HOME/perl5/bin:$PATH"
- Run
$ busybird
Twiggy: Accepting connections at http://127.0.0.1:5000/
- Open timelines
$ firefox http://localhost:5000/
- Post a status
$ curl -d '{"text":"hello, world!"}' http://localhost:5000/timelines/home/statuses.json
See https://metacpan.org/pod/BusyBird::Manual::Tutorial
TRY WITHOUT INSTALLATION
------------------------
You can try BusyBird without installing it. This is recommended if you
try a development version. (You need `cpanm` command. See
[App::cpanminus](https://metacpan.org/pod/App::cpanminus) for detail).
$ git clone https://github.com/debug-ito/busybird.git
$ cd busybird
$ cpanm Module::Build::Prereqs::FromCPANfile
$ cpanm --installdeps .
$ perl Build.PL
$ ./Build
$ ./Build test
...and to start BusyBird, type
$ perl -Iblib/lib blib/script/busybird
AUTHOR
------
Toshio Ito
* https://github.com/debug-ito
* debug.ito [at] gmail.com
bin/busybird view on Meta::CPAN
exit 1;
}
__END__
=pod
=head1 NAME
busybird - BusyBird runner script
=head1 SYNOPSIS
## SYNTAX
$ busybird [OPTIONS] [CONFIG_FILE]
## read config file from ~/.busybird/config.psgi
$ busybird
## explicit config file
$ busybird another_config.psgi
## explicitly binding address/port
$ busybird --host 0.0.0.0 --port 18888
=head1 DESCRIPTION
C<busybird> command runs a L<BusyBird> process instance.
=head1 ARGUMENTS
=over
=item CONFIG_FILE (default: ~/.busybird/config.psgi)
The L<BusyBird> configuration file. See L<BusyBird::Manual::Tutorial> and L<BusyBird::Manual::Config> for how to write this file.
If omitted, C<~/.busybird/config.psgi> is used. If this file does not exist, it is automatically created.
=back
=head1 OPTIONS
=over
=item -o,--host ADDRESS (default: 127.0.0.1)
IP address or hostname to listen.
lib/BusyBird.pm view on Meta::CPAN
package BusyBird;
use v5.8.0;
use strict;
use warnings;
use BusyBird::Main;
use BusyBird::Main::PSGI qw(create_psgi_app);
use Exporter 5.57 qw(import);
our $VERSION = '0.12';
our @EXPORT = our @EXPORT_OK = qw(busybird timeline end);
my $singleton_main;
sub busybird {
return defined($singleton_main)
? $singleton_main : ($singleton_main = BusyBird::Main->new);
}
sub timeline {
my ($timeline_name) = @_;
return busybird()->timeline($timeline_name);
}
sub end {
return create_psgi_app(busybird());
}
1;
__END__
=pod
=head1 NAME
lib/BusyBird.pm view on Meta::CPAN
L<BusyBird> renders statuses based on their B<< Status Levels >>.
Statuses whose level is below the threshold are dynamically hidden,
so you can focus on more relevant statuses.
Status levels are set by you, not by L<BusyBird>.
=back
=head1 SCREENSHOTS
L<https://github.com/debug-ito/busybird/wiki/Screenshots>
=head1 QUICK START
Example in Ubuntu Linux.
=over
=item *
Install C<gcc>, C<make> and C<curl>
lib/BusyBird.pm view on Meta::CPAN
Install
$ curl -L http://cpanmin.us/ | perl - -n BusyBird
$ export PERL5LIB="$HOME/perl5/lib/perl5:$PERL5LIB"
$ export PATH="$HOME/perl5/bin:$PATH"
=item *
Run
$ busybird
Twiggy: Accepting connections at http://127.0.0.1:5000/
=item *
Open timelines
$ firefox http://localhost:5000/
=item *
lib/BusyBird.pm view on Meta::CPAN
Below is detailed documentation of L<BusyBird> module.
Casual users need not to read it.
As a module, L<BusyBird> maintains a singleton L<BusyBird::Main> object,
and exports some functions to manipulate the singleton.
That way, L<BusyBird> makes it easy for users to write their C<config.psgi> file.
=head1 SYNOPSIS
In your C<~/.busybird/config.psgi> file...
use BusyBird;
busybird->set_config(
time_zone => "+0900",
);
timeline("twitter_work")->set_config(
time_zone => "America/Chicago"
);
timeline("twitter_private");
end;
=head1 EXPORTED FUNCTIONS
The following functions are exported by default.
=head2 $main = busybird()
Returns the singleton L<BusyBird::Main> object.
=head2 $timeline = timeline($timeline_name)
Returns the L<BusyBird::Timeline> object named C<$timeline_name> from the singleton.
If there is no such timeline, it automatically creates the timeline.
This is equivalent to C<< busybird()->timeline($timeline_name) >>.
=head2 $psgi_app = end()
Returns a L<PSGI> application object from the singleton L<BusyBird::Main> object.
This is supposed to be called at the end of C<config.psgi> file.
This is equivalent to C<< BusyBird::Main::PSGI::create_psgi_app(busybird()) >>.
=head1 TECHNOLOGIES USED
=over
=item *
L<jQuery|http://jquery.com/>
=item *
lib/BusyBird.pm view on Meta::CPAN
L<spin.js|http://fgnass.github.io/spin.js/>
=item *
... and a lot of Perl modules
=back
=head1 REPOSITORY
L<https://github.com/debug-ito/busybird>
=head1 BUGS AND FEATURE REQUESTS
Please report bugs and feature requests to my Github issues
L<https://github.com/debug-ito/busybird/issues>.
Although I prefer Github, non-Github users can use CPAN RT
L<https://rt.cpan.org/Public/Dist/Display.html?Name=BusyBird>.
Please send email to C<bug-BusyBird at rt.cpan.org> to report bugs
if you do not have CPAN RT account.
=head1 AUTHOR
Toshio Ito, C<< <toshioito at cpan.org> >>
lib/BusyBird/Config.pm view on Meta::CPAN
$_DEFAULTS{timeline} = {
time_zone => sub { "local" },
time_format => sub { '%x (%a) %X %Z' },
time_locale => sub { $ENV{LC_TIME} or "C" },
post_button_url => sub { "https://twitter.com/intent/tweet" },
status_permalink_builder => sub { return sub {
my ($status) = @_;
my $ss = safed($status);
my $permalink_in_status = $ss->val(qw(busybird status_permalink));
return $permalink_in_status if defined $permalink_in_status;
my $id = $ss->val(qw(busybird original id))
|| $ss->val(qw(busybird original id_str))
|| $ss->val("id")
|| $ss->val("id_str");
my $username = $ss->val(qw(user screen_name));
if(defined($id) && defined($username) && $id =~ /^\d+$/) {
return qq{https://twitter.com/$username/status/$id};
}
return undef;
} },
urls_entity_url_builder => sub { sub { my ($text, $entity) = @_; return $entity->{url} }},
lib/BusyBird/Filter.pm view on Meta::CPAN
BusyBird::Filter - common utilities about status filters
=head1 SYNOPSIS
use BusyBird;
use BusyBird::Filter qw(:all);
my $drop_low_level = filter_map sub {
my $status = shift;
return $status->{busybird}{level} > 5 ? ($status) : ();
};
my $set_level = filter_each sub {
my $status = shift;
$status->{busybird}{level} = 10;
};
timeline("home")->add_filter($drop_low_level);
timeline("home")->add_filter($set_level);
=head1 DESCRIPTION
This module provides some functions to create status filters.
A status filter is a subroutine reference to process an array-ref of statuses.
lib/BusyBird/Filter/Twitter.pm view on Meta::CPAN
sub filter_twitter_search_status {
return $FILTER_SEARCH;
}
sub trans_twitter_status_id {
my ($status, $api_url) = @_;
$api_url = "https://api.twitter.com/1.1/" if not defined $api_url;
$api_url =~ s|/+$||;
foreach my $key (qw(id id_str in_reply_to_status_id in_reply_to_status_id_str)) {
next if not defined $status->{$key};
if(vivifiable_as($status->{busybird}, "HASH")
&& vivifiable_as($status->{busybird}{original}, "HASH")) {
$status->{busybird}{original}{$key} = $status->{$key};
}
$status->{$key} = "$api_url/statuses/show/" . $status->{$key} . ".json";
}
return $status;
}
sub filter_twitter_status_id {
my ($api_url) = @_;
return filter_map sub { trans_twitter_status_id($_[0], $api_url) };
}
lib/BusyBird/Filter/Twitter.pm view on Meta::CPAN
Transforms the C<$status> returned by Twitter's Search API v1.0 into something more like a normal status object.
=head2 $status = trans_twitter_status_id($status, [$api_url])
Transforms the C<$status>'s ID fields so that they include API URL of the source.
This transformation is recommended when you load statuses from multiple sources, e.g. twitter.com and loadaverage.org.
Argument C<$api_url> is optional. By default it is C<"https://api.twitter.com/1.1/">.
You should set it appropriately if you import statuses from other sites.
The original IDs are saved under C<< $status->{busybird}{original} >>
=head2 $status = trans_twitter_unescape($status)
Unescapes some HTML entities in the C<$status>'s text field.
HTML-unescape is necessary because twitter.com automatically HTML-escapes some special characters,
AND L<BusyBird> also HTML-escapes status texts when it renders them.
This results in double HTML-escapes.
The transformation changes the status's text length.
lib/BusyBird/Input/Generator.pm view on Meta::CPAN
my $text = defined($args{text}) ? $args{text} : "";
my $level = defined($args{level}) ? $args{level} : 0;
my $cur_time = DateTime->now;
my $status = +{
id => $self->_generate_id(),
text => $text,
created_at => BusyBird::DateTime::Format->format_datetime($cur_time),
user => {
screen_name => $self->{screen_name},
},
busybird => {
status_permalink => ""
}
};
if(defined $level) {
$status->{busybird}{level} = $level + 0;
}
return $status;
}
sub _generate_id {
my ($self) = @_;
my $namespace = $self->{screen_name};
my $uuid = $self->{id_gen}->create_str;
return qq{busybird://$namespace/$uuid};
## $cur_time = DateTime->now if not defined($cur_time);
## my $cur_epoch = $cur_time->epoch;
## if($self->{last_epoch} != $cur_epoch) {
## $self->{next_sequence_number} = 0;
## }
## my $id = qq{busybird://$namespace/$cur_epoch/$self->{next_sequence_number}};
## $self->{next_sequence_number}++;
## $self->{last_epoch} = $cur_epoch;
## return $id;
}
1;
__END__
=pod
lib/BusyBird/Input/Generator.pm view on Meta::CPAN
Fields in C<%args> are:
=over
=item C<text> => STR (optional, default: "")
The C<text> field of the status. It must be a text string, not a binary (octet) string.
=item C<level> => INT (optional, default: 0)
The C<busybird.level> field of the status.
=back
=head1 AUTHOR
Toshio Ito C<< <toshioito [at] cpan.org> >>
=cut
lib/BusyBird/Main/PSGI/View.pm view on Meta::CPAN
$result_text .= _html_link_status_text($url, $url);
$remaining_index = pos($text);
}
$result_text .= html_escape(substr($text, $remaining_index));
return $result_text;
}
sub _format_status_html_destructive {
my ($self, $status, $timeline_name) = @_;
$timeline_name = "" if not defined $timeline_name;
if(ref($status->{retweeted_status}) eq "HASH" && (!defined($status->{busybird}) || ref($status->{busybird}) eq 'HASH')) {
my $retweet = $status->{retweeted_status};
$status->{busybird}{retweeted_by_user} = $status->{user};
foreach my $key (qw(text created_at user entities)) {
$status->{$key} = $retweet->{$key};
}
}
return $self->{renderer}->render(
"status.tx",
{ss => safed($status),
%{$self->template_functions_for_timeline($timeline_name)}}
);
}
lib/BusyBird/Manual/Config.pod view on Meta::CPAN
=pod
=head1 NAME
BusyBird::Manual::Config - how to configure BusyBird
=head1 SYNOPSIS
In your ~/.busybird/config.psgi
use BusyBird;
busybird->set_config(
timeline_list_per_page => 100,
time_zone => "UTC",
time_locale => "en_US",
);
timeline("home")->set_config(
time_zone => "+0900"
);
end;
=head1 DESCRIPTION
=head2 Config File
By default, L<BusyBird> reads the file C<~/.busybird/config.psgi> for configuration.
If it doesn't exist, L<busybird> command automatically generates it.
You can use an arbitrary config file by specifying it to L<busybird> command.
$ busybird another_config.psgi
The configuration file is just a Perl script (actually it's L<PSGI> application script).
Its basic structure is:
use BusyBird;
## Your configuration here
end;
Leave every statement L<busybird> command generated unless you know what you're doing.
=head2 Create/Get Timelines
To create a timeline, call C<timeline()> function.
timeline("timeline A");
timeline("timeline B");
C<timeline()> function returns a L<BusyBird::Timeline> object.
lib/BusyBird/Manual/Config.pod view on Meta::CPAN
my $timeline = timeline("timeline A");
$timeline->add({text => "Hello."});
Timeline names are unique.
If you call C<timeline()> the second time with the same timeline name,
it just returns the existing timeline.
=head2 Global and Per-Timeline Configurations
Global config parameters are set by C<< busybird->set_config() >> method.
busybird->set_config(time_zone => "UTC");
A global config parameter affects all timelines and the overall
behavior of the BusyBird instance.
Per-timeline config parameters are set by C<< timeline(...)->set_config() >> method.
busybird->set_config(time_zone => "UTC");
timeline("foobar")->set_config(time_zone => "+0900");
Per-timeline config parameters always take precedence over global ones.
Any per-timeline config parameter can be set as a global config parameter.
=head2 Under the Hood
Config parameters are stored in L<BusyBird::Main> and L<BusyBird::Timeline> objects.
The keyword C<busybird> is a function that returns the singleton object of L<BusyBird::Main> class.
See L<BusyBird/AS A MODULE> for detail.
When L<BusyBird> needs some config parameters, it reads them by calling C<get_config()> and C<get_timeline_config()> methods
on L<BusyBird::Main>.
=head1 GLOBAL CONFIG PARAMETERS
Below is the complete list of the global config parameters.
Internally, those config parameters are accepted by L<BusyBird::Main>.
=head2 C<default_status_storage> => BusyBird::StatusStorage OBJECT
B<Default:> L<BusyBird::StatusStorage::SQLite> object at C<~/.busybird/statuses.sqlite3>
A StatusStorage object used for Timelines by default.
A StatusStorage is an object where timelines save their statuses.
When a timeline is created by L<BusyBird::Main>'s C<timeline()> method, the default StatusStorage is used for the timeline.
Note that the default StatusStorage object is referred to only when creating timelines via C<timeline()> method.
Existing timelines are not affected by changing the default StatusStorage object.
A StatusStorage object is an implementation of L<BusyBird::StatusStorage> interface.
lib/BusyBird/Manual/Config.pod view on Meta::CPAN
C<LOCALE_STR> is a valid locale string such as C<"en_US">, C<"ja_JP"> etc.
=head2 C<post_button_url> => URL_STR
B<Default:> C<"https://twitter.com/intent/tweet">
Link URL attached to the "Post" button in the navigation bar.
=head2 C<status_permalink_builder> => CODEREF($status)
B<Default:> return C<< $status->{busybird}{status_permalink} >>, or build permalink to the status page of twitter.com, or return C<undef>
Subroutine reference that is supposed to build permalink URL to the status.
The builder subroutine reference is called with the status object, as in:
$url_str = $builder->($status)
C<$url_str> is a string.
If the result C<$url_str> is C<undef> or does not look like a valid URL, it is ignored.
lib/BusyBird/Manual/Config.pod view on Meta::CPAN
The default level threshold for the timeline.
=head1 EXAMPLES
=head2 Customize Timestamps
Status timestamps are rendered using C<time_zone>, C<time_format>, C<time_locale> parameters.
By default L<BusyBird> guesses the "correct" config for your system.
busybird->set_config(
time_zone => 'America/Los_Angeles',
time_format => '%Y-%m-%d %A %H:%M:%S %Z',
time_locale => 'en_US',
);
=head2 Expand URLs for Links in Statuses
By default L<BusyBird> renders Twitter Entities in status objects just like Twitter does,
but you can customize this behavior by changing C<*_entity_url_builder> and C<*_entity_text_builder> parameters.
For example, Twitter truncates the displayed text for C<urls> and C<media> entites.
If you want to see fully expanded URLs in statuses, do the following.
my $builder_expanded_url = sub {
my ($text, $entity) = @_;
return $entity->{expanded_url};
};
busybird->set_config(
urls_entity_text_builder => $builder_expanded_url,
media_entity_text_builder => $builder_expanded_url,
);
=head2 Storage File Location
By default, L<BusyBird> stores statuses in the file C<~/.busybird/statuses.sqlite3>.
To change the storage file location, change C<default_status_storage> global config parameter.
use BusyBird::StatusStorage::SQLite;
busybird->set_config(
default_status_storage => BusyBird::StatusStorage::SQLite->new(
path => "$ENV{HOME}/.busybird/another_statuses.sqlite3"
)
);
The example uses C<~/.busybird/another_statuses.sqlite3> for the status storage.
See L<BusyBird::StatusStorage::SQLite> for detail.
Note that you should change C<default_status_storage> parameter B<< before you call C<timeline()> function. >>
=head1 SEE ALSO
=over
=item L<BusyBird::Manual::Config::Advanced>
lib/BusyBird/Manual/Config/Advanced.pod view on Meta::CPAN
=pod
=head1 NAME
BusyBird::Manual::Config::Advanced - advanced topics about configuring BusyBird
=head1 DESCRIPTION
=head2 Use plackup to Start BusyBird
L<BusyBird> configuration file C<~/.busybird/config.psgi> is just a L<PSGI> application script,
so you can directly use L<plackup> command to start L<BusyBird>.
$ plackup -s Twiggy ~/.busybird/config.psgi
L<BusyBird> needs a L<PSGI> server that supports the non-blocking "delayed response" feature.
We recommend to use L<Twiggy>.
In fact, L<busybird> command is a simple front-end for L<Plack::Runner>.
L<plackup> command accepts more options than L<busybird> command.
=head2 Plack Middlewares
Because C<~/.busybird/config.psgi> is just a L<PSGI> application script,
you can use any L<Plack> middlewares as you like.
To use L<Plack::Builder> with L<BusyBird>,
enclose the C<end> statement at the bottom with C<builder> block.
use BusyBird;
use Plack::Builder;
Plack::Builder::builder {
Plack::Builder::enable "AccessLog", format => '%h %l %u %t "%r" %>s %b %{X-Runtime}o';
Plack::Builder::enable "Runtime";
end;
};
C<end> statement returns the L<PSGI> application of L<BusyBird>.
=head2 Multiple BusyBird Instances
You can set up multiple L<BusyBird> instances in a single L<PSGI> application.
To do that, you have to use L<BusyBird::Main> objects directly,
because C<busybird>, C<timeline> and C<end> functions from L<BusyBird> module
operate only on the singleton instance.
Here is an example of the complete C<~/.busybird/config.psgi> file.
use strict;
use warnings;
use utf8;
use BusyBird::Main;
use BusyBird::Main::PSGI qw(create_psgi_app);
use Plack::Builder;
my @busybird = (
BusyBird::Main->new,
BusyBird::Main->new,
);
$busybird[0]->set_config(
time_zone => "+0900"
);
$busybird[0]->timeline("home");
$busybird[1]->set_config(
time_zone => "UTC"
);
$busybird[1]->timeline("another_home");
builder {
enable "AccessLog";
mount "/busybird0" => create_psgi_app($busybird[0]);
mount "/busybird1" => create_psgi_app($busybird[1]);
};
See L<BusyBird>, L<BusyBird::Main> and L<BusyBird::Main::PSGI> for detail.
=head2 Customize Logging
Sometimes L<BusyBird> components write log messages when it's necessary.
By default the log messages are printed to STDERR, but you can customize this behavior
by setting C<$BusyBird::Log::Logger> variable in C<~/.busybird/config.psgi>.
use BusyBird::Log;
use Log::Dispatch;
my $log = Log::Dispatch->new(
outputs => [
[
'Syslog',
min_level => 'info',
ident => 'BusyBird'
lib/BusyBird/Manual/Config/Advanced.pod view on Meta::CPAN
$BusyBird::Log::Logger = sub {
my ($level, $msg) = @_;
$log->log(level => $level, message => $msg);
};
See L<BusyBird::Log> for detail.
=head2 Customize User Interface Completely
C<~/.busybird/config.psgi> let you configure various aspects of L<BusyBird>,
but you might want to customize its user interface completely.
To do that, set C<sharedir_path> global config parameter.
C<sharedir_path> is the path to the directory containing
static files for L<BusyBird>, including HTML templates, JavaScript files and themes.
B<< WARNING: Customizing "share" directory is only for testing purposes. The directory's content may be changed drastically in future releases. >>
To customize user interface, follow the steps below.
lib/BusyBird/Manual/Config/Advanced.pod view on Meta::CPAN
The example below assumes that you installed it under C</usr/local>.
$ cp -a /usr/local/share/perl/5.14.2/auto/share/dist/BusyBird ~/my_sharedir
=item 2.
Change the content of C<~/my_sharedir> as you like.
=item 3.
Set C<sharedir_path> parameter in C<~/.busybird/config.psgi>.
busybird->set_config(
sharedir_path => "$ENV{HOME}/my_sharedir"
);
=back
=head1 AUTHOR
Toshio Ito C<< <toshioito [at] cpan.org> >>
lib/BusyBird/Manual/Status.pod view on Meta::CPAN
my $status = decode_json(<<'STATUS');
{
"id": "http://api.example.com/2291",
"created_at": "Thu Jan 03 02:24:43 +0000 2013",
"text": "sample status",
"user": {
"screen_name": "debug_ito",
"profile_image_url": "http://img.example.com/user/debug_ito.png",
"name": "Toshio Ito"
},
"busybird": {
"level": 0,
"acked_at": "Thu Jan 03 14:44:12 +0900 2013",
"status_permalink" : "http://example.com/status/2291",
"user_permalink": "http://example.com/user/debug_ito"
}
}
STATUS
=head1 DESCRIPTION
lib/BusyBird/Manual/Status.pod view on Meta::CPAN
A status object is just a hash reference (or an Object in JSON format). It should be serializable to
JSON and deserializable from JSON.
=head1 FIELDS
The following fields in a status object is used by BusyBird.
Note that the following list uses JSON for key notation.
For example, C<busybird.acked_at> field is C<< $status->{busybird}{acked_at} >> in Perl
(NOT C<< $status->{"busybird.acked_at"} >>).
Status object can have fields that are not listed in this page.
L<BusyBird> tries to keep such fields untouched.
=head2 C<busybird.acked_at>
The timestamp string at which the status is acked.
If this field does not exist or it's C<null>, the status is unacked.
The timestamp string must be parsable by L<BusyBird::DateTime::Format>.
=head2 C<busybird.level>
The level of the status. Level must be an integer. It may be positive or negative.
If not set, it is considered as 0.
=head2 C<busybird.original.id>
The original ID of the status.
You should set this field when you somehow convert status ID.
ID conversion is necessary when you import statuses from multiple sources with their own ID spaces,
and you want to avoid ID conflict between them.
L<BusyBird::Filter::Twitter> does such conversion, for example.
If this field is set, L<BusyBird> uses this field if necessary, e.g., when it builds permalink to the status.
=head2 C<busybird.status_permalink>
If set, this string is used for the permalink URL of the status.
To further customize status permalinks, see C<status_permalink_builder> of L<BusyBird::Manual::Config>.
=head2 C<busybird.user_permalink>
B<Experimental>.
If set, this string is used for the permalink URL for the user.
=head2 C<created_at>
The timestamp string at which the status is created.
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
$ export PERL5LIB="$HOME/perl5/lib/perl5:$PERL5LIB"
$ export PATH="$HOME/perl5/bin:$PATH"
These are necessary for perl to find the modules installed in C<~/perl5> directory.
Be sure to write them in C<~/.profile>, too.
=head1 Start BusyBird
After installing L<BusyBird> successfully, type
$ busybird
Twiggy: Accepting connections at http://127.0.0.1:5000/
Then, access the URL ( http://127.0.0.1:5000/ ) by your Web browser.
If you can see the top page, congraturations! L<BusyBird> has successfully started.
Note that if you already use the TCP port 5000, C<busybird> command fails with "Address already in use" error.
In that case, try another port by C<-p> option.
$ busybird -p 4444
You can see complete list of options by C<-h> option.
$ busybird -h
=head1 Input Statuses
By default, your L<BusyBird> instance has a timeline called "home", but the timeline has no status yet.
It won't magically import statuses out of nowhere.
You must input statuses to it.
To input statuses, you can use L<BusyBird>'s Web API.
$ curl -d '{"text":"hello, world!"}' http://127.0.0.1:5000/timelines/home/statuses.json
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
=over
=item 1.
Install another module L<BusyBird::Input::Feed>.
$ cpanm BusyBird::Input::Feed
=item 2.
Then, run L<busybird_input_feed> command bundled with the module.
$ busybird_input_feed https://metacpan.org/feed/recent -p http://127.0.0.1:5000/timelines/home/statuses.json
=back
After that, you can see the imported feed items (in this case, Perl modules recently uploaded) on L<BusyBird>.
Try repeating the command above.
You will see that L<BusyBird> only accepts the new statuses that are not yet in L<BusyBird>'s timeline.
In a L<BusyBird> timeline, all statuses must have unique C<id> field.
If you input a status that is already in the timeline, that status is ignored.
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
consumer_key => "API_KEY",
consumer_secret => "API_SECRET",
access_token => "ACCESS_TOKEN",
access_token_secret => "ACCESS_TOKEN_SECRET",
ssl => 1,
);
my $home = $nt->home_timeline;
my $mentions = $nt->mentions;
foreach my $s (@$mentions) {
$s->{busybird}{level} = 1;
}
print encode_json [@$mentions, @$home];
The above script imports "home timeline" and "mentions timeline".
The C<busybird.level> field of "mentions" statuses is set to 1.
Then, it outputs both kinds of statuses to STDOUT.
Like we did above, mixing multiple Twitter timelines into a single L<BusyBird> timeline is OK.
L<BusyBird> sorts those statuses and renders them in chronological order.
Let's save the above script as "import2.pl" and run it.
$ perl import2.pl | curl -d @- http://127.0.0.1:5000/timelines/home/statuses.json
If you have ever been mentioned by someone, you'll see the statuses metioning you with the status level 1.
Change the B<< level threshold >> by the buttons at the top-right corner.
If you set to the threshold to "Lv. 1", it shows the "mention" statuses only, and hides everything else.
That way, you can quickly review the mentions and reply to them.
You can use arbitrary integer values for the status level (C<busybird.level> field), including negative values.
=head1 Configuration
So far, we use L<BusyBird> with its default configuration,
but you can customize its behavior by writing a configuration file.
L<BusyBird> configuration file is B<~/.busybird/config.psgi>.
By default, it looks like:
use BusyBird;
timeline("home");
end;
config.psgi is a Perl script, so you can write arbitrary Perl codes into it.
However, here is the basic rule:
B<< you must write your config between "use BusyBird;" and "end;" statements. >>
Follow this rule unless you know what you are doing.
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
timeline("foobar");
timeline("foobar");
it creates only one timeline named "foobar".
=head2 Configuration Parameters
You can set various config parameters by C<set_config()> method.
To set a B<< global config parameter, >> use C<< busybird->set_config(...) >>.
busybird->set_config(time_zone => "UTC");
A global config parameter affects all timelines and the overall behavior of the L<BusyBird> instance.
In the above example, status timestamps in all timelines are rendered in UTC time zone.
C<set_config()> method accepts more than one key-value pairs.
busybird->set_config(
time_zone => "+0900",
time_locale => "ja_JP"
);
Some parameters are B<< per-timeline config parameters, >>
which can be set to individual timelines.
To set a per-timeline parameter, use C<< timeline(...)->set_config(...) >>.
busybird->set_config(time_zone => "UTC");
timeline("foobar")->set_config(time_zone => "+0900");
Per-timeline config parameters always take precedence over global ones.
So, in the above example, statuses are renderred in "+0900" time zone only in the timeline "foobar".
In other timelines, the time zone is "UTC".
See L<BusyBird::Manual::Config> for the complete list of config parameters.
=head1 Filters
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
[ Statuses ] --HTTP POST--> [ Filter 1 ] --> [ Filter 2 ] --> ... --> [ Timeline ]
By default, timelines have no filters. Statuses are directly input to them.
To add a status filter to a timeline, use C<add_filter()> method.
timeline("home")->add_filter(sub {
my ($statuses) = @_;
foreach my $status (@$statuses) {
if($status->{text} =~ /\@my_name/) {
$status->{busybird}{level}++;
}
}
return $statuses;
});
A status filter is just a subroutine reference in Perl.
It is called like
$result_arrayref = $filter->($arrayref_of_statuses)
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
If it finds your screen name ("@my_name"), it increments the status's level, meaning that it's important.
Then the filter returns the array of modified statuses.
A timeline can have more than one filters.
Those filters are executed in the order they are added, and the output of one filter becomes the input of the next filter.
timeline("home")->add_filter(sub {
my ($statuses) = @_;
foreach my $status (@$statuses) {
if($status->{text} =~ /\@my_name/) {
$status->{busybird}{level}++;
}
}
return $statuses;
});
timeline("home")->add_filter(sub {
my ($statuses) = @_;
return [grep { $_->{busybird}{level} > 4 } @$statuses];
});
In the above example, the second filter extracts statuses whose level is higher than 4.
Inspecting statuses one by one is a typical pattern in status filters.
L<BusyBird::Filter> defines some functions useful for that purpose.
See L<BusyBird::Timeline> and L<BusyBird::Filter> for more about status filters.
=head2 Pre-defined Filter for Twitter
lib/BusyBird/Manual/Tutorial.pod view on Meta::CPAN
=item L<BusyBird::Manual::Config>
Full list of configuration items.
=item L<BusyBird::Filter>
Functions useful when writing status filters.
=item L<BusyBird::Main>
The class of the object returned by C<busybird> keyword in config.psgi.
=item L<BusyBird::Timeline>
The class of the object returned by C<timeline(...)> keyword in config.psgi.
It has various methods to manipuate statuses in it.
=back
=head1 AUTHOR
lib/BusyBird/Manual/WebAPI.pod view on Meta::CPAN
POST /timelines/home/statuses.json
Request Body:
[
{
"id": "http://example.com/page/2013/0204",
"created_at": "Mon Feb 04 11:02:45 +0900 2013",
"text": "content of the status",
"busybird": { "level": 3 }
},
{
"id": "http://example.com/page/2013/0202",
"created_at": "Sat Feb 02 17:38:12 +0900 2013",
"text": "another content"
}
]
Response Body:
lib/BusyBird/Runner.pm view on Meta::CPAN
#!/usr/bin/perl
use strict;
use warnings;
use BusyBird::Runner qw(run);
run(@ARGV);
=head1 DESCRIPTION
L<BusyBird::Runner> runs L<BusyBird> process instance.
This is the direct back-end of C<busybird> command.
=head1 EXPORTABLE FUNCTIONS
The following functions are exported only by request.
=head2 $need_help = run(@argv)
Runs the L<BusyBird> process instance.
C<@argv> is the command-line arguments. See L<busybird> for detail.
Return value C<$need_help> indicates if the user might need some help.
If C<$need_help> is non-C<undef>, the caller should provide the user with some help.
=head1 AUTHOR
Toshio Ito C<< <toshioito [at] cpan.org> >>
=cut
lib/BusyBird/StatusStorage.pm view on Meta::CPAN
=item 3.
If some statuses given to C<put_statuses()> method do not have their C<id> fields,
the method may either throw an exception or automatically generate IDs for them and proceed.
=back
=head2 acked_at Field
C<ack_statuses()> method should update C<< $status->{busybird}{acked_at} >> field
of the target statuses to the date/time string of the current time.
The date/time format should be parsable by L<BusyBird::DateTime::Format> class.
=head2 Order of Statuses
In timelines, statuses should be sorted in descending order of
C<< $status->{busybird}{acked_at} >> field
(interpreted as date/time).
Unacked statuses should always be above acked statuses.
Ties are broken by sorting the statuses
in descending order of C<< $status->{created_at} >>
field (interpreted as date/time).
So the top of timeline is the latest created unacked status.
Below unacked statuses are layers of acked statuses.
The top of the acked statuses is the latest created status in the latest
acked ones.
lib/BusyBird/StatusStorage/Common.pm view on Meta::CPAN
max_id => $max_id, count => 'all',
ack_state => 'unacked'
);
}
Future::Q->needs_all(@subfutures)->then(sub {
my @statuses_list = @_;
my @target_statuses = _uniq_statuses(map { @$_ } @statuses_list);
if(!@target_statuses) {
return 0;
}
$_->{busybird}{acked_at} = $ack_str foreach @target_statuses;
return future_of(
$self, 'put_statuses',
timeline => $timeline, mode => 'update',
statuses => \@target_statuses,
);
})->then(sub {
## invocations of $callback should be at the same level of
## then() chain, because $callback might throw exception and
## we should not catch that exception.
lib/BusyBird/StatusStorage/Common.pm view on Meta::CPAN
callback => sub {
my ($error, $statuses) = @_;
if(defined($error)) {
@_ = ("get error: $error");
goto $callback;
}
my %count = (total => int(@$statuses));
foreach my $status (@$statuses) {
my $level = do {
no autovivification;
$status->{busybird}{level} || 0;
};
$count{$level}++;
}
@_ = (undef, \%count);
goto $callback;
}
);
}
lib/BusyBird/StatusStorage/Memory.pm view on Meta::CPAN
return -1 if not defined($self->{timelines}{$timeline});
my $tl = $self->{timelines}{$timeline};
my @ret = grep { $tl->[$_]{id} eq $id } 0..$#$tl;
confess "multiple IDs in timeline $timeline." if int(@ret) >= 2;
return int(@ret) == 0 ? -1 : $ret[0];
}
sub _acked {
my ($self, $status) = @_;
no autovivification;
return $status->{busybird}{acked_at};
}
sub save {
my ($self, $filepath) = @_;
if(not defined($filepath)) {
croak '$filepath is not specified.';
}
my $file;
if(!open $file, ">", $filepath) {
$self->_log("error", "Cannot open $filepath to write.");
lib/BusyBird/StatusStorage/Memory.pm view on Meta::CPAN
}elsif(ref($args{statuses}) eq 'HASH') {
$statuses = [ $args{statuses} ];
}elsif(ref($args{statuses}) eq 'ARRAY') {
$statuses = $args{statuses};
}else {
croak 'statuses arg must be STATUS/ARRAYREF_OF_STATUSES';
}
foreach my $s (@$statuses) {
no autovivification;
croak "{id} field is mandatory in statuses" if not defined $s->{id};
croak "{busybird} field must be a hash-ref if present" if defined($s->{busybird}) && ref($s->{busybird}) ne "HASH";
croak "{created_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($s->{created_at});
my $acked_at = $s->{busybird}{acked_at}; ## avoid autovivification
croak "{busybird}{acked_at} field must be parsable by BusyBird::DateTime::Format" if !_is_timestamp_format_ok($acked_at);
}
my $put_count = 0;
foreach my $status_index (reverse 0 .. $#$statuses) {
my $s = $statuses->[$status_index];
my $tl_index = $self->_index($timeline, $s->{id});
my $existent = ($tl_index >= 0);
next if ($mode eq 'insert' && $existent) || ($mode eq 'update' && !$existent);
my $is_insert = ($mode eq 'insert');
if($mode eq 'upsert') {
$is_insert = (!$existent);
lib/BusyBird/StatusStorage/SQLite.pm view on Meta::CPAN
return $self->_get_timeline_id($dbh, $timeline_name);
}
sub _to_status_record {
my ($timeline_id, $status) = @_;
croak "status ID must be set" if not defined $status->{id};
croak "timeline_id must be defined" if not defined $timeline_id;
my $record = {
timeline_id => $timeline_id,
status_id => $status->{id},
level => $status->{busybird}{level} || 0,
};
my $acked_at = $status->{busybird}{acked_at}; ## avoid autovivification
($record->{utc_acked_at}, $record->{timezone_acked_at}) = _extract_utc_timestamp_and_timezone($acked_at);
($record->{utc_created_at}, $record->{timezone_created_at}) = _extract_utc_timestamp_and_timezone($status->{created_at});
$record->{content} = to_json($status);
return $record;
}
sub _from_status_record {
my ($record) = @_;
my $status = from_json($record->{content});
$status->{id} = $record->{status_id};
if($record->{level} != 0 || defined($status->{busybird}{level})) {
$status->{busybird}{level} = $record->{level};
}
my $acked_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_acked_at}, $record->{timezone_acked_at});
if(defined($acked_at_str) || defined($status->{busybird}{acked_at})) {
$status->{busybird}{acked_at} = $acked_at_str;
}
my $created_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_created_at}, $record->{timezone_created_at});
if(defined($created_at_str) || defined($status->{created_at})) {
$status->{created_at} = $created_at_str;
}
return $status;
}
sub _extract_utc_timestamp_and_timezone {
my ($timestamp_str) = @_;
lib/BusyBird/StatusStorage/SQLite.pm view on Meta::CPAN
=head1 SYNOPSIS
use BusyBird;
use BusyBird::StatusStorage::SQLite;
my $storage = BusyBird::StatusStorage::SQLite->new(
path => 'path/to/storage.sqlite3',
max_status_num => 5000
);
busybird->set_config(
default_status_storage => $storage
);
=head1 DESCRIPTION
This is an implementation of L<BusyBird::StatusStorage> interface.
It stores statuses in an SQLite database.
This storage is synchronous, i.e., all operations block the thread
and the callback is called before the method returns.
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
sub status {
my ($id, $level, $acked_at) = @_;
croak "you must specify id" if not defined $id;
my $status = {
id => $id,
created_at => $datetime_formatter->format_datetime(
DateTime->from_epoch(epoch => $id)
),
};
$status->{busybird}{level} = $level if defined $level;
$status->{busybird}{acked_at} = $acked_at if defined $acked_at;
return $status;
}
sub nowstring {
return $datetime_formatter->format_datetime(
DateTime->now(time_zone => 'UTC')
);
}
sub add_datetime_days {
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
}
sub id_list {
my @statuses_or_ids = @_;
return map { ref($_) ? $_->{id} : $_ } @statuses_or_ids;
}
sub acked {
my ($s) = @_;
no autovivification;
return $s->{busybird}{acked_at};
}
sub test_status_id_set {
## unordered status ID set test
my ($got_statuses, $exp_statuses_or_ids, $msg) = @_;
local $Test::Builder::Level = $Test::Builder::Level + 1;
return is_deeply(
{ id_counts @$got_statuses },
{ id_counts @$exp_statuses_or_ids },
$msg
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
$callbacked = 0;
$storage->get_statuses(
timeline => '_test_tl1',
count => 'all',
callback => sub {
my ($error, $statuses) = @_;
is($error, undef, "get_statuses succeed");
test_status_id_set($statuses, [1..5], "1..5 statuses");
foreach my $s (@$statuses) {
no autovivification;
ok(!$s->{busybird}{acked_at}, "status is not acked");
}
$callbacked = 1;
$unloop->();
}
);
$loop->();
ok($callbacked, "callbacked");
note('--- ack_statuses: all');
$callbacked = 0;
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
{ total => 0 },
"all acked"
);
on_statuses $storage, $loop, $unloop, {
timeline => '_test_tl1', count => 'all'
}, sub {
my $statuses = shift;
is(int(@$statuses), 5, "5 statueses");
foreach my $s (@$statuses) {
no autovivification;
ok($s->{busybird}{acked_at}, 'acked');
}
};
note('--- delete_statuses (single deletion)');
$callbacked = 0;
$storage->delete_statuses(
timeline => '_test_tl1',
ids => 3,
callback => sub {
my ($error, $num) = @_;
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
change_and_check(
$storage, $loop, $unloop, timeline => '_test_tl1',
mode => 'upsert', target => [map { status($_, 7, nowstring()) } (4..7)],
exp_change => 4, exp_unacked => [2,3], exp_acked => [1,4..7]
);
note('--- get and put(update): back to unacked');
on_statuses $storage, $loop, $unloop, {
timeline => '_test_tl1', count => 'all', ack_state => 'acked'
}, sub {
my $statuses = shift;
delete $_->{busybird}{acked_at} foreach @$statuses;
change_and_check(
$storage, $loop, $unloop, timeline => '_test_tl1',
mode => 'update', target => $statuses,
exp_change => 5, exp_unacked => [1..7], exp_acked => []
);
};
is_deeply(
{sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1')},
{total => 7, 1 => 1, 2 => 2, 7 => 4}, "3 levels"
);
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
exp_change => 1, exp_acked => ["ãã¡"], exp_unacked => [0]
);
foreach my $status (@statuses) {
my $got_statuses = sync_get(
$storage, $loop, $unloop,
timeline => $timeline_name, count => 1, max_id => $status->{id}
);
test_status_id_list($got_statuses, [$status->{id}], encode_utf8("status ID $status->{id} OK"));
is($got_statuses->[0]{text}, $status->{text}, encode_utf8("status text '$status->{text}' OK"));
if($got_statuses->[0]{id} eq "0") {
ok(!$got_statuses->[0]{busybird}{acked_at}, "status 0 is not acked");
}else {
ok($got_statuses->[0]{busybird}{acked_at}, "status 1 is acked");
}
}
$statuses[1]{busybird}{level} = 5;
change_and_check(
$storage, $loop, $unloop, timeline => $timeline_name, mode => "update", target => $statuses[1],
exp_change => 1, exp_acked => [], exp_unacked => [0, "ãã¡"]
);
$storage->get_unacked_counts(timeline => $timeline_name, callback => sub {
my ($e, $unacked_counts) = @_;
is($e, undef, "get unacked counts succeed");
is_deeply($unacked_counts, {total => 2, 0 => 1, 5 => 1}, "unacked counts OK");
$unloop->();
});
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
acked_at => undef},
{label => "only acked_at set", created_at => undef,
acked_at => "Wed Apr 17 04:23:29 -0500 2013"},
{label => 'both set', created_at => 'Fri Oct 12 00:36:44 +0000 2012',
acked_at => 'Thu Oct 25 13:10:00 +0200 2012'},
) {
note("--- -- case: $case->{label}");
$callbacked = 0;
my $status = status(1);
$status->{created_at} = $case->{created_at};
$status->{busybird}{acked_at} = $case->{acked_at};
$storage->put_statuses(
timeline => "_test_tl3", mode => 'insert', statuses => $status,
callback => sub {
my ($error, $count) = @_;
is($error, undef, "put succeed");
is($count, 1, "1 inserted");
$callbacked = 1;
$unloop->();
}
);
$loop->();
ok($callbacked, "callbacked");
on_statuses $storage, $loop, $unloop, {timeline => '_test_tl3', count => 'all'}, sub {
my $statuses = shift;
is(scalar(@$statuses), 1, "1 status obtained");
is($statuses->[0]{created_at}, $case->{created_at}, "created_at is preserved");
is($statuses->[0]{busybird}{acked_at}, $case->{acked_at}, "acked_at is preserved");
};
change_and_check(
$storage, $loop, $unloop, timeline => '_test_tl3',
mode => 'delete', target => undef, exp_change => 1,
exp_unacked => [], exp_acked => []
);
}
note('--- populate timeline');
change_and_check(
$storage, $loop, $unloop, timeline => '_test_tl3',
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
{%base, ack_state => 'any', count => 'all'},
[reverse(71..80, 1..30, 61..70, 31..60)],
'get: mixed acked_at, no max_id, any state, all'
);
note('--- move from acked to unacked');
on_statuses $storage, $loop, $unloop, {
timeline => '_test_tl3', acked_state => 'acked',
max_id => 30, count => 10
}, sub {
my $statuses = shift;
delete $_->{busybird}{acked_at} foreach @$statuses;
change_and_check(
$storage, $loop, $unloop, timeline => '_test_tl3',
mode => 'update', target => $statuses,
exp_change => 10,
exp_unacked => [21..60], exp_acked => [1..20, 61..80]
);
};
get_and_check_list(
$storage, $loop, $unloop,
{%base, ack_state => 'any', count => 'all'},
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
'sorted by descending order of created_at within acked_at group'
);
note('--- -- ack test');
note('--- change acked_at for testing');
on_statuses $storage, $loop, $unloop, {
%base, count => 'all', ack_state => 'acked'
}, sub {
my $statuses = shift;
foreach my $s (@$statuses) {
$s->{busybird}{acked_at} =
add_datetime_days($s->{busybird}{acked_at}, +2);
}
change_and_check(
$storage, $loop, $unloop, %base, mode => 'update',
target => $statuses, exp_change => 40,
exp_unacked => [21..60], exp_acked => [61..70, 1..20, 71..80]
);
};
change_and_check(
$storage, $loop, $unloop, %base, mode => 'ack', target => 51,
exp_change => 10, exp_unacked => [21..50], exp_acked => [61..70, 1..20, 71..80, 51..60]
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
get_and_check_list(
$storage, $loop, $unloop, {%base4, count => 'all', ack_state => 'unacked'},
[reverse 26..30, 36..45], '15 unacked statuses'
);
note('--- For testing, set acked_at sufficiently old.');
on_statuses $storage, $loop, $unloop, {
%base4, count => 'all', ack_state => 'acked'
}, sub {
my $statuses = shift;
foreach my $s (@$statuses) {
$s->{busybird}{acked_at} = add_datetime_days($s->{busybird}{acked_at}, -1);
}
change_and_check(
$storage, $loop, $unloop, %base4, mode => 'update', target => $statuses,
exp_change => 5, exp_unacked => [26..30, 36..45], exp_acked => [31..35]
);
};
change_and_check(
$storage, $loop, $unloop, %base4, mode => 'ack', target => 40, exp_change => 10,
exp_unacked => [41..45], exp_acked => [36..40, 26..30, 31..35]
);
lib/BusyBird/Test/StatusStorage.pm view on Meta::CPAN
change_and_check(
$storage, $loop, $unloop, %base,
mode => 'insert', target => [map {status($_)} 1..$hard_max],
exp_change => $hard_max, exp_unacked => [1..$hard_max], exp_acked => []
);
note('--- ack the top status');
on_statuses $storage, $loop, $unloop, {
%base, count => 1, max_id => $hard_max
}, sub {
my ($statuses) = @_;
$statuses->[0]{busybird}{acked_at} = nowstring();
change_and_check(
$storage, $loop, $unloop, %base,
mode => 'update', target => $statuses,
exp_change => 1, exp_unacked => [1..($hard_max-1)],
exp_acked => [$hard_max]
);
};
note('--- inserting another one removes the acked status');
change_and_check(
$storage, $loop, $unloop, %base,
lib/BusyBird/Timeline.pm view on Meta::CPAN
croak "statuses argument must be a status or an array-ref of statuses";
}
my $statuses = dclone($args{statuses});
my $final_callback = $args{callback};
$self->{filter_flow}->execute($statuses, sub {
my $filter_result = shift;
my $cur_time;
foreach my $status (@$filter_result) {
next if !defined($status) || ref($status) ne 'HASH';
if(!defined($status->{id})) {
$status->{id} = sprintf('busybird://%s/%s', $self->name, $self->{id_generator}->create_str);
}
if(!defined($status->{created_at})) {
$cur_time ||= DateTime->now;
$status->{created_at} = BusyBird::DateTime::Format->format_datetime($cur_time);
}
}
$self->_write_statuses('put_statuses', {
mode => 'insert', statuses => $filter_result,
callback => $final_callback
});
lib/BusyBird/Timeline.pm view on Meta::CPAN
## Change acked statuses into unacked.
$timeline->get_statuses(
ack_state => 'acked', count => 10,
callback => sub {
my ($error, $statuses) = @_;
if($error) {
warn("error: $error");
return;
}
foreach my $s (@$statuses) {
$s->{busybird}{acked_at} = undef;
}
$timeline->put_statuses(
mode => "update", statuses => $statuses,
callback => sub {
my ($error, $num) = @_;
if($error) {
warn("error: $error");
return;
}
print "Updated $num statuses.\n";
lib/BusyBird/Timeline.pm view on Meta::CPAN
In failure, C<$error> is a truthy value describing the error.
=back
=head2 $timeline->ack_statuses(%args)
Acknowledges statuses in the C<$timeline>, that is, changing 'unacked' statuses into 'acked'.
Acked status is a status whose C<< $status->{busybird}{acked_at} >> field evaluates to true.
Otherwise, the status is unacked.
Fields in C<%args> are as follows.
=over
=item C<ids> => {ID, ARRAYREF_OF_IDS} (optional, default: C<undef>)
Specifies the IDs of the statuses to be acked.
lib/BusyBird/Timeline.pm view on Meta::CPAN
Fields in C<%$unacked_counts> are as follows.
=over
=item LEVEL => COUNT_OF_UNACKED_STATUSES_IN_THE_LEVEL
LEVEL is an integer key that represents the status level.
The value is the number of unacked statuses in the level.
A status's level is the C<< $status->{busybird}{level} >> field.
See L<BusyBird::Manual::Status> for detail.
LEVEL key-value pair is present for each level in which
there are some unacked statuses.
=item C<total> => COUNT_OF_ALL_UNACKED_STATUSES
The key C<"total"> represents the total number of unacked statuses
in the C<$timeline>.
lib/BusyBird/Util.pm view on Meta::CPAN
@result = @$param;
}elsif($refparam eq 'HASH') {
@result = @{$param}{@names};
}else {
$result[0] = $param;
}
return wantarray ? @result : $result[0];
}
sub config_directory {
return File::Spec->catfile(File::HomeDir->my_home, ".busybird");
}
sub config_file_path {
my (@paths) = @_;
return File::Spec->catfile(config_directory, @paths);
}
sub vivifiable_as {
return !defined($_[0]) || ref($_[0]) eq $_[1];
}
lib/BusyBird/Util.pm view on Meta::CPAN
}
sub sort_statuses {
my ($statuses) = @_;
use sort 'stable';
my @dt_statuses = map {
my $safe_status = safed($_);
[
$_,
_epoch_undef($safe_status->val("busybird", "acked_at")),
_epoch_undef($safe_status->val("created_at")),
];
} @$statuses;
return [ map { $_->[0] } sort {
foreach my $sort_key (1, 2) {
my $ret = _sort_compare($a->[$sort_key], $b->[$sort_key]);
return $ret if $ret != 0;
}
return 0;
} @dt_statuses];
lib/BusyBird/Util.pm view on Meta::CPAN
=head1 EXPORTABLE FUNCTIONS
The following functions are exported only by request.
=head2 $sorted = sort_statuses($statuses)
Sorts an array of status objects appropriately. Argument C<$statuses> is an array-ref of statuses.
Return value C<$sorted> is an array-ref of sorted statuses.
The sort refers to C<< $status->{created_at} >> and C<< $status->{busybird}{acked_at} >> fields.
See L<BusyBird::StatusStorage/Order_of_Statuses> section.
=head2 $segments_arrayref = split_with_entities($text, $entities_hashref)
Splits the given C<$text> with the "entities" and returns the split segments.
C<$text> is a string to be split. C<$entities_hashref> is a hash-ref which has the same stucture as
L<Twitter Entities|https://dev.twitter.com/docs/platform-objects/entities>.
Each entity object annotates a part of C<$text> with such information as linked URLs, mentioned users,
mentioned hashtags, etc.
share/sample_config.psgi view on Meta::CPAN
#### default "home" timeline.
timeline("home");
#### To create timeline, just call timeline("NAME") function.
## timeline("hoge");
## timeline("foobar");
#### Global config. This affects all timelines.
## busybird->set_config(time_zone => "UTC");
#### Per-Timeline config. This affects the individual timeline.
#### It overrides global config.
## timeline("hoge")->set_config(time_zone => "America/Chicago");
#### For complete list of config items, run 'perldoc BusyBird::Manual::Config'.
#### You must finish configuration with the 'end' function.
end;
share/www/templates/status.tx view on Meta::CPAN
: ## Arguments: ss (status in SafeData), and template functions
<li class="bb-status" data-bb-status-level="<: bb_level($ss.val('busybird', 'level')) :>">
<span class="bb-status-id"><: $ss.val('id') :></span>
<div class="bb-status-columns-container">
<div class="bb-status-profile-image">
<: image(src => $ss.val('user', 'profile_image_url'), width => 48, height => 48) :>
</div>
<div class="bb-status-main">
<div class="bb-status-header">
<div class="bb-status-attributes">
: if !$ss.val('busybird', 'acked_at') {
<span class="bb-status-new-label label label-success">NEW</span>
: }
<span class="label label-default">Lv. <: bb_level($ss.val('busybird', 'level')) :></span>
</div>
<div>
<span class="bb-status-username"><: link( $ss.val('user', 'screen_name'), href => $ss.val('busybird', 'user_permalink'), target => "_blank" ) :></span>
<span class="bb-status-created-at"><: link( $bb_timestamp($ss.val('created_at')), href => $bb_status_permalink($ss.original()), target => "_blank" ) :></span>
</div>
</div>
<div class="bb-status-text"><: $bb_text($ss.original()) :></div>
: my $retweeted_by_user_name = $ss.val('busybird', 'retweeted_by_user', 'screen_name');
: if $retweeted_by_user_name {
<div class="bb-status-retweeted-by">
<i class="glyphicon glyphicon-retweet"></i> Retweeted by <span class="bb-status-retweeted-by-username"><: $retweeted_by_user_name :></span>
</div>
: }
</div>
</div>
: my $image_urls = $bb_attached_image_urls($ss.original());
: my $extension_exists = $image_urls && $image_urls.size() > 0;
<div class="bb-status-extension-container<: if $extension_exists { :> bb-status-extension-toggler <: }:>">
share/www/templates/timeline.tx view on Meta::CPAN
<a class="btn btn-primary" id="bb-more-button" data-loading-text="Loading..." href="#">More...</a>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="<: path('/static/jquery.js') :>"></script>
<script type="text/javascript" src="<: path('/static/bootstrap/js/bootstrap.min.js') :>"></script>
<script type="text/javascript" src="<: path('/static/spin.js') :>"></script>
<script type="text/javascript" src="<: path('/static/q.js') :>"></script>
<script type="text/javascript" src="<: path('/static/busybird.js') :>"></script>
<script type="text/javascript" src="<: path('/static/timeline.js') :>"></script>
<script type="text/javascript">
"use strict";
(function() {
var showMaxReached = function(is_loading_max_reached, message_banner) {
if(!is_loading_max_reached) return;
var message = "Too many unread statuses. Some of them are not loaded here.";
message_banner.show(message, "warn");
console.warn(message);
share/www/templates/timeline_list.tx view on Meta::CPAN
</table>
<: pagination() :>
</div>
</div>
</div>
<script type="text/javascript" src="<: path('/static/jquery.js') :>"></script>
<script type="text/javascript" src="<: path('/static/bootstrap/js/bootstrap.min.js') :>"></script>
<script type="text/javascript" src="<: path('/static/spin.js') :>"></script>
<script type="text/javascript" src="<: path('/static/q.js') :>"></script>
<script type="text/javascript" src="<: path('/static/busybird.js') :>"></script>
<script type="text/javascript" src="<: path('/static/timeline_list.js') :>"></script>
<script type="text/javascript">
"use strict";
var getTotalUnackedCount = function() {
var total = 0;
$("td.bb-timeline-unacked-counts-total-cell").each(function() {
total += parseInt($(this).text(), 10);
});
return total;