From 272c0c4729573a2bcb6e22e8a5cfc80b8beaef70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Tue, 9 Dec 2025 17:18:05 +0100 Subject: [PATCH] Mute --- .../Administration/RoleController.php | 63 ++++++++++++ src/Controller/DefaultController.php | 39 +++----- src/Entity/User.php | 5 + src/Enum/RolesEnum.php | 1 + src/Repository/UserEntityRepository.php | 43 ++++++++ src/Service/MutedPubkeysService.php | 99 +++++++++++++++++++ 6 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 src/Service/MutedPubkeysService.php diff --git a/src/Controller/Administration/RoleController.php b/src/Controller/Administration/RoleController.php index 57ba1a1..3fce19a 100644 --- a/src/Controller/Administration/RoleController.php +++ b/src/Controller/Administration/RoleController.php @@ -8,6 +8,7 @@ use App\Entity\User; use App\Enum\RolesEnum; use App\Form\RoleType; use App\Repository\UserEntityRepository; +use App\Service\MutedPubkeysService; use App\Service\RedisCacheService; use App\Util\NostrKeyUtil; use Doctrine\ORM\EntityManagerInterface; @@ -28,9 +29,14 @@ class RoleController extends AbstractController $featuredWriters = $userRepository->findFeaturedWriters(); $featuredWritersData = $this->enrichUsersWithMetadata($featuredWriters, $redisCacheService); + // Get muted users for display + $mutedUsers = $userRepository->findMutedUsers(); + $mutedUsersData = $this->enrichUsersWithMetadata($mutedUsers, $redisCacheService); + return $this->render('admin/roles.html.twig', [ 'form' => $form->createView(), 'featuredWriters' => $featuredWritersData, + 'mutedUsers' => $mutedUsersData, ]); } @@ -179,6 +185,63 @@ class RoleController extends AbstractController return $this->redirectToRoute('admin_roles'); } + #[Route('/admin/muted-users/add', name: 'admin_muted_users_add', methods: ['POST'])] + public function addMutedUser(Request $request, UserEntityRepository $userRepository, EntityManagerInterface $em, MutedPubkeysService $mutedPubkeysService): Response + { + $npub = $request->request->get('npub'); + + if (!$npub || !str_starts_with($npub, 'npub1')) { + $this->addFlash('error', 'Invalid npub format'); + return $this->redirectToRoute('admin_roles'); + } + + $user = $userRepository->findOneBy(['npub' => $npub]); + + if (!$user) { + // Create user if not exists + $user = new User(); + $user->setNpub($npub); + $user->setRoles([RolesEnum::MUTED->value]); + $em->persist($user); + $this->addFlash('success', 'Created new user and added to muted list'); + } else { + if ($user->isMuted()) { + $this->addFlash('warning', 'User is already muted'); + return $this->redirectToRoute('admin_roles'); + } + $user->addRole(RolesEnum::MUTED->value); + $this->addFlash('success', 'User added to muted list'); + } + + $em->flush(); + + // Refresh the muted pubkeys cache + $mutedPubkeysService->refreshCache(); + + return $this->redirectToRoute('admin_roles'); + } + + #[Route('/admin/muted-users/remove/{id}', name: 'admin_muted_users_remove', methods: ['POST'])] + public function removeMutedUser(int $id, UserEntityRepository $userRepository, EntityManagerInterface $em, MutedPubkeysService $mutedPubkeysService): Response + { + $user = $userRepository->find($id); + + if (!$user) { + $this->addFlash('error', 'User not found'); + return $this->redirectToRoute('admin_roles'); + } + + $user->removeRole(RolesEnum::MUTED->value); + $em->flush(); + + // Refresh the muted pubkeys cache + $mutedPubkeysService->refreshCache(); + + $this->addFlash('success', 'User removed from muted list'); + + return $this->redirectToRoute('admin_roles'); + } + /** * Enrich user array with Nostr metadata * @param User[] $users diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 1b8ddcd..0beec46 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -7,6 +7,7 @@ namespace App\Controller; use App\Entity\Article; use App\Entity\Event; use App\Enum\KindsEnum; +use App\Service\MutedPubkeysService; use App\Service\NostrClient; use App\Service\RedisCacheService; use App\Service\RedisViewStore; @@ -22,7 +23,6 @@ use Exception; use FOS\ElasticaBundle\Finder\FinderInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; -use swentel\nostr\Key\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -57,7 +57,8 @@ class DefaultController extends AbstractController FinderInterface $finder, RedisCacheService $redisCacheService, RedisViewStore $viewStore, - CacheItemPoolInterface $articlesCache + CacheItemPoolInterface $articlesCache, + MutedPubkeysService $mutedPubkeysService ): Response { // Fast path: Try to get from Redis views first (single GET) @@ -92,20 +93,13 @@ class DefaultController extends AbstractController set_time_limit(300); ini_set('max_execution_time', '300'); - $key = new Key(); - $excludedPubkeys = [ - $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'), - $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'), - $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'), - $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'), - $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'), - $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'), - $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'), - $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'), - ]; + // Get muted pubkeys from database/cache + $excludedPubkeys = $mutedPubkeysService->getMutedPubkeys(); $boolQuery = new BoolQuery(); - $boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys)); + if (!empty($excludedPubkeys)) { + $boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys)); + } $query = new Query($boolQuery); $query->setSize(50); $query->setSort(['createdAt' => ['order' => 'desc']]); @@ -162,21 +156,12 @@ class DefaultController extends AbstractController public function latestArticles( RedisCacheService $redisCacheService, NostrClient $nostrClient, - RedisViewStore $viewStore + RedisViewStore $viewStore, + MutedPubkeysService $mutedPubkeysService ): Response { - // Define excluded pubkeys (needed for template) - $key = new Key(); - $excludedPubkeys = [ - $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'), // Bitcoin Magazine (News Bot) - $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'), // No Bullshit Bitcoin (News Bot) - $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'), // TFTC (News Bot) - $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'), // Discreet Log (News Bot) - $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'), // Batcoinz - $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'), // AGORA Marketplace - $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'), // NSFW - $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'), // LNgigs - ]; + // Get muted pubkeys from database/cache + $excludedPubkeys = $mutedPubkeysService->getMutedPubkeys(); // Fast path: Try Redis cache first (single GET - super fast!) $cachedView = $viewStore->fetchLatestArticles(); diff --git a/src/Entity/User.php b/src/Entity/User.php index d208fa4..4337c10 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -65,6 +65,11 @@ class User implements UserInterface, EquatableInterface return in_array(RolesEnum::FEATURED_WRITER, $this->roles, true); } + public function isMuted(): bool + { + return in_array(RolesEnum::MUTED, $this->roles, true); + } + public function getId(): ?int { return $this->id; diff --git a/src/Enum/RolesEnum.php b/src/Enum/RolesEnum.php index 20fd995..538bcae 100644 --- a/src/Enum/RolesEnum.php +++ b/src/Enum/RolesEnum.php @@ -8,4 +8,5 @@ enum RolesEnum: string case ADMIN = 'ROLE_ADMIN'; case EDITOR = 'ROLE_EDITOR'; case FEATURED_WRITER = 'ROLE_FEATURED_WRITER'; + case MUTED = 'ROLE_MUTED'; } diff --git a/src/Repository/UserEntityRepository.php b/src/Repository/UserEntityRepository.php index 0922810..9a5679d 100644 --- a/src/Repository/UserEntityRepository.php +++ b/src/Repository/UserEntityRepository.php @@ -147,4 +147,47 @@ class UserEntityRepository extends ServiceEntityRepository return $sortedUsers; } + + /** + * Find all users with the ROLE_MUTED role + * @return User[] + * @throws Exception + */ + public function findMutedUsers(): array + { + $conn = $this->entityManager->getConnection(); + $sql = 'SELECT id FROM app_user WHERE roles::text LIKE :role'; + $result = $conn->executeQuery($sql, ['role' => '%' . RolesEnum::MUTED->value . '%']); + + $ids = $result->fetchFirstColumn(); + if (empty($ids)) { + return []; + } + + return $this->createQueryBuilder('u') + ->where('u.id IN (:ids)') + ->setParameter('ids', $ids) + ->getQuery() + ->getResult(); + } + + /** + * Get hex pubkeys of all muted users + * @return string[] + * @throws Exception + */ + public function getMutedPubkeys(): array + { + $mutedUsers = $this->findMutedUsers(); + $pubkeys = []; + + foreach ($mutedUsers as $user) { + $npub = $user->getNpub(); + if (NostrKeyUtil::isNpub($npub)) { + $pubkeys[] = NostrKeyUtil::npubToHex($npub); + } + } + + return $pubkeys; + } } diff --git a/src/Service/MutedPubkeysService.php b/src/Service/MutedPubkeysService.php new file mode 100644 index 0000000..2f7f642 --- /dev/null +++ b/src/Service/MutedPubkeysService.php @@ -0,0 +1,99 @@ +cache->getItem(self::CACHE_KEY); + + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + return $this->refreshCache(); + } catch (\Exception $e) { + $this->logger->error('Error getting muted pubkeys from cache', [ + 'error' => $e->getMessage() + ]); + + // Fallback: get directly from database + try { + return $this->userRepository->getMutedPubkeys(); + } catch (\Exception $e) { + $this->logger->error('Error getting muted pubkeys from database', [ + 'error' => $e->getMessage() + ]); + return []; + } + } + } + + /** + * Refresh the muted pubkeys cache + * Call this when a user is muted or unmuted + * @return string[] The refreshed array of hex pubkeys + */ + public function refreshCache(): array + { + try { + $pubkeys = $this->userRepository->getMutedPubkeys(); + + $cacheItem = $this->cache->getItem(self::CACHE_KEY); + $cacheItem->set($pubkeys); + $cacheItem->expiresAfter(self::CACHE_TTL); + $this->cache->save($cacheItem); + + $this->logger->info('Muted pubkeys cache refreshed', [ + 'count' => count($pubkeys) + ]); + + return $pubkeys; + } catch (\Exception $e) { + $this->logger->error('Error refreshing muted pubkeys cache', [ + 'error' => $e->getMessage() + ]); + return []; + } + } + + /** + * Invalidate the cache (force refresh on next access) + */ + public function invalidateCache(): void + { + try { + $this->cache->deleteItem(self::CACHE_KEY); + $this->logger->info('Muted pubkeys cache invalidated'); + } catch (\Exception $e) { + $this->logger->error('Error invalidating muted pubkeys cache', [ + 'error' => $e->getMessage() + ]); + } + } +} +