From 26c00320532edc62b3ddf1affbc2b59af28d72f2 Mon Sep 17 00:00:00 2001 From: Jon Goldberg Date: Fri, 8 May 2020 18:47:11 -0400 Subject: [PATCH] convert to support multiple queries --- .env.example | 3 + .gitignore | 4 + composer.json | 16 +++ composer.lock | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.php | 12 +++ oncall.php | 244 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 index.php create mode 100644 oncall.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb0ef75 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +REDMINE_URL='https://redmine.example.com' +REDMINE_API_KEY='abcdefg12345' +DEBUG_MODE= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9aae61a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +.env +contact.txt +users.csv diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a970b15 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "name": "megaphone/oncall", + "description": "A script to report on Redmine tickets that need attention.", + "type": "project", + "require": { + "kbsali/redmine-api": "^1.5", + "vlucas/phpdotenv": "^4.1" + }, + "license": "GPL", + "authors": [ + { + "name": "Jon Goldberg", + "email": "jon@megaphonetech.com" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..0de94b9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,269 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9d1110731b1633c82d10df7e5b5077a6", + "packages": [ + { + "name": "kbsali/redmine-api", + "version": "v1.5.21", + "source": { + "type": "git", + "url": "https://github.com/kbsali/php-redmine-api.git", + "reference": "e75295b81e5dc4c858007d8924408e11e49e080c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kbsali/php-redmine-api/zipball/e75295b81e5dc4c858007d8924408e11e49e080c", + "reference": "e75295b81e5dc4c858007d8924408e11e49e080c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "php": "^5.4 || ^7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.4.8 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Redmine\\": "src/Redmine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Saliou", + "email": "kevin@saliou.name", + "homepage": "http://kevin.saliou.name" + } + ], + "description": "Redmine API client", + "homepage": "https://github.com/kbsali/php-redmine-api", + "keywords": [ + "api", + "redmine" + ], + "time": "2019-03-15T17:37:16+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/4acfd6a4b33a509d8c88f50e5222f734b6aeebae", + "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.3", + "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2020-03-21T18:07:53+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-02-27T09:26:54+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v4.1.5", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "539bb6927c101a5605d31d11a2d17185a2ce2bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/539bb6927c101a5605d31d11a2d17185a2ce2bf1", + "reference": "539bb6927c101a5605d31d11a2d17185a2ce2bf1", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "phpoption/phpoption": "^1.7.2", + "symfony/polyfill-ctype": "^1.9" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.3", + "ext-filter": "*", + "ext-pcre": "*", + "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator.", + "ext-pcre": "Required to use most of the library." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "homepage": "https://gjcampbell.co.uk/" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://vancelucas.com/" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2020-05-02T14:08:57+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..bf34878 --- /dev/null +++ b/index.php @@ -0,0 +1,12 @@ + diff --git a/oncall.php b/oncall.php new file mode 100644 index 0000000..fdf47cd --- /dev/null +++ b/oncall.php @@ -0,0 +1,244 @@ +#!/usr/bin/php +retrieveItems(); +$oncall->setEmail(); +$table = $oncall->buildMattermostTable(); +if ($table) { + $oncall->sendToMattermost($table); +} + +//email($item, $contact); + +/** + * The class that does all the magic. + */ +class oncall { + + /** + * Are we in debug mode? + * @var bool + */ + private $debugMode = FALSE; + + /** + * A flag to indicate at least one ticket is overdue. + * @var bool + */ + private $overdue = FALSE; + + /** + * The URL of the Redmine install. + * @var string + */ + private $baseUrl; + + /** + * The Redmine API key. + * @var string + */ + private $apiKey; + + /** + * An array containing queries we want to run, with a label as a key. + * @var array + */ + private $queries; + + /** + * An array of the query results from Redmine. Key is the query label. + * @var array + */ + private $queryResults = []; + + /** + * The email of the person on call. + * @var string; + */ + private $email; + + public function __construct() { + // Load the .env file + $dotenv = Dotenv\Dotenv::createImmutable(__DIR__); + $dotenv->load(); + + // Debug Mode will show more information. + $this->debugMode = (bool) getenv('DEBUG_MODE'); + $this->baseUrl = getenv('REDMINE_URL'); + $this->apiKey = getenv('KEY'); + + $this->queries = [ + 'New Support Tickets' => 'https://hq.megaphonetech.com/issues.json?query_id=17&key=7ebe204bef5804f4effb9b4160a295487dde15f1', + 'New Maintenance Tickets' => 'https://hq.megaphonetech.com/issues.json?query_id=18&key=7ebe204bef5804f4effb9b4160a295487dde15f1', + ]; + } + + /** + * Add the number of minutes since a ticket was created. + * Also set the overdue flag if necessary. + */ + private function getMinutes(array $item) { + // find the time since the ticket was created + $created = $item['created_on']; + $time = strtotime("$created"); + $time_difference = strtotime('now') - $time; + + // put it into minutes + $td_min = (int) ($time_difference / 60); + + // Store the time in minutes in the array. + $item['minutes'] = $td_min; + // Flag if any items are overdue. + if ($td_min > 60) { + $this->overdue = TRUE; + } + return $item; + } + + /** + * Get items from Redmine. + */ + public function retrieveItems() { + foreach ($this->queries as $title => $url) { + // get the contents of the query in a usable format + $json = json_decode(file_get_contents($url), TRUE); + // if the query has any results + if (!empty($json)) { + // go through this loop for each ticket found + foreach ($json['issues'] as $k => $item) { + $item = $this->getMinutes($item); + $this->queryResults[$title][] = $item; + } + } + } + } + + /** + * Render the $items property in Markdown. + * @return string + */ + public function buildMattermostTable() { + $table = ''; + foreach ($this->queryResults as $label => $queryResult) { + + //Add header row + $table .= "\n### $label\n"; + $table .= "| Issue | Project | Author | Subject | Minutes Elapsed |\n| :----- | :------- | :----- | :-------| ----: |\n"; + foreach ($queryResult as $item) { + $table .= "| [{$item['id']}](https://hq.megaphonetech.com/issues/{$item['id']}) "; + $table .= "| {$item['project']['name']} "; + $table .= "| {$item['author']['name']} "; + $table .= "| {$item['subject']} "; + $table .= "| {$item['minutes']} "; + $table .= "|\n"; + } + } + return $table; + } + + public function sendToMattermost($table) { + $mattermostName = $this->findMattermostName(); + $url = 'https://chat.civicrm.org/hooks/4d7wtzzmj3rjmqpjg9z9nfqwjo'; + $data['channel'] = 'Megaphone'; + $data['username'] = 'megaphone-bot'; + $data['icon_url'] = 'http://oncall.megaphonetech.com/oncallbot.jpg'; + $data['text'] = "@$mattermostName\n\n$table"; + $json = json_encode($data); + + // use key 'http' even if you send the request to https://... + $options = [ + 'http' => [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => $json, + ], + ]; + $context = stream_context_create($options); + $result = file_get_contents($url, FALSE, $context); + if ($result === FALSE) { + print_r($result); + } + } + + /** + * Return a Mattermost name from email, or "@channel" if there's an overdue ticket. + * @return string + */ + private function findMattermostName() { + if ($this->overdue) { + return 'channel'; + } + // Check users.csv column 1 for email; return column 2 if it matches. + if (($handle = fopen(__DIR__ . "/users.csv", "r")) !== FALSE) { + while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { + if ($data[0] == $this->email) { + fclose($handle); + return $data[1]; + } + } + } + fclose($handle); + return ''; + } + + public function setEmail() { + // path to file containing email of person on call + $contactfile = './contact.txt'; + // set the email of the person on call + $this->email = trim(file_get_contents("$contactfile")); + } + +} + +/** + * Send an email for this item. + */ +function email($item, $contact) { + // if it's been an hour, email everyone instead + if ($item['minutes'] > 60) { + $contact = 'team@megaphonetech.com'; + } + + // if it's been 25 minutes, make an email + if ($item['minutes'] > 0) { + + // email body + $email = "New issue to attend to:" . "\n"; + $email .= "\n"; + $email .= "Project: " . $item['project']['name'] . "\n"; + $email .= "Author: " . $item['author']['name'] . "\n"; + $email .= "Subject: " . $item['subject'] . "\n"; + $email .= "\n"; + $email .= $item['description'] . "\n"; + $email .= "\n"; + $email .= "Ticket created {$item['minutes']} minutes ago"; + $email .= "\n"; + $email .= 'https://hq.megaphonetech.com/issues/' . $item['id'] . "\n"; + $email .= "\n"; + + // email headers + $to = $contact; + $subject = "[#{$item['id']}]"; + $message = $email; + $headers = 'From: support@megaphonetech.com' . "\r\n" . + 'Reply-To: support@megaphonetech.com' . "\r\n" . + 'X-Mailer: PHP/' . phpversion(); + + // send email + mail($to, $subject, $message, $headers); + + } +}