You are here

fb_friend.module in Drupal for Facebook 7.3

Same filename and directory in other branches
  1. 6.3 contrib/fb_friend.module
  2. 6.2 contrib/fb_friend.module

This module implements several features specific to Facebook friend networks.

The invite blocks work by building a structure, similar to a drupal form array, which reflects the (X)FBML markup which will be used. Then drupal_alter is called, allowing third-party modules to customize the markup. Implementing those alter hooks is the best way to change any aspects of the invite form.

File

contrib/fb_friend.module
View source
<?php

/**
 * @file
 * This module implements several features specific to Facebook friend networks.
 *
 * The invite blocks work by building a structure, similar to a drupal
 * form array, which reflects the (X)FBML markup which will be used.
 * Then drupal_alter is called, allowing third-party modules to
 * customize the markup.  Implementing those alter hooks is
 * the best way to change any aspects of the invite form.
 */

//// Block deltas
define('FB_FRIEND_DELTA_AUTHORIZED', 'fb_friend_authorized');
define('FB_FRIEND_DELTA_INVITE_PAGE', 'fb_friend_invite_page');
define('FB_FRIEND_DELTA_INVITE_APP', 'fb_friend_invite_app');

//// Menu paths
define('FB_FRIEND_PATH_REQUEST_SUBMIT', 'fb_friend/submit');
define('FB_FRIEND_PATH_REQUEST_SUBMIT_ARGS', 2);
define('FB_FRIEND_PATH_REQUEST_ACCEPT', 'fb_friend/accept');
define('FB_FRIEND_PATH_REQUEST_ACCEPT_ARGS', 2);

//// Hook_fb_friend operations
define('FB_FRIEND_OP_REQUEST_SUBMIT', 'fb_friend_request_submit');
define('FB_FRIEND_OP_REQUEST_SKIP', 'fb_friend_request_skip');
define('FB_FRIEND_OP_REQUEST_ACCEPT', 'fb_friend_request_accept');

