Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active May 12, 2024 08:09
Show Gist options
  • Save stecman/e3f8cacc83878aa5ba77 to your computer and use it in GitHub Desktop.
Save stecman/e3f8cacc83878aa5ba77 to your computer and use it in GitHub Desktop.
Pushover notification CLI script

Send Pushover notifications from the command line

Notify is a utility for sending push notifications via Pushover from the command line. It can be run manually, but is most useful for sending notifications from shell scripts, cron jobs, and long running tasks.

Pushover screenshot

Installation

To install, copy the contents of notify.php to somewhere on your computer, and make it executable:

wget https://gist.githubusercontent.com/stecman/e3f8cacc83878aa5ba77/raw/notify.php -O notify && chmod +x notify

I like to put the script in /usr/sbin/notify. If the script is on your PATH, you'll then be able to run the script by simply typing notify (or whatever you called the script) into a terminal and pressing enter. If you installed it somewhere else, you'll need to run it with something like ~/scripts/notify.php.

Configuration

Notify uses the file .pushover.json in your home directory by default, and expects the following structure:

{
    "<application-name>": {
        "token": "<app-token>",
        "user": "<user-key>"
    }
}

Unless you specify an <application-name> to use with the --app option, the first application defintion in the file will be used. In addtion to the required token and user keys, any parameter in the Pushover API can be put in an application definition and will be included in the API call (unless overridden by command line options). For example:

{
    "default": {
        "token": "321",
        "user": "abc"
    },
    
    "website-status": {
        "token": "123",
        "user": "abc",
        "device": "nexus",
        "title": "Website Status",
        "sound": "persistent"
    }
}

Usage

# Show help
notify --help

# Send a basic notification
notify 'Your house is on fire.'

# Send a more customized notification
notify --title 'Warning: House on fire' 'It appears your house may be on fire.' --high-priority

# Pass a message into STDIN
ls | notify --title 'Directory listing'

# Use a specific application definition
notify --app website-status 'Website is unreachable: connection timeout'

Use examples

Send a notification after reboot

Add this to your crontab to get a notification after boot

@reboot notify --title 'Bruce is awake' 'Reboot jobs executed. System is back online.'

Notify that a long running job has finished

