Browse Source

Mute

imwald
Nuša Pukšič 1 month ago
parent
commit
272c0c4729
  1. 63
      src/Controller/Administration/RoleController.php
  2. 39
      src/Controller/DefaultController.php
  3. 5
      src/Entity/User.php
  4. 1
      src/Enum/RolesEnum.php
  5. 43
      src/Repository/UserEntityRepository.php
  6. 99
      src/Service/MutedPubkeysService.php

63
src/Controller/Administration/RoleController.php

@ -8,6 +8,7 @@ use App\Entity\User;
use App\Enum\RolesEnum; use App\Enum\RolesEnum;
use App\Form\RoleType; use App\Form\RoleType;
use App\Repository\UserEntityRepository; use App\Repository\UserEntityRepository;
use App\Service\MutedPubkeysService;
use App\Service\RedisCacheService; use App\Service\RedisCacheService;
use App\Util\NostrKeyUtil; use App\Util\NostrKeyUtil;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -28,9 +29,14 @@ class RoleController extends AbstractController
$featuredWriters = $userRepository->findFeaturedWriters(); $featuredWriters = $userRepository->findFeaturedWriters();
$featuredWritersData = $this->enrichUsersWithMetadata($featuredWriters, $redisCacheService); $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', [ return $this->render('admin/roles.html.twig', [
'form' => $form->createView(), 'form' => $form->createView(),
'featuredWriters' => $featuredWritersData, 'featuredWriters' => $featuredWritersData,
'mutedUsers' => $mutedUsersData,
]); ]);
} }
@ -179,6 +185,63 @@ class RoleController extends AbstractController
return $this->redirectToRoute('admin_roles'); 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 * Enrich user array with Nostr metadata
* @param User[] $users * @param User[] $users

39
src/Controller/DefaultController.php

@ -7,6 +7,7 @@ namespace App\Controller;
use App\Entity\Article; use App\Entity\Article;
use App\Entity\Event; use App\Entity\Event;
use App\Enum\KindsEnum; use App\Enum\KindsEnum;
use App\Service\MutedPubkeysService;
use App\Service\NostrClient; use App\Service\NostrClient;
use App\Service\RedisCacheService; use App\Service\RedisCacheService;
use App\Service\RedisViewStore; use App\Service\RedisViewStore;
@ -22,7 +23,6 @@ use Exception;
use FOS\ElasticaBundle\Finder\FinderInterface; use FOS\ElasticaBundle\Finder\FinderInterface;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException; use Psr\Cache\InvalidArgumentException;
use swentel\nostr\Key\Key;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -57,7 +57,8 @@ class DefaultController extends AbstractController
FinderInterface $finder, FinderInterface $finder,
RedisCacheService $redisCacheService, RedisCacheService $redisCacheService,
RedisViewStore $viewStore, RedisViewStore $viewStore,
CacheItemPoolInterface $articlesCache CacheItemPoolInterface $articlesCache,
MutedPubkeysService $mutedPubkeysService
): Response ): Response
{ {
// Fast path: Try to get from Redis views first (single GET) // Fast path: Try to get from Redis views first (single GET)
@ -92,20 +93,13 @@ class DefaultController extends AbstractController
set_time_limit(300); set_time_limit(300);
ini_set('max_execution_time', '300'); ini_set('max_execution_time', '300');
$key = new Key(); // Get muted pubkeys from database/cache
$excludedPubkeys = [ $excludedPubkeys = $mutedPubkeysService->getMutedPubkeys();
$key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'),
$key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'),
$key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'),
$key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'),
$key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'),
$key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'),
$key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'),
$key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'),
];
$boolQuery = new BoolQuery(); $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 = new Query($boolQuery);
$query->setSize(50); $query->setSize(50);
$query->setSort(['createdAt' => ['order' => 'desc']]); $query->setSort(['createdAt' => ['order' => 'desc']]);
@ -162,21 +156,12 @@ class DefaultController extends AbstractController
public function latestArticles( public function latestArticles(
RedisCacheService $redisCacheService, RedisCacheService $redisCacheService,
NostrClient $nostrClient, NostrClient $nostrClient,
RedisViewStore $viewStore RedisViewStore $viewStore,
MutedPubkeysService $mutedPubkeysService
): Response ): Response
{ {
// Define excluded pubkeys (needed for template) // Get muted pubkeys from database/cache
$key = new Key(); $excludedPubkeys = $mutedPubkeysService->getMutedPubkeys();
$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
];
// Fast path: Try Redis cache first (single GET - super fast!) // Fast path: Try Redis cache first (single GET - super fast!)
$cachedView = $viewStore->fetchLatestArticles(); $cachedView = $viewStore->fetchLatestArticles();

5
src/Entity/User.php

@ -65,6 +65,11 @@ class User implements UserInterface, EquatableInterface
return in_array(RolesEnum::FEATURED_WRITER, $this->roles, true); 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 public function getId(): ?int
{ {
return $this->id; return $this->id;

1
src/Enum/RolesEnum.php

@ -8,4 +8,5 @@ enum RolesEnum: string
case ADMIN = 'ROLE_ADMIN'; case ADMIN = 'ROLE_ADMIN';
case EDITOR = 'ROLE_EDITOR'; case EDITOR = 'ROLE_EDITOR';
case FEATURED_WRITER = 'ROLE_FEATURED_WRITER'; case FEATURED_WRITER = 'ROLE_FEATURED_WRITER';
case MUTED = 'ROLE_MUTED';
} }

43
src/Repository/UserEntityRepository.php

@ -147,4 +147,47 @@ class UserEntityRepository extends ServiceEntityRepository
return $sortedUsers; 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;
}
} }

99
src/Service/MutedPubkeysService.php

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserEntityRepository;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
/**
* Service to manage cached muted pubkeys for filtering articles
*/
class MutedPubkeysService
{
private const CACHE_KEY = 'muted_pubkeys';
private const CACHE_TTL = 86400; // 24 hours
public function __construct(
private readonly UserEntityRepository $userRepository,
private readonly CacheItemPoolInterface $cache,
private readonly LoggerInterface $logger
) {}
/**
* Get muted pubkeys from cache, or refresh if not available
* @return string[] Array of hex pubkeys
*/
public function getMutedPubkeys(): array
{
try {
$cacheItem = $this->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()
]);
}
}
}
Loading…
Cancel
Save