Merge pull request #92 from Poniverse/native-notifications

Native notifications
This commit is contained in:
Josef Citrine 2016-06-17 17:49:48 +01:00 committed by GitHub
commit a9e7a69268
22 changed files with 988 additions and 22 deletions

View file

@ -21,10 +21,13 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Auth; use Auth;
use Carbon\Carbon;
use Input; use Input;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Models\Notification; 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 class NotificationsController extends ApiControllerBase
{ {
@ -59,4 +62,52 @@ class NotificationsController extends ApiControllerBase
return ['notifications_updated' => $numberOfUpdatedRows]; 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'));
if ($input != 'null') {
$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 ['id' => $subscription->id];
} else {
return ['id' => $existing->id];
}
} else {
return ['error' => 'No data'];
}
}
/**
* 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'];
}
} }

View file

@ -134,8 +134,11 @@ Route::group(['prefix' => 'api/web'], function() {
Route::post('/dashboard/read-news', 'Api\Web\DashboardController@postReadNews'); Route::post('/dashboard/read-news', 'Api\Web\DashboardController@postReadNews');
Route::get('/account/settings/{slug}', 'Api\Web\AccountController@getSettings'); Route::get('/account/settings/{slug}', 'Api\Web\AccountController@getSettings');
Route::get('/notifications', 'Api\Web\NotificationsController@getNotifications'); Route::get('/notifications', 'Api\Web\NotificationsController@getNotifications');
Route::put('/notifications/mark-as-read', 'Api\Web\NotificationsController@putMarkAsRead'); 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'); 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 Poniverse\Ponyfm\Jobs\Job;
use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Bus\SelfHandling;
use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver;
use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\User;
use SerializesModels; use SerializesModels;
@ -60,7 +61,8 @@ class SendNotifications extends Job implements SelfHandling, ShouldQueue
// to work around a Laravel bug - namely, the SerializesModels trait // to work around a Laravel bug - namely, the SerializesModels trait
// tries (and fails) to serialize static fields. // tries (and fails) to serialize static fields.
$drivers = [ $drivers = [
PonyfmDriver::class PonyfmDriver::class,
NativeDriver::class
]; ];
foreach ($drivers as $driver) { foreach ($drivers as $driver) {

View file

@ -0,0 +1,129 @@
<?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 Config;
use Poniverse\Ponyfm\Contracts\Favouritable;
use Poniverse\Ponyfm\Models\Activity;
use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\User;
use Minishlink\WebPush\WebPush;
class NativeDriver extends AbstractDriver {
/**
* Method for sending notifications to devices
*
* @param Activity $activity
* @param User[] $recipients collection of {@link User} objects
*/
private function pushNotifications(Activity $activity, $recipients)
{
if (Config::get('ponyfm.gcm_key') != 'default') {
$apiKeys = array(
'GCM' => Config::get('ponyfm.gcm_key'),
);
$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();
}
}
/**
* @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, $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, $this->getRecipients(__FUNCTION__, func_get_args()));
}
public function newFollower(User $userBeingFollowed, User $follower) {
$activity = Activity::where('user_id', $follower->id)
->where('activity_type', Activity::TYPE_NEW_FOLLOWER)
->where('resource_id', $userBeingFollowed->id)
->get()[0];
$this->pushNotifications($activity, $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, $this->getRecipients(__FUNCTION__, func_get_args()));
}
/**
* @inheritdoc
*/
public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) {
$activity = Activity::where('user_id', $favouriter->id)
->where('activity_type', Activity::TYPE_CONTENT_FAVOURITED)
->where('resource_id', $entityBeingFavourited->id)
->get()[0];
$this->pushNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args()));
}
}

View file