//// Request/invite status
define('FB_FRIEND_STATUS_REQUEST_ACCEPTED', 2);
function fb_friend_menu() {
  $items = array();

  // Facebook will send user here after invites have been sent (or skipped).
  $items[FB_FRIEND_PATH_REQUEST_SUBMIT] = array(
    'page callback' => 'fb_friend_request_submit_page',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items[FB_FRIEND_PATH_REQUEST_ACCEPT] = array(
    'page callback' => 'fb_friend_request_accept_page',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}
function fb_friend_request_submit_page() {

  // Parse args
  $params = array();
  $i = FB_FRIEND_PATH_REQUEST_SUBMIT_ARGS;
  while (($key = arg($i)) && ($value = arg($i + 1))) {
    $params[$key] = $value;
    $i += 2;
  }
  if (isset($_POST['ids']) && count($_POST['ids'])) {
    $params['ids'] = $_POST['ids'];

    // Notify third parties
    $redirect = fb_invoke(FB_FRIEND_OP_REQUEST_SUBMIT, $params, NULL, 'fb_friend');
  }
  else {

    // User pressed skip instead of inviting.
    $redirect = fb_invoke(FB_FRIEND_OP_REQUEST_SKIP, $params, NULL, 'fb_friend');
  }
  if ($redirect) {
    if (FALSE && function_exists('fb_canvas_goto')) {
      fb_canvas_goto($redirect);
    }
    else {
      drupal_goto($redirect);
    }
  }
  return "fb_friend_request_submit_page";
}
function fb_friend_request_accept_page() {

  // Parse args
  $params = array();
  $i = FB_FRIEND_PATH_REQUEST_SUBMIT_ARGS;
  while (($key = arg($i)) && ($value = arg($i + 1))) {
    $params[$key] = $value;
    $i += 2;
  }
  if ($fbu = fb_facebook_user()) {

    // Confirm invitation is in the fb_friend table.
    $where = array(
      'fbf.status > %d',
      'fbf.fbu_target = %d',
    );
    $args = array(
      0,
      $fbu,
    );
    foreach (array(
      'ref_id' => '%s',
      'module' => "'%s'",
    ) as $key => $pattern) {
      if (isset($params[$key])) {
        $where[] = "fbf.{$key} = {$pattern}";
        $args[] = $params[$key];
      }
    }
    $result = db_query("SELECT * FROM {fb_friend} AS fbf WHERE " . implode(' AND ', $where), $args);
    $rows = array();
    while ($row = db_fetch_array($result)) {

      // Update fb_friend.status to indicate accepted.
      $row['status'] = FB_FRIEND_STATUS_REQUEST_ACCEPTED;
      if (fb_friend_write_record($row, 'fb_friend_id')) {
        $rows[] = $row;
      }
    }
    $params['fb_friend'] = $rows;
  }

  // Notify third parties
  $redirect = fb_invoke(FB_FRIEND_OP_REQUEST_ACCEPT, $params, NULL, 'fb_friend');
  if ($redirect) {
    if (FALSE && function_exists('fb_canvas_goto')) {
      fb_canvas_goto($redirect);
    }
    else {
      drupal_goto($redirect);
    }
  }
  return "fb_friend_request_accept_page";
}
function fb_friend_request_submit_path($params) {
  $path = FB_FRIEND_PATH_REQUEST_SUBMIT;
  foreach ($params as $key => $value) {
    $path .= "/{$key}/{$value}";
  }
  return $path;
}
function fb_friend_request_accept_path($params) {
  $path = FB_FRIEND_PATH_REQUEST_ACCEPT;
  foreach ($params as $key => $value) {
    $path .= "/{$key}/{$value}";
  }
  return $path;
}

/**
 * Implementation of hook_block().
 *
 * Blocks include
 * - Invite friends to install the application.
 * - Invite friends to visit the page currently being browsed.
 * - Show which friends are already users of the current application.
 *
 */
function fb_friend_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $items = array();

    // Blocks which use the current application.
    $items[FB_FRIEND_DELTA_INVITE_PAGE] = array(
      'info' => t('Invite Facebook friends to current page'),
    );
    $items[FB_FRIEND_DELTA_INVITE_APP] = array(
      'info' => t('Invite Facebook friends to install the current app'),
    );
    $items[FB_FRIEND_DELTA_AUTHORIZED] = array(
      'info' => t('Facebook friends who have authorized the current app'),
    );
    return $items;
  }
  elseif ($op == 'view') {
    try {

      // Calls to facebook can fail.
      // None of our blocks are shown if user is not logged in.
      $fbu = fb_facebook_user();
      if (!$fbu) {
        return;
      }

      // Our blocks use current application
      global $_fb, $_fb_app;
      if ($delta == FB_FRIEND_DELTA_INVITE_PAGE) {
        $content = fb_friend_request_content(array(
          'module' => 'fb_friend_invite_page',
          'delta' => FB_FRIEND_DELTA_INVITE_PAGE,
        ), array(
          'hook' => 'fb_friend_invite_page',
          'invite' => TRUE,
          'action_path' => request_path(),
          // Where sender goes on submit or skip.
          'accept_path' => request_path(),
        ));
        return array(
          'subject' => t('Invite friends to visit this page'),
          'content' => drupal_render($content),
        );
      }
      elseif ($delta == FB_FRIEND_DELTA_INVITE_APP) {

        // Exclude users who have already installed.
        $rs = fb_fql_query($_fb, "SELECT uid FROM user WHERE has_added_app=1 and uid IN (SELECT uid2 FROM friend WHERE uid1 = {$fbu})");

        // FQL not SQL, no {curly_brackets}
        $exclude_ids = array();

        //  Build an delimited list of users...
        if ($rs) {
          foreach ($rs as $data) {
            $exclude_ids[] = $data["uid"];
          }
        }
        $content = fb_friend_request_content(array(
          'module' => 'fb_friend_invite_app',
          'delta' => FB_FRIEND_DELTA_INVITE_APP,
          'next' => request_path(),
        ), array(
          'hook' => 'fb_friend_invite_app',
          'invite' => TRUE,
          'title' => variable_get('site_name', $GLOBALS['_fb_app']->title),
          'exclude_ids' => $exclude_ids,
        ));
        return array(
          'subject' => t('Invite friends to this application'),
          'content' => drupal_render($content),
        );
      }
      elseif ($delta == FB_FRIEND_DELTA_INVITE_APP && FALSE) {

        // Old way...
        $app_url = url('<front>', array(
          'absolute' => TRUE,
          'fb_canvas' => fb_is_canvas(),
        ));
        $page_url = url(request_path(), array(
          'absolute' => TRUE,
          'fb_canvas' => fb_is_canvas(),
        ));
        $site_name = variable_get('site_name', t('application'));

        // Build the alterable data structure.
        // http://wiki.developers.facebook.com/index.php/Fb:request-form
        $fbml = fb_form_requestform(array(
          'type' => $site_name,
          'content' => array(
            'markup' => array(
              '#value' => t('You may like this site - <a href="!url">!site</a>.', array(
                '!url' => $app_url,
                '!site' => $site_name,
              )),
            ),
            'choice' => array(
              '#type' => 'fb_form_req_choice',
              '#attributes' => array(
                'url' => $app_url,
                'label' => t('Accept'),
              ),
            ),
          ),
          'invite' => TRUE,
          'action' => $page_url,
          'method' => 'POST',
        ));

        // Exclude users who have already installed.
        $rs = fb_fql_query($_fb, "SELECT uid FROM user WHERE has_added_app=1 and uid IN (SELECT uid2 FROM friend WHERE uid1 = {$fbu})");

        // FQL not SQL, no {curly_brackets}
        // @TODO - confirm new API returns same data structure in $rs!
        $arFriends = '';
        $exclude_ids = '';

        //  Build an delimited list of users...
        if ($rs) {
          $exclude_ids .= $rs[0]["uid"];
          for ($i = 1; $i < count($rs); $i++) {
            if ($exclude_ids != "") {
              $exclude_ids .= ",";
            }
            $exclude_ids .= $rs[$i]["uid"];
          }
        }
        $fbml['selector'] = fb_form_multi_selector(array(
          'actiontext' => t('Invite friends'),
          'exclude_ids' => $exclude_ids,
        ));

        // Allow third-party to modify the form
        drupal_alter('fb_friend_invite_app', $fbml);
        if ($fbml) {

          // Wrap in serverfbml.
          $xfbml = array(
            '#type' => 'fb_form_serverfbml',
            'fbml' => $fbml,
          );
        }

        // Allow third-party to modify wrapper
        drupal_alter('fb_friend_invite_app_wrap', $xfbml);
        $content = drupal_render($xfbml);
        return array(
          'subject' => t('Invite friends to use !site', array(
            '!site' => $site_name,
          )),
          'content' => $content,
        );
      }
      elseif ($delta == FB_FRIEND_DELTA_AUTHORIZED) {
        if ($fbu = fb_facebook_user()) {

          // Get list of friends who have authorized this app.
          $rs = fb_fql_query($_fb, "SELECT uid FROM user WHERE has_added_app=1 AND uid IN (SELECT uid2 FROM friend WHERE uid1 = {$fbu})");

          // FQL not SQL, no {curly_brackets}
          // @TODO - confirm the data structure returned from new API still works with this code.
          if (isset($rs) && is_array($rs)) {
            foreach ($rs as $friend_data) {
              $friend_fbu = $friend_data['uid'];

              // TODO: make size and markup configurable
              $content .= "<fb:profile-pic uid={$friend_fbu} size=square></fb:profile-pic>";
            }
          }
          $subject = t('Friends who use !app', array(
            '!app' => $GLOBALS['_fb_app']->label,
          ));

          // TODO - fix title
          return array(
            'subject' => $subject,
            'content' => $content,
          );
        }
      }
    } catch (Exception $e) {

      // We reach this when Facebook Connect sessions are no longer valid.
      // Javascript should detect this and refresh.  Relatively harmless.
      if (fb_verbose() === 'extreme') {
        fb_log_exception($e, t('Failed to render fb_friend block.'));
      }
    }
  }
}