$ cp huge-big-file /media/backup-drive/; notify 'Finished copying your huge-big-file'
#!/usr/bin/env php
<?php
// Fallback title for notifications without a title specified
define('UNKNOWN_TITLE', 'Push over notification on ' . gethostname());
// PushOver's message limit is 1024 4-byte UTF8 characters, but a
// smaller limit can be enforced to keep notifications readable.
define('MESSAGE_LENGTH_LIMIT', 800);
function dieHelp()
{
echo <<<EOD
Usage: notify [options] <message>
<Message> can be alternatively be piped into stdin.
Content options:
--title <title> Message title. Defaults to app name
--url <url> URL to show with message
--url-name <name> Title to show for URL
--app <name> Name of application in config to use. Defaults to the
first item in ~/.pushover.json
--config <path> Path to a specific config file
Fallback options:
--fallback-stdout If the message is too large for a single notification,
the full message will be written to stdout. This is
useful for use with cron, which includes more context
in its job output notifcation emails.
--fallback-email <email> If the message is too large for a single notification,
a copy of the full message will be sent via email.
Notification options:
--slient Silent notification
--high-priority Display as high priority
--critical Require user confirmation of the notification
--critical-expire <sec> Time before critical alert stops prompting in seconds
(max 86400, defaults to 1 hour)
--critical-retry <sec> Time in seconds between delivery retries for a critical
alert (minimum 30 seconds, defaults to 2 minutes)
External configuration:
This script requires a configuration file containing Pushover application and user
tokens. Unless a file is specified with the --config-file option, the script uses
~/.pushover.json. In each named set, the 'token' and 'user' value are required.
API requests are built on top of a config, so defaults can effectively be set for
the request. See the Pushover API doc for accepted key-value pairs.
{
"<application-name>": {
"token": "<app-token>",
"user": "<user-key>"
}
}
EOD;
exit(1);
}
function parseCommandLine($existingMessage = null)
{
// Boolean options
$options = array(
'silent',
'high-priority',
'critical',
'fallback-stdout',
);
// Options that take an argument
$argOptions = array(
'title',
'url',
'url-name',
'confirm-expire',
'config',
'app',
'fallback-email',
);
global $argv;
$args = array_slice($argv, 1);
$message = $existingMessage;
$settings = array();
// Normalise default values
foreach ($options as $option) {
$settings[$option] = false;
}
foreach ($argOptions as $option) {
$settings[$option] = null;
}
// Collect input values from commandline
for ($i = 0, $c = count($args); $i<$c; $i++) {
$value = $args[$i];
// If not using option syntax, assume this argument is the message
if (substr($value, 0, 2) != '--') {
if (!$message) {
$message = $value;
continue;
} else if ($existingMessage) {
echo "A non-option argument cannot be passed in addition to stdin!\n";
exit(3);
} else {
echo "Multiple non-option arguments passed!\n";
dieHelp();
}
}
$name = substr($value, 2);
foreach ($options as $option) {
if ($name == $option) {
$settings[$option] = true;
$found = true;
continue 2;
}
}
foreach ($argOptions as $option) {
if ($name == $option) {
if (!isset($args[$i+1])) {
echo "No matching argument for option '$value'\n";
exit(3);
} else if (substr($args[$i+1], 0, 2) == '--') {
echo "No matching argument for option '$value'\n";
exit(3);
} else {
$settings[$option] = $args[$i+1];
}
$i++;
continue 2;
}
}
echo "Invalid option '$value'. Specify --help for usage information.\n";
exit(4);
}
if (!$message) {
echo "A message must be specified!\n";
exit(2);
}
$settings['message'] = $message;
return $settings;
}
function loadConfig($file)
{
if (!file_exists($file)) {
echo "Config file $file not found.\n";
exit(4);
}
if (!is_readable($file)) {
echo "Config file $file is not readable.\n";
}
$config = json_decode(file_get_contents($file), true);
if (!$config) {
echo "$file parsed to nothing. Maybe check the syntax.\n";
exit(5);
}
return $config;
}
// Check if help needed
if (in_array('--help', $argv)) {
dieHelp();
}
$stdinData = null;
// Read message from stdin if we're being used in a pipe
if (!posix_isatty(STDIN)) {
$stdinData = stream_get_contents(STDIN);
}
$settings = parseCommandLine($stdinData);
$configFile = isset($settings['config']) ? $settings['config'] : getenv('HOME').'/.pushover.json';
$applications = loadConfig($configFile);
// Build base request
if (isset($settings['app'])) {
if (isset($applications[$settings['app']])) {
$config = $applications[$settings['app']];
} else {
echo "No application defined with the name '{$settings['app']}' in $configFile\n";
exit(4);
}
} else {
// Default to the first item in $applications
$config = reset($applications);
}
// Alert level
if ($settings['critical']) {
$config['priority'] = '2';
if (isset($settings['critical-expire'])) {
$config['expire'] = intval($settings['critical-expire']);
} else {
// Default to 1 hour
$config['expire'] = 3600;
}
if (isset($settings['critical-retry'])) {
$config['retry'] = intval($settings['critical-retry']);
} else {
$config['retry'] = 120;
}
if ($config['retry'] < 30) {
echo "Critical alert retry rate cannot be less than 30 seconds\n";
exit(1);
}
if ($config['expire'] > 86400) {
echo "Critical alert expiry delay cannot be more than 86400 seconds\n";
exit(1);
}
} else if ($settings['high-priority']) {
$config['priority'] = '1';
} else if ($settings['silent']) {
$config['priority'] = '-1';
}
// Content settings
if (isset($settings['title'])) {
$config['title'] = $settings['title'];
}
if (isset($settings['url'])) {
$config['url'] = $settings['url'];
}
if (isset($settings['url-name'])) {
$config['url_title'] = $settings['url-name'];
}
// Truncate message for notification and apply any selected fallback
if (strlen($settings['message']) > MESSAGE_LENGTH_LIMIT) {
// Data for fallback handlers
$fallbackSubject = (isset($config['title']) ? $config['title'] : UNKNOWN_TITLE) . ' (full message)';
$fallbackMsg = "Notification message truncated. The full message was:\n\n{$settings['message']}";
// Message to tack onto the end of the truncated PushOver message.
// Fallback handlers can modify this to inficate the truncated data is not lost.
$truncationMsg = ' ... [truncated]';
if ($settings['fallback-stdout']) {
$truncationMsg .= ' [full message dumped to stdout]';
echo $fallbackMsg;
}
if (isset($settings['fallback-email'])) {
$toAddr = $settings['fallback-email'];
$truncationMsg .= " [full message emailed to $toAddr]";
mail($toAddr, $fallbackSubject, $fallbackMsg);
}
// Truncate the notification for PushOver
$settings['message'] = substr($settings['message'], 0, 900) . $truncationMsg;
}
$config['message'] = $settings['message'];
// Send request
curl_setopt_array($ch = curl_init(), array(
CURLOPT_URL => "https://api.pushover.net/1/messages.json",
CURLOPT_POSTFIELDS => $config,
CURLOPT_RETURNTRANSFER => true
));
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status != 200) {
echo "Pushover returned status $status\n.";
print_r(json_decode($response));
exit(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment