Changeset bc56a0d for perl/modules
- Timestamp:
- Sep 19, 2011, 1:31:34 PM (13 years ago)
- Children:
- ac45403
- Parents:
- a8e1fcf
- git-author:
- Edward Z. Yang <ezyang@mit.edu> (06/22/11 21:53:17)
- git-committer:
- Edward Z. Yang <ezyang@mit.edu> (09/19/11 13:31:34)
- Location:
- perl/modules/Facebook
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
perl/modules/Facebook/README
rc833c3d rbc56a0d 22 22 23 23 (4) Start receiving wall updates in Barnowl! 24 You can post updates with the ":facebook" command. 24 You can post updates with the ":facebook" command. 25 26 INFELICITIES 27 * Polling Facebook is pretty slow (on order of a second or more), 28 and blocks the entire BarnOwl interface. We have a plan for 29 fixing this, which involves creating an async version of 30 Facebook::Graph. I have been in contact with the original 31 author JT Smith about this. 32 33 * BarnOwl will not receive all comments for news feed items, only 34 comments for items that were recently published. There is not 35 currently a way to see starks for conversations that you did not 36 participate in, and the only way to see starks for conversations 37 you did participate in are Facebook's email notifications. (This 38 is a deficiency of the Facebook API, see http://bugs.developers.facebook.net/show_bug.cgi?id=18594.) 39 40 * By default, BarnOwl will not display posts from organizations (such 41 as bands you have liked) or application invites. This is a 42 judgment of taste from the original author. This is not currently 43 configurable. 44 45 * Users and applications that you have hidden via the Facebook 46 web interface will show up in the BarnOwl stream. (This appears to 47 be a deficiency of the Facebook API, see 48 http://stackoverflow.com/questions/6405364/facebook-api-access-hide-posts-from-settings). 49 Users are encouraged to work around this by using traditional Zephyr 50 filters. 51 52 * We are missing support for some notable features, including 53 messaging (Facebook has not publically released the API for this, 54 though we could sign up for the whitelist), events (seeing 55 unresponded to events requires a custom FQL query 56 http://stackoverflow.com/questions/4752967/facebook-api-only-returns-25-events-max), 57 notifications (not supported in Graph API yet). 58 59 WISHLIST 60 * Smarter name de-duplication (see Facebook/Handle.pm for details.) 61 * URL minification. 62 * Multiple accounts. (Does anyone do this? I don't think so...) 63 * Zephyr class mirroring. 25 64 26 65 POLLING 27 66 Facebook.par polls for normal messages once a minute. To disable 28 67 polling, you can unset the 'facebook:poll' variable in BarnOwl. 29 30 TODO31 * Polling Facebook is pretty slow (on order of a second or more),32 and blocks the entire BarnOwl interface. We need to confront33 Perl's threading demon.34 * No messaging support. (We'll add it when Facebook makes the new endpoint.)35 * Smarter name de-duplication (see code comments for details.)36 * Grep for XXX and TODO for more work items.37 38 TECHNICAL NOTES39 This module uses 100% undeprecated Facebook Graph API, and should be40 an interesting case study of how to implement a desktop application in41 the new Facebook world order. In particular, we do not use the old42 infinite session keys trick described in43 <http://www.emcro.com/blog/2009/01/facebook-infinite-session-keys-no-more/>,44 instead, we use offline_access to get non-expiring tokens.45 46 If we decide to extend our permissions to include read_friendlists47 (for filtering) and rsvp_event (RSVP from BarnOwl), we will need48 to make sure the UI for upgrading is correct.49 50 We'll be rolling our own version of Facebook::Graph, as the original51 uses the synchronous LWP::UserAgent, and we'd like our web requests52 not to block the user interface. Furthermore, Facebook::Graph doesn't53 actually use any of the features of LWP::UserAgent, so we may be able54 to use a simpler module AnyEvent::HTTP. -
perl/modules/Facebook/lib/BarnOwl/Module/Facebook.pm
rc833c3d rbc56a0d 24 24 our $facebook_handle = undef; 25 25 26 # did not implement class monitoring27 # did not implement multiple accounts28 29 26 BarnOwl::new_variable_bool( 30 27 'facebook:poll', … … 32 29 default => 1, 33 30 summary => 'Poll Facebook for wall updates', 34 # TODO: Make thisconfigurable31 # XXX: Make poll time configurable 35 32 description => "If set, will poll Facebook every minute for updates.\n" 36 33 } 37 34 ); 38 35 39 sub fail { 40 my $msg = shift; 41 # reset global state here 42 BarnOwl::admin_message('Facebook Error', $msg); 43 die("Facebook Error: $msg\n"); 36 sub init { 37 my $conffile = BarnOwl::get_config_dir() . "/facebook"; 38 my $cfg = {}; 39 if (open(my $fh, "<", "$conffile")) { 40 my $raw_cfg = do {local $/; <$fh>}; 41 close($fh); 42 43 eval { $cfg = from_json($raw_cfg); }; 44 if ($@) { BarnOwl::admin_message('Facebook', "Unable to parse $conffile: $@"); } 45 } 46 eval { $facebook_handle = BarnOwl::Module::Facebook::Handle->new($cfg); }; 47 if ($@) { BarnOwl::error($@); } 44 48 } 45 49 46 # We only load up when the conf file is present, to reduce resource 47 # usage. Though, probably not by very much, so maybe a 'facebook-init' 48 # command would be more appropriate. 49 50 my $conffile = BarnOwl::get_config_dir() . "/facebook"; 51 52 if (open(my $fh, "<", "$conffile")) { 53 read_config($fh); 54 close($fh); 55 } 56 57 sub read_config { 58 my $fh = shift; 59 my $raw_cfg = do {local $/; <$fh>}; 60 close($fh); 61 62 my $cfg; 63 if ($raw_cfg) { 64 eval { $cfg = from_json($raw_cfg); }; 65 if($@) { 66 fail("Unable to parse $conffile: $@"); 67 } 68 } else { 69 $cfg = {}; 70 } 71 72 eval { 73 $facebook_handle = BarnOwl::Module::Facebook::Handle->new($cfg); 74 }; 75 if ($@) { 76 BarnOwl::error($@); 77 next; 78 } 79 } 80 81 # Ostensibly here as a convenient shortcut for Perl hackery 82 sub facebook { 83 $facebook_handle->facebook(@_); 84 } 50 init(); 85 51 86 52 # Should also add support for posting to other people's walls (this … … 89 55 BarnOwl::new_command('facebook' => \&cmd_facebook, { 90 56 summary => 'Post a status update to your wall from BarnOwl', 91 usage => 'facebook ',92 description => 'Post a status update to your wall .'57 usage => 'facebook [USER]', 58 description => 'Post a status update to your wall, or post on another user\'s wall. Autocomplete is supported.' 93 59 }); 94 60 95 # How do we allow people to specify the USER?96 61 #BarnOwl::new_command('facebook-message' => \&cmd_facebook_direct, { 97 62 # summary => 'Send a Facebook message', … … 103 68 summary => 'Comment on a wall post.', 104 69 usage => 'facebook-comment POST_ID', 105 description => 'Comment on a friend\'s wall post. Using r is recommended.'70 description => 'Comment on a friend\'s wall post.' 106 71 }); 107 72 … … 116 81 summary => 'Force a poll of Facebook.', 117 82 usage => 'facebook-poll', 118 description => 'Get updates from Facebook.'83 description => 'Get updates (news, friends) from Facebook.' 119 84 }); 120 121 # XXX: UI: probably should bug out immediately if we're not logged in.122 85 123 86 sub cmd_facebook { … … 125 88 my $user = shift; 126 89 90 return unless check_ready(); 91 127 92 BarnOwl::start_edit_win( 128 93 defined $user ? "Write something to $user..." : "What's on your mind?", 129 sub{ facebook($user, shift) }94 sub{ $facebook_handle->facebook($user, shift) } 130 95 ); 131 96 } … … 135 100 my $post_id = shift; 136 101 102 return unless check_ready(); 103 137 104 my $topic = $facebook_handle->get_topic($post_id); 138 105 139 # XXX UI should give some (better) indication /which/ conversation140 # is being commented on141 106 BarnOwl::start_edit_win("Write a comment on '$topic'...", 142 107 sub { $facebook_handle->facebook_comment($post_id, shift) }); … … 145 110 sub cmd_facebook_poll { 146 111 my $cmd = shift; 112 113 return unless check_ready(); 147 114 148 115 $facebook_handle->sleep(0); … … 154 121 my $url = shift; 155 122 123 if ($facebook_handle->{logged_in}) { 124 BarnOwl::message("Already logged in. (To force, run ':reload-module Facebook', or deauthorize BarnOwl.)"); 125 return; 126 } 127 156 128 $facebook_handle->facebook_auth($url); 157 129 } 158 130 131 sub check_ready { 132 if (!$facebook_handle->{logged_in}) { 133 BarnOwl::message("Need to login to Facebook first with ':facebook-auth'."); 134 return 0; 135 } 136 return 1; 137 } 138 159 139 BarnOwl::filter(qw{facebook type ^facebook$}); 160 161 # Autocompletion support162 140 163 141 sub complete_user { return keys %{$facebook_handle->{friends}}; } -
perl/modules/Facebook/lib/BarnOwl/Module/Facebook/Handle.pm
ra8e1fcf rbc56a0d 35 35 use Date::Parse; 36 36 use POSIX; 37 use Ouch; 37 38 38 39 use Scalar::Util qw(weaken); … … 65 66 # comments less frequently than polling for new posts. 66 67 67 sub fail {68 my $self = shift;69 my $msg = shift;70 undef $self->{facebook};71 die("[Facebook] Error: $msg\n");72 }73 74 68 sub new { 75 69 my $class = shift; … … 83 77 # but we can't assume that the BarnOwl lives on a publically 84 78 # addressable server (XXX maybe we can setup an option for this.) 85 'last_friend_poll' => 0,86 79 'friend_timer' => undef, 87 80 88 81 # Initialized with our 'time', but will be synced to Facebook 89 82 # soon enough. (Subtractive amount is just to preseed with some 90 # values.) 91 'last_poll' => time - 60 * 60 * 24 * 2,83 # values.) XXX Remove subtraction altogether. 84 'last_poll' => time - 60 * 60, 92 85 'timer' => undef, 93 86 … … 99 92 # $fb->authorize, but at time of writing (1.0300) they didn't support 100 93 # the response_type parameter. 101 # 'login_url' => 'https://www.facebook.com/dialog/oauth?client_id=235537266461636&scope=read_stream,read_mailbox,publish_stream,offline_access &redirect_uri=http://www.facebook.com/connect/login_success.html&response_type=token',94 # 'login_url' => 'https://www.facebook.com/dialog/oauth?client_id=235537266461636&scope=read_stream,read_mailbox,publish_stream,offline_access,read_friendlists,rsvp_event,user_events&redirect_uri=http://www.facebook.com/connect/login_success.html&response_type=token', 102 95 # minified to fit in most terminal windows. 103 'login_url' => 'http://goo.gl/yA42G', 96 # Be careful about updating these values, since BarnOwl will not 97 # notice that it is missing necessary permissions until it 98 # attempt to perform an operation which fails due to lack of 99 # permissions. 100 'login_url' => 'http://goo.gl/rcM9s', 104 101 105 102 'logged_in' => 0, … … 164 161 } 165 162 163 sub check_result { 164 my $self = shift; 165 if (kiss 400) { 166 # Ugh, no easy way of accessing the JSON error type 167 # which is OAuthException. 168 $self->{logged_in} = 0; 169 $self->facebook_do_auth; 170 return 0; 171 } elsif (hug) { 172 my $code = $@->code; 173 warn "Poll failed with $code: $@"; 174 return 0; 175 } 176 return 1; 177 } 178 166 179 sub poll_friends { 167 180 my $self = shift; … … 171 184 172 185 my $friends = eval { $self->{facebook}->fetch('me/friends'); }; 173 if ($@) { 174 warn "Poll failed $@"; 175 return; 176 } 177 178 $self->{last_friend_poll} = time; 186 return unless $self->check_result; 187 179 188 $self->{friends} = {}; 180 189 … … 195 204 # The most recent one.) Since getting this information 196 205 # involves extra queries, there are also caching and 197 # efficiency concerns. 198 # We may want a facility for users to specify custom 206 # efficiency concerns (though hopefully you don't have too 207 # many friends with the same name). Furthermore, accessing 208 # this information requires a pretty hefty extra set of 209 # permissions requests, which we don't currently ask for. 210 # It may just be better to let users specify custom 199 211 # aliases for Facebook users, which are added into this 200 212 # hash. See also username support. … … 213 225 # query, it would require a rather expensive set of queries. We 214 226 # might try to preserve old data, but all-in-all it's a bit 215 # complicated, so we don't bother. 227 # complicated. One possible way of fixing this is to construct a 228 # custom FQL query that joins the friends table and the users table. 216 229 } 217 230 … … 219 232 my $self = shift; 220 233 221 #return unless ( time - $self->{last_poll} ) >= 60;222 234 return unless BarnOwl::getvar('facebook:poll') eq 'on'; 223 235 return unless $self->{logged_in}; 224 225 #BarnOwl::message("Polling Facebook...");226 236 227 237 # XXX Oh no! This blocks the user interface. Not good. … … 237 247 ->from("my_news") 238 248 # Not using this, because we want to pick up comment 239 # updates. We need to manually de-dup , though.249 # updates. We need to manually de-duplicate, though. 240 250 # ->where_since( "@" . $self->{last_poll} ) 251 # Facebook doesn't actually give us that many results. 252 # But it can't hurt to ask! 241 253 ->limit_results( 200 ) 242 ->request ()243 ->as_hashref ()254 ->request 255 ->as_hashref 244 256 }; 245 if ($@) { 246 warn "Poll failed $@"; 247 return; 248 } 257 return unless $self->check_result; 249 258 250 259 my $new_last_poll = $self->{last_poll}; … … 262 271 } 263 272 264 # XXX Need to somehow access Facebook's user hiding265 # mechanism266 267 273 # There can be multiple recipients! Strange! Pick the first one. 268 274 my $name = $post->{to}{data}[0]{name} || $post->{from}{name}; … … 270 276 my $post_id = $post->{id}; 271 277 278 my $topic; 272 279 if (defined $old_topics->{$post_id}) { 273 $self->{topics}->{$post_id} = $old_topics->{$post_id}; 280 $topic = $old_topics->{$post_id}; 281 $self->{topics}->{$post_id} = $topic; 274 282 } else { 275 283 my @keywords = keywords($post->{name} || $post->{message}); 276 my$topic = $keywords[0] || 'personal';284 $topic = $keywords[0] || 'personal'; 277 285 $topic =~ s/ /-/g; 278 286 $self->{topics}->{$post_id} = $topic; … … 292 300 body => $self->format_body($post), 293 301 post_id => $post_id, 294 topic => $ self->get_topic($post_id),302 topic => $topic, 295 303 time => asctime(localtime $created_time), 296 304 # XXX The intent is to get the 'Comment' link, which also … … 302 310 } 303 311 304 # This will have funky interleaving of times (they'll all be305 # sorted linearly), but since we don't expect too many updates between312 # This will interleave times (they'll all be organized by parent 313 # post), but since we don't expect too many updates between 306 314 # polls this is pretty acceptable. 307 315 my $updated_time = str2time($post->{updated_time}); … … 320 328 direction => 'in', 321 329 body => $comment->{message}, 322 post_id 323 topic => $ self->get_topic($post_id),330 post_id => $post_id, 331 topic => $topic, 324 332 time => asctime(localtime $comment_time), 325 333 ); … … 355 363 } 356 364 365 # Invariant: we don't become logged out between entering text field 366 # and actually processing the request. XXX I don't think this actually 367 # holds, but such a case would rarely happen. 368 357 369 sub facebook { 358 370 my $self = shift; … … 361 373 my $msg = shift; 362 374 363 if (!defined $self->{facebook} || !$self->{logged_in}) {364 BarnOwl::admin_message('Facebook', 'You are not currently logged into Facebook.');365 return;366 }367 375 if (defined $user) { 368 376 $user = $self->{friends}{$user} || $user; 369 $self->{facebook}->add_post( $user )->set_message( $msg )->publish; 377 eval { $self->{facebook}->add_post( $user )->set_message( $msg )->publish; }; 378 return unless $self->check_result; 370 379 } else { 371 $self->{facebook}->add_post->set_message( $msg )->publish; 380 eval { $self->{facebook}->add_post->set_message( $msg )->publish; }; 381 return unless $self->check_result; 372 382 } 373 383 $self->sleep(0); … … 380 390 my $msg = shift; 381 391 382 $self->{facebook}->add_comment( $post_id )->set_message( $msg )->publish; 392 eval { $self->{facebook}->add_comment( $post_id )->set_message( $msg )->publish; }; 393 return unless $self->check_result; 383 394 $self->sleep(0); 384 395 } … … 388 399 389 400 my $url = shift; 401 390 402 # http://www.facebook.com/connect/login_success.html#access_token=TOKEN&expires_in=0 391 403 $url =~ /access_token=([^&]+)/; # XXX Ew regex 404 405 if (!defined $1) { 406 BarnOwl::message("Invalid URL."); 407 return; 408 } 392 409 393 410 $self->{cfg}->{token} = $1; … … 402 419 if ( ! defined $self->{cfg}->{token} ) { 403 420 BarnOwl::admin_message('Facebook', "Login to Facebook at ".$self->{login_url} 404 . "\nand run command ':facebook-auth URL' with the URL you are redirected to."); 421 . "\nand run command ':facebook-auth URL' with the URL you are redirected to." 422 . "\n\nWhat does Barnowl use these permissions for? As a desktop" 423 . "\nmessaging application, we need persistent read/write access to your" 424 . "\nnews feed and your inbox. Other permissions are for pending" 425 . "\nfeatures: we intend on adding support for event streaming, RSVP," 426 . "\nand BarnOwl filtering on friend lists." 427 ); 405 428 return 0; 406 429 } 407 430 $self->{facebook}->access_token($self->{cfg}->{token}); 408 431 # Do a quick check to see if things are working 409 my $result = eval { $self->{facebook}-> fetch('me'); };432 my $result = eval { $self->{facebook}->query()->find('me')->select_fields('name')->request->as_hashref; }; 410 433 if ($@) { 411 434 BarnOwl::admin_message('Facebook', "Failed to authenticate! Login to Facebook at ".$self->{login_url} … … 421 444 } 422 445 423 sub get_topic {424 my $self = shift;425 426 my $post_id = shift;427 428 return $self->{topics}->{$post_id} || 'personal';429 }430 431 446 1;
Note: See TracChangeset
for help on using the changeset viewer.