. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.
namespace App\Models;
use App\Models\Traits\WithDbCursorHelper;
use Carbon\Carbon;
use Sentry\State\Scope;
/**
* @property Beatmap $beatmap
* @property int|null $beatmap_id
* @property Beatmapset $beatmapset
* @property int|null $beatmapset_id
* @property \Carbon\Carbon $date
* @property int $epicfactor
* @property int $event_id
* @property int $private
* @property string $text
* @property string|null $text_clean
* @property User $user
* @property int|null $user_id
*/
class Event extends Model
{
use WithDbCursorHelper;
protected const DEFAULT_SORT = 'id_desc';
protected const SORTS = [
'id_asc' => [
['column' => 'event_id', 'order' => 'ASC'],
],
'id_desc' => [
['column' => 'event_id', 'order' => 'DESC'],
],
];
public ?array $details = null;
public $parsed = false;
public ?string $type = null;
public $patterns = [
'achievement' => "!^(?:)+(?.+?)(?:)+ unlocked the \"(?.+?)\" achievement\!$!",
'beatmapPlaycount' => "!^(?.+?) has been played (?[\d,]+) times\!$!",
'beatmapsetApprove' => "!^(?.+?) by (?.+?) has just been (?ranked|approved|qualified|loved)\!$!",
'beatmapsetDelete' => "!^(?.*?) has been deleted.$!",
'beatmapsetRevive' => "!^(?.*?) has been revived from eternal slumber(?: by (?.+?))?\.$!",
'beatmapsetUpdate' => "!^(?.+?) has updated the beatmap \"(?.*?)\"$!",
'beatmapsetUpload' => "!^(?.+?) has submitted a new beatmap \"(?.*?)\"$!",
'medal' => "!^(?:)+(?.+?)(?:)+ unlocked the \"(?.+?)\" medal\!$!",
'rank' => "!^
(?.+?) achieved (?:)?rank #(?\d+?)(?:)? on (?.+?) \((?.+?)\)$!",
'rankLost' => "!^(?.+?) has lost first place on (?.+?) \((?.+?)\)$!",
'userSupportAgain' => "!^(?.+?) has once again chosen to support osu\! - thanks for your generosity\!$!",
'userSupportFirst' => "!^(?.+?) has become an osu\! supporter - thanks for your generosity\!$!",
'userSupportGift' => "!^(?.+?) has received the gift of osu\! supporter\!$!",
'usernameChange' => "!^(?.+?) has changed their username to (?.+)\!$!",
];
public $timestamps = false;
protected $casts = ['date' => 'datetime'];
protected $primaryKey = 'event_id';
protected $table = 'osu_events';
public static function generate($type, $options)
{
switch ($type) {
case 'achievement':
$achievement = $options['achievement'];
$user = $options['user'];
// not escaped because it's not in the old system either
$achievementName = $achievement->name;
$userUrl = e(route('users.show', $user, false));
$userName = e($user->username);
$params = [
// taken from medal
'text' => "{$userName} unlocked the \"{$achievementName}\" medal!",
'user_id' => $user->getKey(),
'private' => false,
'epicfactor' => 4,
];
break;
case 'beatmapsetApprove':
$beatmapset = $options['beatmapset'];
$beatmapsetParams = static::beatmapsetParams($beatmapset);
$userParams = static::userParams($options['beatmapset']->user);
$approval = e($beatmapset->status());
$template = '%s by %s has just been %s!';
$params = [
'text' => sprintf($template, "{$beatmapsetParams['title']}", "{$userParams['username']}", $approval),
'text_clean' => sprintf($template, "[{$beatmapsetParams['url_clean']} {$beatmapsetParams['title']}]", "[{$userParams['url_clean']} {$userParams['username']}]", $approval),
'beatmap_id' => 0,
'beatmapset_id' => $beatmapset->getKey(),
'user_id' => $beatmapset->user->getKey(),
'private' => false,
'epicfactor' => 8,
];
break;
case 'beatmapsetDelete':
$beatmapset = $options['beatmapset'];
$beatmapsetParams = static::beatmapsetParams($beatmapset);
$params = [
'text' => "{$beatmapsetParams['title']} has been deleted.",
'beatmapset_id' => $beatmapset->getKey(),
'user_id' => $options['user']->getKey(),
'private' => false,
'epicfactor' => 1,
];
break;
case 'beatmapsetRevive':
$beatmapset = $options['beatmapset'];
$beatmapsetParams = static::beatmapsetParams($beatmapset);
$userParams = static::userParams($beatmapset->user);
$template = '%s has been revived from eternal slumber by %s.';
$params = [
'text' => sprintf($template, "{$beatmapsetParams['title']}", "{$userParams['username']}"),
'text_clean' => sprintf($template, "[{$beatmapsetParams['url_clean']} {$beatmapsetParams['title']}]", "[{$userParams['url_clean']} {$userParams['username']}]"),
'beatmapset_id' => $beatmapset->getKey(),
'user_id' => $beatmapset->user->getKey(),
'private' => false,
'epicfactor' => 5,
];
break;
case 'beatmapsetUpdate':
$beatmapset = $options['beatmapset'];
$beatmapsetParams = static::beatmapsetParams($beatmapset);
// retrieved separately from options because it doesn't necessarily need to be the same user
// as $beatmapset->user in some cases (see: direct guest difficulty update)
$user = $options['user'];
$userParams = static::userParams($user);
$template = '%s has updated the beatmap "%s"';
$params = [
'text' => sprintf($template, "{$userParams['username']}", "{$beatmapsetParams['title']}"),
'text_clean' => sprintf($template, "[{$userParams['url_clean']} {$userParams['username']}]", "[{$beatmapsetParams['url_clean']} {$beatmapsetParams['title']}]"),
'beatmapset_id' => $beatmapset->getKey(),
'user_id' => $user->getKey(),
'private' => false,
'epicfactor' => 2,
];
break;
case 'beatmapsetUpload':
$beatmapset = $options['beatmapset'];
$beatmapsetParams = static::beatmapsetParams($beatmapset);
$userParams = static::userParams($beatmapset->user);
$template = '%s has submitted a new beatmap "%s"';
$params = [
'text' => sprintf($template, "{$userParams['username']}", "{$beatmapsetParams['title']}"),
'text_clean' => sprintf($template, "[{$userParams['url_clean']} {$userParams['username']}]", "[{$beatmapsetParams['url_clean']} {$beatmapsetParams['title']}]"),
'beatmapset_id' => $beatmapset->getKey(),
'user_id' => $beatmapset->user->getKey(),
'private' => false,
'epicfactor' => 4,
];
break;
case 'usernameChange':
$user = static::userParams($options['user']);
$oldUsername = e($options['history']->username_last);
$newUsername = e($options['history']->username);
$params = [
'text' => "{$oldUsername} has changed their username to {$newUsername}!",
'user_id' => $user['id'],
'date' => $options['history']->timestamp,
'private' => false,
'epicfactor' => 4,
];
break;
case 'userSupportGift':
$user = static::userParams($options['user']);
$params = [
'text' => "{$user['username']} has received the gift of osu! supporter!",
'user_id' => $user['id'],
'date' => $options['date'],
'private' => false,
'epicfactor' => 2,
];
break;
case 'userSupportFirst':
$user = static::userParams($options['user']);
$params = [
'text' => "{$user['username']} has become an osu! supporter - thanks for your generosity!",
'user_id' => $user['id'],
'date' => $options['date'],
'private' => false,
'epicfactor' => 2,
];
break;
case 'userSupportAgain':
$user = static::userParams($options['user']);
$params = [
'text' => "{$user['username']} has once again chosen to support osu! - thanks for your generosity!",
'user_id' => $user['id'],
'date' => $options['date'],
'private' => false,
'epicfactor' => 2,
];
break;
}
if (isset($params)) {
if (!isset($params['date'])) {
$params['date'] = Carbon::now();
}
return static::create($params);
}
}
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'user_id');
}
public function beatmap()
{
return $this->belongsTo(Beatmap::class, 'beatmap_id', 'beatmap_id');
}
public function beatmapset()
{
return $this->belongsTo(Beatmapset::class, 'beatmapset_id', 'beatmapset_id');
}
public function arrayBeatmap($matches)
{
$beatmapTitle = presence($matches['beatmapTitle'], '(no title)');
return [
'title' => html_entity_decode_better($beatmapTitle),
'url' => html_entity_decode_better($matches['beatmapUrl']),
];
}
public function arrayBeatmapset($matches)
{
$beatmapsetTitle = presence($matches['beatmapsetTitle'], '(no title)');
return [
'title' => html_entity_decode_better($beatmapsetTitle),
'url' => html_entity_decode_better($matches['beatmapsetUrl']),
];
}
public function arrayUser($matches)
{
if (isset($matches['userName'])) {
$username = html_entity_decode_better($matches['userName']);
$userUrl = html_entity_decode_better($matches['userUrl']);
} else {
$user = $this->user;
$username = $user->username;
$userUrl = route('users.show', $user->user_id);
}
return [
'username' => $username,
'url' => $userUrl,
];
}
public function stringMode($mode)
{
switch ($mode) {
case 'osu!mania':
return 'mania';
case 'Taiko':
case 'osu!taiko':
return 'taiko';
case 'osu!':
return 'osu';
case 'Catch the Beat':
case 'osu!catch':
return 'fruits';
}
}
public function parseFailure($reason)
{
app('sentry')->getClient()->captureMessage(
'Failed parsing event',
null,
(new Scope())
->setTag('reason', $reason)
->setExtra('event', $this->toArray())
);
return ['parse_error' => true];
}
public function parseMatchesAchievement($matches)
{
$achievement = app('medals')->byNameIncludeDisabled($matches['achievementName']);
if ($achievement === null) {
return $this->parseFailure("unknown achievement ({$matches['achievementName']})");
}
return [
'achievement' => json_item($achievement, 'Achievement'),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesBeatmapPlaycount($matches)
{
$count = intval(str_replace(',', '', $matches['count']));
return [
'beatmap' => $this->arrayBeatmap($matches),
'count' => $count,
];
}
public function parseMatchesBeatmapsetApprove($matches)
{
return [
'approval' => $matches['approval'],
'beatmapset' => $this->arrayBeatmapset($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesBeatmapsetDelete($matches)
{
return [
'beatmapset' => $this->arrayBeatmapset($matches),
];
}
public function parseMatchesBeatmapsetRevive($matches)
{
return [
'beatmapset' => $this->arrayBeatmapset($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesBeatmapsetUpdate($matches)
{
return [
'beatmapset' => $this->arrayBeatmapset($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesBeatmapsetUpload($matches)
{
return [
'beatmapset' => $this->arrayBeatmapset($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesMedal($matches)
{
$this->type = 'achievement';
return $this->parseMatchesAchievement($matches);
}
public function parseMatchesRank($matches)
{
$mode = $this->stringMode($matches['mode']);
if ($mode === null) {
return $this->parseFailure("unknown mode ({$matches['mode']})");
}
return [
'scoreRank' => $matches['scoreRank'],
'rank' => intval($matches['rank']),
'mode' => $mode,
'beatmap' => $this->arrayBeatmap($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesRankLost($matches)
{
$mode = $this->stringMode($matches['mode']);
if ($mode === null) {
return $this->parseFailure("unknown mode ({$matches['mode']})");
}
return [
'mode' => $mode,
'beatmap' => $this->arrayBeatmap($matches),
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesUsernameChange($matches)
{
return [
'user' => array_merge(
$this->arrayUser($matches),
['previousUsername' => html_entity_decode_better($matches['previousUsername'])]
),
];
}
public function parseMatchesUserSupportAgain($matches)
{
return [
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesUserSupportFirst($matches)
{
return [
'user' => $this->arrayUser($matches),
];
}
public function parseMatchesUserSupportGift($matches)
{
return [
'user' => $this->arrayUser($matches),
];
}
public function parse()
{
if (!$this->parsed) {
foreach ($this->patterns as $name => $pattern) {
if (preg_match($pattern, $this->text, $matches) !== 1) {
continue;
}
$this->type = $name;
$fname = 'parseMatches'.ucfirst($name);
$this->details = $this->$fname($matches);
break;
}
if ($this->details === null) {
$this->details = $this->parseFailure('no matching pattern');
}
$this->parsed = true;
}
return $this;
}
public function scopeRecent($query)
{
return $query->orderBy('event_id', 'desc')->limit(5);
}
private static function userParams($user)
{
$url = e(route('users.show', $user, false));
return [
'id' => $user->getKey(),
'username' => e($user->username),
'url' => $url,
'url_clean' => $GLOBALS['cfg']['app']['url'].$url,
];
}
private static function beatmapsetParams($beatmapset)
{
$url = e(route('beatmapsets.show', $beatmapset, false));
return [
'title' => e($beatmapset->artist.' - '.$beatmapset->title),
'url' => $url,
'url_clean' => $GLOBALS['cfg']['app']['url'].$url,
];
}
}