Skip to content

Commit

Permalink
Merge branch 'master' into ticket-achievement-delete-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
wescopeland authored Feb 23, 2025
2 parents 8d069b6 + 03aec24 commit edd4559
Show file tree
Hide file tree
Showing 28 changed files with 592 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
DISCORD_KEY=
DISCORD_SECRET=
DISCORD_INVITE_ID=
# RABot
DISCORD_GUILD_ID=
DISCORD_RABOT_TOKEN=
# public feeds
DISCORD_WEBHOOK_ACHIEVEMENTS=
DISCORD_WEBHOOK_CLAIMS=
Expand Down
29 changes: 29 additions & 0 deletions app/Actions/FindUserByIdentifierAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\User;

class FindUserByIdentifierAction
{
public function execute(?string $identifier): ?User
{
if ($identifier === null) {
return null;
}

$ulidLength = 26;
if (mb_strlen($identifier) === $ulidLength) {
return User::whereUlid($identifier)->first();
}

return User::query()
->where(function ($query) use ($identifier) {
$query->where('display_name', $identifier)
->orWhere('User', $identifier);
})
->first();
}
}
12 changes: 8 additions & 4 deletions app/Community/Actions/ApproveNewDisplayNameAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Community\Actions;

use App\Http\Actions\UpdateDiscordNicknameAction;
use App\Models\User;
use App\Models\UserUsername;
use GuzzleHttp\Client;
Expand All @@ -14,22 +15,25 @@ class ApproveNewDisplayNameAction
public function execute(User $user, UserUsername $changeRequest): void
{
$oldDisplayName = $user->display_name;
$newDisplayName = $changeRequest->username;

// Automatically mark conflicting requests as denied.
UserUsername::where('username', $changeRequest->username)
UserUsername::where('username', $newDisplayName)
->where('id', '!=', $changeRequest->id)
->whereNull('approved_at')
->whereNull('denied_at')
->update(['denied_at' => now()]);

$changeRequest->update(['approved_at' => now()]);

$user->display_name = $changeRequest->username;
$user->display_name = $newDisplayName;
$user->save();

sendDisplayNameChangeConfirmationEmail($user, $changeRequest->username);
sendDisplayNameChangeConfirmationEmail($user, $newDisplayName);

$this->notifyDiscord($user, $oldDisplayName, $changeRequest->username);
(new UpdateDiscordNicknameAction())->execute($oldDisplayName, $newDisplayName);

$this->notifyDiscord($user, $oldDisplayName, $newDisplayName);
}

private function notifyDiscord(User $user, string $oldName, string $newName): void
Expand Down
4 changes: 4 additions & 0 deletions app/Enums/SearchType.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ abstract class SearchType

public const UserModerationComment = 12;

public const Hub = 13;

public static function cases(): array
{
// NOTE: this order determines the order of the items in the 'search in' dropdown
Expand All @@ -50,6 +52,7 @@ public static function cases(): array
self::GameHashComment,
self::SetClaimComment,
self::UserModerationComment,
self::Hub,
];
}

Expand All @@ -74,6 +77,7 @@ public static function toString(int $type): string
SearchType::UserModerationComment => "User Moderation Comments",
SearchType::GameHashComment => "Game Hash Comments",
SearchType::SetClaimComment => "Set Claim Comments",
SearchType::Hub => "Hubs",
default => "Invalid search type",
};
}
Expand Down
7 changes: 5 additions & 2 deletions app/Filament/Resources/GameResource/Pages/Hubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ public function table(Table $table): Table
->searchable(query: function (Builder $query, string $search): Builder {
return $query->where('game_sets.id', 'like', "%{$search}");
})
->sortable(),
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderBy('game_sets.id', $direction);
}),

Tables\Columns\TextColumn::make('title')
->label('Title')
->searchable(),
->searchable()
->sortable(),
])
->filters([

Expand Down
18 changes: 13 additions & 5 deletions app/Filament/Resources/GameResource/Pages/SimilarGames.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,22 @@ public function table(Table $table): Table
->label('')
->size(config('media.icon.sm.width')),

Tables\Columns\TextColumn::make('id')
Tables\Columns\TextColumn::make('ID')
->label('ID')
->sortable()
->searchable(),
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderBy('GameData.ID', $direction);
})
->searchable(query: function (Builder $query, string $search): Builder {
return $query->where('GameData.ID', 'LIKE', "%{$search}%");
}),

