#25: New tracks have working email notifications now!

This commit is contained in:
Peter Deltchev 2016-12-09 02:53:32 -08:00
parent 510d0e80ac
commit 5822408655
21 changed files with 1091 additions and 639 deletions

View file

@ -0,0 +1,45 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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\Http\Controllers;
use Poniverse\Ponyfm\Models\Email;
use Poniverse\Ponyfm\Models\EmailClick;
use Poniverse\Ponyfm\Models\EmailSubscription;
use View;
class NotificationsController extends Controller {
public function getEmailClick($emailKey) {
$emailKey = decrypt($emailKey);
/** @var Email $email */
$email = Email::findOrFail($emailKey);
$email->emailClicks()->create(['ip_address' => \Request::ip()]);
return redirect($email->getActivity()->url);
}
public function getEmailUnsubscribe($subscriptionKey) {
$subscriptionId = decrypt($subscriptionKey);
$subscription = EmailSubscription::findOrFail($subscriptionId);
return var_export($subscription);
}
}

View file

@ -24,6 +24,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Poniverse\Ponyfm\Jobs\Job; use Poniverse\Ponyfm\Jobs\Job;
use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver; use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\EmailDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver; 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;
@ -64,6 +65,9 @@ class SendNotifications extends Job implements ShouldQueue
//NativeDriver::class //NativeDriver::class
]; ];
// NOTE: PonyfmDriver MUST execute before any other drivers; it creates
// the Notification records that the other drivers depend on!
foreach ($drivers as $driver) { foreach ($drivers as $driver) {
/** @var $driver AbstractDriver */ /** @var $driver AbstractDriver */
$driver = new $driver; $driver = new $driver;

View file

@ -31,7 +31,16 @@ abstract class AbstractDriver implements NotificationHandler
public function __construct() public function __construct()
{ {
$this->recipientFinder = new RecipientFinder(get_class($this)); $notificationDriverClass = get_class($this);
switch ($notificationDriverClass) {
case EmailDriver::class:
case PonyfmDriver::class:
$this->recipientFinder = new RecipientFinder(get_class($this));
break;
default:
throw new \Exception("Invalid notification driver!");
}
} }
/** /**

View file

@ -22,9 +22,12 @@ namespace Poniverse\Ponyfm\Library\Notifications\Drivers;
use ArrayAccess; use ArrayAccess;
use Carbon\Carbon; use Carbon\Carbon;
use Log;
use Mail;
use Poniverse\Ponyfm\Contracts\Favouritable; use Poniverse\Ponyfm\Contracts\Favouritable;
use Poniverse\Ponyfm\Models\Activity; use Poniverse\Ponyfm\Models\Activity;
use Poniverse\Ponyfm\Models\Comment; use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Models\Email;
use Poniverse\Ponyfm\Models\Notification; use Poniverse\Ponyfm\Models\Notification;
use Poniverse\Ponyfm\Models\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
@ -35,21 +38,36 @@ class PonyfmDriver extends AbstractDriver
/** /**
* A helper method for bulk insertion of notification records. * A helper method for bulk insertion of notification records.
* *
* @param int $activityId * @param Activity $activity
* @param User[] $recipients collection of {@link User} objects * @param User[] $recipients collection of {@link User} objects
*/ */
private function insertNotifications(int $activityId, $recipients) private function insertNotifications(Activity $activity, $recipients)
{ {
$notifications = []; $notifications = [];
foreach ($recipients as $recipient) { foreach ($recipients as $recipient) {
$notifications[] = [ $notifications[] = [
'activity_id' => $activityId, 'activity_id' => $activity->id,
'user_id' => $recipient->id 'user_id' => $recipient->id
]; ];
} }
Notification::insert($notifications); Notification::insert($notifications);
} }
/**
* Sends out an email about the given activity to the given set of users.
*
* @param Activity $activity
* @param User[] $recipients collection of {@link User} objects
*/
private function sendEmails(Activity $activity, $recipients) {
foreach ($recipients as $recipient) {
$notification = $activity->notifications->where('user_id', $recipient->id)->first();
$email = $notification->email()->create([]);
Log::debug("Attempting to send an email about notification {$notification->id} to {$recipient->email}.");
Mail::to($recipient->email)->queue(new \Poniverse\Ponyfm\Mail\NewTrack($email));
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -63,7 +81,10 @@ class PonyfmDriver extends AbstractDriver
'resource_id' => $track->id, 'resource_id' => $track->id,
]); ]);
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); $recipientsQuery = $this->getRecipients(__FUNCTION__, func_get_args());
$this->insertNotifications($activity, $recipientsQuery->get());
$this->sendEmails($activity, $recipientsQuery->withEmailSubscriptionFor(Activity::TYPE_PUBLISHED_TRACK)->get());
} }
/** /**
@ -79,7 +100,7 @@ class PonyfmDriver extends AbstractDriver
'resource_id' => $playlist->id, 'resource_id' => $playlist->id,
]); ]);
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); $this->insertNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args()));
} }
public function newFollower(User $userBeingFollowed, User $follower) public function newFollower(User $userBeingFollowed, User $follower)
@ -92,7 +113,7 @@ class PonyfmDriver extends AbstractDriver
'resource_id' => $userBeingFollowed->id, 'resource_id' => $userBeingFollowed->id,
]); ]);
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); $this->insertNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args()));
} }
/** /**
@ -108,7 +129,7 @@ class PonyfmDriver extends AbstractDriver
'resource_id' => $comment->id, 'resource_id' => $comment->id,
]); ]);
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); $this->insertNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args()));
} }
/** /**
@ -124,6 +145,6 @@ class PonyfmDriver extends AbstractDriver
'resource_id' => $entityBeingFavourited->id, 'resource_id' => $entityBeingFavourited->id,
]); ]);
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args())); $this->insertNotifications($activity, $this->getRecipients(__FUNCTION__, func_get_args()));
} }
} }

View file

@ -24,8 +24,10 @@ 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\EmailDriver;
use Poniverse\Ponyfm\Library\Notifications\Drivers\NativeDriver; 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\Activity;
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\Subscription;
@ -63,7 +65,13 @@ 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 EmailDriver::class:
return $track->user->followers()->whereHas('emailSubscriptions', function($query) {
$query->where('activity_type', Activity::TYPE_PUBLISHED_TRACK);
})->get();
case NativeDriver::class: case NativeDriver::class:
$followerIds = []; $followerIds = [];
$subIds = []; $subIds = [];
@ -119,6 +127,7 @@ class RecipientFinder implements NotificationHandler
{ {
switch ($this->notificationDriver) { switch ($this->notificationDriver) {
case PonyfmDriver::class: case PonyfmDriver::class:
case EmailDriver::class:
return [$userBeingFollowed]; return [$userBeingFollowed];
case NativeDriver::class: case NativeDriver::class:
return Subscription::where('user_id', '=', $userBeingFollowed->id)->get(); return Subscription::where('user_id', '=', $userBeingFollowed->id)->get();

View file

@ -0,0 +1,101 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Poniverse\Ponyfm\Models\Email;
abstract class BaseNotification extends Mailable {
use Queueable, SerializesModels;
/** @var Email */
protected $emailRecord;
/** @var \Poniverse\Ponyfm\Models\Notification */
protected $notificationRecord;
/** @var \Poniverse\Ponyfm\Models\Activity */
protected $activityRecord;
/** @var \Poniverse\Ponyfm\Models\User */
protected $initiatingUser;
/**
* Create a new message instance.
*
* @param Email $email
*/
public function __construct(Email $email) {
$this->emailRecord = $email;
$this->notificationRecord = $email->notification;
$this->activityRecord = $email->notification->activity;
$this->initiatingUser = $email->notification->activity->initiatingUser;
}
/**
* Build the message.
*
* @return $this
*/
abstract public function build();
/**
* Generates an unsubscribe URL unique to the user.
*
* @return string
*/
protected function generateUnsubscribeUrl() {
$subscriptionKey = encrypt($this->emailRecord->getSubscription()->id);
return route('email:unsubscribe', ['subscriptionKey' => $subscriptionKey]);
}
/**
* Generates a trackable URL to the content item this email is about.
*
* @return string
*/
protected function generateNotificationUrl() {
$emailKey = encrypt($this->emailRecord->id);
return route('email:click', ['emailKey' => $emailKey]);
}
/**
* Helper method to eliminate duplication between different types of notifications.
* Use it inside the build() method on this class's children.
*
* @param string $templateName
* @param string $subject
* @param array $extraVariables
* @return $this
*/
protected function renderEmail(string $templateName, string $subject, array $extraVariables) {
return $this
->subject($subject)
->view("emails.{$templateName}")
->text("emails.{$templateName}_plaintext")
->with(array_merge($extraVariables, [
'notificationUrl' => $this->generateNotificationUrl(),
'unsubscribeUrl' => $this->generateUnsubscribeUrl()
]));
}
}

