the browser-facing portion of osu!
at master 235 lines 8.2 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Jobs\Notifications; 7 8use App\Events\NewPrivateNotificationEvent; 9use App\Exceptions\InvalidNotificationException; 10use App\Models\Notification; 11use App\Models\User; 12use App\Models\UserGroup; 13use App\Models\UserNotification; 14use App\Models\UserNotificationOption; 15use App\Traits\NotificationQueue; 16use Illuminate\Bus\Queueable; 17use Illuminate\Contracts\Queue\ShouldQueue; 18use Illuminate\Queue\SerializesModels; 19 20abstract class BroadcastNotificationBase implements ShouldQueue 21{ 22 use NotificationQueue, Queueable, SerializesModels; 23 24 const CONTENT_TRUNCATE = 36; 25 const DELIVERY_MODE_DEFAULTS = ['mail' => false, 'push' => true]; 26 const NOTIFICATION_OPTION_NAME = null; 27 28 protected $name; 29 protected $source; 30 protected $timestamp; 31 32 public static function getBaseKey(Notification $notification): string 33 { 34 return "{$notification->notifiable_type}.{$notification->category}.{$notification->name}"; 35 } 36 37 public static function getMailGroupingKey(Notification $notification): string 38 { 39 $base = static::getBaseKey($notification); 40 41 return "{$base}-{$notification->notifiable_type}-{$notification->notifiable_id}"; 42 } 43 44 abstract public static function getMailLink(Notification $notification): string; 45 46 public static function getNotificationClass(string $name) 47 { 48 $class = get_class_namespace(static::class).'\\'.studly_case($name); 49 50 if (!class_exists($class)) { 51 throw new InvalidNotificationException('Invalid event name: '.$name); 52 } 53 54 return $class; 55 } 56 57 public static function getNotificationClassFromNotification(Notification $notification) 58 { 59 return static::getNotificationClass($notification->name); 60 } 61 62 /** 63 * Checks if a notification should be included into the user notification mail digest. 64 * This mainly used to exclude the more generic notifications from being re-notified if they haven't been 'read'. 65 * The notifications with more specific messages or links should still be included, since they're usually going to be 66 * different in each digest. 67 * 68 * @param Notification $notification The notification to check. 69 * @param $watches A keyed collection of watches; this is here because there's no DB context cache but watches need preloading, 70 * so it's preloaded and then passed in. 71 * @param $time The time the mail notification is considered run at. 72 * @return bool 73 */ 74 public static function shouldSendMail(Notification $notification, $watches, $time): bool 75 { 76 return true; 77 } 78 79 private static function applyDeliverySettings(array $userIds) 80 { 81 if (static::NOTIFICATION_OPTION_NAME !== null) { 82 $notificationOptionsQuery = UserNotificationOption 83 ::where(['name' => static::NOTIFICATION_OPTION_NAME]) 84 ->whereNotNull('details'); 85 } 86 87 $deliverySettings = []; 88 89 foreach (array_chunk($userIds, 10000) as $chunkedUserIds) { 90 if (isset($notificationOptionsQuery)) { 91 $notificationOptions = (clone $notificationOptionsQuery) 92 ->whereIntegerInRaw('user_id', $chunkedUserIds) 93 ->get() 94 ->keyBy('user_id'); 95 } else { 96 $notificationOptions = []; 97 } 98 99 foreach ($chunkedUserIds as $userId) { 100 $details = $notificationOptions[$userId]->details ?? static::DELIVERY_MODE_DEFAULTS; 101 $delivery = 0; 102 foreach (UserNotification::DELIVERY_OFFSETS as $type => $_offset) { 103 if ($details[$type] ?? static::DELIVERY_MODE_DEFAULTS[$type]) { 104 $delivery |= UserNotification::deliveryMask($type); 105 } 106 } 107 108 if ($delivery !== 0) { 109 $deliverySettings[$userId] = $delivery; 110 } 111 } 112 } 113 114 return $deliverySettings; 115 } 116 117 private static function excludeBotUserIds(array $userIds) 118 { 119 $botGroupId = app('groups')->byIdentifier('bot')->getKey(); 120 121 $allBotUserIds = UserGroup 122 ::where('group_id', $botGroupId) 123 ->select('user_id'); 124 125 // only consider users with bot as their primary group 126 $botUserIds = User 127 ::where('group_id', $botGroupId) 128 ->whereIn('user_id', $allBotUserIds) 129 ->pluck('user_id') 130 ->all(); 131 132 return array_values(array_diff($userIds, $botUserIds)); 133 } 134 135 public function __construct(?User $source = null) 136 { 137 $this->name = snake_case(get_class_basename(get_class($this))); 138 $this->source = $source; 139 $this->afterCommit = true; 140 } 141 142 abstract public function getDetails(): array; 143 144 abstract public function getListeningUserIds(): array; 145 146 public function getName() 147 { 148 return $this->name; 149 } 150 151 abstract public function getNotifiable(); 152 153 /** 154 * In most cases this is a deduplicated list that excludes the user id that 155 * triggered the notifications. This should be overriden in cases where the source user id shouldn't be removed. 156 * e.g. UserAchievementUnlock. 157 */ 158 public function getReceiverIds(): array 159 { 160 return array_values(array_unique(array_diff($this->getListeningUserIds(), [optional($this->source)->getKey()]))); 161 } 162 163 public function getTimestamp() 164 { 165 if ($this->timestamp === null) { 166 $this->timestamp = now(); 167 } 168 169 return $this->timestamp; 170 } 171 172 public function handle() 173 { 174 $deliverySettings = static::applyDeliverySettings(static::excludeBotUserIds($this->getReceiverIds())); 175 176 if (empty($deliverySettings)) { 177 return; 178 } 179 180 $notification = $this->makeNotification(); 181 $notification->saveOrExplode(); 182 183 // client should now be able to handle push notifications that come in after notification has been loaded, 184 // so, it should be fine to create the user notifications first. 185 186 $pushReceiverIds = []; 187 $notification->getConnection()->transaction(function () use ($deliverySettings, $notification, &$pushReceiverIds) { 188 $timestamp = $this->getTimestamp()->format('Y-m-d H:i:s'); 189 $notificationId = $notification->getKey(); 190 $tempUserNotification = new UserNotification(); 191 192 $userNotifications = []; 193 foreach ($deliverySettings as $userId => $delivery) { 194 $userNotifications[] = [ 195 'created_at' => $timestamp, 196 'delivery' => $delivery, 197 'notification_id' => $notificationId, 198 'updated_at' => $timestamp, 199 'user_id' => $userId, 200 ]; 201 $tempUserNotification->delivery = $delivery; 202 $tempUserNotification->isPush() && $pushReceiverIds[] = $userId; 203 204 if (count($userNotifications) === 1000) { 205 UserNotification::insert($userNotifications); 206 $userNotifications = []; 207 } 208 } 209 UserNotification::insert($userNotifications); 210 }); 211 212 if (!empty($pushReceiverIds)) { 213 (new NewPrivateNotificationEvent($notification, $pushReceiverIds))->broadcast(); 214 } 215 } 216 217 public function makeNotification(): Notification 218 { 219 $params['created_at'] = $this->getTimestamp(); 220 $params['details'] = $this->getDetails(); 221 $params['name'] = $this->name; 222 223 if ($this->source !== null) { 224 $params['details']['username'] = $this->source->username; 225 } 226 227 $notification = new Notification($params); 228 $notification->notifiable()->associate($this->getNotifiable()); 229 if ($this->source !== null) { 230 $notification->source()->associate($this->source); 231 } 232 233 return $notification; 234 } 235}