the browser-facing portion of osu!
at master 5.6 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\Models; 7 8use App\Exceptions\ValidationException; 9use App\Libraries\MorphMap; 10use App\Models\Chat\Message; 11use App\Models\Score\Best; 12use App\Models\Score\Best\Model as BestModel; 13use App\Traits\Validatable; 14use Illuminate\Notifications\Notification; 15use Illuminate\Notifications\RoutesNotifications; 16 17/** 18 * @property string $comments 19 * @property int $mode 20 * @property mixed $reason 21 * @property int $report_id 22 * @property mixed $reportable 23 * @property int|null $reportable_id 24 * @property mixed|null $reportable_type 25 * @property User $reporter 26 * @property int $reporter_id 27 * @property int $score_id 28 * @property \Carbon\Carbon $timestamp 29 * @property User $user 30 * @property int $user_id 31 */ 32class UserReport extends Model 33{ 34 use RoutesNotifications, Validatable; 35 36 const BEATMAPSET_TYPE_REASONS = ['UnwantedContent', 'Other']; 37 const MAX_FIELD_LENGTHS = [ 38 'comments' => 2000, 39 ]; 40 const POST_TYPE_REASONS = ['Insults', 'Spam', 'UnwantedContent', 'Nonsense', 'Other']; 41 const SCORE_TYPE_REASONS = ['Cheating', 'MultipleAccounts', 'Other']; 42 43 const ALLOWED_REASONS = [ 44 MorphMap::MAP[BeatmapDiscussionPost::class] => self::POST_TYPE_REASONS, 45 MorphMap::MAP[Beatmapset::class] => self::BEATMAPSET_TYPE_REASONS, 46 MorphMap::MAP[Best\Fruits::class] => self::SCORE_TYPE_REASONS, 47 MorphMap::MAP[Best\Mania::class] => self::SCORE_TYPE_REASONS, 48 MorphMap::MAP[Best\Osu::class] => self::SCORE_TYPE_REASONS, 49 MorphMap::MAP[Best\Taiko::class] => self::SCORE_TYPE_REASONS, 50 MorphMap::MAP[Chat\Message::class] => self::POST_TYPE_REASONS, 51 MorphMap::MAP[Comment::class] => self::POST_TYPE_REASONS, 52 MorphMap::MAP[Forum\Post::class] => self::POST_TYPE_REASONS, 53 MorphMap::MAP[Solo\Score::class] => self::SCORE_TYPE_REASONS, 54 ]; 55 56 const CREATED_AT = 'timestamp'; 57 58 public $timestamps = false; 59 60 protected $casts = ['timestamp' => 'datetime']; 61 protected $primaryKey = 'report_id'; 62 protected $table = 'osu_user_reports'; 63 64 public function reportable() 65 { 66 return $this->morphTo(); 67 } 68 69 public function reporter() 70 { 71 return $this->belongsTo(User::class, 'reporter_id'); 72 } 73 74 public function routeNotificationForSlack(?Notification $_notification): ?string 75 { 76 $reason = $this->reason; 77 $reportableModel = $this->reportable()->getModel(); 78 79 if ( 80 $reason === 'Cheating' 81 || $reason === 'MultipleAccounts' 82 || $reportableModel instanceof BestModel 83 || $reportableModel instanceof Solo\Score 84 ) { 85 return $GLOBALS['cfg']['osu']['user_report_notification']['endpoint_cheating']; 86 } else { 87 $type = match ($reportableModel::class) { 88 BeatmapDiscussionPost::class => 'beatmapset_discussion', 89 Beatmapset::class => 'beatmapset', 90 Chat\Message::class => 'chat', 91 Comment::class => 'comment', 92 Forum\Post::class => 'forum', 93 User::class => 'user', 94 }; 95 96 return $GLOBALS['cfg']['osu']['user_report_notification']['endpoint'][$type] 97 ?? $GLOBALS['cfg']['osu']['user_report_notification']['endpoint_moderation']; 98 } 99 } 100 101 public function user() 102 { 103 return $this->belongsTo(User::class, 'user_id'); 104 } 105 106 public function isRecent(): bool 107 { 108 return $this->timestamp->addDays(1)->isFuture(); 109 } 110 111 public function isValid() 112 { 113 $this->validationErrors()->reset(); 114 115 if (!present(trim($this->comments)) && (!($this->reportable instanceof Chat\Message) || $this->reason === 'Other')) { 116 $this->validationErrors()->add('comments', 'required'); 117 } 118 119 if ($this->user_id === $this->reporter_id) { 120 $this->validationErrors()->add( 121 'user_id', 122 '.self' 123 ); 124 } 125 126 $allowedReasons = static::ALLOWED_REASONS[$this->reportable_type] ?? [ 127 ...static::BEATMAPSET_TYPE_REASONS, 128 ...static::POST_TYPE_REASONS, 129 ...static::SCORE_TYPE_REASONS, 130 ]; 131 132 if (!in_array($this->reason, $allowedReasons, true)) { 133 $this->validationErrors()->add( 134 'reason', 135 '.reason_not_valid', 136 ['reason' => $this->reason] 137 ); 138 } 139 140 if ($this->reportable instanceof Beatmapset && $this->reportable->isScoreable()) { 141 $this->validationErrors()->add( 142 'reason', 143 '.no_ranked_beatmapset' 144 ); 145 } 146 147 if ( 148 $this->reportable instanceof Message 149 && $this->reportable->channel->isHideable() 150 && !$this->reportable->channel->hasUser($this->reporter) 151 ) { 152 $this->validationErrors()->add( 153 'reportable', 154 '.not_in_channel' 155 ); 156 } 157 158 $this->validateDbFieldLengths(); 159 160 return $this->validationErrors()->isEmpty(); 161 } 162 163 public function save(array $options = []) 164 { 165 if (!$this->isValid()) { 166 throw new ValidationException($this->validationErrors()); 167 } 168 169 return parent::save(); 170 } 171 172 public function validationErrorsTranslationPrefix(): string 173 { 174 return 'user_report'; 175 } 176}