25
app/Mail/NewTrack.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace Poniverse\Ponyfm\Mail;
class NewTrack extends BaseNotification
{
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$artistName = $this->initiatingUser->display_name;
$trackTitle = $this->activityRecord->resource->title;
return $this->renderEmail(
'new-track',
"{$artistName} published \"{$trackTitle}\"",
[
'artist' => $artistName,
'trackTitle' => $trackTitle,
]);
}
}

View file

@ -66,6 +66,11 @@ class Activity extends Model
'resource_id' => 'integer', 'resource_id' => 'integer',
]; ];
/**
* These constants are stored in the "activity_types" table for the purpose
* of referential data integrity. Any additions or changes to them MUST
* include a database migration to apply the changes to that table as well.
*/
const TYPE_NEWS = 1; const TYPE_NEWS = 1;
const TYPE_PUBLISHED_TRACK = 2; const TYPE_PUBLISHED_TRACK = 2;
const TYPE_PUBLISHED_ALBUM = 3; const TYPE_PUBLISHED_ALBUM = 3;

69
app/Models/Email.php Normal file
View file

@ -0,0 +1,69 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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 Alsofronie\Uuid\UuidModelTrait;
use Illuminate\Database\Eloquent\Model;
use Poniverse\Ponyfm\Models\Notification;
/**
* Poniverse\Ponyfm\Models\Email
*
* @property string $id
* @property integer $notification_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Poniverse\Ponyfm\Models\Notification $notification
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\EmailClick[] $emailClicks
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Email whereId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Email whereNotificationId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Email whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Email whereUpdatedAt($value)
* @mixin \Eloquent
*/
class Email extends Model
{
use UuidModelTrait;
public function notification() {
return $this->belongsTo(Notification::class, 'notification_id', 'id', 'notifications');
}
public function emailClicks() {
return $this->hasMany(EmailClick::class, 'email_id', 'id');
}
public function getActivity():Activity {
return $this->notification->activity;
}
public function getUser():User {
return $this->notification->recipient;
}
public function getSubscription():EmailSubscription {
return $this
->getUser()
->emailSubscriptions()
->where('activity_type', $this->getActivity()->activity_type)
->first();
}
}