/**
 * Implements hook_fb_friend().
 */
function fb_friend_fb_friend($op, $data, &$return) {
  if ($data['module'] != 'fb_friend_invite_app') {
    return;
  }
  if ($data['delta'] == FB_FRIEND_DELTA_INVITE_APP) {
    if ($op == FB_FRIEND_OP_REQUEST_SUBMIT) {

      // Save the invites to fb_friend table.
      $data['data'] = serialize(array(
        'delta' => $data['delta'],
      ));
      $results = fb_friend_write_records($data, $data['ids']);
      $return = $data['next'];
    }
    elseif ($op == FB_FRIEND_OP_REQUEST_SKIP) {
      $return = $data['next'];
    }
    elseif ($op == FB_FRIEND_OP_REQUEST_ACCEPT) {

      // @TODO present a connect button?
      $return = '<front>';
    }
  }
}

/**
 * @deprecated use fb_friend_request_content instead.
 */
function fb_friend_invite_page_fbml($alter_hook = NULL, $submit_path = NULL, $accept_path = NULL) {

  // Defaults
  if (!isset($submit_path)) {
    $submit_path = request_path();
  }
  if (!isset($accept_path)) {
    $accept_path = request_path();
  }
  $submit_url = url($submit_path, array(
    'absolute' => TRUE,
  ));
  $accept_url = url($accept_path, array(
    'absolute' => TRUE,
  ));

  // Build the alterable data structure.
  // http://developers.facebook.com/docs/reference/fbml/request-form
  $fbml = fb_form_requestform(array(
    'type' => variable_get('site_name', t('page view')),
    //'style' => "width: 500px;",
    'content' => array(
      'markup' => array(
        '#value' => t('You may want to see this. <a href="!url">!title</a>', array(
          '!url' => $accept_url,
          '!title' => drupal_get_title(),
        )),
      ),
      'choice' => array(
        '#type' => 'fb_form_req_choice',
        '#attributes' => array(
          'url' => $accept_url,
          'label' => t('Accept'),
        ),
      ),
    ),
    'invite' => TRUE,
    'action' => $submit_url,
    'method' => 'POST',
  ));
  $fbml['selector'] = fb_form_multi_selector(array(
    'actiontext' => t('Invite friends'),
  ));

  // Allow third-party to modify the form
  if (isset($alter_hook)) {
    drupal_alter($alter_hook, $fbml);
  }
  if ($fbml) {

    // Wrap in serverfbml.
    $xfbml = array(
      '#type' => 'fb_form_serverfbml',
      'fbml' => $fbml,
      '#prefix' => '<div class="fb-request-form">',
      '#suffix' => '</div>',
    );

    // Allow third-party to modify wrapper
    if (isset($alter_hook)) {
      drupal_alter($alter_hook . '_wrap', $xfbml);
    }
    return $xfbml;
  }
}

