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\Models;
7
8use App\Exceptions\InvariantException;
9use App\Models\Chat\Channel;
10use App\Models\Forum\Topic;
11
12/**
13 * @property \Carbon\Carbon|null $created_at
14 * @property string $category
15 * @property array|null $details
16 * @property int $id
17 * @property Model $notifiable
18 * @property string $notifiable_type
19 * @property int $notifiable_id
20 * @property int $priority
21 * @property User|null $source
22 * @property int|null $source_user_id
23 * @property string $name
24 * @property \Carbon\Carbon|null $updated_at
25 * @property \Illuminate\Database\Eloquent\Collection $userNotifications UserNotification
26 */
27class Notification extends Model
28{
29 const BEATMAP_OWNER_CHANGE = 'beatmap_owner_change';
30 const BEATMAPSET_DISCUSSION_LOCK = 'beatmapset_discussion_lock';
31 const BEATMAPSET_DISCUSSION_POST_NEW = 'beatmapset_discussion_post_new';
32 const BEATMAPSET_DISCUSSION_QUALIFIED_PROBLEM = 'beatmapset_discussion_qualified_problem';
33 const BEATMAPSET_DISCUSSION_REVIEW_NEW = 'beatmapset_discussion_review_new';
34 const BEATMAPSET_DISCUSSION_UNLOCK = 'beatmapset_discussion_unlock';
35 const BEATMAPSET_DISQUALIFY = 'beatmapset_disqualify';
36 const BEATMAPSET_LOVE = 'beatmapset_love';
37 const BEATMAPSET_NOMINATE = 'beatmapset_nominate';
38 const BEATMAPSET_QUALIFY = 'beatmapset_qualify';
39 const BEATMAPSET_RANK = 'beatmapset_rank';
40 const BEATMAPSET_REMOVE_FROM_LOVED = 'beatmapset_remove_from_loved';
41 const BEATMAPSET_RESET_NOMINATIONS = 'beatmapset_reset_nominations';
42 const CHANNEL_ANNOUNCEMENT = 'channel_announcement';
43 const CHANNEL_MESSAGE = 'channel_message';
44 const COMMENT_NEW = 'comment_new';
45 const FORUM_TOPIC_REPLY = 'forum_topic_reply';
46 const USER_ACHIEVEMENT_UNLOCK = 'user_achievement_unlock';
47 const USER_BEATMAPSET_NEW = 'user_beatmapset_new';
48 const USER_BEATMAPSET_REVIVE = 'user_beatmapset_revive';
49
50 const NAME_TO_CATEGORY = [
51 self::BEATMAP_OWNER_CHANGE => 'beatmap_owner_change',
52 self::BEATMAPSET_DISCUSSION_LOCK => 'beatmapset_discussion',
53 self::BEATMAPSET_DISCUSSION_POST_NEW => 'beatmapset_discussion',
54 self::BEATMAPSET_DISCUSSION_QUALIFIED_PROBLEM => 'beatmapset_problem',
55 self::BEATMAPSET_DISCUSSION_REVIEW_NEW => 'beatmapset_discussion',
56 self::BEATMAPSET_DISCUSSION_UNLOCK => 'beatmapset_discussion',
57 self::BEATMAPSET_DISQUALIFY => 'beatmapset_state',
58 self::BEATMAPSET_LOVE => 'beatmapset_state',
59 self::BEATMAPSET_NOMINATE => 'beatmapset_state',
60 self::BEATMAPSET_QUALIFY => 'beatmapset_state',
61 self::BEATMAPSET_RANK => 'beatmapset_state',
62 self::BEATMAPSET_REMOVE_FROM_LOVED => 'beatmapset_state',
63 self::BEATMAPSET_RESET_NOMINATIONS => 'beatmapset_state',
64 self::CHANNEL_ANNOUNCEMENT => 'announcement',
65 self::CHANNEL_MESSAGE => 'channel',
66 self::COMMENT_NEW => 'comment',
67 self::FORUM_TOPIC_REPLY => 'forum_topic_reply',
68 self::USER_ACHIEVEMENT_UNLOCK => 'user_achievement_unlock',
69 self::USER_BEATMAPSET_NEW => 'user_beatmapset_new',
70 self::USER_BEATMAPSET_REVIVE => 'user_beatmapset_new',
71 ];
72
73 const NOTIFIABLE_CLASSES = [
74 Beatmapset::class,
75 Build::class,
76 Channel::class,
77 Topic::class,
78 NewsPost::class,
79 User::class,
80 ];
81
82 const SUBTYPES = [
83 self::COMMENT_NEW => 'comment',
84 self::USER_BEATMAPSET_NEW => 'mapping',
85 ];
86
87 protected $casts = [
88 'details' => 'array',
89 ];
90
91 public static function namesInCategory($category)
92 {
93 static $categories = [];
94
95 if ($categories === []) {
96 foreach (static::NAME_TO_CATEGORY as $key => $value) {
97 if (!array_key_exists($value, $categories)) {
98 $categories[$value] = [];
99 }
100
101 $categories[$value][] = $key;
102 }
103 }
104
105 return $categories[$category] ?? [$category];
106 }
107
108 public function scopeByIdentity($query, array $params)
109 {
110 $category = $params['category'] ?? null;
111 $objectId = $params['object_id'] ?? null;
112 $objectType = $params['object_type'] ?? null;
113
114 if ($objectType !== null) {
115 $query->where('notifiable_type', $objectType);
116 }
117
118 if ($objectId !== null && $category !== null) {
119 if ($objectType === null) {
120 throw new InvariantException('object_type is required.');
121 }
122
123 $names = Notification::namesInCategory($category);
124 $query
125 ->where('notifiable_id', $objectId)
126 ->whereIn('name', $names);
127 }
128
129 return $query;
130 }
131
132 public function getCategoryAttribute()
133 {
134 return static::NAME_TO_CATEGORY[$this->name] ?? $this->name;
135 }
136
137 public function notifiable()
138 {
139 return $this->morphTo();
140 }
141
142 public function source()
143 {
144 return $this->belongsTo(User::class);
145 }
146
147 public function toIdentityJson()
148 {
149 return [
150 'category' => $this->category,
151 'id' => $this->getKey(),
152 'object_id' => $this->notifiable_id,
153 'object_type' => $this->notifiable_type,
154 ];
155 }
156
157 public function userNotifications()
158 {
159 return $this->hasMany(UserNotification::class);
160 }
161}