File Coverage

lib/App/Twimap.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package App::Twimap;
2 1     1   3598 use Moose;
  0            
  0            
3             use Algorithm::TokenBucket;
4             use App::Twimap::Tweet;
5             use Email::MIME;
6             use Email::MIME::Creator;
7             use Encode;
8             use List::Util qw(max);
9             use LWP::UserAgent;
10             use Web::oEmbed::Common;
11             use Time::HiRes;
12             use TryCatch;
13             use URI::WithBase;
14             has 'mail_imapclient' =>
15             ( is => 'ro', isa => 'Mail::IMAPClient', required => 1 );
16             has 'net_twitter' => ( is => 'ro', isa => 'Net::Twitter', required => 1 );
17             has 'mailbox' => ( is => 'ro', isa => 'Str', required => 1 );
18             our $VERSION = '0.03';
19              
20             sub imap_tids {
21             my $self = shift;
22             my $imap = $self->mail_imapclient;
23             my $mailbox = $self->mailbox;
24              
25             warn "Fetching message_ids...";
26              
27             $self->select_mailbox;
28              
29             my $message_ids
30             = $imap->fetch_hash('BODY.PEEK[HEADER.FIELDS (Message-Id)]')
31             or die "Fetch hash $mailbox error: ", $imap->LastError;
32              
33             my %tids;
34              
35             foreach my $uid ( keys %$message_ids ) {
36             my $message_id
37             = $message_ids->{$uid}->{'BODY[HEADER.FIELDS (MESSAGE-ID)]'};
38             my ($tid) = $message_id =~ /Message-Id: <(\d+)\@twitter>/;
39             next unless $tid;
40             $tids{$tid} = 1;
41             }
42             return \%tids;
43             }
44              
45             sub sync_home_timeline {
46             my $self = shift;
47             my $twitter = $self->net_twitter;
48             my $tids = $self->imap_tids;
49              
50             my $bucket = new Algorithm::TokenBucket 15 / (15 * 60), 1;
51              
52             my $since_id = max( keys %$tids );
53             my $max_id = 0;
54             while (1) {
55             warn
56             "Fetching home timeline since id $since_id and max_id $max_id...";
57             my $new_tweets = 0;
58             my $conf = {
59             count => 200,
60             include_entities => 1,
61             };
62             $conf->{since_id} = $since_id if $since_id;
63             $conf->{max_id} = $max_id if $max_id;
64             my $tweets = $twitter->home_timeline($conf);
65              
66             foreach my $data (@$tweets) {
67             my $tweet = App::Twimap::Tweet->new( data => $data );
68             my $tid = $tweet->id;
69              
70             $max_id = $tid unless $max_id;
71             $max_id = $tid if $tid < $max_id;
72              
73             next if $tids->{$tid};
74             $new_tweets++;
75              
76             my $email = $tweet->to_email;
77             $self->append_email($email);
78             $tids->{$tid} = 1;
79             }
80             last unless $new_tweets;
81             warn "sleeping...";
82             Time::HiRes::sleep $bucket->until(1);
83             $bucket->count(1);
84             }
85             }
86              
87             sub sync_replies {
88             my $self = shift;
89             my $twitter = $self->net_twitter;
90             my $imap = $self->mail_imapclient;
91             my $mailbox = $self->mailbox;
92             my $tids = $self->imap_tids;
93              
94             warn "Fetching in_reply_tos...";
95              
96             $self->select_mailbox;
97              
98             my @todo;
99             my $replies = $imap->fetch_hash('BODY.PEEK[HEADER.FIELDS (IN-REPLY-TO)]')
100             or die "Fetch hash $mailbox error: ", $imap->LastError;
101             foreach my $uid ( keys %$replies ) {
102             my $header = $replies->{$uid}->{'BODY[HEADER.FIELDS (IN-REPLY-TO)]'};
103             my ($tid) = $header =~ /In-Reply-To: <(\d+)\@twitter>/;
104             next unless $tid;
105             push @todo, $tid;
106             }
107              
108             my $bucket = new Algorithm::TokenBucket 180 / (15 * 60), 1;
109              
110             foreach my $tid (@todo) {
111             next if $tids->{$tid};
112             warn "sleeping...";
113             Time::HiRes::sleep $bucket->until(1);
114             $bucket->count(1);
115             warn "fetching $tid...";
116             my $data;
117             try {
118             $data = $twitter->show_status( $tid, { include_entities => 1 } );
119             }
120             catch($err) {
121             warn $err;
122             next;
123             };
124             my $tweet = App::Twimap::Tweet->new( data => $data );
125             push @todo, $tweet->in_reply_to_status_id
126             if $tweet->in_reply_to_status_id;
127             my $email = $tweet->to_email;
128             $self->append_email($email);
129             $tids->{$tid} = 1;
130             }
131             }
132              
133             sub append_email {
134             my ( $self, $email ) = @_;
135             my $imap = $self->mail_imapclient;
136             my $mailbox = $self->mailbox;
137              
138             my $uid
139             = $imap->append_string( $mailbox, encode_utf8( $email->as_string ) )
140             or die "Could not append_string to $mailbox: ", $imap->LastError;
141             }
142              
143             sub select_mailbox {
144             my $self = shift;
145             my $imap = $self->mail_imapclient;
146             my $mailbox = $self->mailbox;
147             $imap->select($mailbox)
148             or die "Select $mailbox error: ", $imap->LastError;
149             }
150              
151             __PACKAGE__->meta->make_immutable;
152              
153             1;
154              
155             =head1 NAME
156              
157             App::Twimap - Push your Twitter home timeline to an IMAP server
158              
159             =head1 SYNOPSIS
160              
161             $ twimap twimap.conf
162              
163             =head1 DESCRIPTION
164              
165             Twitter is an online social networking and microblogging service. The Internet
166             Message Access Protocol (IMAP) is an Internet standard protocols for e-mail
167             retrieval from a server. This module stores your Twitter home timeline in a
168             folder on an IMAP server.
169              
170             Why would you do this?
171              
172             =over 4
173              
174             =item * Offline access to your Twitter home timeline
175              
176             =item * Your email client can do message threading
177              
178             =item * Use multiple devices and they sync read messages
179              
180             =item * URLs are expanded
181              
182             =item * Images and videos are embedded via oEmbed
183              
184             =back
185              
186             To use this application you need to create a Twitter API application on:
187              
188             https://dev.twitter.com/apps/new
189              
190             You need to use the examples/oauth_desktop.pl application distributed
191             with Net::Twitter to obtain the OAuth tokens. First replace the consumer
192             tokens with those of your application, then run the application and see
193             oauth_desktop.dat.
194              
195             Create a twimap.conf (an example is shipped with this distribution)
196             with the IMAP server details and Twitter access details.
197              
198             ... and now you can run the application as in the synopsis.
199              
200             =head1 AUTHOR
201              
202             Leon Brocard <acme@astray.com>.
203              
204             =head1 COPYRIGHT
205              
206             Copyright (C) 2011, Leon Brocard
207              
208             =head1 LICENSE
209              
210             This module is free software; you can redistribute it or modify it
211             under the same terms as Perl itself.