51
app/Models/EmailClick.php Normal file
View file

@ -0,0 +1,51 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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 Alsofronie\Uuid\UuidModelTrait;
use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\EmailClick
*
* @property string $id
* @property string $email_id
* @property string $ip_address
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Poniverse\Ponyfm\Models\Email $email
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailClick whereId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailClick whereEmailId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailClick whereIpAddress($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailClick whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailClick whereUpdatedAt($value)
* @mixin \Eloquent
*/
class EmailClick extends Model
{
use UuidModelTrait;
protected $fillable = ['ip_address'];
public function email() {
return $this->belongsTo(Email::class, 'email_id', 'id', 'emails');
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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 Alsofronie\Uuid\UuidModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Poniverse\Ponyfm\Models\User;
/**
* Poniverse\Ponyfm\EmailSubscription
*
* @property string $id
* @property integer $user_id
* @property integer $activity_type
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $deleted_at
* @property-read \Poniverse\Ponyfm\Models\User $user
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereUserId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereActivityType($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\EmailSubscription whereDeletedAt($value)
* @mixin \Eloquent
*/
class EmailSubscription extends Model
{
use UuidModelTrait, SoftDeletes;
public function user() {
return $this->belongsTo(User::class, 'user_id', 'id', 'users');
}
}

View file

@ -31,6 +31,7 @@ use Illuminate\Database\Eloquent\Model;
* @property integer $user_id * @property integer $user_id
* @property boolean $is_read * @property boolean $is_read
* @property-read \Poniverse\Ponyfm\Models\Activity $activity * @property-read \Poniverse\Ponyfm\Models\Activity $activity
* @property-read \Poniverse\Ponyfm\Models\Email $email
* @property-read \Poniverse\Ponyfm\Models\User $recipient * @property-read \Poniverse\Ponyfm\Models\User $recipient
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Notification forUser($user) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Notification forUser($user)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Notification whereId($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Notification whereId($value)
@ -60,6 +61,11 @@ class Notification extends Model
return $this->belongsTo(User::class, 'user_id', 'id'); return $this->belongsTo(User::class, 'user_id', 'id');
} }
public function email()
{
return $this->hasOne(Email::class, 'notification_id', 'id');
}
/** /**
* This scope grabs eager-loaded notifications for the given user. * This scope grabs eager-loaded notifications for the given user.
* *

View file

@ -73,6 +73,8 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @property-read mixed $user * @property-read mixed $user
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $notificationActivities * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $notificationActivities
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Email[] $emails
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\EmailSubscription[] $emailSubscriptions
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereId($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereDisplayName($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereDisplayName($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereUsername($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereUsername($value)
@ -91,6 +93,7 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereRememberToken($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereIsArchived($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereIsArchived($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereDisabledAt($value) * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User whereDisabledAt($value)
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User withEmailSubscriptionFor($activityType)
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable, Commentable class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable, Commentable
@ -126,6 +129,19 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return !$query; return !$query;
} }
/**
* Returns users with an email subscription to the given activity type.
*
* @param $query
* @param int $activityType one of the TYPE_* constants in the Activity class
* @return mixed
*/
public function scopeWithEmailSubscriptionFor($query, int $activityType) {
return $query->whereHas('emailSubscriptions', function ($query) use ($activityType) {
$query->where('activity_type', $activityType);
});
}
/** /**
* Takes the given string, slugifies it, and increments a counter if needed * Takes the given string, slugifies it, and increments a counter if needed
* to generate a unique slug version of it. * to generate a unique slug version of it.
@ -239,6 +255,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->hasManyThrough(Activity::class, Notification::class, 'user_id', 'notification_id', 'id'); return $this->hasManyThrough(Activity::class, Notification::class, 'user_id', 'notification_id', 'id');
} }
public function emails()
{
return $this->hasManyThrough(Email::class, Notification::class, 'user_id', 'notification_id', 'id');
}
public function emailSubscriptions()
{
return $this->hasMany(EmailSubscription::class, 'user_id', 'id');
}
public function getIsArchivedAttribute() public function getIsArchivedAttribute()
{ {
return (bool) $this->attributes['is_archived']; return (bool) $this->attributes['is_archived'];

View file

@ -24,7 +24,9 @@
"predis/predis": "^1.0", "predis/predis": "^1.0",
"ksubileau/color-thief-php": "^1.3", "ksubileau/color-thief-php": "^1.3",
"graham-campbell/exceptions": "^9.1", "graham-campbell/exceptions": "^9.1",
"minishlink/web-push": "^1.0" "minishlink/web-push": "^1.0",
"laravel/legacy-encrypter": "^1.0",
"alsofronie/eloquent-uuid": "^1.0"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",

1125
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -54,7 +54,7 @@ return [
| |
*/ */
'from' => ['address' => null, 'name' => null], 'from' => ['address' => 'hello@pony.fm', 'name' => 'Pony.fm'],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -0,0 +1,107 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* 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/>.
*/
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateEmailNotificationTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::transaction(function(){
// This table is used to enforce referential data integrity
// for the polymorphic "activity" table.
Schema::create('activity_types', function (Blueprint $table) {
$table->unsignedTinyInteger('activity_type')->primary();
$table->string('description');
});
DB::table('activity_types')->insert([
['activity_type' => 1, 'description' => 'news'],
['activity_type' => 2, 'description' => 'followee published a track'],
['activity_type' => 3, 'description' => 'followee published an album'],
['activity_type' => 4, 'description' => 'followee published a playlist'],
['activity_type' => 5, 'description' => 'new follower'],
['activity_type' => 6, 'description' => 'new comment'],
['activity_type' => 7, 'description' => 'new favourite'],
]);
Schema::table('activities', function (Blueprint $table) {
$table->foreign('activity_type')->references('activity_type')->on('activity_types');
});
Schema::create('email_subscriptions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->unsignedInteger('user_id');
$table->unsignedInteger('activity_type');
$table->timestamps();
$table->softDeletes();
$table->foreign('user_id')->references('id')->on('users');
$table->foreign('activity_type')->references('activity_type')->on('activity_types');
});
Schema::create('emails', function (Blueprint $table) {
$table->uuid('id')->primary();
// Clicking the email link should mark the corresponding on-site notification as read.
$table->unsignedBigInteger('notification_id')->index();
$table->timestamps();
$table->foreign('notification_id')->references('id')->on('notifications');
});
Schema::create('email_clicks', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('email_id')->index();
$table->ipAddress('ip_address');
$table->timestamps();
$table->foreign('email_id')->references('id')->on('emails');
});
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::transaction(function() {
Schema::drop('email_clicks');
Schema::drop('emails');
Schema::drop('email_subscriptions');
Schema::table('activities', function (Blueprint $table) {
$table->dropForeign('activities_activity_type_foreign');
});
Schema::drop('activity_types');
});
}
}