@ -25,9 +25,11 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
use Poniverse\Ponyfm\Contracts\Favouritable; use Poniverse\Ponyfm\Contracts\Favouritable;
use Poniverse\Ponyfm\Contracts\NotificationHandler; use Poniverse\Ponyfm\Contracts\NotificationHandler;
use Poniverse\Ponyfm\Jobs\SendNotifications; use Poniverse\Ponyfm\Jobs\SendNotifications;
use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver;
use Poniverse\Ponyfm\Models\Comment; use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Models\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Models\Subscription;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\User;
@ -59,6 +61,21 @@ class RecipientFinder implements NotificationHandler {
switch ($this->notificationDriver) { switch ($this->notificationDriver) {
case PonyfmDriver::class: case PonyfmDriver::class:
return $track->user->followers; 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: default:
return $this->fail(); return $this->fail();
} }
@ -71,6 +88,21 @@ class RecipientFinder implements NotificationHandler {
switch ($this->notificationDriver) { switch ($this->notificationDriver) {
case PonyfmDriver::class: case PonyfmDriver::class:
return $playlist->user->followers; 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: default:
return $this->fail(); return $this->fail();
} }
@ -83,6 +115,8 @@ class RecipientFinder implements NotificationHandler {
switch ($this->notificationDriver) { switch ($this->notificationDriver) {
case PonyfmDriver::class: case PonyfmDriver::class:
return [$userBeingFollowed]; return [$userBeingFollowed];
case NativeDriver::class:
return Subscription::where('user_id', '=', $userBeingFollowed->id)->get();
default: default:
return $this->fail(); return $this->fail();
} }
@ -98,6 +132,8 @@ class RecipientFinder implements NotificationHandler {
$comment->user->id === $comment->resource->user->id $comment->user->id === $comment->resource->user->id
? [] ? []
: [$comment->resource->user]; : [$comment->resource->user];
case NativeDriver::class:
return Subscription::where('user_id', '=', $comment->resource->user->id)->get();
default: default:
return $this->fail(); return $this->fail();
} }
@ -113,6 +149,8 @@ class RecipientFinder implements NotificationHandler {
$favouriter->id === $entityBeingFavourited->user->id $favouriter->id === $entityBeingFavourited->user->id
? [] ? []
: [$entityBeingFavourited->user]; : [$entityBeingFavourited->user];
case NativeDriver::class:
return Subscription::where('user_id', '=', $entityBeingFavourited->user->id)->get();
default: default:
return $this->fail(); return $this->fail();
} }

View file

@ -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 * @return string human-readable Markdown string describing this notification
* @throws \Exception * @throws \Exception

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", "barryvdh/laravel-debugbar": "^2.2",
"predis/predis": "^1.0", "predis/predis": "^1.0",
"ksubileau/color-thief-php": "^1.3", "ksubileau/color-thief-php": "^1.3",
"graham-campbell/exceptions": "^8.6" "graham-campbell/exceptions": "^8.6",
"minishlink/web-push": "^1.0"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "5c9f86045c835b8964e1eb198a7727ea", "hash": "070553e4e21387213808a4cb779e5f16",
"content-hash": "a09f15b4c222efb8e676a4efdd979b4f", "content-hash": "98c97b7ca37abf031e353edaf4ac2ae3",
"packages": [ "packages": [
{ {
"name": "barryvdh/laravel-debugbar", "name": "barryvdh/laravel-debugbar",
@ -124,6 +124,59 @@
], ],
"time": "2016-03-03 08:45:00" "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", "name": "classpreloader/classpreloader",
"version": "3.0.0", "version": "3.0.0",
@ -883,6 +936,58 @@
], ],
"time": "2016-03-18 16:31:37" "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", "name": "graham-campbell/exceptions",
"version": "v8.6.1", "version": "v8.6.1",
@ -1454,6 +1559,54 @@
], ],
"time": "2015-12-05 17:17:57" "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", "name": "ksubileau/color-thief-php",
"version": "v1.3.0", "version": "v1.3.0",
@ -1780,6 +1933,118 @@
], ],
"time": "2016-01-22 12:22:23" "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", "name": "monolog/monolog",
"version": "1.19.0", "version": "1.19.0",
@ -2400,6 +2665,115 @@
"description": "A lightweight implementation of CommonJS Promises/A for PHP", "description": "A lightweight implementation of CommonJS Promises/A for PHP",
"time": "2016-05-03 17:50:52" "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", "name": "swiftmailer/swiftmailer",
"version": "v5.4.2", "version": "v5.4.2",

View file

@ -115,5 +115,18 @@ return [
| |
*/ */
'user_slug_minimum_length' => 3 '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'),
]; ];

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", "background_color": "#EEE",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"orientation": "portrait" "orientation": "portrait",
"gcm_sender_id": "628116355343"
} }

View file

@ -23,6 +23,14 @@ var urlsToCache = [
var CACHE_NAME = 'pfm-offline-v1'; 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 // Set the callback for the install step
self.addEventListener('install', function(event) { self.addEventListener('install', function(event) {
// Perform install steps // Perform install steps
@ -48,6 +56,10 @@ self.addEventListener('activate', function (event) {
// Basic offline mode // Basic offline mode
// Just respond with an offline error page for now // Just respond with an offline error page for now
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.url.indexOf('stage.pony.fm') > -1 || event.request.url.indexOf('upload') > -1) {
// Ignore some requests
return;
} else {
event.respondWith( event.respondWith(
caches.match(event.request).then(function (response) { caches.match(event.request).then(function (response) {
return response || fetch(event.request); return response || fetch(event.request);
@ -57,4 +69,45 @@ self.addEventListener('fetch', function(event) {
} }
}) })
) )
}
});
self.addEventListener('push', function(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);
}
})
);
}); });

