Added code for notifications changes

- Toggle for notification types
- Custom time for overview
This commit is contained in:
Henry Whitaker
2020-06-21 20:32:31 +01:00
parent 8b9e87e699
commit 832a10589d
18 changed files with 613 additions and 37 deletions

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use App\Events\SpeedtestOverviewEvent;
use App\Helpers\SpeedtestHelper;
use App\Notifications\SpeedtestOverviewSlack;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification;
class SpeedtestOverviewCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'speedtest:overview';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Trigger a speedtest overview event';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
event(new SpeedtestOverviewEvent());
}
}

View File

@@ -37,9 +37,6 @@ class SpeedtestVersionCommand extends Command
*/
public function handle()
{
$this->info('Speedtest Tracker');
$this->info('Author: Henry Whitaker');
$this->info('');
$this->info('Installed version v' . config('speedtest.version'));
$this->info('Speedtest Tracker v' . config('speedtest.version'));
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console;
use App\Events\SpeedtestOverviewEvent;
use App\Helpers\SettingsHelper;
use App\Helpers\SpeedtestHelper;
use App\Jobs\SpeedtestJob;
@@ -28,6 +29,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
$schedule->job(new SpeedtestJob)->cron(SettingsHelper::get('schedule')['value']);
$schedule->command('speedtest:overview')->cron('0 ' . SettingsHelper::get('speedtest_overview_time')->value . ' * * *');
}
/**

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SpeedtestOverviewEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@@ -5,12 +5,9 @@ namespace App\Helpers;
use App\Speedtest;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use JsonException;
use SimpleXMLElement;
class SpeedtestHelper {
@@ -70,6 +67,22 @@ class SpeedtestHelper {
return shell_exec($binPath . ' -f json');
}
/**
* Get a 24 hour average of speedtest results
*
* @return array
*/
public static function last24Hours()
{
$t = Carbon::now()->subDay();
$s = Speedtest::select(DB::raw('AVG(ping) as ping, AVG(download) as download, AVG(upload) as upload'))
->where('created_at', '>=', $t)
->first()
->toArray();
return $s;
}
/**
* Converts bytes/s to Mbps
*

View File

@@ -67,6 +67,49 @@ class SettingsController extends Controller
], 200);
}
/**
* Bulk store/update a setting
*
* @param Request $request
* @return Response
*/
public function bulkStore(Request $request)
{
$rule = [
'data' => [ 'array', 'required' ],
'data.*.name' => [ 'string', 'required' ],
'data.*.value' => [ 'required' ],
];
$validator = Validator::make($request->all(), $rule);
if($validator->fails()) {
return response()->json([
'method' => 'Bulk store a setting',
'error' => $validator->errors()
], 422);
}
$settings = [];
foreach($request->data as $d) {
if($d['name'] == 'speedtest_overview_time') {
$ok = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23' ];
if(!in_array($d['value'], $ok)) {
return response()->json([
'method' => 'Bulk store a setting',
'error' => 'Invalid speedtest_overview_time value'
], 422);
}
}
$setting = SettingsHelper::set($d['name'], $d['value']);
array_push($settings, $setting);
}
return response()->json([
'method' => 'Bulk store a setting',
'data' => $settings,
], 200);
}
/**
* Returns instance config
*

View File

@@ -2,6 +2,7 @@
namespace App\Listeners;
use App\Helpers\SettingsHelper;
use App\Notifications\SpeedtestCompleteSlack;
use App\Notifications\SpeedtestCompleteTelegram;
use Exception;
@@ -31,6 +32,7 @@ class SpeedtestCompleteListener
*/
public function handle($event)
{
if(SettingsHelper::get('speedtest_notifications')->value == true) {
$data = $event->speedtest;
if(env('SLACK_WEBHOOK')) {
try {
@@ -53,3 +55,4 @@ class SpeedtestCompleteListener
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Listeners;
use App\Helpers\SettingsHelper;
use App\Helpers\SpeedtestHelper;
use App\Notifications\SpeedtestOverviewSlack;
use App\Notifications\SpeedtestOverviewTelegram;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use NotificationChannels\Telegram\TelegramChannel;
class SpeedtestOverviewListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
if(SettingsHelper::get('speedtest_overview_notification')->value == true) {
$data = SpeedtestHelper::last24Hours();
if(env('SLACK_WEBHOOK')) {
try {
Notification::route('slack', env('SLACK_WEBHOOK'))
->notify(new SpeedtestOverviewSlack($data));
} catch(Exception $e) {
Log::notice('Your sleck webhook is invalid');
Log::notice($e);
}
}
if(env('TELEGRAM_BOT_TOKEN') && env('TELEGRAM_CHAT_ID')) {
try {
Notification::route(TelegramChannel::class, env('TELEGRAM_CHAT_ID'))
->notify(new SpeedtestOverviewTelegram($data));
} catch(Exception $e) {
Log::notice('Your telegram settings are invalid');
Log::notice($e);
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class SpeedtestOverviewSlack extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($data)
{
$data['ping'] = number_format((float)$data['ping'], 1, '.', '');
$data['download'] = number_format((float)$data['download'], 1, '.', '');
$data['upload'] = number_format((float)$data['upload'], 1, '.', '');
$this->data = $data;
}
/**
* 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)
{
$data = $this->data;
return (new SlackMessage)
->warning()
->attachment(function ($attachment) use ($data) {
$attachment->title('Speedtest Daily Overview')
->fields([
'Average ping' => $data['ping'] . ' ms',
'Average download' => $data['download'] . ' Mbit/s',
'Average upload' => $data['upload'] . ' Mbit/s',
]);
});
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use NotificationChannels\Telegram\TelegramChannel;
use NotificationChannels\Telegram\TelegramMessage;
class SpeedtestOverviewTelegram extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($data)
{
$data['ping'] = number_format((float)$data['ping'], 1, '.', '');
$data['download'] = number_format((float)$data['download'], 1, '.', '');
$data['upload'] = number_format((float)$data['upload'], 1, '.', '');
$this->data = $data;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return [
TelegramChannel::class
];
}
/**
* Format tekegram notification
*
* @param mixed $notifiable
* @return TelegramMessage
*/
public function toTelegram($notifiable)
{
$data = $this->data;
$msg = "*Speedtest Daily Overview*
Average ping: *".$data["ping"]."*
Average download: *".$data["download"]."*
Average upload: *".$data["upload"]."*";
return TelegramMessage::create()
->to(env('TELEGRAM_CHAT_ID'))
->content($msg)
->options(['parse_mode' => 'Markdown']);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@@ -3,7 +3,9 @@
namespace App\Providers;
use App\Events\SpeedtestCompleteEvent;
use App\Events\SpeedtestOverviewEvent;
use App\Listeners\SpeedtestCompleteListener;
use App\Listeners\SpeedtestOverviewListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -23,6 +25,9 @@ class EventServiceProvider extends ServiceProvider
SpeedtestCompleteEvent::class => [
SpeedtestCompleteListener::class,
],
SpeedtestOverviewEvent::class => [
SpeedtestOverviewListener::class
],
];
/**

View File

@@ -0,0 +1,49 @@
<?php
use App\Setting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddNotificationsSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Setting::create([
'name' => 'speedtest_notifications',
'value' => true,
'description' => 'Enable notifications for every speedtest that runs'
]);
Setting::create([
'name' => 'speedtest_overview_notification',
'value' => true,
'description' => 'Enable a daily notification with average values for the last 24 hours.'
]);
Setting::create([
'name' => 'speedtest_overview_time',
'value' => '12',
'description' => 'The hour (24-hour format) that the daily overview notification will be sent.'
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Setting::whereIn('name', [
'speedtest_notifications',
'speedtest_overview_notification',
'speedtest_overview_time',
])->delete();
}
}

2
public/css/main.css vendored
View File

@@ -53,5 +53,5 @@
}
.setting-card {
width: 500px;
height: 270px;
}

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -55,8 +55,9 @@ export default class Setting extends Component {
var description = this.state.description;
return (
<Card className="m-2">
<Card.Body>
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{this.ucfirst(name)}</h4>
<div dangerouslySetInnerHTML={{ __html: description}} />
<Form.Group controlId={name}>
@@ -64,6 +65,7 @@ export default class Setting extends Component {
<Form.Control type="text" label={name} defaultValue={value} onInput={this.updateValue} />
</Form.Group>
<Button variant="primary" onClick={this.update}>Save</Button>
</div>
</Card.Body>
</Card>
);

View File

@@ -0,0 +1,153 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card, Form, Button, Modal, Row, Col } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class SettingWithModal extends Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title,
description: this.props.description,
settings: this.props.settings,
show: false
}
}
ucfirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
update = () => {
var url = 'api/settings/bulk';
var data = [];
var settings = this.state.settings;
settings.forEach(e => {
var res = {
name: e.obj.name,
value: e.obj.value
};
data.push(res);
});
data = {
data: data
};
Axios.post(url, data)
.then((resp) => {
toast.success(this.state.title + ' updated');
this.toggleShow();
})
.catch((err) => {
if(err.response.status == 422) {
toast.error('Your input was invalid');
} else {
toast.error('Something went wrong')
}
})
}
updateValue = (e) => {
var name = e.target.id;
if(e.target.type == 'checkbox') {
var val = e.target.checked;
} else {
var val = e.target.value;
}
var settings = this.state.settings;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
settings: settings
});
}
toggleShow = () => {
var show = this.state.show;
if(show) {
this.setState({
show: false
});
} else {
this.setState({
show: true
});
}
}
render() {
var title = this.state.title;
var description = this.state.description;
var show = this.state.show;
var settings = this.state.settings;
return (
<>
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{title}</h4>
<p>{description}</p>
<Button variant="primary" onClick={this.toggleShow}>Edit</Button>
</div>
</Card.Body>
</Card>
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
{settings.map((e,i) => {
var name = e.obj.name.split('_');
name[0] = this.ucfirst(name[0]);
name = name.join(' ');
if(e.type == 'checkbox') {
return (
<Row key={e.obj.id} className="d-flex align-items-center">
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<Form.Group controlId={e.obj.name}>
<Form.Check type="checkbox" label={name} defaultChecked={e.obj.value} onInput={this.updateValue} />
</Form.Group>
</Col>
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<p>{e.obj.description}</p>
</Col>
</Row>
);
} else if(e.type == 'number') {
return (
<Row key={e.obj.id}>
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<Form.Group controlId={e.obj.name}>
<Form.Label>{name}</Form.Label>
<Form.Control type="number" min={e.min} max={e.max} defaultValue={e.obj.value} onInput={this.updateValue} />
</Form.Group>
</Col>
<Col md={{ span: 6 }} sm={{ span: 12 }}>
<p>{e.obj.description}</p>
</Col>
</Row>
);
}
})}
<Button variant="primary" type="submit" onClick={this.update} >Save</Button>
</Modal.Body>
</Modal>
</>
);
}
}
if (document.getElementById('Setting')) {
ReactDOM.render(<Setting />, document.getElementById('Setting'));
}

View File

@@ -4,6 +4,7 @@ import { Modal, Container, Row, Col, Collapse } from 'react-bootstrap';
import Loader from '../Loader';
import Axios from 'axios';
import Setting from './Setting';
import SettingWithModal from './SettingWithModal';
export default class Settings extends Component {
constructor(props) {
@@ -53,12 +54,30 @@ export default class Settings extends Component {
var e = this.state.data;
return (
<Row>
<Col lg={{ span: 3, offset: 3 }} md={{ span: 6 }} sm={{ span: 12 }}>
<Col lg={{ span: 2, offset: 3 }} md={{ span: 6 }} sm={{ span: 12 }}>
<Setting name={e.schedule.name} value={e.schedule.value} description={e.schedule.description} />
</Col>
<Col lg={{ span: 3 }} md={{ span: 6 }} sm={{ span: 12 }}>
<Col lg={{ span: 2 }} md={{ span: 6 }} sm={{ span: 12 }}>
<Setting name={e.server.name} value={e.server.value} description={e.server.description} />
</Col>
<Col lg={{ span: 2 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="Notification settings" description="Control which types of notifications the server sends." settings={[
{
obj: e.speedtest_notifications,
type: 'checkbox'
},
{
obj: e.speedtest_overview_notification,
type: 'checkbox'
},
{
obj: e.speedtest_overview_time,
type: 'number',
min: 0,
max: 23
}
]} />
</Col>
</Row>
)
}

View File

@@ -66,4 +66,6 @@ Route::group([
->name('settings.store');
Route::post('/', 'SettingsController@store')
->name('settings.update');
Route::post('/bulk', 'SettingsController@bulkStore')
->name('settings.bulk.update');
});