Tables\Columns\TextColumn::make('Title')
->sortable()
->searchable(),
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderBy('GameData.Title', $direction);
})
->searchable(query: function (Builder $query, string $search): Builder {
return $query->where('GameData.Title', 'LIKE', "%{$search}%");
}),

Tables\Columns\TextColumn::make('system')
->label('System')
Expand Down
21 changes: 21 additions & 0 deletions app/Filament/Resources/UserUsernameResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ public static function table(Table $table): Table
->dateTime()
->sortable(),

Tables\Columns\TextColumn::make('previous_usages')
->label('Previously Used By')
->state(fn (UserUsername $record): string => empty($record->previous_usages) ? '-' : 'has_users')
->description(fn (UserUsername $record): ?string => count($record->previous_usages) > 1
? "Used by " . count($record->previous_usages) . " different users"
: null
)
->formatStateUsing(function (string $state, UserUsername $record): string {
if ($state === '-') {
return '-';
}

return collect($record->previous_usages)
->map(fn ($usage) => "<a href='" . route('user.show', $usage['user']) . "'
class='underline text-warning-600'
target='_blank'>" . $usage['user']->display_name . "</a>"
)
->implode(', ');
})
->html(),

Tables\Columns\TextColumn::make('status')
->label('Status')
->state(fn (UserUsername $record): string => match (true) {
Expand Down
22 changes: 21 additions & 1 deletion app/Helpers/database/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use App\Community\Enums\ArticleType;
use App\Enums\Permissions;
use App\Enums\SearchType;
use App\Platform\Enums\GameSetType;

function canSearch(int $searchType, int $permissions): bool
{
Expand Down Expand Up @@ -51,11 +52,30 @@ function performSearch(
FROM GameData AS gd
LEFT JOIN Achievements AS ach ON ach.GameID = gd.ID AND ach.Flags = 3
LEFT JOIN Console AS c ON gd.ConsoleID = c.ID
WHERE gd.Title LIKE '%$searchQuery%'
WHERE gd.ConsoleID != 100
AND gd.Title LIKE '%$searchQuery%'
GROUP BY gd.ID, gd.Title
ORDER BY SecondarySort, REPLACE(gd.Title, '|', ''), gd.Title";
}

if (in_array(SearchType::Hub, $searchType)) {
$counts[] = "SELECT COUNT(*) AS Count FROM game_sets WHERE deleted_at IS NULL AND type = '" . GameSetType::Hub->value . "' AND title LIKE '%$searchQuery%'";
$parts[] = "
SELECT " . SearchType::Hub . " AS Type, gs.id AS ID, CONCAT('/hub/', gs.id) AS Target,
CONCAT(gs.title, ' (Hub)') AS Title,
CASE
WHEN gs.title LIKE '$searchQuery%' THEN 0
WHEN gs.title LIKE '%~ $searchQuery%' THEN 1
ELSE 2
END AS SecondarySort
FROM game_sets AS gs
WHERE gs.deleted_at IS NULL
AND gs.type = '" . GameSetType::Hub->value . "'
AND gs.title LIKE '%$searchQuery%'
GROUP BY gs.id, gs.title
ORDER BY SecondarySort, REPLACE(gs.title, '|', ''), gs.title";
}

if (in_array(SearchType::Achievement, $searchType)) {
$counts[] = "SELECT COUNT(*) AS Count FROM Achievements WHERE Title LIKE '%$searchQuery%'";
$parts[] = "
Expand Down
79 changes: 79 additions & 0 deletions app/Http/Actions/UpdateDiscordNicknameAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace App\Http\Actions;

use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Throwable;

class UpdateDiscordNicknameAction
{
private Client $client;
private string $botToken;
private string $guildId;

public function __construct()
{
$this->client = new Client();
$this->botToken = config('services.discord.rabot_token');
$this->guildId = config('services.discord.guild_id');
}

public function execute(string $oldUsername, string $newUsername): void
{
if (!$this->botToken || !$this->guildId) {
return;
}

try {
$member = $this->findMemberByNickname($oldUsername);
if (!$member) {
return;
}

$this->updateUserNickname($member['user']['id'], $newUsername);
} catch (Throwable $e) {
Log::error("Failed to update Discord nickname: " . $e->getMessage());
}
}

private function findMemberByNickname(string $nickname): ?array
{
$response = $this->client->get(
"https://discord.com/api/v10/guilds/{$this->guildId}/members/search",
[
'headers' => [
'Authorization' => "Bot {$this->botToken}",
],
'query' => ['query' => $nickname],
]
);

$members = json_decode($response->getBody()->getContents(), true);

// Find a case-insensitive match.
foreach ($members as $member) {
$memberNick = $member['nick'] ?? $member['user']['username'];
if (strcasecmp($memberNick, $nickname) === 0) {
return $member;
}
}

return null;
}

private function updateUserNickname(string $userId, string $newNickname): void
{
$this->client->patch(
"https://discord.com/api/v10/guilds/{$this->guildId}/members/{$userId}",
[
'headers' => [
'Authorization' => "Bot {$this->botToken}",
],
'json' => ['nick' => $newNickname],
]
);
}
}
21 changes: 21 additions & 0 deletions app/Models/UserUsername.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ public function getIsExpiredAttribute(): bool
return $this->created_at->isPast() && $this->created_at->diffInDays(now()) >= 30;
}

public function getPreviousUsagesAttribute(): array
{
$usages = [];

$previousApprovedChanges = static::query()
->with('user')
->where('username', $this->username)
->where('id', '!=', $this->id)
->whereNotNull('approved_at')
->get();

foreach ($previousApprovedChanges as $change) {
$usages[] = [
'user' => $change->user,
'when' => $change->approved_at,
];
}

return $usages;
}

// == mutators

// == relations
Expand Down
1 change: 0 additions & 1 deletion app/Policies/MessagePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public function sendToRecipient(User $user, User $targetUser): bool
*/
$canUserAlwaysPierceNoContactPreference = $user->hasAnyRole([
Role::ADMINISTRATOR,
Role::DEVELOPER_JUNIOR,
Role::DEVELOPER,
Role::EVENT_MANAGER,
Role::FORUM_MANAGER,
Expand Down
2 changes: 2 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
'client_id' => env('DISCORD_KEY'),
'client_secret' => env('DISCORD_SECRET'),
'invite_id' => env('DISCORD_INVITE_ID'),
'guild_id' => env('DISCORD_GUILD_ID'),
'rabot_token' => env('DISCORD_RABOT_TOKEN'),
'webhook' => [
// public
'achievements' => env('DISCORD_WEBHOOK_ACHIEVEMENTS'),
Expand Down
5 changes: 3 additions & 2 deletions public/dorequest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Actions\FindUserByIdentifierAction;
use App\Community\Enums\ActivityType;
use App\Connect\Actions\BuildClientPatchDataAction;
use App\Connect\Actions\GetClientSupportLevelAction;
Expand Down Expand Up @@ -150,7 +151,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null)
return DoRequestError('Access denied.', 405, 'access_denied');
}

$foundDelegateToUser = User::whereName($delegateTo)->first();
$foundDelegateToUser = (new FindUserByIdentifierAction())->execute($delegateTo);
if (!$foundDelegateToUser) {
return DoRequestError("The target user couldn't be found.", 404, 'not_found');
}
Expand All @@ -177,7 +178,7 @@ function DoRequestError(string $error, ?int $status = 200, ?string $code = null)

// Replace the initiating user's properties with those of the user being delegated.
$user = $foundDelegateToUser;
$username = $delegateTo;
$username = $foundDelegateToUser->username;
}

switch ($requestType) {
Expand Down
2 changes: 1 addition & 1 deletion public/request/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
} elseif ($source == 'user' || $source == 'game-compare') {
$order = [SearchType::User];
} else {
$order = [SearchType::Game, SearchType::Achievement, SearchType::User];
$order = [SearchType::Game, SearchType::Hub, SearchType::Achievement, SearchType::User];
}

performSearch($order, $searchTerm, 0, $maxResults, $permissions, $results, wantTotalResults: false);
Expand Down
5 changes: 4 additions & 1 deletion resources/js/common/components/+vendor/BaseSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ const BaseSelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border light:border-neutral-200 light:bg-white',
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border light:border-neutral-200 light:bg-white',
'shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out light:text-neutral-950',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95',
'data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'border-neutral-800 bg-neutral-950 text-neutral-50',

// Don't overflow on mobile Android Chrome.
'max-h-[var(--radix-select-content-available-height)]',

position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',

Expand Down
Loading

0 comments on commit edd4559

Please sign in to comment.