View file

@ -44,11 +44,14 @@ Adding new notification types
- [`NotificationManager`](../app/Library/Notifications/NotificationManager.php) - [`NotificationManager`](../app/Library/Notifications/NotificationManager.php)
- [`RecipientFinder`](../app/Library/Notifications/RecipientFinder.php) - [`RecipientFinder`](../app/Library/Notifications/RecipientFinder.php)
- [`PonyfmDriver`](../app/Library/Notifications/PonyfmDriver.php) - [`PonyfmDriver`](../app/Library/Notifications/PonyfmDriver.php)
3. Ensure you create HTML and plaintext templates for the email version of the
notification.
3. Call the new method on the `Notification` facade from wherever the 4. Call the new method on the `Notification` facade from wherever the
new notification gets triggered. new notification gets triggered.
4. Implement any necessary logic for the new notification type in the 5. Implement any necessary logic for the new notification type in the
[`Activity`](../app/Models/Activity.php) model. [`Activity`](../app/Models/Activity.php) model.
@ -92,3 +95,25 @@ There's one exception to the use of `NotificationHandler` - the
data we store about an activity in the database to a notification's API data we store about an activity in the database to a notification's API
representation had to go somewhere, and using the `NotificationHandler` representation had to go somewhere, and using the `NotificationHandler`
interface here would have made this logic a lot more obtuse. interface here would have made this logic a lot more obtuse.
### Data flow
1. Some action that triggers a notification calls the `NotificationManager`
facade.
2. An asynchronous job is kicked off that figures out how to send the
notification.
3. An `Activity` record is created for the action.
4. A `Notification` record is created for every user who is to receive a
notification about that activity. These records act as Pony.fm's on-site
notifications and cannot be disabled.
5. Depending on subscription preferences, push and email notifications will be
sent out as well, each creating their own respective database records. These
are linked to a `Notification` record for unified read/unread tracking.
6. A `Notification` record is marked read when it is viewed on-site or any other
notification type associated with it (like an email or push notification) is
clicked.

