BusyBird

 view release on metacpan or  search on metacpan

lib/BusyBird.pm  view on Meta::CPAN


L<BusyBird> is extremely B<programmable>.
You are free to customize L<BusyBird> to view any statuses, e.g.,
Twitter tweets, RSS feeds, IRC chat logs, system log files etc.
In fact L<BusyBird> is not much of use without programming.

=item *

L<BusyBird> has well-documented B<Web API>.
You can easily write scripts that GET/POST statuses from/to a L<BusyBird> instance.
Some endpoints support real-time notification via HTTP long-polling.

=item *

L<BusyBird> maintains B<read/unread> states of individual statuses.
You can mark statuses as "read" via Web API.

=item *

L<BusyBird> renders statuses based on their B<< Status Levels >>.
Statuses whose level is below the threshold are dynamically hidden,

lib/BusyBird/Manual/Tutorial.pod  view on Meta::CPAN

=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

C<< POST /timelines/home/statuses.json >> endpoint inputs statuses to the "home" timeline.
Statuses are in the HTTP request body, encoded in JSON.

You can input more than one statuses by posting an array of statuses. Here is a bit more complicated example.

    $ curl \
      -d '[{"text":"Hello, Bob!", "user":{"screen_name":"Alice"}}, {"text":"Hello, Alice!", "user":{"screen_name":"Bob"}}]' \
      http://127.0.0.1:5000/timelines/home/statuses.json

This time, the statuses have C<user.screen_name> fields.
L<BusyBird> renders this field as the person or object that created the status.

lib/BusyBird/Manual/Tutorial.pod  view on Meta::CPAN

For detail, see L<BusyBird::Filter::Twitter>.

=head1 What's Next?

For advanced and more detailed topics, see also the following documents.

=over

=item L<BusyBird::Manual::WebAPI>

Reference manual of L<BusyBird> Web API, including endpoints that get, post or ack statuses.

=item L<BusyBird::Manual::Status>

Object structure of L<BusyBird> statuses.

=item L<BusyBird::Manual::Config>

Full list of configuration items.

=item L<BusyBird::Filter>

lib/BusyBird/Manual/WebAPI.pod  view on Meta::CPAN


This is a reference guide to Web API of L<BusyBird>.

The API paths are based on the path root (C</>) of the application.

For all HTTP requests with the message body,
data format of the body must be JSON encoded by UTF-8,
so C<Content-Type> header should be C<application/json; charset=utf-8>.

The data format of HTTP responses is determined by the extension (string after period (C<.>))
of endpoint paths. Text responses are always encoded by UTF-8.


=head1 ENDPOINTS

=head2 GET /timelines/{timeline}/statuses.{format}

Fetches an array of statuses from the specified timeline.
See L<BusyBird::Manual::Status> for the structure of a status object.


lib/BusyBird/Manual/WebAPI.pod  view on Meta::CPAN


Response Body:

    {"error": null, "count": 2}


=head2 POST /timelines/{timeline}/statuses.json

Adds new statuses to the specified timeline.

This endpoint uses L<BusyBird::Timeline>'s C<add_statuses()> method.
Therefore, it applies the status filter to the input statuses, and it generates C<id> and C<created_at> fields if they don't exist.

B<Path Parameters>

=over

=item C<timeline> = STR (required)

Timeline name.

lib/BusyBird/Manual/WebAPI.pod  view on Meta::CPAN


Response Body:

    {"error": null, "count": 2}


=head2 GET /timelines/{timeline}/updates/unacked_counts.json

Watches updates in numbers of unacked statuses (i.e. unacked counts) in the specified timeline, and gets them when necessary.

This is an endpoint for long-polling (Comet) access.
The response is delayed until the current unacked counts are different
from the unacked counts given in the query parameters.


B<Path Parameters>

=over

=item C<timeline> = STR (required)

lib/BusyBird/Manual/WebAPI.pod  view on Meta::CPAN

        "0": 1,
        "3": 1
      }
    }


=head2 GET /updates/unacked_counts.json

Watches updates in numbers of unacked statuses (i.e. unacked counts) in multiple timelines, and gets them when necessary.

This is an endpoint for long-polling (Comet) access.
The response is delayed until the current unacked counts are different from the unacked counts given in the query parameters.


B<Query Parameters>

The query parameters specify the assumed unacked counts.
As long as the current unacked counts are the same as the assumed unacked counts,
the response is delayed.

This endpoint allows you to watch updates in multiple timelines,
but you can watch only one status level (or 'total').

=over

=item C<level> = {total,NUM} (optional, default: total)

Specifies the status level to be watched.

=item C<tl_{timeline}> = NUM (optional)

t/WebAPI.t  view on Meta::CPAN

    my $request_url = "/timelines/$timeline_name/statuses.json";
    if($query_str) {
        $request_url .= "?$query_str";
    }
    my $res_obj = $tester->get_json_ok($request_url, qr/^200$/, "$label: GET statuses OK");
    is($res_obj->{error}, undef, "$label: GET statuses error = null OK");
    test_status_id_list($res_obj->{statuses}, $exp_id_list, "$label: GET statuses ID list OK");
}

sub test_error_request {
    my ($tester, $endpoint, $content, $label) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    my ($method, $request_url) = split(/ +/, $endpoint);
    $label ||= "";
    my $msg = "$label: $endpoint returns error";
    $tester->request_ok($method, $request_url, $content, qr/^[45]/, $msg);
}

