Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Community/Actions/CreateGameClaimAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ private function maybeSendClaimWithUnresolvedTicketsAlert(User $currentUser, Gam
return;
}

$ticketCount = Ticket::forDeveloper($currentUser)->awaitingDeveloper()->count();
$ticketCount = Ticket::forAssignee($currentUser)->awaitingDeveloper()->count();
if ($ticketCount < 2) { // two or more suggests the developer may be ignoring tickets
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private function fetchTickets(array $ticketIds): Collection
return collect();
}

return Ticket::with('achievement')
return Ticket::with('ticketable')
->whereIn('id', $ticketIds)
->get()
->map(fn (Ticket $ticket) => TicketData::fromTicket($ticket)->include('state', 'ticketable'));
Expand Down
4 changes: 2 additions & 2 deletions app/Community/Actions/SendDailyDigestAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ private function buildTicketTitles(array $ids): array
{
$result = [];

$tickets = Ticket::whereIn('id', $ids)->with('achievement')->get();
$tickets = Ticket::whereIn('id', $ids)->with('ticketable')->get();
foreach ($tickets as $ticket) {
$result[$ticket->id] = "{$ticket->achievement->title}";
$result[$ticket->id] = $ticket->getTicketableModel()->getTicketableTitle();
}

return $result;
Expand Down
27 changes: 25 additions & 2 deletions app/Community/Enums/TicketType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@

namespace App\Community\Enums;

use App\Platform\Enums\TicketableType;
use InvalidArgumentException;
use LogicException;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
enum TicketType: string
{
case TriggeredAtWrongTime = 'triggered_at_wrong_time';
case DidNotCancel = 'did_not_cancel';
case DidNotStart = 'did_not_start';
case DidNotSubmit = 'did_not_submit';
case DidNotTrigger = 'did_not_trigger';
case SubmittedWrongValue = 'submitted_wrong_value';
case TriggeredAtWrongTime = 'triggered_at_wrong_time';

public function appliesTo(TicketableType $type): bool
{
return match ($this) {
self::TriggeredAtWrongTime, self::DidNotTrigger => $type === TicketableType::Achievement,
self::DidNotStart, self::DidNotCancel, self::DidNotSubmit, self::SubmittedWrongValue => $type === TicketableType::Leaderboard,
};
}

public function label(): string
{
return match ($this) {
self::TriggeredAtWrongTime => 'Triggered at the wrong time',
self::DidNotCancel => 'Did not cancel',
self::DidNotStart => 'Did not start',
self::DidNotSubmit => 'Did not submit',
self::DidNotTrigger => 'Did not trigger',
self::SubmittedWrongValue => 'Submitted wrong value',
self::TriggeredAtWrongTime => 'Triggered at the wrong time',
};
}

Expand All @@ -31,6 +49,11 @@ public function toLegacyInteger(): int
return match ($this) {
self::TriggeredAtWrongTime => 1,
self::DidNotTrigger => 2,

self::DidNotStart,
self::DidNotCancel,
self::DidNotSubmit,
self::SubmittedWrongValue => throw new LogicException("TicketType {$this->value} has no legacy integer mapping."),
};
}

Expand Down
51 changes: 32 additions & 19 deletions app/Community/Services/SubscriptionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Models\Subscription;
use App\Models\Ticket;
use App\Models\User;
use App\Platform\Enums\TicketableType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
Expand Down Expand Up @@ -259,7 +260,7 @@ private function getHandler(SubscriptionSubjectType $subjectType): BaseSubscript
{
return match ($subjectType) {
SubscriptionSubjectType::Achievement => new AchievementWallSubscriptionHandler(),
SubscriptionSubjectType::AchievementTicket => new AchievementTicketSubscriptionHandler(),
SubscriptionSubjectType::AchievementTicket => new TicketSubscriptionHandler(),
SubscriptionSubjectType::ForumTopic => new ForumTopicSubscriptionHandler(),
SubscriptionSubjectType::GameAchievements => new GameAchievementsSubscriptionHandler(),
SubscriptionSubjectType::GameTickets => new GameTicketsSubscriptionHandler(),
Expand Down Expand Up @@ -442,7 +443,7 @@ public function getImplicitSubscriptionQuery(?int $subjectId, ?int $forUserId, ?
}
}

class AchievementTicketSubscriptionHandler extends CommentSubscriptionHandler
class TicketSubscriptionHandler extends CommentSubscriptionHandler
{
protected function getCommentableType(): CommentableType
{
Expand All @@ -454,17 +455,25 @@ protected function getCommentableType(): CommentableType
*/
public function getSubjectQuery(array $subjectIds): Builder
{
/** @var Builder<Model> $query */
$query = Ticket::whereIn(DB::raw('tickets.id'), $subjectIds)
/** @var Builder<Model> $achievementQuery */
$achievementQuery = Ticket::whereIn(DB::raw('tickets.id'), $subjectIds)
->join('achievements', DB::raw('tickets.ticketable_id'), '=', 'achievements.id')
->where(DB::raw('tickets.ticketable_type'), 'achievement')
->where(DB::raw('tickets.ticketable_type'), TicketableType::Achievement->value)
->select([
DB::raw('tickets.id as subject_id'),
DB::raw('achievements.title as title'),
])
->orderBy('title');
]);

return $query;
/** @var Builder<Model> $leaderboardQuery */
$leaderboardQuery = Ticket::whereIn(DB::raw('tickets.id'), $subjectIds)
->join('leaderboards', DB::raw('tickets.ticketable_id'), '=', 'leaderboards.id')
->where(DB::raw('tickets.ticketable_type'), TicketableType::Leaderboard->value)
->select([
DB::raw('tickets.id as subject_id'),
DB::raw('leaderboards.title as title'),
]);

return $achievementQuery->union($leaderboardQuery)->orderBy('title');
}

/**
Expand All @@ -476,13 +485,16 @@ public function getImplicitSubscriptionQuery(?int $subjectId, ?int $forUserId, ?
$query = parent::getImplicitSubscriptionQuery($subjectId, $forUserId, $ignoreSubjectIds, $ignoreUserIds);

if ($subjectId !== null) {
$ticket = Ticket::with('achievement')->find($subjectId);
$ticket = Ticket::with('ticketable')->find($subjectId);
if ($ticket) {
// find any users subscribed to GameTickets for the game owning the ticketed achievement
$ticketable = $ticket->getTicketableModel();
$ticketableGame = $ticketable->getTicketableGame();

// find any users subscribed to GameTickets for the game owning the ticketed achievement/leaderboard
/** @var Builder<Model> $query2 */
$query2 = Subscription::query()
->where('subject_type', SubscriptionSubjectType::GameTickets)
->where('subject_id', $ticket->achievement->game_id)
->where('subject_id', $ticketableGame->id)
->where('state', true)
->select(['user_id', 'subject_id']);

Expand Down Expand Up @@ -514,23 +526,24 @@ public function getImplicitSubscriptionQuery(?int $subjectId, ?int $forUserId, ?
$query->union($query3);
}

// achievement maintainer should also be implicitly subscribed if they still have a development role
$includeMaintainer = false;
$maintainer = $ticket->achievement->getMaintainerAt(now());
if ($maintainer && $maintainer->hasAnyRole([Role::DEVELOPER, Role::DEVELOPER_JUNIOR])) {
// ticketable assignee (achievement maintainer or leaderboard author) should
// also be implicitly subscribed if they still have a development role
$includeAssignee = false;
$assignee = $ticketable->getTicketableAssignee(now());
if ($assignee && $assignee->hasAnyRole([Role::DEVELOPER, Role::DEVELOPER_JUNIOR])) {
if ($forUserId !== null) {
$includeMaintainer = $maintainer->id === $forUserId;
$includeAssignee = $assignee->id === $forUserId;
} else {
$includeMaintainer = !$ignoreUserIds || !in_array($maintainer->id, $ignoreUserIds);
$includeAssignee = !$ignoreUserIds || !in_array($assignee->id, $ignoreUserIds);
}
}

if ($includeMaintainer) {
if ($includeAssignee) {
/** @var Builder<Model> $query4 */
$query4 = Ticket::query()
->where('id', $ticket->id)
->select([
DB::raw($maintainer->id . ' as user_id'),
DB::raw($assignee->id . ' as user_id'),
DB::raw('id as subject_id'),
]);

Expand Down
7 changes: 3 additions & 4 deletions app/Helpers/database/ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function getTicket(int $ticketID): ?array

function updateTicket(User $userModel, int $ticketID, TicketState $ticketVal, ?string $reason = null): bool
{
$ticket = Ticket::with(['reporter', 'author', 'achievement.game.system'])->find($ticketID);
$ticket = Ticket::with(['reporter', 'author', 'ticketable.game.system'])->find($ticketID);

if (!$ticket) {
return false;
Expand All @@ -160,9 +160,8 @@ function updateTicket(User $userModel, int $ticketID, TicketState $ticketVal, ?s

switch ($ticketVal) {
case TicketState::Closed:
if ($reason == TicketState::REASON_DEMOTED && $ticket->achievement?->is_promoted) {
updateAchievementPromotedStatus($ticket->achievement->id, false);
addArticleComment("Server", CommentableType::Achievement, $ticket->achievement->id, "{$userModel->display_name} demoted this achievement to Unofficial.", $userModel->display_name);
if ($reason === TicketState::REASON_DEMOTED) {
$ticket->getTicketableModel()->demoteForTicket($userModel);
}
$comment = "Ticket closed by {$userModel->display_name}. Reason: \"$reason\".";
break;
Expand Down
2 changes: 1 addition & 1 deletion app/Helpers/database/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function GetDeveloperStatsFull(int $count, int $offset = 0, int $sortBy = 0, int
} elseif ($sortBy == 4) { // TicketsResolvedForOthers DESC
$query = "SELECT ua.id, SUM(!ISNULL(ach.id)) as total
FROM users as ua
LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id
LEFT JOIN tickets tick ON tick.resolver_id = ua.id AND tick.state = 'resolved' AND tick.resolver_id != tick.reporter_id AND tick.ticketable_type = 'achievement'
LEFT JOIN achievements as ach ON ach.id = tick.ticketable_id AND ach.is_promoted = 1 AND ach.user_id != ua.id
WHERE $stateCond
GROUP BY ua.id
Expand Down
12 changes: 9 additions & 3 deletions app/Helpers/render/ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ function ticketAvatar(
TicketState::Closed, TicketState::Resolved, TicketState::Quarantined => 'closed',
};

$badgeUrl = $safeTicket->getTicketableModel()->getTicketableIconUrl();

return avatar(
resource: 'ticket',
id: $safeTicket->id,
label: "Ticket #{$safeTicket->id}",
link: route('ticket.show', ['ticket' => $safeTicket->id]),
tooltip: is_array($tooltip) ? renderAchievementCard($tooltip) : $tooltip,
class: "ticket-avatar $ticketStateClass",
iconUrl: media_asset("/Badge/" . $safeTicket->achievement->image_name . ".png"),
iconUrl: $badgeUrl,
iconSize: $iconSize,
iconClass: $iconClass,
context: $context,
Expand All @@ -57,10 +59,14 @@ function renderTicketCard(int|Ticket $ticket): string
TicketState::Closed, TicketState::Resolved, TicketState::Quarantined => 'closed',
};

$ticketable = $ticket->getTicketableModel();
$game = $ticketable->getTicketableGame();
$badgeUrl = $ticketable->getTicketableIconUrl();

return "<div class='tooltip-body flex items-start' style='max-width: 400px'>" .
"<img style='margin-right:5px' src='" . media_asset('/Badge/' . $ticket->achievement->image_name . '.png') . "' width='64' height='64' />" .
"<img style='margin-right:5px' src='" . $badgeUrl . "' width='64' height='64' />" .
"<div class='ticket-tooltip-info $ticketStateClass'>" .
"<div><b>" . $ticket->achievement->title . "</b> <i>(" . $ticket->achievement->game->title . ")</i></div>" .
"<div><b>" . $ticketable->getTicketableTitle() . "</b> <i>(" . $game->title . ")</i></div>" .
"<div>Reported by {$ticket->reporter->display_name}</div>" .
"<div>Issue: " . $ticket->type->label() . "</div>" .
($ticket->resolver ? "<div class='tooltip-closer'>Closed by {$ticket->resolver->display_name}, " . getNiceDate(strtotime($ticket->resolved_at)) . "</div>" : "") .
Expand Down
7 changes: 4 additions & 3 deletions app/Helpers/util/mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,13 @@ function informAllSubscribersAboutActivity(
break;

case CommentableType::AchievementTicket: // Ticket
$ticket = Ticket::with(['achievement.game', 'reporter'])->find($commentableId);
if (!$ticket) {
$ticket = Ticket::with(['ticketable.game', 'reporter'])->find($commentableId);
if (!$ticket || !$ticket->ticketable) {
return;
}

$articleTitle = "{$ticket->achievement->title} ({$ticket->achievement->game->title})";
$ticketable = $ticket->getTicketableModel();
$articleTitle = "{$ticketable->getTicketableTitle()} ({$ticketable->getTicketableGame()->title})";
$urlTarget = route('ticket.show', ['ticket' => $ticket->id]);
$subjectAuthor = $ticket->reporter;
$articleEmailPreference = UserPreference::EmailOn_TicketActivity;
Expand Down
54 changes: 52 additions & 2 deletions app/Models/Achievement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Community\Enums\CommentableType;
use App\Platform\Contracts\HasPermalink;
use App\Platform\Contracts\HasVersionedTrigger;
use App\Platform\Contracts\Ticketable;
use App\Platform\Enums\AchievementAuthorTask;
use App\Platform\Enums\AchievementSetType;
use App\Platform\Enums\AchievementType;
Expand All @@ -22,6 +23,7 @@
use App\Platform\Events\AchievementTypeChanged;
use App\Platform\Events\AchievementUnpromoted;
use App\Support\Database\Eloquent\BaseModel;
use Carbon\CarbonInterface;
use Database\Factories\AchievementFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
Expand Down Expand Up @@ -51,7 +53,7 @@
/**
* @implements HasVersionedTrigger<Achievement>
*/
class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger
class Achievement extends BaseModel implements HasPermalink, HasVersionedTrigger, Ticketable
{
/*
* Community Traits
Expand Down Expand Up @@ -260,6 +262,54 @@ public function shouldBeSearchable(): bool
return $this->is_promoted;
}

// == ticketable

public function getTicketableType(): TicketableType
{
return TicketableType::Achievement;
}

public function getTicketableGame(): Game
{
return $this->game;
}

public function getTicketableAssignee(?CarbonInterface $at = null): ?User
{
return $this->getMaintainerAt($at ?? now());
}

public function getTicketableTitle(): string
{
return $this->title;
}

public function getTicketableUrl(): string
{
return $this->getCanonicalUrlAttribute();
}

public function getTicketableIconUrl(): string
{
return $this->getBadgeUrlAttribute();
}

public function demoteForTicket(User $byUser): void
{
if (!$this->is_promoted) {
return;
}

updateAchievementPromotedStatus($this->id, false);
addArticleComment(
'Server',
CommentableType::Achievement,
$this->id,
"{$byUser->display_name} demoted this achievement to Unofficial.",
$byUser->display_name,
);
}

// == helpers

public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task, ?Carbon $backdate = null): AchievementAuthor
Expand All @@ -270,7 +320,7 @@ public function ensureAuthorshipCredit(User $user, AchievementAuthorTask $task,
);
}

public function getMaintainerAt(Carbon $timestamp): ?User
public function getMaintainerAt(CarbonInterface $timestamp): ?User
{
$maintainer = $this->maintainers()
->where('effective_from', '<=', $timestamp)
Expand Down
Loading
Loading