From 2b72b4dcdbe73cfa48e1bebce70caa9a8b3554b3 Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Fri, 10 Jun 2016 02:19:16 +0100 Subject: [PATCH] #25: Service worker subscription --- .../Api/Web/NotificationsController.php | 32 +- app/Http/routes.php | 2 + app/Jobs/SendNotifications.php | 4 +- .../Notifications/Drivers/NativeDriver.php | 108 +++++ app/Models/Subscription.php | 49 +++ composer.json | 3 +- composer.lock | 378 +++++++++++++++++- ...06_10_010314_create_subscription_table.php | 36 ++ public/manifest.json | 3 +- public/service-worker.js | 4 + .../app/controllers/application.coffee | 1 + .../app/directives/notification-list.coffee | 14 +- 12 files changed, 626 insertions(+), 8 deletions(-) create mode 100644 app/Library/Notifications/Drivers/NativeDriver.php create mode 100644 app/Models/Subscription.php create mode 100644 database/migrations/2016_06_10_010314_create_subscription_table.php diff --git a/app/Http/Controllers/Api/Web/NotificationsController.php b/app/Http/Controllers/Api/Web/NotificationsController.php index c0a0d710..badec332 100644 --- a/app/Http/Controllers/Api/Web/NotificationsController.php +++ b/app/Http/Controllers/Api/Web/NotificationsController.php @@ -21,10 +21,11 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Auth; -use Carbon\Carbon; use Input; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Models\Notification; +use Poniverse\Ponyfm\Models\Subscription; +use Minishlink\WebPush\WebPush; class NotificationsController extends ApiControllerBase { @@ -59,4 +60,33 @@ class NotificationsController extends ApiControllerBase return ['notifications_updated' => $numberOfUpdatedRows]; } + + /** + * Subscribe a user to native push notifications. Takes an endpoint and + * encryption keys from the client and stores them in the database + * for future use. + * + * @return string + */ + public function postSubscribe() + { + $input = json_decode(Input::json('subscription')); + + $existing = Subscription::where('endpoint', '=', $input->endpoint) + ->where('user_id', '=', Auth::user()->id) + ->first(); + + if ($existing === null) { + $subscription = Subscription::create([ + 'user_id' => Auth::user()->id, + 'endpoint' => $input->endpoint, + 'p256dh' => $input->keys->p256dh, + 'auth' => $input->keys->auth + ]); + + return $subscription->id; + } else { + return $existing->id; + } + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 976c50ee..a59b82bc 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -134,8 +134,10 @@ Route::group(['prefix' => 'api/web'], function() { Route::post('/dashboard/read-news', 'Api\Web\DashboardController@postReadNews'); Route::get('/account/settings/{slug}', 'Api\Web\AccountController@getSettings'); + Route::get('/notifications', 'Api\Web\NotificationsController@getNotifications'); Route::put('/notifications/mark-as-read', 'Api\Web\NotificationsController@putMarkAsRead'); + Route::post('/notifications/subscribe', 'Api\Web\NotificationsController@postSubscribe'); Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit'); diff --git a/app/Jobs/SendNotifications.php b/app/Jobs/SendNotifications.php index f586834e..53d1080d 100644 --- a/app/Jobs/SendNotifications.php +++ b/app/Jobs/SendNotifications.php @@ -25,6 +25,7 @@ use Illuminate\Queue\InteractsWithQueue; use Poniverse\Ponyfm\Jobs\Job; use Illuminate\Contracts\Bus\SelfHandling; use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver; +use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver; use Poniverse\Ponyfm\Models\User; use SerializesModels; @@ -60,7 +61,8 @@ class SendNotifications extends Job implements SelfHandling, ShouldQueue // to work around a Laravel bug - namely, the SerializesModels trait // tries (and fails) to serialize static fields. $drivers = [ - PonyfmDriver::class + PonyfmDriver::class, + //NativeDriver::class ]; foreach ($drivers as $driver) { diff --git a/app/Library/Notifications/Drivers/NativeDriver.php b/app/Library/Notifications/Drivers/NativeDriver.php new file mode 100644 index 00000000..4243a4a0 --- /dev/null +++ b/app/Library/Notifications/Drivers/NativeDriver.php @@ -0,0 +1,108 @@ +. + */ + +namespace Poniverse\Ponyfm\Library\Notifications\Drivers; + + +use Carbon\Carbon; +use Poniverse\Ponyfm\Contracts\Favouritable; +use Poniverse\Ponyfm\Models\Activity; +use Poniverse\Ponyfm\Models\Comment; +use Poniverse\Ponyfm\Models\Notification; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; + +class NativeDriver extends AbstractDriver { + /** + * A helper method for bulk insertion of notification records. + * + * @param int $activityId + * @param User[] $recipients collection of {@link User} objects + */ + private function pushNotifications(int $activityId, $recipients) { + $notifications = []; + foreach ($recipients as $recipient) { + $notifications[] = [ + 'activity_id' => $activityId, + 'user_id' => $recipient->id + ]; + } + Notification::insert($notifications); + } + + /** + * @inheritdoc + */ + public function publishedNewTrack(Track $track) { + $activity = Activity::where('user_id', $track->user_id) + ->where('activity_type', Activity::TYPE_PUBLISHED_TRACK) + ->where('resource_id', $track->id) + ->get()[0]; + + + $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + } + + /** + * @inheritdoc + */ + public function publishedNewPlaylist(Playlist $playlist) { + $activity = Activity::where('user_id', $playlist->user_id) + ->where('activity_type', Activity::TYPE_PUBLISHED_PLAYLIST) + ->where('resource_id', $playlist->id) + ->get()[0]; + + $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + } + + public function newFollower(User $userBeingFollowed, User $follower) { + $activity = Activity::where('user_id', $follower->user_id) + ->where('activity_type', Activity::TYPE_NEW_FOLLOWER) + ->where('resource_id', $userBeingFollowed->id) + ->get()[0]; + + $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + } + + /** + * @inheritdoc + */ + public function newComment(Comment $comment) { + $activity = Activity::where('user_id', $comment->user_id) + ->where('activity_type', Activity::TYPE_NEW_COMMENT) + ->where('resource_id', $comment->id) + ->get()[0]; + + $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + } + + /** + * @inheritdoc + */ + public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) { + $activity = Activity::where('user_id', $favouriter->user_id) + ->where('activity_type', Activity::TYPE_CONTENT_FAVOURITED) + ->where('resource_id', $entityBeingFavourited->id) + ->get()[0]; + + $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + } +} diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php new file mode 100644 index 00000000..eaffdd76 --- /dev/null +++ b/app/Models/Subscription.php @@ -0,0 +1,49 @@ +. + */ + +namespace Poniverse\Ponyfm\Models; + +use Illuminate\Database\Eloquent\Model; + +/** + * Poniverse\Ponyfm\Models\Subscription + * + * @property integer $id + * @property integer $user_id + * @property string $endpoint + * @property string $p256dh + * @property string $auth + * @property-read \Poniverse\Ponyfm\Models\User $user + */ +class Subscription extends Model { + public $timestamps = false; + protected $fillable = ['user_id', 'endpoint', 'p256dh', 'auth']; + protected $casts = [ + 'id' => 'integer', + 'user_id' => 'integer', + 'endpoint' => 'string', + 'p256dh' => 'string', + 'auth' => 'string' + ]; + + public function user() { + return $this->belongsTo(User::class, 'user_id', 'id'); + } +} diff --git a/composer.json b/composer.json index f8fcedae..e1a15e3c 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "barryvdh/laravel-debugbar": "^2.2", "predis/predis": "^1.0", "ksubileau/color-thief-php": "^1.3", - "graham-campbell/exceptions": "^8.6" + "graham-campbell/exceptions": "^8.6", + "minishlink/web-push": "^1.0" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/composer.lock b/composer.lock index da69a173..49ff6f90 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5c9f86045c835b8964e1eb198a7727ea", - "content-hash": "a09f15b4c222efb8e676a4efdd979b4f", + "hash": "070553e4e21387213808a4cb779e5f16", + "content-hash": "98c97b7ca37abf031e353edaf4ac2ae3", "packages": [ { "name": "barryvdh/laravel-debugbar", @@ -124,6 +124,59 @@ ], "time": "2016-03-03 08:45:00" }, + { + "name": "beberlei/assert", + "version": "v2.5", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "91e2690c4ecc8a4e3e2d333430069f6a0c694a7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/91e2690c4ecc8a4e3e2d333430069f6a0c694a7a", + "reference": "91e2690c4ecc8a4e3e2d333430069f6a0c694a7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-0": { + "Assert": "lib/" + }, + "files": [ + "lib/Assert/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "time": "2016-03-22 14:34:51" + }, { "name": "classpreloader/classpreloader", "version": "3.0.0", @@ -883,6 +936,58 @@ ], "time": "2016-03-18 16:31:37" }, + { + "name": "fgrosse/phpasn1", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "ee6d1abd18f8bcbaf0b55563ba87e5ed16cd0c98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/ee6d1abd18f8bcbaf0b55563ba87e5ed16cd0c98", + "reference": "ee6d1abd18f8bcbaf0b55563ba87e5ed16cd0c98", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "time": "2015-07-15 21:26:40" + }, { "name": "graham-campbell/exceptions", "version": "v8.6.1", @@ -1454,6 +1559,54 @@ ], "time": "2015-12-05 17:17:57" }, + { + "name": "kriswallsmith/buzz", + "version": "v0.15", + "source": { + "type": "git", + "url": "https://github.com/kriswallsmith/Buzz.git", + "reference": "d4041666c3ffb379af02a92dabe81c904b35fab8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kriswallsmith/Buzz/zipball/d4041666c3ffb379af02a92dabe81c904b35fab8", + "reference": "d4041666c3ffb379af02a92dabe81c904b35fab8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "suggest": { + "ext-curl": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "Buzz": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kris Wallsmith", + "email": "kris.wallsmith@gmail.com", + "homepage": "http://kriswallsmith.net/" + } + ], + "description": "Lightweight HTTP client", + "homepage": "https://github.com/kriswallsmith/Buzz", + "keywords": [ + "curl", + "http client" + ], + "time": "2015-06-25 17:26:56" + }, { "name": "ksubileau/color-thief-php", "version": "v1.3.0", @@ -1780,6 +1933,118 @@ ], "time": "2016-01-22 12:22:23" }, + { + "name": "mdanter/ecc", + "version": "v0.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpecc/phpecc.git", + "reference": "8b588fc094ba743d8f8c84980bcc6b470c4baed7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpecc/phpecc/zipball/8b588fc094ba743d8f8c84980bcc6b470c4baed7", + "reference": "8b588fc094ba743d8f8c84980bcc6b470c4baed7", + "shasum": "" + }, + "require": { + "ext-gmp": "*", + "ext-mcrypt": "*", + "fgrosse/phpasn1": "~1.3.1", + "php": ">=5.4.0", + "symfony/console": "~2.6" + }, + "require-dev": { + "phpunit/phpunit": "~4.1", + "squizlabs/php_codesniffer": "~2", + "symfony/yaml": "~2.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mdanter\\Ecc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matyas Danter", + "homepage": "http://matejdanter.com/", + "role": "Author" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io", + "homepage": "http://aztech.io", + "role": "Maintainer" + }, + { + "name": "Drak", + "email": "drak@zikula.org", + "homepage": "http://zikula.org", + "role": "Maintainer" + } + ], + "description": "PHP Elliptic Curve Cryptography library", + "homepage": "https://github.com/mdanter/phpecc", + "time": "2014-07-07 12:44:15" + }, + { + "name": "minishlink/web-push", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Minishlink/web-push.git", + "reference": "ad407ca84e87595760ff87a836a55ad107fa8245" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Minishlink/web-push/zipball/ad407ca84e87595760ff87a836a55ad107fa8245", + "reference": "ad407ca84e87595760ff87a836a55ad107fa8245", + "shasum": "" + }, + "require": { + "kriswallsmith/buzz": ">=0.6", + "lib-openssl": "*", + "mdanter/ecc": "^0.3.0", + "php": ">=5.4", + "spomky-labs/base64url": "^1.0", + "spomky-labs/php-aes-gcm": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Minishlink\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Louis Lagrange", + "email": "lagrange.louis@gmail.com", + "homepage": "https://github.com/Minishlink" + } + ], + "description": "Web Push library for PHP", + "homepage": "https://github.com/Minishlink/web-push", + "keywords": [ + "Push API", + "WebPush", + "notifications", + "push", + "web" + ], + "time": "2016-05-13 21:21:54" + }, { "name": "monolog/monolog", "version": "1.19.0", @@ -2400,6 +2665,115 @@ "description": "A lightweight implementation of CommonJS Promises/A for PHP", "time": "2016-05-03 17:50:52" }, + { + "name": "spomky-labs/base64url", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "ef6d5fb93894063d9cee996022259fd08d6646ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/ef6d5fb93894063d9cee996022259fd08d6646ea", + "reference": "ef6d5fb93894063d9cee996022259fd08d6646ea", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0|^5.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "time": "2016-01-21 19:50:30" + }, + { + "name": "spomky-labs/php-aes-gcm", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/php-aes-gcm.git", + "reference": "7d415c6eeb5133804a38451d6ed93b5af76872ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/php-aes-gcm/zipball/7d415c6eeb5133804a38451d6ed93b5af76872ce", + "reference": "7d415c6eeb5133804a38451d6ed93b5af76872ce", + "shasum": "" + }, + "require": { + "beberlei/assert": "^2.0", + "lib-openssl": "*", + "php": ">=5.4", + "symfony/polyfill-mbstring": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "AESGCM\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/php-aes-gcm/contributors" + } + ], + "description": "AES GCM (Galois Counter Mode) PHP implementation.", + "homepage": "https://github.com/Spomky-Labs/php-aes-gcm", + "keywords": [ + "Galois Counter Mode", + "gcm" + ], + "time": "2016-04-28 13:58:02" + }, { "name": "swiftmailer/swiftmailer", "version": "v5.4.2", diff --git a/database/migrations/2016_06_10_010314_create_subscription_table.php b/database/migrations/2016_06_10_010314_create_subscription_table.php new file mode 100644 index 00000000..3d4b5bd3 --- /dev/null +++ b/database/migrations/2016_06_10_010314_create_subscription_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->unsignedInteger('user_id')->index(); + $table->string('endpoint'); + $table->string('p256dh'); + $table->string('auth'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('subscriptions'); + } +} diff --git a/public/manifest.json b/public/manifest.json index 133c8434..5c2f814b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -22,5 +22,6 @@ "background_color": "#EEE", "start_url": "/", "display": "standalone", - "orientation": "portrait" + "orientation": "portrait", + "gcm_sender_id": "628116355343" } diff --git a/public/service-worker.js b/public/service-worker.js index 0e24768e..dd410e97 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -58,3 +58,7 @@ self.addEventListener('fetch', function(event) { }) ) }); + +self.addEventListener('push', function(event) { + console.log('Push message', event); +}); \ No newline at end of file diff --git a/resources/assets/scripts/app/controllers/application.coffee b/resources/assets/scripts/app/controllers/application.coffee index c5e6cf37..bbed7738 100644 --- a/resources/assets/scripts/app/controllers/application.coffee +++ b/resources/assets/scripts/app/controllers/application.coffee @@ -31,6 +31,7 @@ module.exports = angular.module('ponyfm').controller "application", [ console.log 'Service Worker is supported' navigator.serviceWorker.register('service-worker.js').then((reg) -> console.log 'SW registered', reg + ).catch (err) -> console.log 'SW register failed', err diff --git a/resources/assets/scripts/app/directives/notification-list.coffee b/resources/assets/scripts/app/directives/notification-list.coffee index 63abd0d8..0edfe4d9 100644 --- a/resources/assets/scripts/app/directives/notification-list.coffee +++ b/resources/assets/scripts/app/directives/notification-list.coffee @@ -21,8 +21,8 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () -> scope: {} controller: [ - '$scope', 'notifications', '$timeout', '$rootScope' - ($scope, notifications, $timeout, $rootScope) -> + '$scope', 'notifications', '$timeout', '$rootScope', '$http' + ($scope, notifications, $timeout, $rootScope, $http) -> $scope.notifications = [] isTimeoutScheduled = false @@ -31,6 +31,15 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () -> $rootScope.$on 'shouldUpdateNotifications', () -> refreshNotifications() + checkSubscription = () -> + navigator.serviceWorker.ready.then((reg) -> + reg.pushManager.subscribe({userVisibleOnly: true}).then((sub) -> + console.log 'Push sub', JSON.stringify(sub) + subData = JSON.stringify(sub) + $http.post('/api/web/notifications/subscribe', {subscription: subData}) + ) + ) + refreshNotifications = () -> notifications.getNotifications().done (result) -> if $scope.notifications.length > 0 @@ -51,5 +60,6 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () -> isTimeoutScheduled = false , 60000) + checkSubscription() refreshNotifications() ]