/**
 * Builds a data structure, similar to Drupal's form API structure, which renders a facebook request-form.
 *
 * By default, hook_fb_friend will be invoked both when the user submits the
 * invitation form and when their friends accept the invitation.  (Default
 * behavior can be overridden by passing parameters in $form_data.)
 *
 * @param $invite_data
 * Name-value pairs describing the invitation or request.  Recommended to include:
 *   - 'module': Not necessarily the name of a module, but identifies which module is responsible for this invite.
 *   - 'ref_id': Node id or other drupal object.  Along with 'module', identifies what the invitation is about.
 *
 * @param $form_data
 * Name-value pairs used in building the request-form markup.
 * @TODO - better documentation here.
 */
function fb_friend_request_content($invite_data, $form_data) {

  // defaults
  $defaults = array(
    'action_path' => fb_friend_request_submit_path($invite_data),
    'action_text' => 'Invite',
    'accept_path' => fb_friend_request_accept_path($invite_data),
    'accept_text' => 'Accept',
    'exclude_ids' => array(),
    'text' => 'has invited you to <a href="!url">!title</a>.',
    'title' => drupal_get_title(),
    'type' => variable_get('site_name', t('page view')),
    'invite' => FALSE,
    'target' => '_top',
    // _top helpful on canvas pages.
    'hook' => NULL,
    'ref_id' => NULL,
    'module' => NULL,
    'style' => NULL,
  );
  $params = array_merge($defaults, $form_data, $invite_data);
  $submit_url = url($params['action_path'], array(
    'absolute' => TRUE,
    'fb_canvas' => FALSE,
  ));

  // No canvas, http://forum.developers.facebook.net/viewtopic.php?pid=209230#p209230
  $accept_url = url($params['accept_path'], array(
    'absolute' => TRUE,
    'fb_canvas' => fb_is_canvas(),
  ));

  // Learn which users to exclude
  $result = db_query("SELECT fbu_target FROM {fb_friend} WHERE module='%s' AND fbu_actor=%d AND ref_id=%d", $params['module'], fb_facebook_user(), $params['ref_id']);
  while ($data = db_fetch_object($result)) {
    $params['exclude_ids'][] = $data->fbu_target;
  }
  if (!$params['title']) {
    $params['title'] = variable_get('site_name', $GLOBALS['_fb_app']->title);
  }

  //dpm($params['exclude_ids'], "exclude_ids"); // debug

  // Build the alterable data structure.
  // http://developers.facebook.com/docs/reference/fbml/request-form
  $fbml = fb_form_requestform(array(
    'type' => $params['type'],
    'style' => $params['style'],
    'content' => array(
      'markup' => array(
        '#value' => t($params['text'], array(
          '!url' => $accept_url,
          '!title' => t($params['title']),
        )),
      ),
      'choice' => array(
        '#type' => 'fb_form_req_choice',
        '#attributes' => array(
          'url' => $accept_url,
          'label' => t($params['accept_text']),
        ),
      ),
    ),
    'invite' => $params['invite'],
    'action' => $submit_url,
    'method' => 'POST',
    'target' => $params['target'],
  ));
  $fbml['selector'] = fb_form_multi_selector(array(
    'actiontext' => t($params['action_text']),
    'target' => $params['target'],
    'exclude_ids' => implode(',', $params['exclude_ids']),
  ));

  // Change some of facebook's bizarre defaults.
  foreach (array(
    'cols',
    'email_invite',
    'import_external_friends',
  ) as $key) {
    if (isset($params[$key])) {
      $fbml['selector']['#attributes'][$key] = $params[$key];
    }
  }

  // Allow third-party to modify the form
  if ($params['hook']) {
    drupal_alter($params['hook'], $fbml);
  }
  if ($fbml) {

    // Wrap in serverfbml.
    $xfbml = array(
      '#type' => 'fb_form_serverfbml',
      'fbml' => $fbml,
      '#prefix' => '<div class="fb-request-form">',
      '#suffix' => '</div>',
    );

    // Allow third-party to modify wrapper
    if ($params['hook']) {
      drupal_alter($params['hook'] . '_wrap', $xfbml);
    }
    return $xfbml;
  }
}
function fb_friend_write_record(&$row, $update = array()) {
  if (!isset($row['created'])) {
    $row['created'] = REQUEST_TIME;
  }
  if (!isset($row['fbu_actor'])) {
    $row['fbu_actor'] = fb_facebook_user();
  }
  if (!isset($row->label) && isset($GLOBALS['_fb_app'])) {
    $row['label'] = $GLOBALS['_fb_app']->label;
  }
  return drupal_write_record('fb_friend', $row, $update);
}
function fb_friend_write_records($data, $targets) {
  if (!isset($data['created'])) {
    $data['created'] = REQUEST_TIME;
  }
  if (!isset($data['fbu_actor'])) {
    $data['fbu_actor'] = fb_facebook_user();
  }
  if (!isset($row->label) && isset($GLOBALS['_fb_app'])) {
    $data['label'] = $GLOBALS['_fb_app']->label;
  }
  $rows = array();
  foreach ($targets as $fbu) {
    $row = $data;
    $row['fbu_target'] = $fbu;
    $result = fb_friend_write_record($row);
    if ($result) {
      $rows[] = $row;
    }
  }
  return $rows;
}
function fb_friend_query_records($args) {
  $types = array(
    'fbu_actor' => '%d',
    'fbu_target' => '%d',
    'ref_id' => '%d',
    'module' => "'%s'",
    'label' => "'%s'",
  );
  $where = array();
  $a = array();
  foreach ($args as $key => $value) {
    if ($type = $types[$key]) {
      $where[] = "{$key}={$type}";
      $a[] = $value;
    }
  }
  $result = db_query("SELECT * FROM {fb_friend} WHERE " . implode(' AND ', $where), $a);
  $items = array();
  while ($row = db_fetch_array($result)) {
    $items[$row['fb_friend_id']] = $row;
  }
  return $items;
}