From 2b72b4dcdbe73cfa48e1bebce70caa9a8b3554b3 Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Fri, 10 Jun 2016 02:19:16 +0100 Subject: [PATCH 01/10] #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() ] From 61520815de15a955b88fc320352b29160f07536d Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Sun, 12 Jun 2016 00:58:10 +0100 Subject: [PATCH 02/10] #25: Basic service worker notifications --- .../Api/Web/NotificationsController.php | 22 +++++- app/Http/routes.php | 1 + app/Jobs/SendNotifications.php | 2 +- .../Notifications/Drivers/NativeDriver.php | 55 +++++++++----- app/Library/Notifications/RecipientFinder.php | 38 ++++++++++ app/Models/Activity.php | 18 +++++ public/service-worker.js | 46 +++++++++++- .../directives/notification-list.html | 15 +++- .../app/controllers/application.coffee | 1 - .../app/directives/notification-list.coffee | 36 +++++++-- .../scripts/app/services/notifications.coffee | 73 +++++++++++++++++++ resources/assets/styles/content.less | 8 ++ resources/assets/styles/forms.less | 52 +++++++++++++ 13 files changed, 332 insertions(+), 35 deletions(-) diff --git a/app/Http/Controllers/Api/Web/NotificationsController.php b/app/Http/Controllers/Api/Web/NotificationsController.php index badec332..5843b3b3 100644 --- a/app/Http/Controllers/Api/Web/NotificationsController.php +++ b/app/Http/Controllers/Api/Web/NotificationsController.php @@ -25,6 +25,8 @@ use Input; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Models\Notification; use Poniverse\Ponyfm\Models\Subscription; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; use Minishlink\WebPush\WebPush; class NotificationsController extends ApiControllerBase @@ -84,9 +86,25 @@ class NotificationsController extends ApiControllerBase 'auth' => $input->keys->auth ]); - return $subscription->id; + return ['id' => $subscription->id]; } else { - return $existing->id; + return ['id' => $existing->id]; } } + + /** + * Removes a user's notification subscription + * + * @return string + */ + public function postUnsubscribe() + { + $input = json_decode(Input::json('subscription')); + + $existing = Subscription::where('endpoint', '=', $input->endpoint) + ->where('user_id', '=', Auth::user()->id) + ->delete(); + + return ['result' => 'success']; + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index a59b82bc..a7e09d50 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -138,6 +138,7 @@ Route::group(['prefix' => 'api/web'], function() { 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::post('/notifications/unsubscribe', 'Api\Web\NotificationsController@postUnsubscribe'); Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit'); diff --git a/app/Jobs/SendNotifications.php b/app/Jobs/SendNotifications.php index 53d1080d..9d71b30e 100644 --- a/app/Jobs/SendNotifications.php +++ b/app/Jobs/SendNotifications.php @@ -62,7 +62,7 @@ class SendNotifications extends Job implements SelfHandling, ShouldQueue // tries (and fails) to serialize static fields. $drivers = [ PonyfmDriver::class, - //NativeDriver::class + NativeDriver::class ]; foreach ($drivers as $driver) { diff --git a/app/Library/Notifications/Drivers/NativeDriver.php b/app/Library/Notifications/Drivers/NativeDriver.php index 4243a4a0..1690d402 100644 --- a/app/Library/Notifications/Drivers/NativeDriver.php +++ b/app/Library/Notifications/Drivers/NativeDriver.php @@ -21,31 +21,48 @@ 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; +use Minishlink\WebPush\WebPush; -class NativeDriver extends AbstractDriver { +class NativeDriver extends AbstractDriver { /** - * A helper method for bulk insertion of notification records. + * Method for sending notifications to devices * - * @param int $activityId + * @param Activity $activity * @param User[] $recipients collection of {@link User} objects */ - private function pushNotifications(int $activityId, $recipients) { - $notifications = []; + private function pushNotifications(Activity $activity, $recipients) { + $apiKeys = array( + 'GCM' => 'AIzaSyCLmCVIgASWL280rHyPz8OP7il3pf8SrGg', + ); + + $webPush = new WebPush($apiKeys); + + $data = [ + 'id' => $activity->id, + 'text' => $activity->getTextAttribute(), + 'title' => $activity->getTitleFromActivityType(), + 'image' => $activity->getThumbnailUrlAttribute(), + 'url' => $activity->url + ]; + + $jsonData = json_encode($data); + foreach ($recipients as $recipient) { - $notifications[] = [ - 'activity_id' => $activityId, - 'user_id' => $recipient->id - ]; + $webPush->sendNotification( + $recipient->endpoint, + $jsonData, + $recipient->p256dh, + $recipient->auth + ); } - Notification::insert($notifications); + + $webPush->flush(); } /** @@ -58,7 +75,7 @@ class NativeDriver extends AbstractDriver { ->get()[0]; - $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + $this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args())); } /** @@ -70,16 +87,16 @@ class NativeDriver extends AbstractDriver { ->where('resource_id', $playlist->id) ->get()[0]; - $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + $this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args())); } public function newFollower(User $userBeingFollowed, User $follower) { - $activity = Activity::where('user_id', $follower->user_id) + $activity = Activity::where('user_id', $follower->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())); + $this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args())); } /** @@ -91,18 +108,18 @@ class NativeDriver extends AbstractDriver { ->where('resource_id', $comment->id) ->get()[0]; - $this->pushNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); + $this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args())); } /** * @inheritdoc */ public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) { - $activity = Activity::where('user_id', $favouriter->user_id) + $activity = Activity::where('user_id', $favouriter->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())); + $this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args())); } } diff --git a/app/Library/Notifications/RecipientFinder.php b/app/Library/Notifications/RecipientFinder.php index e92aef7c..94f8e022 100644 --- a/app/Library/Notifications/RecipientFinder.php +++ b/app/Library/Notifications/RecipientFinder.php @@ -25,9 +25,11 @@ use Illuminate\Foundation\Bus\DispatchesJobs; use Poniverse\Ponyfm\Contracts\Favouritable; use Poniverse\Ponyfm\Contracts\NotificationHandler; use Poniverse\Ponyfm\Jobs\SendNotifications; +use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver; use Poniverse\Ponyfm\Models\Comment; use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\Subscription; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\User; @@ -59,6 +61,21 @@ class RecipientFinder implements NotificationHandler { switch ($this->notificationDriver) { case PonyfmDriver::class: return $track->user->followers; + case NativeDriver::class: + $followerIds = []; + $subIds = []; + $rawSubIds = Subscription::select('id')->get(); + + foreach ($track->user->followers as $follower) { + array_push($followerIds, $follower->id); + } + + foreach ($rawSubIds as $sub) { + array_push($subIds, $sub->id); + } + + $targetIds = array_intersect($followerIds, $subIds); + return Subscription::whereIn('user_id', $targetIds)->get(); default: return $this->fail(); } @@ -71,6 +88,21 @@ class RecipientFinder implements NotificationHandler { switch ($this->notificationDriver) { case PonyfmDriver::class: return $playlist->user->followers; + case NativeDriver::class: + $followerIds = []; + $subIds = []; + $rawSubIds = Subscription::select('id')->get(); + + foreach ($playlist->user->followers as $follower) { + array_push($followerIds, $follower->id); + } + + foreach ($rawSubIds as $sub) { + array_push($subIds, $sub->id); + } + + $targetIds = array_intersect($followerIds, $subIds); + return Subscription::whereIn('user_id', $targetIds)->get(); default: return $this->fail(); } @@ -83,6 +115,8 @@ class RecipientFinder implements NotificationHandler { switch ($this->notificationDriver) { case PonyfmDriver::class: return [$userBeingFollowed]; + case NativeDriver::class: + return Subscription::where('user_id', '=', $userBeingFollowed->id)->get(); default: return $this->fail(); } @@ -98,6 +132,8 @@ class RecipientFinder implements NotificationHandler { $comment->user->id === $comment->resource->user->id ? [] : [$comment->resource->user]; + case NativeDriver::class: + return Subscription::where('user_id', '=', $comment->resource->user->id)->get(); default: return $this->fail(); } @@ -113,6 +149,8 @@ class RecipientFinder implements NotificationHandler { $favouriter->id === $entityBeingFavourited->user->id ? [] : [$entityBeingFavourited->user]; + case NativeDriver::class: + return Subscription::where('user_id', '=', $entityBeingFavourited->user->id)->get(); default: return $this->fail(); } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index fdb91215..5e661d36 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -165,6 +165,24 @@ class Activity extends Model { } } + public function getTitleFromActivityType() { + switch($this->activity_type) { + case static::TYPE_PUBLISHED_TRACK: + return "Pony.fm - New track"; + case static::TYPE_PUBLISHED_PLAYLIST: + return "Pony.fm - New playlist"; + case static::TYPE_NEW_FOLLOWER: + return "Pony.fm - New follower"; + case static::TYPE_NEW_COMMENT: + return "Pony.fm - New comment"; + case static::TYPE_CONTENT_FAVOURITED: + return "Pony.fm - Favourited"; + + default: + return "Pony.fm - Unknown"; + } + } + /** * @return string human-readable Markdown string describing this notification * @throws \Exception diff --git a/public/service-worker.js b/public/service-worker.js index dd410e97..2d3a8f0f 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -23,6 +23,14 @@ var urlsToCache = [ var CACHE_NAME = 'pfm-offline-v1'; +var notifUrlCache = {}; + +function getChromeVersion () { + var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + + return raw ? parseInt(raw[2], 10) : false; +} + // Set the callback for the install step self.addEventListener('install', function(event) { // Perform install steps @@ -60,5 +68,41 @@ self.addEventListener('fetch', function(event) { }); self.addEventListener('push', function(event) { - console.log('Push message', event); + console.log(event); + var data = {}; + + if (event.data) { + console.log(event.data.json()); + data = JSON.parse(event.data.text()); + } + + notifUrlCache['pfm-' + data.id] = data.url; + + self.registration.showNotification(data.title, { + body: data.text, + icon: data.image, + tag: 'pfm-' + data.id + }) +}); + +self.addEventListener('notificationclick', function(event) { + event.notification.close(); + + event.waitUntil( + clients.matchAll({ + type: "window" + }) + .then(function(clientList) { + var url = notifUrlCache[event.notification.tag]; + for (var i = 0; i < clientList.length; i++) { + var client = clientList[i]; + if (client.url == url && 'focus' in client) + return client.focus(); + } + + if (clients.openWindow) { + return clients.openWindow(url); + } + }) + ); }); \ No newline at end of file diff --git a/public/templates/directives/notification-list.html b/public/templates/directives/notification-list.html index 036b2352..1dac7b41 100644 --- a/public/templates/directives/notification-list.html +++ b/public/templates/directives/notification-list.html @@ -1,7 +1,14 @@
-
- -

