Private
Public Access
1
0

chg: usr: make fancy og tags - and create a special one for battle sharing #4

This commit is contained in:
2026-04-14 18:54:44 +02:00
parent 5d6aff8d90
commit d515f42cfd
21 changed files with 782 additions and 318 deletions

View File

@@ -10,21 +10,31 @@
namespace App\Controller;
use App\Entity\PlayedGame;
use App\Entity\User;
use App\Repository\PlayedGameRepository;
use App\Service\BattleCardGenerator;
use App\Service\WebAuthnService;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\Uuid;
use Throwable;
use function count;
/**
* Class ProfileController
@@ -41,7 +51,8 @@ class ProfileController extends AbstractController
{
public function __construct(
private readonly PlayedGameRepository $repo,
private readonly WebAuthnService $webAuthnService
private readonly WebAuthnService $webAuthnService,
private readonly LoggerInterface $logger,
) {
}
@@ -57,15 +68,15 @@ class ProfileController extends AbstractController
$losses = $this->repo->countLossesForUser($user);
$draws = $this->repo->countDrawsForUser($user);
// Build monthly buckets for the last 6 months
/** Build monthly buckets for the last 6 months */
$monthlyData = [];
for ($i = 5; $i >= 0; $i--) {
$dt = new \DateTime("first day of -$i months midnight");
$dt = new DateTime("first day of -$i months midnight");
$key = $dt->format('Y-m');
$monthlyData[$key] = ['label' => $dt->format('M'), 'wins' => 0, 'losses' => 0, 'draws' => 0];
}
$since = new \DateTime('first day of -5 months midnight');
$since = new DateTime('first day of -5 months midnight');
$recentGames = $this->repo->findFinishedForUserSince($user, $since);
$userId = $user->getId();
@@ -113,7 +124,7 @@ class ProfileController extends AbstractController
'bestScore' => $this->repo->findBestScoreForUser($user),
],
'recent' => ($recent = $this->repo->findRecentFinishedForUser($user)),
'gamesData' => array_map(static function (\App\Entity\PlayedGame $game) use ($userId): array {
'gamesData' => array_map(static function (PlayedGame $game) use ($userId): array {
$isRed = $game->getRed()?->getId() === $userId;
$resign = $game->getResign();
$myColor = $isRed ? 'red' : 'blue';
@@ -131,6 +142,7 @@ class ProfileController extends AbstractController
return [
'id' => $game->getId(),
'uuid' => $game->getUuid()?->toRfc4122(),
'redName' =>
$game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest',
'blueName' =>
@@ -160,16 +172,21 @@ class ProfileController extends AbstractController
]);
}
#[Route('/battle/{id}', name: 'MineSeekerBundle_battle_share', requirements: ['id' => '\d+'], methods: ['GET'])]
public function battleShare(int $id): Response
#[Route(
'/battle/{uuid}',
name: 'MineSeekerBundle_battle_share',
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
methods: ['GET'],
)]
public function battleShare(Uuid $uuid): Response
{
$game = $this->repo->find($id);
$game = $this->repo->findOneBy(['uuid' => $uuid]);
if (!$game) {
throw $this->createNotFoundException('Battle not found.');
}
$redName = $game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest';
$blueName = $game->getBlue()?->getUsername() ?? $game->getBlueAnon()?->getUserName() ?? 'Guest';
$redName = $game->getRed()?->getUsername() ?? ($game->getRedAnon() !== null ? 'Anonymous' : 'Guest');
$blueName = $game->getBlue()?->getUsername() ?? ($game->getBlueAnon() !== null ? 'Anonymous' : 'Guest');
$redPts = $game->getRedPoints();
$bluePts = $game->getBluePoints();
$resign = $game->getResign();
@@ -202,6 +219,29 @@ class ProfileController extends AbstractController
]);
}
#[Route(
'/og/battle/{uuid}.png',
name: 'MineSeekerBundle_og_battle',
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
methods: ['GET'],
)]
public function battleOgImage(Uuid $uuid, BattleCardGenerator $generator): BinaryFileResponse
{
$game = $this->repo->findOneBy(['uuid' => $uuid]);
if (!$game) {
throw $this->createNotFoundException();
}
$path = $generator->generate($game);
$response = new BinaryFileResponse($path);
$response->headers->set('Content-Type', 'image/png');
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
$response->setMaxAge(86400 * 30);
$response->setPublic();
return $response;
}
#[Route('/profile/avatar', name: 'MineSeekerBundle_profile_avatar', methods: ['POST'])]
public function uploadAvatar(
Request $request,
@@ -232,18 +272,24 @@ class ProfileController extends AbstractController
$newPath = sprintf('avatar/%s.%s', Uuid::v4()->toRfc4122(), $ext);
$oldPath = $user->getAvatarPath();
// Remove old file and any cached thumbnails
/** Remove old file and any cached thumbnails */
if ($oldPath) {
try {
$mediaStorage->delete($oldPath);
} catch (\Throwable) {
} catch (Throwable) {
$this->logger->error('Unable to delete old avatar: ' . $oldPath);
}
$cacheManager->remove($oldPath, 'avatar_thumb');
}
// Upload original to MinIO media/avatar/
$stream = fopen($file->getPathname(), 'r');
$mediaStorage->writeStream($newPath, $stream);
/** Upload original to MinIO media/avatar/ */
$stream = fopen($file->getPathname(), 'rb');
try {
$mediaStorage->writeStream($newPath, $stream);
} catch (FilesystemException $e) {
$this->logger->error('Unable to write new avatar: ' . $e->getMessage());
throw new RuntimeException('Unable to write new avatar: ' . $e->getMessage());
}
fclose($stream);
$user->setAvatarPath($newPath);
@@ -274,7 +320,7 @@ class ProfileController extends AbstractController
return $this->render('Security/profile_security.html.twig', [
'credentials' => $credentialsData,
'isTotpEnabled' => $user->isTotpAuthenticationEnabled(),
'backupCodesCount' => \count($user->getBackupCodes()),
'backupCodesCount' => count($user->getBackupCodes()),
]);
}
}