- Getting users back to buddycloud using email and push notifications
- "Someone commented on your photo"
- "Someone commented in a thread you commented in"
https://github.com/buddycloud/buddycloud-pusher/blob/master/resources/schema/create-schema.sql
Should we store time for last sent event in order to avoid oversending?
- New component: pusher.example.com
- Receives triggers from buddycloud channel server
- Only reacts on triggers from buddycloud server (avoid strange uses)
- Stores emails addresses during users signup
- Stores preferences regarding email notification
The Buddycloud server sends standard XEP-0060 notification messages to the pusher, as in http://www.xmpp.org/extensions/xep-0060.html#intro-howitworks.
The Pusher basic design relies on Message Consumers and on Query Handlers.
A Message Consumer must figure out what notifications to trigger based on a message arriving from the Buddycloud server, and then create an IQ to be sent back to the pusher itself.
For instance, the UserPostedMentionConsumer applies a regular expression to the message content in order to figure out whether there is a mention in such a message, creates an IQ with the notification and sends it back to the pusher via loopback:
public void consume(Message message, List<String> recipients) {
Element eventEl = message.getChildElement("event", ConsumerUtils.PUBSUB_EVENT_NS);
Element itemsEl = eventEl.element("items");
AtomEntry entry = AtomEntry.parse(itemsEl);
if (entry == null) {
return;
}
Matcher matcher = JID_REGEX.matcher(entry.getContent());
while (matcher.find()) {
String mentionedJid = matcher.group();
newMention(mentionedJid, entry, recipients);
}
}
private void newMention(String mentionedJid, AtomEntry entry, List<String> recipients) {
if (mentionedJid.equals(entry.getAuthor()) || recipients.contains(mentionedJid)) {
return;
}
IQ iq = createIq(entry.getAuthor(), mentionedJid,
entry.getNode(), entry.getContent());
try {
IQ iqResponse = getXmppComponent().handleIQLoopback(iq);
if (iqResponse.getError() == null) {
recipients.add(mentionedJid);
}
} catch (Exception e) {
LOGGER.warn(e);
}
}
private IQ createIq(String authorJid, String mentionedJid,
String channelJid, String content) {
IQ iq = new IQ();
Element queryEl = iq.getElement().addElement("query",
"http://buddycloud.com/pusher/userposted-mention");
queryEl.addElement("authorJid").setText(authorJid);
queryEl.addElement("mentionedJid").setText(mentionedJid);
queryEl.addElement("channel").setText(ConsumerUtils.getChannelAddress(channelJid));
queryEl.addElement("postContent").setText(content);
return iq;
}
A Query Handler, on its turn, will process the IQ sent by the Message Consumer based on its namespace. It will decide whether the recipient should receive this notification, create a data dictionary to be sent and then call all the push strategies (email, GCM, ...) passing this data dictionary.
The UserPostedMentionQueryHandler, for instance, reads the IQ sent by the UserPostedMentionConsumer, creates the data dictionary and then decide if the recipient should receive this notification based on its notification settings:
Map<String, String> tokens = new HashMap<String, String>();
|tokens.put("AUTHOR_JID", authorJid);
tokens.put("MENTIONED_JID", mentionedJid);
tokens.put("CHANNEL_JID", channelJid);
tokens.put("CONTENT", postContent);
List<NotificationSettings> allNotificationSettings = NotificationUtils.getNotificationSettings(
mentionedJid, getDataSource());
for (NotificationSettings notificationSettings : allNotificationSettings) {
if (!notificationSettings.getPostMentionedMe()) {
getLogger().warn("User " + mentionedJid + " won't receive mention notifications.");
continue;
}
if (notificationSettings.getTarget() == null) {
getLogger().warn("User " + mentionedJid + " has no target registered.");
continue;
}
Pusher pusher = Pushers.getInstance(getProperties()).get(notificationSettings.getType());
pusher.push(notificationSettings.getTarget(), Event.MENTION, tokens);
}
New Query Handlers should be registered in the [XMPPComponent.initHandlers] (https://github.com/buddycloud/buddycloud-pusher/blob/master/src/main/java/org/buddycloud/pusher/XMPPComponent.java#L83) method, while new Message Consumers should be added to the MessageProcessor.initConsumers method.
git clone https://github.com/buddycloud/buddycloud-pusher.git
cd buddycloud-pusher
cp configuration.properties.example configuration.properties
mvn package
java -jar target/pusher-0.1.0-jar-with-dependencies.jar
Create a GCM project and get an API key as per http://developer.android.com/google/gcm/gs.html#create-proj Change GCM settings in the pusher's configuration.properties
# GCM project id
gcm.google_project_id=
# GCM API key
gcm.api_key=
The first thing your app should do is to find out which GCM project to subscribe to and then subscribe to its notifications. Your app must send a metadata stanza to the pusher, as following:
<!-- Only the fields to be modified need to be included here -->
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='get'>
<query xmlns='http://buddycloud.com/pusher/metadata'>
<type>gcm</type>
</query>
</iq>
<!-- Only the fields to be modified need to be included here -->
<iq from='pusher.example.com'
to='[email protected]'
id='023FE3AA4'
type='result'>
<query xmlns='http://buddycloud.com/pusher/metadata'>
<google_project_id>123456789</google_project_id>
</query>
</iq>
As in http://developer.android.com/google/gcm/client.html#sample-register
With your registration id, you should register for Pusher notifications and configure what events you want to be notified of.
<!-- Only the fields to be modified need to be included here -->
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='set'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<notificationSettings>
<type>gcm</type>
<target>$REGISTRATION_ID</target>
<postAfterMe>false</postAfterMe>
<postMentionedMe>false</postMentionedMe>
</notificationSettings>
</query>
</iq>
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<notificationSettings>
<type>gcm</type>
<target>$REGISTRATION_ID</target>
<postAfterMe>false</postAfterMe>
<postMentionedMe>false</postMentionedMe>
<postOnMyChannel>true</postOnMyChannel>
<postOnSubscribedChannel>false</postOnSubscribedChannel>
<followMyChannel>true</followMyChannel>
<followRequest>true</followRequest>
</notificationSettings>
</query>
</iq>
As in http://developer.android.com/google/gcm/client.html#sample-receive, you need a GCMBroadcastReceiver and GCMIntentService to listen to GCM. Take a look at Buddycloud's GCMIntentService for an example on how to handle those.
- The HTTP API sends the email address to the buddycloud pusher
- The pusher creates a notification profile with default preferences
- The pusher creates an unique string to do one-click unsubscribe “click here to un-subuscribe”
send all emails from the friendly "[email protected]"
-
0h: # welcome email explaining buddycloud and giving them a link directly back to their channel.
-
every 24hours: activity report "posts in the channels you follow" + random channel post of the day.
-
other emails (triggered by events passed from buddycloud-server to pusher.example.com component:
- someone follows
- someone posts into their channel (plus holdoff for 24 hours)
- someone followed your channel (plus holdoff for 24 hours)
- someone comments on your post
- someone mentions you
- when they are invited to a channel, send an email
-
bonus: after two days "you can now create a topic channel" cool!
- no email validation step - wrong email just goes to the wrong person and they hit unsubscribe / user can’t recover password
- No unsubscribe link
HTTP API to pusher
<!-- Only the fields to be modified need to be included here -->
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='set'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<notificationSettings>
<type>email</type>
<target>[email protected]</target>
<postAfterMe>false</postAfterMe>
<postMentionedMe>false</postMentionedMe>
</notificationSettings>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<notificationSettings>
<type>email</type>
<target>[email protected]</target>
<postAfterMe>false</postAfterMe>
<postMentionedMe>false</postMentionedMe>
<postOnMyChannel>true</postOnMyChannel>
<postOnSubscribedChannel>false</postOnSubscribedChannel>
<followMyChannel>true</followMyChannel>
<followRequest>true</followRequest>
</notificationSettings>
</query>
</iq>
Removes all records associated to this jid.
HTTP API to pusher
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='set'>
<query xmlns='jabber:iq:register'>
<remove/>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='jabber:iq:register'/>
</iq>
The same stanza can be used to remove specific records Both type and target fields are optional.
HTTP API to pusher
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='set'>
<query xmlns='jabber:iq:register'>
<remove>
<type>email</type>
<target>[email protected]</target>
</remove>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='jabber:iq:register'/>
</iq>
HTTP API to pusher
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='get'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<type>email</type>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>"
<notificationSettings>
<type>email</type>
<target>[email protected]</target>
<postAfterMe>true</postAfterMe>
<postMentionedMe>true</postMentionedMe>
<postOnMyChannel>true</postOnMyChannel>
<postOnSubscribedChannel>false</postOnSubscribedChannel>
<followMyChannel>true</followMyChannel>
<followRequest>true</followRequest>
</notificationSettings>
<notificationSettings>
<type>email</type>
<target>[email protected]</target>
<postAfterMe>true</postAfterMe>
<postMentionedMe>true</postMentionedMe>
<postOnMyChannel>true</postOnMyChannel>
<postOnSubscribedChannel>false</postOnSubscribedChannel>
<followMyChannel>false</followMyChannel>
<followRequest>false</followRequest>
</notificationSettings>
</query>
</iq>
HTTP API to pusher
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='get'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>
<type>email</type>
<target>[email protected]</target>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='http://buddycloud.com/pusher/notification-settings'>"
<notificationSettings>
<type>email</type>
<target>[email protected]</target>
<postAfterMe>true</postAfterMe>
<postMentionedMe>true</postMentionedMe>
<postOnMyChannel>true</postOnMyChannel>
<postOnSubscribedChannel>false</postOnSubscribedChannel>
<followMyChannel>true</followMyChannel>
<followRequest>true</followRequest>
</notificationSettings>
</query>
</iq>
HTTP API to pusher
<iq from='[email protected]'
to='pusher.example.com'
id='023FE3AA4'
type='set'>
<query xmlns='http://buddycloud.com/pusher/signup'>
<email>[email protected]</email>
</query>
</iq>
pusher to HTTP API
<iq from='pusher.example.com'
id='023FE3AA4'
to='[email protected]'
type='result'>
<query xmlns='http://buddycloud.com/pusher/signup'>
<info>User [[email protected]] signed up.</info>
</query>
</iq>
https://github.com/buddycloud/buddycloud-pusher/tree/master/templates/email
Email templates (tpl files) use the following structure
SENDER_EMAIL_ADDRESS
RECIPIENT_NAME <RECIPIENT_EMAIL_ADDRESS>
SUBJECT
HTML CONTENT (From the 4th line on)
Tokens use the <%TOKEN_NAME%> format, and are replaced by their values during runtime. Token values can be defined in the configuration.properties file, using the mail.template. prefix.