I am working on a Notifications plugin, and after starting to write my notes down about how to do this, decided to just post them here. Please feel free to come make modifications and changes. Eventually I hope to post this on the Drupal handbook as well. Thanks. --Adrian
Sending automated e-mails from Drupal using Messaging and Notifications
To implement a notifications plugin, you must implement the following functions:
- Use hook_messaging, hook_token_list and hook_token_values to create the messages that will be sent.
- Use hook_notifications to create the subscription types
- Add code to fire events (eg in hook_nodeapi)
- Add all UI elements to allow users to subscribe/unsubscribe
Understanding Messaging
The Messaging module is used to compose messages that can be delivered using various formats, such as simple mail, HTML mail, Twitter updates, etc. These formats are called "send methods." The backend details do not concern us here; what is important are the following concepts:
TOKENS: tokens are provided by the "tokens" module. They allow you to write keywords in square brackets, [like-this], that can be replaced by any arbitrary value. Note: the token groups you create must match the keys you add to the $events->objects[$key] array.
MESSAGE KEYS: A key is a part of a message, such as the greetings line. Keys can be different for each send method. For example, a plaintext mail's greeting might be "Hi, [user]," while an HTML greeing might be "Hi, [user]," and Twitter's might just be "[user-firstname]: ". Keys can have any arbitrary name. Keys are very simple and only have a machine-readable name and a user-readable description, the latter of which is only seen by admins.
MESSAGE GROUPS: A group is a bunch of keys that often, but not always, might be used together to make up a complete message. For example, a generic group might include keys for a greeting, body, closing and footer. Groups can also be "subclassed" by selecting a "fallback" group that will supply any keys that are missing. Groups are also associated with modules; I'm not sure what these are used for.
Understanding Notifications
The Notifications module revolves around the following concepts:
SUBSCRIPTIONS: Notifications plugins may define one or more types of subscriptions. For example, notifications_content defines subscriptions for:
- Threads (users are notified whenever a node or its comments change)
- Content types (users are notified whenever a node of a certain type is created or is changed)
- Users (users are notified whenever another user is changed)
Subscriptions refer to both the user who's subscribed, how often they wish to be notified, the send method (for Messaging) and what's being subscribed to. This last part is defined in two steps. Firstly, a plugin defines several "subscription fields" (through a hook_notifications op of the same name), and secondly, "subscription types" (also an op) defines which fields apply to each type of subscription. For example, notifications_content defines the fields "nid," "author" and "type," and the subscriptions "thread" (nid), "nodetype" (type), "author" (author) and "typeauthor" (type and author), the latter referring to something like "any STORY by JOE." Fields are used to link events to subscriptions; an event must match all fields of a subscription (for all normal subscriptions) to be delivered to the recipient.
The $subscriptions object is defined in subsequent sections. Notifications prefers that you don't create these objects yourself, preferring you to call the notifications_get_link() function to create a link that users may click on, but you can also use notifications_save_subscription and notifications_delete_subscription to do it yourself.
EVENTS: An event is something that users may be notified about. Plugins create the $event object then call notifications_event($event). This either sends out notifications immediately, queues them to send out later, or both. Events include the type of thing that's changed (eg 'node', 'user'), the ID of the thing that's changed (eg $node->nid, $user->uid) and what's happened to it (eg 'create'). These are, respectively, $event->type, $event->oid (object ID) and $event->action.
Warning: notifications_content_nodeapi also adds a $event->node field, referring to the node itself and not just $event->oid = $node->nid. This is not used anywhere in the core notifications module; however, when the $event is passed back to the 'query' op (see below), we assume the node is still present.
Events do not refer to the user they will be referred to; instead, Notifications makes the connection between subscriptions and events, using the subscriptions' fields.
MATCHING EVENTS TO SUBSCRIPTIONS: An event matches a subscription if it has the same type as the event (eg "node") and if the event matches all the correct fields. This second step is determined by the "query" hook op, which is called with the $event object as a parameter. The query op is responsible for giving Notifications a value for all the fields defined by the plugin. For example, notifications_content defines the 'nid', 'type' and 'author' fields, so its query op looks like this (ignore the case where $event_or_user = 'user' for now):
$event_or_user = $arg0;
$event_type = $arg1;
$event_or_object = $arg2;
if ($event_or_user == 'event' && $event_type == 'node' && ($node = $event_or_object->node) ||
$event_or_user == 'user' && $event_type == 'node' && ($node = $event_or_object)) {
$query[]['fields'] = array(
'nid' => $node->nid,
'type' => $node->type,
'author' => $node->uid,
);
return $query;
After extracting the $node from the $event, we set $query[]['fields'] to a dictionary defining, for this event, all the fields defined by the module. As you can tell from the presence of the $query object, there's way more you can do with this op, but they are not covered here.
DIGESTING AND DEDUPING:
Understanding the relationship between Messaging and Notifications
Usually, the name of a message group doesn't matter, but when being used with Notifications, the names must follow very strict patterns. Firstly, they must start with the name "notifications," and then are followed by either "event" or "digest," depending on whether the message group is being used to represent either a single event or a group of events.
For 'events,' the third part of the name is the "type," which we get from Notification's $event->type (eg: notifications_content uses 'node'). The last part of the name is the operation being performed, which comes from Notification's $event->action. For example:
- notifications-event-node-comment might refer to the message group used when someone comments on a node
- notifications-event-user-update to a user who's updated their profile
Hyphens cannot appear anywhere other than to separate the parts of these words.
For 'digest' messages, the third and fourth part of the name come from hook_notification's "event types" callback, specifically this line:
$types[] = array(
'type' => 'node',
'action' => 'insert',
...
'digest' => array('node', 'type'),
);
$types[] = array(
'type' => 'node',
'action' => 'update',
...
'digest' => array('node', 'nid'),
);
In this case, the first event type (node insertion) will be digested with the notifications-digest-node-type message template providing the header and footer, likely saying something like "the following [type] was created." The second event type (node update) will be digested with the notifications-digest-node-nid message template.
Data Structure and Callback Reference
$event
The $event object has the following members:
- $event->type: The type of event. Must match the type in hook_notification::"event types". {notifications_event}
- $event->action: The action the event describes. Most events are sorted by [$event->type][$event->action]. {notifications_event}.
- $event->object[$object_type]: All objects relevant to the event. For example, $event->object['node'] might be the node that the event describes. $object_type can come from the 'event types' hook (see below). The main purpose appears to be to be passed to token_replace_multiple as the second parameter. $event->object[$event->type] is assumed to exist in the short digest processing functions, but this doesn't appear to be used anywhere. Not saved in the database; loaded by hook_notifications::"event load"
- $event->oid: apparently unused. The id of the primary object relevant to this event (eg the node's nid).
- $event->module: apparently unused
- $event->params[$key]: Mainly a place for plugins to save random data. The main module will serialize the contents of this array but does not use it in any way. However, notifications_ui appears to do something weird with it, possibly by using subscriptions' fields as keys into this array. I'm not sure why though.
hook_notifications
op 'subscription types': returns an array of subscription types provided by the plugin, in the form $key => array(...) with the following members:
- event_type: this subscription can only match events whose $event->type has this value. Stored in the database as notifications.event_type for every individual subscription. Apparently, this can be overiden in code but I wouldn't try it (see notifications_save_subscription).
- fields: an unkeyed array of fields that must be matched by an event (in addition to the event_type) for it to match this subscription. Each element of this array must be a key of the array returned by op 'subscription fields' which in turn must be used by op 'query' to actually perform the matching.
- title: user-readable title for their subscriptions page (eg the 'type' column in user/%uid/notifications/subscriptions)
- description: a user-readable description.
- page callback: used to add a supplementary page at user/%uid/notifications/blah. This and the following are used by notifications_ui as a part of hook_menu_alter. Appears to be partially deprecated.
- user page: user/%uid/notifications/blah.
op 'subscription fields': returns a keyed array of detailed information about how to match events to subscriptions. The key must be an element of the 'fields' array returned by op 'subscription types' and will be used by op 'query'; each value is an array in the following format. For more on fields, see examples above.
- 'type': either 'int' or else assumed to be a string. Used to create a typesafe query; eg int will become %d
- 'field': does not appear to be used for anything...
- 'name': user-readable name, used for subscription form
- 'format callback': given a field, returns a user-friendly description of the field. Eg if the field is a nid, the function would return a translated string with the name of the node.
- 'autocomplete path': used by notifications_ui
- 'autocomplete callback': "
- 'value callback': "
op 'query': returns a $query object used to build the query. I have no idea how most of this works, but under normal circumstances you'll want to only add one thing to this object as follows: $query[]['fields'] = array( 'fieldname 1' => $field_val_1, 'fieldname 2' => $field_val_2, // etc ); For this op, $arg0 is either 'event' or 'user,' and $arg1 is the event type (either $event->type or event_type from op 'subscription types'). If $arg0 is 'event', $arg2 is the $event object; if $arg0 is 'user', $arg2 is the $object parameter in notifications_user_get_subscriptions, which is not actually used by the core notifications but is a useful API for other modules. In practice, this typically means that $arg2 will be the 'thing' that's being subscribed to (eg a $node).
op 'event types': returns an unkeyed array of event types, with each event type being an array with the following members:
- type: this must match $event->type and is used to look up the rest of the info.
- action: this must match $event->action and is used to look up the rest of the info. It would probably have been better for this op to return an array of types indexed by $type and $action (eg $event_types['type']['action'] = array(everything else)), but what can you do? "type" and "action" can be used in the admin pages to disable certain types of events from being fired - eg, if you don't want to tell people when you're deleting their stuff, you might disable a type='node', action='delete' event.
- digest: an array with two ordered (non-keyed) elements, "type" and "field." 'type' is used as an index into $event->objects. 'field' is also used to group events like so: $event->objects[$type]->$field. For example, 'field' might be 'nid' - if the object is a node, the digest lines will be grouped by node ID. Finally, both are used to find the correct Messaging template; see discussion above. If missing, the first element will default to $event->type and the second to $event->action.
- description: used on the admin "Notifications->Events" page
- name: unused, use Messaging instead
- line: deprecated, use Messaging instead
op 'event load': the event is &$arg0. Typically, $event->params will already be loaded and you can use this to load up $event->objects, which are used as part of token processing. For example, you might say $event->objects['node'] = node_load($event->params['nid']).
op 'access': not exactly sure how this works, but returns an array containing a true value if the user $arg1 is allowed to subscribe to the object $arg2 (if $arg0 is 'subscribe') or receive an event pertaining to object $arg2 (if $arg0 is 'event'). If you don't want to worry about this, say return array(TRUE);
Other Stuff
This is an example of the main query that inserts an event into the queue. Note how the queue will hold one row for every message that will be sent out (eg one per subscribed user) as a result of the event being fired.
INSERT INTO {notifications_queue}
(uid,
destination,
sid,
module,
eid,
send_interval,
send_method,
cron,
created,
conditions)
SELECT DISTINCT
s.uid,
s.destination,
s.sid,
s.module,
%d, // event ID
s.send_interval,
s.send_method,
s.cron,
%d, // time of the event
s.conditions
FROM {notifications} s
INNER JOIN {notifications_fields} f ON s.sid = f.sid
WHERE (s.status = 1)
AND (s.event_type = '%s') // subscription type
AND (s.send_interval >= 0)
AND (s.uid <> %d)
AND (
(f.field = '%s' AND f.intval IN (%d)) // everything from 'query' op
OR (f.field = '%s' AND f.intval = %d)
OR (f.field = '%s' AND f.value = '%s')
OR (f.field = '%s' AND f.intval = %d))
GROUP BY
s.uid,
s.destination,
s.sid,
s.module,
s.send_interval,
s.send_method,
s.cron,
s.conditions
HAVING s.conditions = count(f.sid)