#25: Service worker subscription

This commit is contained in:
Josef Citrine 2016-06-10 02:19:16 +01:00
parent fbb42b27e6
commit 2b72b4dcdb
12 changed files with 626 additions and 8 deletions

View file

@ -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;
}
}
}

View file

@ -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');

View file

@ -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) {

View file

@ -0,0 +1,108 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Josef Citrine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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()));
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Josef Citrine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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');
}
}

View file

@ -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",

378
composer.lock generated
View file

@ -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",

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSubscriptionTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('subscriptions', function (Blueprint $table) {
$table->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');
}
}

View file

@ -22,5 +22,6 @@
"background_color": "#EEE",
"start_url": "/",
"display": "standalone",
"orientation": "portrait"
"orientation": "portrait",
"gcm_sender_id": "628116355343"
}

View file

@ -58,3 +58,7 @@ self.addEventListener('fetch', function(event) {
})
)
});
self.addEventListener('push', function(event) {
console.log('Push message', event);
});

View file

@ -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

View file

@ -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()
]