{{ ::notification.text }}

+
+ + + Enable push notifications +
+
+ +

No notifications :(

-

No notifications :(

\ 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 bbed7738..c5e6cf37 100644 --- a/resources/assets/scripts/app/controllers/application.coffee +++ b/resources/assets/scripts/app/controllers/application.coffee @@ -31,7 +31,6 @@ 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 0edfe4d9..45d0fa57 100644 --- a/resources/assets/scripts/app/directives/notification-list.coffee +++ b/resources/assets/scripts/app/directives/notification-list.coffee @@ -24,21 +24,43 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () -> '$scope', 'notifications', '$timeout', '$rootScope', '$http' ($scope, notifications, $timeout, $rootScope, $http) -> $scope.notifications = [] + $scope.subscribed = false + $scope.switchDisabled = true + $scope.switchHidden = false isTimeoutScheduled = false # TODO: ADD REFRESH BUTTON $rootScope.$on 'shouldUpdateNotifications', () -> refreshNotifications() + + $scope.switchToggled = () -> + if $scope.subscribed + $scope.switchDisabled = true + notifications.subscribe().done (result) -> + if result + $scope.switchDisabled = false + else + $scope.switchDisabled = true + notifications.unsubscribe().done (result) -> + if result + $scope.switchDisabled = false + 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}) - ) - ) + $scope.disabled = true + notifications.checkSubscription().done (subStatus) -> + switch subStatus + when 0 + $scope.subscribed = false + $scope.switchDisabled = false + when 1 + $scope.subscribed = true + $scope.switchDisabled = false + else + $scope.subscribed = false + $scope.switchDisabled = true + $scope.hidden = true refreshNotifications = () -> notifications.getNotifications().done (result) -> diff --git a/resources/assets/scripts/app/services/notifications.coffee b/resources/assets/scripts/app/services/notifications.coffee index 7fa2ad8b..0a10bb94 100644 --- a/resources/assets/scripts/app/services/notifications.coffee +++ b/resources/assets/scripts/app/services/notifications.coffee @@ -61,5 +61,78 @@ module.exports = angular.module('ponyfm').factory('notifications', [ else return 0 + subscribe: () -> + def = new $.Deferred() + navigator.serviceWorker.ready.then (reg) -> + reg.pushManager.subscribe({userVisibleOnly: true}).then (sub) -> + console.log 'Push sub', JSON.stringify(sub) + self.sendSubscriptionToServer(sub).done (result) -> + def.resolve result + + def.promise() + + unsubscribe: () -> + def = new $.Deferred() + navigator.serviceWorker.ready.then (reg) -> + reg.pushManager.getSubscription().then (sub) -> + sub.unsubscribe().then (result) -> + self.removeSubscriptionFromServer(sub).done (result) -> + def.resolve true + .catch (e) -> + console.warn('Unsubscription error: ', e) + def.resolve false + + def.promise() + + sendSubscriptionToServer: (sub) -> + def = new $.Deferred() + subData = JSON.stringify(sub) + $http.post('/api/web/notifications/subscribe', {subscription: subData}).success () -> + def.resolve true + .error () -> + def.resolve false + + def.promise() + + removeSubscriptionFromServer: (sub) -> + def = new $.Deferred() + subData = JSON.stringify(sub) + $http.post('/api/web/notifications/unsubscribe', {subscription: subData}).success () -> + def.resolve true + .error () -> + def.resolve false + + def.promise() + + checkSubscription: () -> + def = new $.Deferred() + if 'serviceWorker' of navigator + if !('showNotification' of ServiceWorkerRegistration.prototype) + console.warn('Notifications aren\'t supported.') + def.resolve -1 + + if Notification.permission == 'denied' + console.warn('The user has blocked notifications.') + def.resolve -1 + + if !('PushManager' of window) + console.warn('Push messaging isn\'t supported.') + def.resolve -1 + + navigator.serviceWorker.ready.then (reg) -> + reg.pushManager.getSubscription().then (sub) -> + if !sub + def.resolve 0 + + self.sendSubscriptionToServer(sub) + + def.resolve 1 + + + else + console.warn('Service worker isn\'t supported.') + def.resolve -1 + + def.promise() self ]) diff --git a/resources/assets/styles/content.less b/resources/assets/styles/content.less index 8afa958d..982a33fa 100644 --- a/resources/assets/styles/content.less +++ b/resources/assets/styles/content.less @@ -626,6 +626,14 @@ html { } } +.notif-switch { + margin-bottom: 15px; + + span { + margin-left: 5px; + } +} + .notif-list { .error { text-align: center; diff --git a/resources/assets/styles/forms.less b/resources/assets/styles/forms.less index b1f2a86b..db34eed0 100644 --- a/resources/assets/styles/forms.less +++ b/resources/assets/styles/forms.less @@ -121,3 +121,55 @@ label { textarea { height: 60px; } + +.switch { + display: inline-block; + position: relative; + width: 40px; + height: 16px; + border-radius: 8px; + background: rgba(0,0,0,0.26); + -webkit-transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1); + transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1); + vertical-align: middle; + cursor: pointer; + + &::before { + content: ''; + position: absolute; + top: -4px; + left: -4px; + width: 24px; + height: 24px; + background: #fafafa; + box-shadow: 0 2px 8px rgba(0,0,0,0.28); + border-radius: 50%; + transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); + } + + &:active::before { + box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px rgba(128,128,128,0.1); + } +} + +input:checked + .switch { + background: rgba(132,82,138,0.5); +} + +input:checked + .switch::before { + left: 20px; + background: #84528a; +} + +input:checked + .switch:active::before { + box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px rgba(0,150,136,0.2); +} + +.switch.disabled { + background: #bfbfbf; +} + +.switch.disabled::before { + background: #dadada; +} + From 88aa2950b37caeefb96c5f085adf4947dd3ed7f2 Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Sun, 12 Jun 2016 01:07:14 +0100 Subject: [PATCH 03/10] #25: Hide toggle if service worker fails to register --- .../app/controllers/application.coffee | 1 + .../app/directives/notification-list.coffee | 29 ++++++++++--------- .../scripts/app/services/notifications.coffee | 1 + 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/resources/assets/scripts/app/controllers/application.coffee b/resources/assets/scripts/app/controllers/application.coffee index c5e6cf37..7e09bd5b 100644 --- a/resources/assets/scripts/app/controllers/application.coffee +++ b/resources/assets/scripts/app/controllers/application.coffee @@ -33,6 +33,7 @@ module.exports = angular.module('ponyfm').controller "application", [ console.log 'SW registered', reg ).catch (err) -> console.log 'SW register failed', err + notifications.serviceWorkerSupported = false $scope.menuToggle = () -> $rootScope.$broadcast('sidebarToggled') diff --git a/resources/assets/scripts/app/directives/notification-list.coffee b/resources/assets/scripts/app/directives/notification-list.coffee index 45d0fa57..d67d64eb 100644 --- a/resources/assets/scripts/app/directives/notification-list.coffee +++ b/resources/assets/scripts/app/directives/notification-list.coffee @@ -48,19 +48,22 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () -> checkSubscription = () -> - $scope.disabled = true - notifications.checkSubscription().done (subStatus) -> - switch subStatus - when 0 - $scope.subscribed = false - $scope.switchDisabled = false - when 1 - $scope.subscribed = true - $scope.switchDisabled = false - else - $scope.subscribed = false - $scope.switchDisabled = true - $scope.hidden = true + if 'serviceWorker' of navigator && notifications.serviceWorkerSupported + $scope.disabled = true + notifications.checkSubscription().done (subStatus) -> + switch subStatus + when 0 + $scope.subscribed = false + $scope.switchDisabled = false + when 1 + $scope.subscribed = true + $scope.switchDisabled = false + else + $scope.subscribed = false + $scope.switchDisabled = true + $scope.hidden = true + else + $scope.switchHidden = true refreshNotifications = () -> notifications.getNotifications().done (result) -> diff --git a/resources/assets/scripts/app/services/notifications.coffee b/resources/assets/scripts/app/services/notifications.coffee index 0a10bb94..56aa66d9 100644 --- a/resources/assets/scripts/app/services/notifications.coffee +++ b/resources/assets/scripts/app/services/notifications.coffee @@ -19,6 +19,7 @@ module.exports = angular.module('ponyfm').factory('notifications', [ ($rootScope, $http) -> self = notificationList: [] + serviceWorkerSupported: true getNotifications: () -> def = new $.Deferred() From a29b5a62bbf738ef08590b93c1ce65169063668e Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Sun, 12 Jun 2016 01:12:11 +0100 Subject: [PATCH 04/10] #25: Installed GMP, needed for PHP web-push --- vagrant/install.sh | 3 +++ vagrant/php-overrides.ini | 1 + 2 files changed, 4 insertions(+) diff --git a/vagrant/install.sh b/vagrant/install.sh index 14b55c27..a3a9b513 100755 --- a/vagrant/install.sh +++ b/vagrant/install.sh @@ -24,6 +24,9 @@ sudo apt-get -qq update echo "Installing tagging tools & other dependencies..." sudo apt-get -qq install -y AtomicParsley flac vorbis-tools imagemagick oracle-java8-installer elasticsearch pkg-config yasm libfaac-dev libmp3lame-dev libvorbis-dev libtheora-dev +echo "Installing PHP extensions" +sudo apt-get -qq install -y libgmp-dev php-gmp + if type ffmpeg &>/dev/null; then echo "ffmpeg is installed!" diff --git a/vagrant/php-overrides.ini b/vagrant/php-overrides.ini index b2c1457d..2f5b8293 100644 --- a/vagrant/php-overrides.ini +++ b/vagrant/php-overrides.ini @@ -3,3 +3,4 @@ expose_php = Off post_max_size = 250M upload_max_filesize = 200M +extension=php_gmp.dll \ No newline at end of file From 0c1a283867a6c65a495bd8728c903c9305924cc1 Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Sun, 12 Jun 2016 02:51:40 +0100 Subject: [PATCH 05/10] Removed old developement API key --- .../Notifications/Drivers/NativeDriver.php | 51 ++++++++++--------- config/ponyfm.php | 13 +++++ resources/environments/.env.local | 1 + 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/app/Library/Notifications/Drivers/NativeDriver.php b/app/Library/Notifications/Drivers/NativeDriver.php index 1690d402..eac30fdd 100644 --- a/app/Library/Notifications/Drivers/NativeDriver.php +++ b/app/Library/Notifications/Drivers/NativeDriver.php @@ -21,6 +21,7 @@ namespace Poniverse\Ponyfm\Library\Notifications\Drivers; +use Config; use Poniverse\Ponyfm\Contracts\Favouritable; use Poniverse\Ponyfm\Models\Activity; use Poniverse\Ponyfm\Models\Comment; @@ -37,32 +38,34 @@ class NativeDriver extends AbstractDriver { * @param User[] $recipients collection of {@link User} objects */ private function pushNotifications(Activity $activity, $recipients) { - $apiKeys = array( - 'GCM' => 'AIzaSyCLmCVIgASWL280rHyPz8OP7il3pf8SrGg', - ); - - $webPush = new WebPush($apiKeys); - - $data = [ - 'id' => $activity->id, - 'text' => $activity->getTextAttribute(), - 'title' => $activity->getTitleFromActivityType(), - 'image' => $activity->getThumbnailUrlAttribute(), - 'url' => $activity->url - ]; - - $jsonData = json_encode($data); - - foreach ($recipients as $recipient) { - $webPush->sendNotification( - $recipient->endpoint, - $jsonData, - $recipient->p256dh, - $recipient->auth + if (Config::get('ponyfm.gcm_key') != 'default') { + $apiKeys = array( + 'GCM' => Config::get('ponyfm.gcm_key'), ); - } - $webPush->flush(); + $webPush = new WebPush($apiKeys); + + $data = [ + 'id' => $activity->id, + 'text' => $activity->getTextAttribute(), + 'title' => $activity->getTitleFromActivityType(), + 'image' => $activity->getThumbnailUrlAttribute(), + 'url' => $activity->url + ]; + + $jsonData = json_encode($data); + + foreach ($recipients as $recipient) { + $webPush->sendNotification( + $recipient->endpoint, + $jsonData, + $recipient->p256dh, + $recipient->auth + ); + } + + $webPush->flush(); + } } /** diff --git a/config/ponyfm.php b/config/ponyfm.php index 6d7ae949..c8184243 100644 --- a/config/ponyfm.php +++ b/config/ponyfm.php @@ -116,4 +116,17 @@ return [ */ 'user_slug_minimum_length' => 3 + + /* + |-------------------------------------------------------------------------- + | Indexing queue name + |-------------------------------------------------------------------------- + | + | Google Cloud Messaging API key. Needs to be generated in the Google Cloud + | Console as a browser key. This is used to send notifications to users + | with push notifications enabled. + | + */ + + 'gcm_key' => env('GCM_KEY', 'default'), ]; diff --git a/resources/environments/.env.local b/resources/environments/.env.local index 042397be..42c8fe39 100644 --- a/resources/environments/.env.local +++ b/resources/environments/.env.local @@ -24,3 +24,4 @@ PONI_CLIENT_SECRET=null PONYFM_DATASTORE=/vagrant/storage/app/datastore GOOGLE_ANALYTICS_ID=null +GCM_KEY=12345 From 64d28f0b1f2caccf374be9c75d85017063fb358b Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Mon, 13 Jun 2016 13:32:49 +0100 Subject: [PATCH 06/10] Attempt to fix page load issues on staging server --- public/service-worker.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index 2d3a8f0f..a0f174cf 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -56,15 +56,19 @@ self.addEventListener('activate', function (event) { // Basic offline mode // Just respond with an offline error page for now self.addEventListener('fetch', function(event) { - event.respondWith( - caches.match(event.request).then(function(response) { - return response || fetch(event.request); - }).catch(function () { - if (event.request.mode == 'navigate') { - return caches.match('/offline.html'); - } - }) - ) + if (event.request.url.indexOf('stage.pony.fm') > -1) { + event.respondWith(fetch(event.request)); + } else { + event.respondWith( + caches.match(event.request).then(function (response) { + return response || fetch(event.request); + }).catch(function () { + if (event.request.mode == 'navigate') { + return caches.match('/offline.html'); + } + }) + ) + } }); self.addEventListener('push', function(event) { From 6e87e330f359028ac09f89eb342adcdabe95826a Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Mon, 13 Jun 2016 13:49:48 +0100 Subject: [PATCH 07/10] Ignore upload in service worker --- public/service-worker.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index a0f174cf..58eaa9db 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -56,8 +56,9 @@ self.addEventListener('activate', function (event) { // Basic offline mode // Just respond with an offline error page for now self.addEventListener('fetch', function(event) { - if (event.request.url.indexOf('stage.pony.fm') > -1) { - event.respondWith(fetch(event.request)); + if (event.request.url.indexOf('stage.pony.fm') > -1 || event.request.url.indexOf('upload') > -1) { + // Ignore some requests + return; } else { event.respondWith( caches.match(event.request).then(function (response) { From 60f55e622325f24e14e5d4b5dcfa1444f4c990ab Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Tue, 14 Jun 2016 00:40:14 +0100 Subject: [PATCH 08/10] Ignore requests with no subscription data --- .../Api/Web/NotificationsController.php | 29 ++++++++++--------- .../Notifications/Drivers/NativeDriver.php | 3 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Api/Web/NotificationsController.php b/app/Http/Controllers/Api/Web/NotificationsController.php index 5843b3b3..10a25a37 100644 --- a/app/Http/Controllers/Api/Web/NotificationsController.php +++ b/app/Http/Controllers/Api/Web/NotificationsController.php @@ -73,22 +73,25 @@ class NotificationsController extends ApiControllerBase public function postSubscribe() { $input = json_decode(Input::json('subscription')); + if ($input != 'null') { + $existing = Subscription::where('endpoint', '=', $input->endpoint) + ->where('user_id', '=', Auth::user()->id) + ->first(); - $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 + ]); - if ($existing === null) { - $subscription = Subscription::create([ - 'user_id' => Auth::user()->id, - 'endpoint' => $input->endpoint, - 'p256dh' => $input->keys->p256dh, - 'auth' => $input->keys->auth - ]); - - return ['id' => $subscription->id]; + return ['id' => $subscription->id]; + } else { + return ['id' => $existing->id]; + } } else { - return ['id' => $existing->id]; + return ['error' => 'No data']; } } diff --git a/app/Library/Notifications/Drivers/NativeDriver.php b/app/Library/Notifications/Drivers/NativeDriver.php index eac30fdd..c340b5e9 100644 --- a/app/Library/Notifications/Drivers/NativeDriver.php +++ b/app/Library/Notifications/Drivers/NativeDriver.php @@ -37,7 +37,8 @@ class NativeDriver extends AbstractDriver { * @param Activity $activity * @param User[] $recipients collection of {@link User} objects */ - private function pushNotifications(Activity $activity, $recipients) { + private function pushNotifications(Activity $activity, $recipients) + { if (Config::get('ponyfm.gcm_key') != 'default') { $apiKeys = array( 'GCM' => Config::get('ponyfm.gcm_key'), From c649763409c20f123e46372bea21ed486061feec Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Wed, 15 Jun 2016 13:28:45 +0100 Subject: [PATCH 09/10] Moved feature detect to seperate function --- .../scripts/app/services/notifications.coffee | 37 ++++++++++++++----- vagrant/php-overrides.ini | 3 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/resources/assets/scripts/app/services/notifications.coffee b/resources/assets/scripts/app/services/notifications.coffee index 56aa66d9..9904e157 100644 --- a/resources/assets/scripts/app/services/notifications.coffee +++ b/resources/assets/scripts/app/services/notifications.coffee @@ -107,17 +107,9 @@ module.exports = angular.module('ponyfm').factory('notifications', [ checkSubscription: () -> def = new $.Deferred() + if 'serviceWorker' of navigator - if !('showNotification' of ServiceWorkerRegistration.prototype) - console.warn('Notifications aren\'t supported.') - def.resolve -1 - - if Notification.permission == 'denied' - console.warn('The user has blocked notifications.') - def.resolve -1 - - if !('PushManager' of window) - console.warn('Push messaging isn\'t supported.') + if !self.checkPushSupport() def.resolve -1 navigator.serviceWorker.ready.then (reg) -> @@ -135,5 +127,30 @@ module.exports = angular.module('ponyfm').factory('notifications', [ def.resolve -1 def.promise() + + checkPushSupport: () -> + if !('showNotification' of ServiceWorkerRegistration.prototype) + console.warn('Notifications aren\'t supported.') + return false + + if Notification.permission == 'denied' + console.warn('The user has blocked notifications.') + return false + + if !('PushManager' of window) + console.warn('Push messaging isn\'t supported.') + return false + + # If Chrome 50+ + if !!window.chrome && !!window.chrome.webstore + if parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) >= 50 + return true + # If Firefox 46+ + else if typeof InstallTrigger != 'undefined' + if parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1]) >= 46 + return true + + return false + self ]) diff --git a/vagrant/php-overrides.ini b/vagrant/php-overrides.ini index 2f5b8293..9a38c514 100644 --- a/vagrant/php-overrides.ini +++ b/vagrant/php-overrides.ini @@ -2,5 +2,4 @@ expose_php = Off post_max_size = 250M -upload_max_filesize = 200M -extension=php_gmp.dll \ No newline at end of file +upload_max_filesize = 200M \ No newline at end of file From 66c6334b10cc116c8069c3582781ab81d898df8c Mon Sep 17 00:00:00 2001 From: Josef Citrine Date: Wed, 15 Jun 2016 14:18:55 +0100 Subject: [PATCH 10/10] God damn it --- config/ponyfm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/ponyfm.php b/config/ponyfm.php index c8184243..a0d0ff2b 100644 --- a/config/ponyfm.php +++ b/config/ponyfm.php @@ -115,7 +115,7 @@ return [ | */ - 'user_slug_minimum_length' => 3 + 'user_slug_minimum_length' => 3, /* |--------------------------------------------------------------------------