PHP
Article

Push your Drupal Site’s Events to your Phone with Pushover

By Daniel Sipos

In this article I am going to show you how you can integrate Pushover with your Drupal site. I will illustrate a couple of examples of how you can use Pushover to notify yourself as soon as something happens on your site.

The code I write in this article is also available in this repository so you can just clone that if you want to follow along.

What is Pushover?

Pushover is a web and mobile application that allows you to get real time notifications on your mobile device. The way it works is that you install an app on your Android or Apple device and using a handy API you can send that app notifications. The great thing about this is that it happens more or less in real time (depending on your internet connection) as Pushover uses the Google and Apple servers to send the notifications.

The price is also very affordable. At a rate of $4.99 USD per platform (Android, Apple or desktop) paid only once, you can use it on any number of devices under that platform. And you also get a 5 day trial period for free the moment you create your account.

What am I doing here?

In this article I am going to set up a Pushover application and use it from my Drupal site to notify my phone of various events. I will give you two example use cases that Pushover can be handy with:

  • Whenever an anonymous user posts a comment that awaits administrative approval, I’ll send a notification to my phone
  • Whenever the admin user 1 logs into the site, I’ll send an emergency notification to my phone (useful if you are the only user of that admin account).

Naturally, these are examples and you may not find them useful. But they only serve as illustration of the power you can have by using Pushover.

Pushover account

First, a quick look at creating your Pushover account. To follow along, go on over to Pushover and sign up if you haven’t already and you can start your 5 day free trial. Then, go to Google Play or the App Store and install the app on your device. You’ll need to log in to your account and give the device a name. Mine is called simply Nexus (hint: I don’t have an iPhone).

Go back then to the Pushover website and you can already test it out by sending yourself a test notification to one or all of your active devices. You can even specify which sound it should make.

Next, if you want to use the API, you’ll need to create an application. This will generate for you an app_token to use later on. And that should be pretty much it.

Drupal

Now that the Pushover account creation is taken care of, the device app is installed (Android in my case) and I have my Pushover application, it’s time to see how I can achieve the goal set out in the beginning. As a brief roadmap, I will do the following:

  • Create a custom Drupal module
  • Add to it the Pushover class created by Chris Schalenborgh (a handy wrapper over the curl calls to the API)
  • Implement some hooks that will trigger the notifications based on certain conditions
  • Profit

The custom module I’ll be working with in this example is called pushover and it contains a pushover.info and a pushover.module file (as required). Additionally, I will create a lib/Pushover folder in it to store the external class I will use to connect to Pushover. There are other – more recommended ways – of importing external libraries into Drupal modules (see the Libraries API), but for the sake of brevity, this will work just fine.

The first thing I want to do in my pushover.module file is to import this external class. Based on my folder structure, I can do so with this line:

require_once(DRUPAL_ROOT . '/' . drupal_get_path('module', 'pushover') . '/lib/Pushover/Pushover.php');

Next, I want to create a reusable helper function that will return a pushable object. That is an object already populated with my own defaults (such as credentials) and that takes some parameters for the more common properties. But first, I want to put my Pushover account credentials into the settings.php file because they do not belong in my git repository:

$conf['pushover_credentials'] = array(
  'user_token' => 'uCpygdjfsndfi7233sdasdo33Yv',
  'app_token' => 'aKH8Nwsdasdanl342jmsdaBWgoVe',
);

Obviously neither of these tokens are now valid but if you are following along, you should replace them with yours: the user_token you get on the main page as you log in to the Pushover website and the app_token is the one generated for your application.

Then I can continue with my helper function I mentioned earlier:

/**
 * Helper function that returns a pushable object using the Pushover class
 * 
 * @param $vars
 * @return bool|Pushover
 */
function pushover_get_pushable($vars) {
  global $conf;
  if (isset($conf['pushover_credentials'])) {
    $push = new Pushover();
    $push->setToken($conf['pushover_credentials']['app_token']);
    $push->setUser($conf['pushover_credentials']['user_token']);
    $push->setTitle($vars['title']);
    $push->setMessage($vars['message']);
    if (isset($vars['url'])) {
      $push->setUrl($vars['url']);
    }
    if (isset($vars['device'])) {
      $push->setDevice($vars['device']);
    }
    $push->setTimestamp(time());

    return $push;
  }
  else {
    return FALSE;
  }
}

In this function I instantiate a new Pushover object if there are credentials in my settings.php file. Otherwise, I fail silently by returning false. The function takes some parameters that are set on the object: title and message are mandatory whereas the url and device are not. The device is actually the name of the device to which you want to restrict this notification.

Additionally, I set the current timestamp and then return the object.

Next, it’s time to use this function inside some hooks. The first one is going to be hook_comment_insert():

/**
 * Implements hook_comment_insert().
 */
