Keeping Up with the Joneses: Activity Aggregation in Drupal

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.

Facebook's newsfeed feature introduced social network users to continuous updates of news about the goings-on in the lives of their friends and contacts. Activity aggregators have turned out to be a pretty useful feature for social networking sites, and can even be a little addictive when done right. Most sites that bill themselves as a social or professional network now have some kind of newsfeed, friend feed, lifestream or other feed.

The Activity module is your best bet if you want to aggregate activities from within your Drupal community. Its API is a little more complicated than similar modules like Activity Stream, but allows for a greater awareness of the context of activity items, so that their appearance can change depending on who is viewing them. Activity comes with seven contrib modules that cover most of the common activities on a community-based Drupal site (creating/editing nodes, adding/removing buddies, voting on content, etc.). This makes it very easy to get activity streams up and running on your site quickly.

Trellon recently finished a project that required the Activity module, and discovered that there's precious little documentation of the module's API, which makes it challenging to develop new plug-ins. This post contains the basics of what you need to know in order to work with friend feeds produced by the Activity module. As an example, we'll show how to build an add-on module that adds information about users' birthdays to their friends' activity streams. Note that this is a Drupal 5-centric post, since Activity is not yet available for Drupal 6, but many of the same lessons will apply.

Describing Activities

If you want to write a module that inserts customized activities into activity streams, you must first describe the types of activities your module will be recording, and provide sensible default text for the messages that will show up in the activity streams. To do this, developers should implement hook_activity_info(). That hook should return an array with keys for "ops", "types" and "roles".

  • ops are the types of activity operations that can be added. They correspond to the verb in the activity message. For instance, the Node activity module has ops for "create", "update" and "delete", which correspond to node operations. The value of the ops key is an array whose keys are machine-readable activity names, and whose values are human-readable versions of the keys.
  • types are the kinds of stuff that can be objects of activities. In other words, they are the different categories of things that users can perform activities on. This is useful for being able to configure different activity messages for operations performed on different kinds of stuff. Like operations, the types value is an array with machine-readable keys and human-readable names.
  • roles are classes of users who can view the activity. The activity item can be formatted based on the user viewing it. In our birthday example, displaying "incidentist has a birthday today!" is all well and good, but the message should be different if the user is the one viewing it. So we distinguish the "Birthday User" role from the "Everyone" role. We also have a "Buddies" role that can display a third type of message if the birthday user is a buddy of the user viewing the activity.

Here's what the birthday_activity_info() function looks like.

function birthdayactivity_activity_info() {
return array(
// the types of activity operations that can be added.
'ops' => array(
'birthday' => t('User birthday')),
'types' => array('birthday' => t("Birthday")),
'roles' => array(
'author' => array(
'#name' => t('Birthday User'),
'#description' => t('The user who has the birthday.'),
'#default' => 'Happy birthday from [site-name]!',
),
'buddy' => array(
'#name' => t('Buddies'),
'#description' => t('Buddies of the user with the birthday'),
'#default' => 'Your friend [username] has a birthday today!',
),
// This is what corresponds to ACTIVITY_ALL
'all' => array(
'#name' => t('Everyone'),
'#description' => t('The general public.'),
'#default' => '[username] has a birthday today!',
),
),
);
}

There's only one operation that our module records (the "having a birthday" operation), and it only operates on one type of data (birthdays). It seems redundant, but we need to specify them both anyway or they won't show up in the settings page. The "type" field is there for modules such as nodeactivity (one of the contrib modules), which can be configured differently for messages concerning different types of nodes.

On the Activity Settings page, you can set up the activity message template for every combination of operation, type and role defined in hook_activity_info(). Below you can see the settings for the buddylistactivity contrib module. The templates in the "All" role are left blank, so Buddylist activities will not be visible to the general public. The module project page notes that, after activating a module that provides new activity types and operations, you actually have to hit "Save" on this page in order to get streams to work properly.

The message templates rely heavily on tokens, which are placeholders such as [buddy-user-link] that are replaced with values when the page is loaded. If you want to define new tokens for your module to use, you will have to specify what the tokens are and how they should be replaced. This involves implementing two hooks -- hook_token_list() and hook_token_values() -- that are called by the Token module. Fortunately, that API is well-documented, so take a look. We won't define new tokens in our birthday module, because the only token it uses is [username], which is already set up for us.

Creating Activities

Use activity_insert() to create activities and define how they should be displayed. Here's an extended version of the PHPdoc for that function:

/**
* activity_insert - API function
* Insert an activity record. This gets called by modules wishing to record
* their activities.
* @param $module The name of the module that is doing the recording, eg. 'node'
* @param $type Modules can track more than one type of activity. For example,
* the nodeactivity module tracks activities for each content type separately.
* $type should be an identifier for the calling module to use.
* @param $operation The operation that was performed, as defined in hook_activity_info().
* @param $data An array that maps token names to their replacement values. The tokens
* must be defined in hook_token_list().
* @param $target_user_roles An array mapping user ids to the role that the activity should
* display when being viewed by that user. Roles are defined in hook_activity_info().
*/

Our birthday module implements hook_cron() to do a daily check of users' birthdays. It passes the list of birthday uids to the following function:

/**
* Creates a birthday activity item for each user in $uids.
* @param uids
* An array of user ids.
*/
function birthdayactivity_add_items($uids) {
foreach ($uids as $uid) {
$user = user_load(array('uid' => $uid));
if ($user) {
$data = array('username' => theme('username', $user));
$target_users_roles = array(
// the general public gets the "all" role
ACTIVITY_ALL => 'all',
// the user with the birthday gets the "author" role
$user->uid => 'author'
);
// each buddy of the birthday user gets the 'buddy' role
foreach ($buddies as $buddy_uid) {
$target_users_roles[$buddy_uid] => 'buddy';
}
activity_insert('birthdayactivity', 'birthday', 'birthday', $data, $target_users_roles);

}
}
}

Modifying Activities

If you want to modify the activities produced by other modules, there's yet another hook you have to tap into: hook_activityapi(). Here's some more extended PHPdoc:

/**
* hook_activityapi() - Modify activity items.
*
* @param $&activity
* An array of the form:
* 'created' => Timestamp of activity creation date,
* 'aid' => Activity ID,
* 'module' => The module that created the activity,
* 'type' => Type of activity (see hook_activity_info()),
* 'operation' => Operation of activity (see hook_activity_info()),
* 'data' => An array whose keys are token names and values are token
* replacement values. Also contains copies of the above values.
* @param $op
* A string containing the name of the nodeapi operation.
* 'insert' is called when a new activity is created
* 'load' is called when an activity is loaded
* 'render' is called before token replacement begins
* @return A single result or array of results. Note: these are not currently
* used by activity.module.
*/

Note that if you use the API to filter activities by setting some $&activity to NULL, you can run into paging issues. If you filter out 5 activities from a list of 11, the paging system still thinks it's getting 11 activities, and will add links to a second page even though it only displays 6 items.

Fetching Activities

Finally, you can put together a custom activity feed using activity_get_activity(), which can filter items according to the criteria one would expect:

/**
* activity_get_activity() - API function
* Retrieves a filtered array of activities.
* The API supports:
* @param $uids
* - a single uid
* - an array of uids
* - can include the special uid ACTIVITY_ALL
* @param $filters
* - an array where keys are one of module, type, operation, target_role
* - values are arrays of possible values for the keys. The key of the
* array of possible values can be 'include' or 'exclude' to indicate
* if the filter is positive or negative
* For example:
* array('target_role' => array('include' => 'Author'), 'operation' =>
* array('include' => 'delete'))
* this would find activity where the author had deleted something.
* Example 2:
* array('target_role' => array('include' => array('Requester', 'Requestee')))
* This shows that the values can be arrays as well.
* Example 3:
* array('module' => array('include' => array('nodeactivity',
* 'commentactivity')), 'operation' => array('exclude' => array('delete',
* 'unpublish')))
* @param $limit
* The number of results desired
* @param $tablesort_headers
* An array that determines the sorting of the result set.
* @return
* An array of activities that match the filter conditions.
*
*/

In our birthday example, a block that listed only birthdays of friends would look a little something like:

/**
* Shows recent birthdays of the current user's friends.
*/
function birthdayactivity_block_content() {
global $user;
$uids = array_keys(buddylist_get_buddies($user->uid, 'uid'));
$activities = activity_get_activity((empty($uids) ? $user->uid : $uids),
array('module' => 'birthdayactivity'), 5);
$table = activity_table($activities);
return $table;
}

Personalized activity streams are a great way for users to get relevant information about what's going on in their online community. The Activity module is a flexible way to offer activity-stream-like features to your users, but its flexibility can make it hard to understand without guidance. This post covered the basics, but the best way to get a feel for the possibilities of activity streams is to take a look at the module's contrib directory.

Now if you'll excuse me, I need to turn large chunks of this post into a documentation patch...