View file

@ -0,0 +1,5 @@
<p>{{ $artist }} published a new track on Pony.fm! Listen to it now:</p>
<p><a href="{{ $notificationUrl }}" target="_blank">{{ $trackTitle }}</a></p>
<p><a href="{{ $unsubscribeUrl }}" target="_blank">Unsubscribe from email notifications for new tracks</a></p>

View file

@ -0,0 +1,10 @@
{{ $artist }} published a new track on Pony.fm!
Title: {{ $trackTitle }}
Listen to it:
{{ $notificationUrl }}
---
Unsubscribe from email notifications for new tracks:
{{ $unsubscribeUrl }}

View file

@ -77,6 +77,10 @@ Route::get('p{id}/dl.{extension}', 'PlaylistsController@getDownload');
Route::get('notifications', 'AccountController@getNotifications'); Route::get('notifications', 'AccountController@getNotifications');
Route::get('notifications/email/unsubscribe/{subscriptionKey}', 'NotificationsController@getEmailUnsubscribe')->name('email:unsubscribe');
Route::get('notifications/email/click/{emailKey}', 'NotificationsController@getEmailClick')->name('email:click');
Route::get('oembed', 'TracksController@getOembed'); Route::get('oembed', 'TracksController@getOembed');
Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function () { Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function () {