## success if $got matches one of the choices.
sub test_list_choice {
    my ($got_list, $exp_choices, $msg) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    CHOICE_LOOP: foreach my $exp_list (@$exp_choices) {
        next CHOICE_LOOP if @$got_list != @$exp_list;
        foreach my $i (0 .. $#$got_list) {

t/WebAPI.t  view on Meta::CPAN

    };
}

{
    my $main = create_main();
    $main->timeline('test');
    note('--- Not Found cases');
    test_psgi create_psgi_app($main), sub {
        my $tester = testlib::HTTP->new(requester => shift);
        foreach my $case (
            {endpoint => "GET /timelines/foobar/statuses.json"},
            {endpoint => "GET /timelines/foobar/updates/unacked_counts.json"},
            {endpoint => "POST /timelines/foobar/ack.json"},
            {endpoint => "POST /timelines/foobar/statuses.json", content => create_json_status(1)},
            {endpoint => "POST /timelines/test/statuses.json"},
            {endpoint => "POST /timelines/test/updates/unacked_counts.json"},
            {endpoint => "GET /timelines/test/ack.json"},
            {endpoint => "POST /updates/unacked_counts.json?tl_test=10"},
        ) {
            test_error_request($tester, $case->{endpoint}, $case->{content});
        }
    };
}

{
    foreach my $storage_case (
        {label => "dying", storage => create_dying_status_storage()},
        {label => "erroneous", storage => create_erroneous_status_storage()},
    ) {
        note("--- $storage_case->{label} status storage");
        my $main = create_main();
        $main->set_config(default_status_storage => $storage_case->{storage});
        $main->timeline('test');
        test_psgi create_psgi_app($main), sub {
            my $tester = testlib::HTTP->new(requester => shift);
            foreach my $case (
                {endpoint => "GET /timelines/test/statuses.json"},
                ## {endpoint => "GET /timelines/test/updates/unacked_counts.json"},
                {endpoint => "POST /timelines/test/ack.json"},
                {endpoint => "POST /timelines/test/statuses.json", content => create_json_status(1)},
                ## {endpoint => "GET /updates/unacked_counts.json?tl_test=3"}
            ) {
                my $label = "$storage_case->{label} $case->{endpoint}";
                my ($method, $path) = split(/ +/, $case->{endpoint});
                my $got = $tester->request_json_ok($method, $path, $case->{content}, qr/^[45]/, "$label: request OK");
                ok(defined($got->{error}), "$label: error message defined OK");
            }

            my $got = $tester->get_json_ok('/timelines/test/statuses.json?only_statuses=1', qr/^[45]/,
                                           "$storage_case->{label}: GET only_statuses HTTP error OK");
            is_deeply $got, [], "$storage_case->{label}: GET only_statuses returns an empty array OK";
        }
    }
}

t/WebAPI.t  view on Meta::CPAN

                                        qr/^200$/, 'GET statuses from hidden OK');
        is $res_obj->{error}, undef, "GET statuses no error OK";
    };
}

{
    note('--- For examples');
    my $main = create_main();
    $main->timeline("home");
    my @cases = (
        {endpoint => 'POST /timelines/home/statuses.json',
         content => <<EOD,
[
  {
    "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"
  }
]
EOD
         exp_response => q{{"error": null, "count": 2}}},
        {endpoint => 'GET /timelines/home/statuses.json?count=1&ack_state=any&max_id=http://example.com/page/2013/0202',
         exp_response => <<EOD},
{
  "error": null,
  "statuses": [
    {
      "id": "http://example.com/page/2013/0202",
      "created_at": "Sat Feb 02 17:38:12 +0900 2013",
      "text": "another content"
    }
  ]
}
EOD
        {endpoint => 'GET /timelines/home/statuses.json?count=1&ack_state=any&max_id=http://example.com/page/2013/0202&only_statuses=1',
         exp_response => <<EOD},
[
    {
      "id": "http://example.com/page/2013/0202",
      "created_at": "Sat Feb 02 17:38:12 +0900 2013",
      "text": "another content"
    }
]
EOD
        {endpoint => 'GET /timelines/home/updates/unacked_counts.json?total=2&0=2',
         exp_response => <<EOD},
{
  "error": null,
  "unacked_counts": {
    "total": 2,
    "0": 1,
    "3": 1
  }
}
EOD
        {endpoint => 'GET /updates/unacked_counts.json?level=total&tl_home=0&tl_foobar=0',
         exp_response => <<EOD},
{
  "error": null,
  "unacked_counts": {
    "home": {
      "total": 2,
      "0": 1,
      "3": 1
    }
  }
}
EOD
        {endpoint => 'POST /timelines/home/ack.json',
         content => <<EOD,
{
  "max_id": "http://example.com/page/2013/0202",
  "ids": [
    "http://example.com/page/2013/0204"
   ]
}
EOD
         exp_response => q{{"error": null, "count": 2}}}
    );
    test_psgi create_psgi_app($main), sub {
        my $tester = testlib::HTTP->new(requester => shift);
        foreach my $case (@cases) {
            my ($method, $request_url) = split(/ +/, $case->{endpoint});
            my $res_obj = $tester->request_json_ok($method, $request_url, $case->{content},
                                                   qr/^200$/, "$case->{endpoint} OK");
            my $exp_obj = decode_json($case->{exp_response});
            is_deeply($res_obj, $exp_obj, "$case->{endpoint} response OK") or diag(explain $res_obj);
        }
    };
}

done_testing();



( run in 0.375 second using v1.01-cache-2.11-cpan-27979f6cc8f )