Changeset 2f6c263


Ignore:
Timestamp:
Jun 22, 2011, 9:56:47 PM (13 years ago)
Author:
Edward Z. Yang <ezyang@mit.edu>
Children:
3d304b9, 26217b9
Parents:
2c4c4c4
git-author:
Edward Z. Yang <ezyang@mit.edu> (06/22/11 21:53:17)
git-committer:
Edward Z. Yang <ezyang@mit.edu> (06/22/11 21:56:47)
Message:
Improve docs, error handling and refactor.

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
Location:
perl/modules/Facebook
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • perl/modules/Facebook/README

    r68fa539 r2f6c263  
    2222
    2323  (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
     26INFELICITIES
     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
     59WISHLIST
     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.
    2564
    2665POLLING
    2766  Facebook.par polls for normal messages once a minute. To disable
    2867  polling, you can unset the 'facebook:poll' variable in BarnOwl.
    29 
    30 TODO
    31   * Polling Facebook is pretty slow (on order of a second or more),
    32     and blocks the entire BarnOwl interface.  We need to confront
    33     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 NOTES
    39   This module uses 100% undeprecated Facebook Graph API, and should be
    40   an interesting case study of how to implement a desktop application in
    41   the new Facebook world order.  In particular, we do not use the old
    42   infinite session keys trick described in
    43   <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_friendlists
    47   (for filtering) and rsvp_event (RSVP from BarnOwl), we will need
    48   to make sure the UI for upgrading is correct.
    49 
    50   We'll be rolling our own version of Facebook::Graph, as the original
    51   uses the synchronous LWP::UserAgent, and we'd like our web requests
    52   not to block the user interface.  Furthermore, Facebook::Graph doesn't
    53   actually use any of the features of LWP::UserAgent, so we may be able
    54   to use a simpler module AnyEvent::HTTP.
  • perl/modules/Facebook/lib/BarnOwl/Module/Facebook.pm

    r68fa539 r2f6c263  
    2424our $facebook_handle = undef;
    2525
    26 # did not implement class monitoring
    27 # did not implement multiple accounts
    28 
    2926BarnOwl::new_variable_bool(
    3027    'facebook:poll',
     
    3229        default => 1,
    3330        summary => 'Poll Facebook for wall updates',
    34         # TODO: Make this configurable
     31        # XXX: Make poll time configurable
    3532        description => "If set, will poll Facebook every minute for updates.\n"
    3633     }
    3734 );
    3835
    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");
     36sub 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($@); }
    4448}
    4549
    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 }
     50init();
    8551
    8652# Should also add support for posting to other people's walls (this
     
    8955BarnOwl::new_command('facebook' => \&cmd_facebook, {
    9056    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.'
    9359});
    9460
    95 # How do we allow people to specify the USER?
    9661#BarnOwl::new_command('facebook-message' => \&cmd_facebook_direct, {
    9762#    summary     => 'Send a Facebook message',
     
    10368    summary     => 'Comment on a wall post.',
    10469    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.'
    10671});
    10772
     
    11681    summary     => 'Force a poll of Facebook.',
    11782    usage       => 'facebook-poll',
    118     description => 'Get updates from Facebook.'
     83    description => 'Get updates (news, friends) from Facebook.'
    11984});
    120 
    121 # XXX: UI: probably should bug out immediately if we're not logged in.
    12285
    12386sub cmd_facebook {
     
    12588    my $user = shift;
    12689
     90    return unless check_ready();
     91
    12792    BarnOwl::start_edit_win(
    12893        defined $user ? "Write something to $user..." : "What's on your mind?",
    129         sub{ facebook($user, shift) }
     94        sub{ $facebook_handle->facebook($user, shift) }
    13095    );
    13196}
     
    135100    my $post_id = shift;
    136101
     102    return unless check_ready();
     103
    137104    my $topic = $facebook_handle->get_topic($post_id);
    138105
    139     # XXX UI should give some (better) indication /which/ conversation
    140     # is being commented on
    141106    BarnOwl::start_edit_win("Write a comment on '$topic'...",
    142107                            sub { $facebook_handle->facebook_comment($post_id, shift) });
     
    145110sub cmd_facebook_poll {
    146111    my $cmd = shift;
     112
     113    return unless check_ready();
    147114
    148115    $facebook_handle->sleep(0);
     
    154121    my $url = shift;
    155122
     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
    156128    $facebook_handle->facebook_auth($url);
    157129}
    158130
     131sub 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
    159139BarnOwl::filter(qw{facebook type ^facebook$});
    160 
    161 # Autocompletion support
    162140
    163141sub complete_user { return keys %{$facebook_handle->{friends}}; }
  • perl/modules/Facebook/lib/BarnOwl/Module/Facebook/Handle.pm

    r2c4c4c4 r2f6c263  
    3535use Date::Parse;
    3636use POSIX;
     37use Ouch;
    3738
    3839use Scalar::Util qw(weaken);
     
    6566#   comments less frequently than polling for new posts.
    6667
    67 sub fail {
    68     my $self = shift;
    69     my $msg  = shift;
    70     undef $self->{facebook};
    71     die("[Facebook] Error: $msg\n");
    72 }
    73 
    7468sub new {
    7569    my $class = shift;
     
    8377        # but we can't assume that the BarnOwl lives on a publically
    8478        # addressable server (XXX maybe we can setup an option for this.)
    85         'last_friend_poll' => 0,
    8679        'friend_timer' => undef,
    8780
    8881        # Initialized with our 'time', but will be synced to Facebook
    8982        # 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,
    9285        'timer' => undef,
    9386
     
    9992        # $fb->authorize, but at time of writing (1.0300) they didn't support
    10093        # 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',
    10295        # 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',
    104101
    105102        'logged_in' => 0,
     
    164161}
    165162
     163sub 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
    166179sub poll_friends {
    167180    my $self = shift;
     
    171184
    172185    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
    179188    $self->{friends} = {};
    180189
     
    195204            # The most recent one.)  Since getting this information
    196205            # 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
    199211            # aliases for Facebook users, which are added into this
    200212            # hash.  See also username support.
     
    213225    # query, it would require a rather expensive set of queries. We
    214226    # 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.
    216229}
    217230
     
    219232    my $self = shift;
    220233
    221     #return unless ( time - $self->{last_poll} ) >= 60;
    222234    return unless BarnOwl::getvar('facebook:poll') eq 'on';
    223235    return unless $self->{logged_in};
    224 
    225     #BarnOwl::message("Polling Facebook...");
    226236
    227237    # XXX Oh no! This blocks the user interface.  Not good.
     
    237247             ->from("my_news")
    238248             # 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.
    240250             # ->where_since( "@" . $self->{last_poll} )
     251             # Facebook doesn't actually give us that many results.
     252             # But it can't hurt to ask!
    241253             ->limit_results( 200 )
    242              ->request()
    243              ->as_hashref()
     254             ->request
     255             ->as_hashref
    244256    };
    245     if ($@) {
    246         warn "Poll failed $@";
    247         return;
    248     }
     257    return unless $self->check_result;
    249258
    250259    my $new_last_poll = $self->{last_poll};
     
    262271        }
    263272
    264         # XXX Need to somehow access Facebook's user hiding
    265         # mechanism
    266 
    267273        # There can be multiple recipients! Strange! Pick the first one.
    268274        my $name    = $post->{to}{data}[0]{name} || $post->{from}{name};
     
    270276        my $post_id  = $post->{id};
    271277
     278        my $topic;
    272279        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;
    274282        } else {
    275283            my @keywords = keywords($post->{name} || $post->{message});
    276             my $topic = $keywords[0] || 'personal';
     284            $topic = $keywords[0] || 'personal';
    277285            $topic =~ s/ /-/g;
    278286            $self->{topics}->{$post_id} = $topic;
     
    292300                body      => $self->format_body($post),
    293301                post_id   => $post_id,
    294                 topic     => $self->get_topic($post_id),
     302                topic     => $topic,
    295303                time      => asctime(localtime $created_time),
    296304                # XXX The intent is to get the 'Comment' link, which also
     
    302310        }
    303311
    304         # This will have funky interleaving of times (they'll all be
    305         # sorted linearly), but since we don't expect too many updates between
     312        # This will interleave times (they'll all be organized by parent
     313        # post), but since we don't expect too many updates between
    306314        # polls this is pretty acceptable.
    307315        my $updated_time = str2time($post->{updated_time});
     
    320328                    direction => 'in',
    321329                    body      => $comment->{message},
    322                     post_id    => $post_id,
    323                     topic     => $self->get_topic($post_id),
     330                    post_id   => $post_id,
     331                    topic     => $topic,
    324332                    time      => asctime(localtime $comment_time),
    325333                   );
     
    355363}
    356364
     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
    357369sub facebook {
    358370    my $self = shift;
     
    361373    my $msg = shift;
    362374
    363     if (!defined $self->{facebook} || !$self->{logged_in}) {
    364         BarnOwl::admin_message('Facebook', 'You are not currently logged into Facebook.');
    365         return;
    366     }
    367375    if (defined $user) {
    368376        $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;
    370379    } 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;
    372382    }
    373383    $self->sleep(0);
     
    380390    my $msg = shift;
    381391
    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;
    383394    $self->sleep(0);
    384395}
     
    388399
    389400    my $url = shift;
     401
    390402    # http://www.facebook.com/connect/login_success.html#access_token=TOKEN&expires_in=0
    391403    $url =~ /access_token=([^&]+)/; # XXX Ew regex
     404
     405    if (!defined $1) {
     406        BarnOwl::message("Invalid URL.");
     407        return;
     408    }
    392409
    393410    $self->{cfg}->{token} = $1;
     
    402419    if ( ! defined $self->{cfg}->{token} ) {
    403420        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        );
    405428        return 0;
    406429    }
    407430    $self->{facebook}->access_token($self->{cfg}->{token});
    408431    # 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; };
    410433    if ($@) {
    411434        BarnOwl::admin_message('Facebook', "Failed to authenticate! Login to Facebook at ".$self->{login_url}
     
    421444}
    422445
    423 sub get_topic {
    424     my $self = shift;
    425 
    426     my $post_id = shift;
    427 
    428     return $self->{topics}->{$post_id} || 'personal';
    429 }
    430 
    4314461;
Note: See TracChangeset for help on using the changeset viewer.