diff --git a/README.md b/README.md index 18dd11c1..3bbe595c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Speedtest Tracker -[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.9.0-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE) +[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.9.1-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE) This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses [Ookla's Speedtest cli](https://www.speedtest.net/apps/cli) to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results. diff --git a/conf/site/README.md b/conf/site/README.md index bcc3a30d..945a3e46 100644 --- a/conf/site/README.md +++ b/conf/site/README.md @@ -1,6 +1,6 @@ # Speedtest Tracker -[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.9.0-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE) +[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.9.1-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE) This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses the [Ookla's speedtest cli](https://www.speedtest.net/apps/cli) package to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results. diff --git a/conf/site/app/Helpers/NotificationsHelper.php b/conf/site/app/Helpers/NotificationsHelper.php new file mode 100644 index 00000000..d110299a --- /dev/null +++ b/conf/site/app/Helpers/NotificationsHelper.php @@ -0,0 +1,67 @@ + 1 && $i < (sizeof($errors) - 1)) { + $msg = $msg . ', '; + } + } + + if($msg[-1] != '') { + $msg = $msg . ' '; + } + + if(sizeof($errors) > 1) { + $msg = $msg . 'values '; + } else { + $msg = $msg . 'value '; + } + + return $msg; + } +} diff --git a/conf/site/app/Helpers/SpeedtestHelper.php b/conf/site/app/Helpers/SpeedtestHelper.php index f41bf317..ffd77891 100644 --- a/conf/site/app/Helpers/SpeedtestHelper.php +++ b/conf/site/app/Helpers/SpeedtestHelper.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use InvalidArgumentException; use JsonException; class SpeedtestHelper { @@ -301,4 +302,77 @@ class SpeedtestHelper { 'msg' => 'There was an error backing up the database. No speedtests have been deleted.' ]; } + + /** + * Work out if a test is lower than the threshold for historic tests + * + * @param String $type + * @param Speedtest $test + * @return array + */ + public static function testIsLowerThanThreshold(String $type, Speedtest $test) + { + if($type == 'percentage') { + $avg = Speedtest::select(DB::raw('AVG(ping) as ping, AVG(download) as download, AVG(upload) as upload')) + ->where('failed', false) + ->get() + ->toArray()[0]; + + $threshold = SettingsHelper::get('threshold_alert_percentage')->value; + + if($threshold == '') { + return []; + } + + $errors = []; + + foreach($avg as $key => $value) { + if($key == 'ping') { + $threshold = (float)$value * (1 + ( $threshold / 100 )); + + if($test->$key > $threshold) { + array_push($errors, $key); + } + } else { + $threshold = (float)$value * (1 - ( $threshold / 100 )); + + if($test->$key < $threshold) { + array_push($errors, $key); + } + } + } + + return $errors; + } + + if($type == 'absolute') { + $thresholds = [ + 'download' => SettingsHelper::get('threshold_alert_absolute_download')->value, + 'upload' => SettingsHelper::get('threshold_alert_absolute_upload')->value, + 'ping' => SettingsHelper::get('threshold_alert_absolute_ping')->value, + ]; + + $errors = []; + + foreach($thresholds as $key => $value) { + if($value == '') { + continue; + } + + if($key == 'ping') { + if($test->$key > $value) { + array_push($errors, $key); + } + } else { + if($test->$key < $value) { + array_push($errors, $key); + } + } + } + + return $errors; + } + + throw new InvalidArgumentException(); + } } diff --git a/conf/site/app/Jobs/SpeedtestJob.php b/conf/site/app/Jobs/SpeedtestJob.php index 0ccd5dbf..bbf43f8e 100644 --- a/conf/site/app/Jobs/SpeedtestJob.php +++ b/conf/site/app/Jobs/SpeedtestJob.php @@ -82,16 +82,17 @@ class SpeedtestJob implements ShouldQueue private function healthcheck(String $method) { try { + $hc = new Healthchecks(SettingsHelper::get('healthchecks_uuid')->value); if($method === 'start') { - Healthcheck::start(); + $hc->start(); } if($method === 'success') { - Healthcheck::success(); + $hc->success(); } if($method === 'fail') { - Healthcheck::fail(); + $hc->fail(); } } catch(Exception $e) { Log::error($e->getMessage()); diff --git a/conf/site/app/Listeners/SpeedtestCompleteListener.php b/conf/site/app/Listeners/SpeedtestCompleteListener.php index b8a329c9..010dcca0 100644 --- a/conf/site/app/Listeners/SpeedtestCompleteListener.php +++ b/conf/site/app/Listeners/SpeedtestCompleteListener.php @@ -3,8 +3,13 @@ namespace App\Listeners; use App\Helpers\SettingsHelper; +use App\Helpers\SpeedtestHelper; +use App\Notifications\SpeedtestAbsoluteThresholdNotificationSlack; +use App\Notifications\SpeedtestAbsoluteThresholdTelegram; use App\Notifications\SpeedtestCompleteSlack; use App\Notifications\SpeedtestCompleteTelegram; +use App\Notifications\SpeedtestPercentageThresholdNotificationSlack; +use App\Notifications\SpeedtestPercentageThresholdTelegram; use Exception; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; @@ -32,9 +37,63 @@ class SpeedtestCompleteListener */ public function handle($event) { + if((bool)SettingsHelper::get('threshold_alert_percentage_notifications')->value == true) { + $data = $event->speedtest; + $errors = SpeedtestHelper::testIsLowerThanThreshold('percentage', $data); + if(sizeof($errors) > 0) { + if(SettingsHelper::get('slack_webhook')->value) { + try { + Notification::route('slack', SettingsHelper::get('slack_webhook')->value) + ->notify(new SpeedtestPercentageThresholdNotificationSlack($errors)); + } catch(Exception $e) { + Log::notice('Your sleck webhook is invalid'); + Log::notice($e); + } + } + + if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) { + try { + config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]); + Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_chat_id')->value) + ->notify(new SpeedtestPercentageThresholdTelegram($errors)); + } catch(Exception $e) { + Log::notice('Your telegram settings are invalid'); + Log::notice($e); + } + } + } + } + + if((bool)SettingsHelper::get('threshold_alert_absolute_notifications')->value == true) { + $data = $event->speedtest; + $errors = SpeedtestHelper::testIsLowerThanThreshold('absolute', $data); + if(sizeof($errors) > 0) { + if(SettingsHelper::get('slack_webhook')->value) { + try { + Notification::route('slack', SettingsHelper::get('slack_webhook')->value) + ->notify(new SpeedtestAbsoluteThresholdNotificationSlack($errors)); + } catch(Exception $e) { + Log::notice('Your sleck webhook is invalid'); + Log::notice($e); + } + } + + if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) { + try { + config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]); + Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_chat_id')->value) + ->notify(new SpeedtestAbsoluteThresholdTelegram($errors)); + } catch(Exception $e) { + Log::notice('Your telegram settings are invalid'); + Log::notice($e); + } + } + } + } + if(SettingsHelper::get('speedtest_notifications')->value == true) { $data = $event->speedtest; - if(SettingsHelper::get('slack_webhook')) { + if(SettingsHelper::get('slack_webhook')->value) { try { Notification::route('slack', SettingsHelper::get('slack_webhook')->value) ->notify(new SpeedtestCompleteSlack($data)); diff --git a/conf/site/app/Notifications/SpeedtestAbsoluteThresholdNotificationSlack.php b/conf/site/app/Notifications/SpeedtestAbsoluteThresholdNotificationSlack.php new file mode 100644 index 00000000..1f4d0fb8 --- /dev/null +++ b/conf/site/app/Notifications/SpeedtestAbsoluteThresholdNotificationSlack.php @@ -0,0 +1,57 @@ +errors = $errors; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + /** + * Format slack notification + * + * @param mixed $notifiable + * @return SlackMessage + */ + public function toSlack($notifiable) + { + $msg = NotificationsHelper::formatAbsoluteThresholdMessage($this->errors); + + return (new SlackMessage) + ->warning() + ->attachment(function ($attachment) use ($msg) { + $attachment->title('Speedtest absolute threshold error') + ->content($msg); + }); + } +} diff --git a/conf/site/app/Notifications/SpeedtestAbsoluteThresholdTelegram.php b/conf/site/app/Notifications/SpeedtestAbsoluteThresholdTelegram.php new file mode 100644 index 00000000..a0dc004f --- /dev/null +++ b/conf/site/app/Notifications/SpeedtestAbsoluteThresholdTelegram.php @@ -0,0 +1,58 @@ +errors = $errors; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return [ + TelegramChannel::class + ]; + } + + /** + * Format telegram notification + * + * @param mixed $notifiable + * @return TelegramMessage + */ + public function toTelegram($notifiable) + { + $msg = NotificationsHelper::formatAbsoluteThresholdMessage($this->errors); + + return TelegramMessage::create() + ->to(SettingsHelper::get('telegram_chat_id')->value) + ->content($msg) + ->options(['parse_mode' => 'Markdown']); + } +} diff --git a/conf/site/app/Notifications/SpeedtestPercentageThresholdNotificationSlack.php b/conf/site/app/Notifications/SpeedtestPercentageThresholdNotificationSlack.php new file mode 100644 index 00000000..fd9332ba --- /dev/null +++ b/conf/site/app/Notifications/SpeedtestPercentageThresholdNotificationSlack.php @@ -0,0 +1,57 @@ +errors = $errors; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + /** + * Format slack notification + * + * @param mixed $notifiable + * @return SlackMessage + */ + public function toSlack($notifiable) + { + $msg = NotificationsHelper::formatPercentageThresholdMessage($this->errors); + + return (new SlackMessage) + ->warning() + ->attachment(function ($attachment) use ($msg) { + $attachment->title('Speedtest percentage threshold error') + ->content($msg); + }); + } +} diff --git a/conf/site/app/Notifications/SpeedtestPercentageThresholdTelegram.php b/conf/site/app/Notifications/SpeedtestPercentageThresholdTelegram.php new file mode 100644 index 00000000..0789fa7f --- /dev/null +++ b/conf/site/app/Notifications/SpeedtestPercentageThresholdTelegram.php @@ -0,0 +1,58 @@ +errors = $errors; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return [ + TelegramChannel::class + ]; + } + + /** + * Format telegram notification + * + * @param mixed $notifiable + * @return TelegramMessage + */ + public function toTelegram($notifiable) + { + $msg = NotificationsHelper::formatAbsoluteThresholdMessage($this->errors); + + return TelegramMessage::create() + ->to(SettingsHelper::get('telegram_chat_id')->value) + ->content($msg) + ->options(['parse_mode' => 'Markdown']); + } +} diff --git a/conf/site/changelog.json b/conf/site/changelog.json index 2bc17fb2..8e3535ba 100644 --- a/conf/site/changelog.json +++ b/conf/site/changelog.json @@ -1,4 +1,10 @@ { + "1.9.1": [ + { + "description": "Added conditional notifications.", + "link": "" + } + ], "1.9.0": [ { "description": "Added optional authentication.", diff --git a/conf/site/config/speedtest.php b/conf/site/config/speedtest.php index 6f43b3bb..1f47cacf 100644 --- a/conf/site/config/speedtest.php +++ b/conf/site/config/speedtest.php @@ -7,7 +7,7 @@ return [ |-------------------------------------------------------------------------- */ - 'version' => '1.9.0', + 'version' => '1.9.1', /* |-------------------------------------------------------------------------- diff --git a/conf/site/database/migrations/2020_08_21_204656_add_conditional_notifications_settings.php b/conf/site/database/migrations/2020_08_21_204656_add_conditional_notifications_settings.php new file mode 100644 index 00000000..8c36d2c2 --- /dev/null +++ b/conf/site/database/migrations/2020_08_21_204656_add_conditional_notifications_settings.php @@ -0,0 +1,83 @@ + 'threshold_alert_percentage_notifications', + 'value' => false, + 'description' => 'Enable/disable theshold percentage notifications' + ]); + } + + if(!SettingsHelper::get('threshold_alert_percentage')) { + Setting::create([ + 'name' => 'threshold_alert_percentage', + 'value' => 15, + 'description' => 'When any value of a speedtest is x percent lower than the average, a notification will be sent.' + ]); + } + + if(!SettingsHelper::get('threshold_alert_absolute_notifications')) { + Setting::create([ + 'name' => 'threshold_alert_absolute_notifications', + 'value' => false, + 'description' => 'Enable/disable absolute theshold notifications' + ]); + } + + if(!SettingsHelper::get('threshold_alert_absolute_download')) { + Setting::create([ + 'name' => 'threshold_alert_absolute_download', + 'value' => '', + 'description' => 'When the download is lower than this value, a notification will be sent. Leave blank to disable' + ]); + } + + if(!SettingsHelper::get('threshold_alert_absolute_upload')) { + Setting::create([ + 'name' => 'threshold_alert_absolute_upload', + 'value' => '', + 'description' => 'When the upload is lower than this value, a notification will be sent. Leave blank to disable' + ]); + } + + if(!SettingsHelper::get('threshold_alert_absolute_ping')) { + Setting::create([ + 'name' => 'threshold_alert_absolute_ping', + 'value' => '', + 'description' => 'When the ping is higher than this value, a notification will be sent. Leave blank to disable' + ]); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Setting::whereIn('name', [ + 'threshold_alert_percentage', + 'threshold_alert_absolute_download', + 'threshold_alert_absolute_upload', + 'threshold_alert_absolute_ping', + 'threshold_alert_percentage_notifications', + 'threshold_alert_absolute_notifications' + ])->delete(); + } +} diff --git a/conf/site/resources/js/components/Settings/Settings.js b/conf/site/resources/js/components/Settings/Settings.js index d2ac48a8..9c0bfcc0 100644 --- a/conf/site/resources/js/components/Settings/Settings.js +++ b/conf/site/resources/js/components/Settings/Settings.js @@ -158,6 +158,43 @@ export default class Settings extends Component { type: 'number', min: 0, max: 23 + }, + { + obj: { + id: (Math.floor(Math.random() * 10000) + 1), + name: "Conditional Notifications", + description: "" + }, + type: 'group', + children: [ + + ] + }, + { + obj: e.threshold_alert_percentage_notifications, + type: 'checkbox', + }, + { + obj: e.threshold_alert_percentage, + type: 'number', + min: 0, + max: 100 + }, + { + obj: e.threshold_alert_absolute_notifications, + type: 'checkbox', + }, + { + obj: e.threshold_alert_absolute_download, + type: 'number', + }, + { + obj: e.threshold_alert_absolute_upload, + type: 'number', + }, + { + obj: e.threshold_alert_absolute_ping, + type: 'number', } ]} />