function pushover_comment_insert($comment) {

  // Send a push notification if a new comment is created by an anonymous user
  // and it is not yet published.
  if ($comment->status == 0 && $comment->is_anonymous == TRUE) {
    global $base_url;
    $vars = array(
      'title' => 'New comment on ' . variable_get('site_name') . '!',
      'message' => 'Subject: ' . $comment->subject,
      'url' => $base_url . '/node/' . $comment->nid . '#comment-' . $comment->cid,
      'device' => 'Nexus'
    );
    $pushable = pushover_get_pushable($vars);
    if ($pushable) {
      $pushed = $pushable->send();
      if ($pushed == false) {
        watchdog('Pushover', t('A comment has been created but there was an error pushing that over.'), array(), WATCHDOG_ERROR, NULL);
      }
    }
  }
}

In here I check if the commenter is anonymous and the status is 0 (to be on the safe side). If that’s the case, I build the array of parameters for my helper function with some information about the site and comment and use the send() method to send the notification. You’ll notice that I restricted this to the Nexus device.

At the end, I check whether the notification went out successfully (the send() method returns false if the Pushover service does not return the success status code of 1. If something went wrong, I quickly log it to the watchdog.

So now if an anonymous user writes a comment, I get a push notification with the site name, comment subject and URL. Nifty.

Now let’s turn to the second example in which I implement an emergency notification if my admin user logs into the site. If it’s not me, I’ll know something is wrong and I probably got hacked. I do this inside a hook_user_login() implementation:

/**
 * Implements hook_user_login().
 */
function pushover_user_login(&$edit, $account) {
  // If the admin user logs in, send a push notification.
  if ($account->uid == 1) {
    $whitelist = array('1.1.1.1');
    if (!in_array(ip_address(), $whitelist)) {
      global $base_url;
      $vars = array(
        'title' => 'Admin user sign in',
        'message' => 'Admin user has logged into this site: ' . variable_get('site_name') . '!',
        'url' => $base_url,
      );
      $pushable = pushover_get_pushable($vars);
      if ($pushable) {
        $pushable->setPriority(2);
        $pushable->setRetry(30);
        $pushable->setExpire(60);
        $pushed = $pushable->send();
        if ($pushed == false) {
          watchdog('Pushover', t('An admin user has logged into the site but there was an error pushing this over.'), array(), WATCHDOG_ERROR, NULL);
        }
      }
    }
  }
}

In here, I first check if the user logging in is the one with the id of 1 (the main admin user). Then I create an array with whitelisted IPs to check against the user logging in. I don’t want to get notified if I log in from home or from the office (1.1.1.1 is just an example ip address).

Then just like before, I get my pushable object with the usual variables (no device this time, I want this to go out to all my devices). On top of those, I set a priority of 2 (marking it an emergency notification), a retry value of 30 seconds and an expire value of 60 seconds. The latter 2 in combination with the priority make it so that if left unacknowledged by my phone, the notification gets resent every 30 seconds for a total duration of 60 seconds. For more information about the possible options you have with Pushover, make sure you check out their API docs.

And there it is. I will now get an emergency notification if someone logs in with my admin account. Of course, not very good if many people can log in with that account, but you get the point.

Conclusion

In this tutorial I showed you how you can use Pushover from your Drupal site to send notifications to your phone when certain events occur. I covered 2 examples but I’m sure you can find more. I would like to also mention that I found a contrib Drupal module called Pushover which uses Rules in order to send out Pushover notifications. I haven’t really used it, but make sure to check it out if you want to use Pushover and your site is already making use of the Rules module. Or, you know, you hate writing code.

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
Lito

Sorry, but I think that your PHP code is terrible.

Please, compare your examples with my organized and classified code http://pastebin.com/sFiFWWmV

I think that you need to improve a lot of your PHP skills.

Anyway, regards smile

upchuk

Hey Lito,

First of all, I believe you need to learn some manners before you comment on any posts. Some basics are in order at least.

Second of all, you didn't seem to get that this is an example for Drupal where you need to implement procedural functions to hook into existing functionality. I wrote a simple demo helper function just so I don't reuse the boilerplate client instantiation code and didn't write a class for it because it would have been an overkill.

Your commentInsert() and userLogin() methods are superfluous because you'd use them only one time: inside a hook_comment_insert() and hook_user_login() respectively. And with this approach, if you want to send a push notification in another hook, you'd create a different method on the same class just for that as well. If you don't, the whole point of your classes no longer exists.

Other than that, you just wrapped the existing client code in another class which I guess makes you better: I am perfectly fine living in a world where you think (or are) much better than me.

So good luck with that.

swader

While I appreciate the time you took to write up an alternative solution, that's not a nice way to leave feedback.

In the future, perhaps sending a PR (so we can also see a diff, not just a pastebin which needs to be manually compared) would be a more productive approach, as it could then also be discussed in the pull request's issue, no?

Lito

Sorry, I thought that my code was clear, do you thing that a diff is needed to compare code differences?

I think that the problem isn't the functions or de classes or if is a wrapper or not, is a protest by the bad code in this "article". I think that haven't enough effort or quality to be published here, regardless of what he wants to show.

Sorry for my previous bad feedback.

oddz

Drupal 7 is driven by procedural event mechanism known as hooks. The code supplied by the author is completely adequate given the environment. Drupal <8 is not really known for OOP. It is mostly procedural and any attempt to clean things up using OOP typically results in making things less cleaner. Though you would have to be familiar with system to understand that. Yes, you can use OOP but really there is a specific time and place for it in older versions of Drupal. Showing a concept in the simplest of forms is not really one them fyi. Not to mention the code you supplied as an alternative within the context of Drupal does absolutely nothing since it isn't hooked into the event/hook system.

Lito

Ok, I know, but I haven't talking about finger, but about the moon.

Nested ifs, globals, simple comparisons, code not clear, not spaces, etc...

Here my constructive contribution to this article, using same functions and idea about Drupal.

<?php
define('PUSHOVER_APP_TOKEN', 'aKH8Nwsdasdanl342jmsdaBWgoVe');
define('PUSHOVER_USER_TOKEN', 'uCpygdjfsndfi7233sdasdo33Yv');

function pushover_send($vars)
{
    if (!defined('PUSHOVER_APP_TOKEN') || !defined('PUSHOVER_USER_TOKEN')) {
        return false;
    }

    $error = false;

    $push = new Pushover();
    $push->setToken(PUSHOVER_APP_TOKEN);
    $push->setUser(PUSHOVER_APP_TOKEN);
    $push->setTimestamp(time());

    if (array_key_exists('error', $vars)) {
        $error = $vars['error'];
        unset($vars['error']);
    }

    foreach ($vars as $key => $value) {
        if (method_exists($push, $key)) {
            call_user_func_array(array($push, 'set'.$key), array($value));
        }
    }

    if ($push->send()) {
        return true;
    }

    if ($error) {
        watchdog('Pushover', $error, array(), WATCHDOG_ERROR, NULL);
    }

    return false;
}

/**
* Implements hook_comment_insert().
*/
function pushover_comment_insert($comment) {
    if (($comment->status !== 0) || ($comment->is_anonymous !== true)) {
        return false;
    }

    return pushover_send(array(
        'title' => 'New comment on ' . variable_get('site_name') . '!',
        'message' => 'Subject: ' . $comment->subject,
        'url' => url('/node/'.$comment->nid.'#comment-'.$comment->cid, array('absolute' => true)),
        'device' => 'Nexus',
        'error' => t('A comment has been created but there was an error pushing that over.')
    ));
}

/**
* Implements hook_user_login().
*/
function pushover_user_login($account) {
    if (($account->uid !== 1) || in_array(ip_address(), array('1.1.1.1'))) {
        return false;
    }

    return pushover_send(array(
        'title' => 'Admin user sign in',
        'message' => 'Admin user has logged into this site: '.variable_get('site_name').'!',
        'url' => url('/', array('absolute' => true)),
        'priority' => 2,
        'retry' => 30,
        'expire' => 60,
        'error' => t('An admin user has logged into the site but there was an error pushing this over.')
    ));
}

Do you think that this code is enough professional and clear?

Anyway, thanks for your article and sorry my lack of tact.
Lito.

oddz

I agree that Drupal <8 promotes some terrible practices. That is why Drupal 8 was completely rebuilt from the ground up. When I'm using Drupal 7 I more often than not take the stance of if you can't beat them join them.

swader

A diff would make it immediately clearer for other people joining the discussion, allowing them to benefit from your changes faster, should they prove valid. Either way, thank you for taking the time to contribute - just be more tactful in the future, please. There are more constructive approaches to criticism, and everyone appreciates constructive criticism.

Lito

I'm sorry, I know. My english is not the best in the world and sometimes I can be too rude or direct with my expressions.

Sorry all my errors to try to explain some solutions more elegant than article.

Regards,
Lito.

mvdvenne

Nice article and useful solution.

On the Drupal best practices side, you could improve one important part:
Drupal uses a couple of standardized ways to include files. inlude_once is never used.

Best best way:
Use the Libaries API contrib module.
The library is placed in the sites/libraries folder. This way, the module and library are not hard linked to each other.

The basic drupal way:
Include the file in your module info file:
files[] = lib/Pushover/Pushover.php

upchuk

Hey there,

Thanks for the comment.

I agree that using require_once is not the best way to go. Hence I mentioned in the article that this is just for demonstration (brevity) purposes only. I also mentioned the Libraries module as the more recommended approach. I'm not sure if you didn't miss that smile

Cheers!

mvdvenne

Ah my bad. Looked over it. The link is pretty obvious flushed

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.