View file

@ -1,7 +1,14 @@
<div class="notif-list"> <div class="notif-list">
<div class="notif-switch" ng-hide="switchHidden">
<input id="test" type="checkbox" hidden="hidden" ng-model="subscribed" ng-change="switchToggled()" ng-disabled="switchDisabled"/>
<label for="test" class="switch" ng-class="{'disabled': switchDisabled}"></label>
<span>Enable push notifications</span>
</div>
<div class="notif-scroll">
<div ng-repeat="notification in notifications" class="notification" ng-class="{'unread': !notification.is_read}"> <div ng-repeat="notification in notifications" class="notification" ng-class="{'unread': !notification.is_read}">
<a href="{{ ::notification.url }}" class="img-link"><img pfm-src-loader="::notification.thumbnail_url" pfm-src-size="thumbnail"></a> <a href="{{ ::notification.url }}" class="img-link"><img pfm-src-loader="::notification.thumbnail_url" pfm-src-size="thumbnail"></a>
<a href="{{ ::notification.url }}" class="message"><p>{{ ::notification.text }}</p></a> <a href="{{ ::notification.url }}" class="message"><p>{{ ::notification.text }}</p></a>
</div> </div>
<p ng-show="notifications.length < 1" class="error">No notifications :(</p> <p ng-show="notifications.length < 1" class="error">No notifications :(</p>
</div> </div>
</div>

View file

@ -33,6 +33,7 @@ module.exports = angular.module('ponyfm').controller "application", [
console.log 'SW registered', reg console.log 'SW registered', reg
).catch (err) -> ).catch (err) ->
console.log 'SW register failed', err console.log 'SW register failed', err
notifications.serviceWorkerSupported = false
$scope.menuToggle = () -> $scope.menuToggle = () ->
$rootScope.$broadcast('sidebarToggled') $rootScope.$broadcast('sidebarToggled')

View file

@ -21,9 +21,12 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () ->
scope: {} scope: {}
controller: [ controller: [
'$scope', 'notifications', '$timeout', '$rootScope' '$scope', 'notifications', '$timeout', '$rootScope', '$http'
($scope, notifications, $timeout, $rootScope) -> ($scope, notifications, $timeout, $rootScope, $http) ->
$scope.notifications = [] $scope.notifications = []
$scope.subscribed = false
$scope.switchDisabled = true
$scope.switchHidden = false
isTimeoutScheduled = false isTimeoutScheduled = false
# TODO: ADD REFRESH BUTTON # TODO: ADD REFRESH BUTTON
@ -31,6 +34,37 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () ->
$rootScope.$on 'shouldUpdateNotifications', () -> $rootScope.$on 'shouldUpdateNotifications', () ->
refreshNotifications() 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 = () ->
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 = () -> refreshNotifications = () ->
notifications.getNotifications().done (result) -> notifications.getNotifications().done (result) ->
if $scope.notifications.length > 0 if $scope.notifications.length > 0
@ -51,5 +85,6 @@ module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () ->
isTimeoutScheduled = false isTimeoutScheduled = false
, 60000) , 60000)
checkSubscription()
refreshNotifications() refreshNotifications()
] ]

View file

@ -19,6 +19,7 @@ module.exports = angular.module('ponyfm').factory('notifications', [
($rootScope, $http) -> ($rootScope, $http) ->
self = self =
notificationList: [] notificationList: []
serviceWorkerSupported: true
getNotifications: () -> getNotifications: () ->
def = new $.Deferred() def = new $.Deferred()
@ -61,5 +62,95 @@ module.exports = angular.module('ponyfm').factory('notifications', [
else else
return 0 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 !self.checkPushSupport()
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()
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 self
]) ])

View file

@ -626,6 +626,14 @@ html {
} }
} }
.notif-switch {
margin-bottom: 15px;
span {
margin-left: 5px;
}
}
.notif-list { .notif-list {
.error { .error {
text-align: center; text-align: center;

View file

@ -121,3 +121,55 @@ label {
textarea { textarea {
height: 60px; 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;
}

View file

@ -24,3 +24,4 @@ PONI_CLIENT_SECRET=null
PONYFM_DATASTORE=/vagrant/storage/app/datastore PONYFM_DATASTORE=/vagrant/storage/app/datastore
GOOGLE_ANALYTICS_ID=null GOOGLE_ANALYTICS_ID=null
GCM_KEY=12345

View file

@ -24,6 +24,9 @@ sudo apt-get -qq update
echo "Installing tagging tools & other dependencies..." 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 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 if type ffmpeg &>/dev/null; then
echo "ffmpeg is installed!" echo "ffmpeg is installed!"