mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-17 17:28:02 +00:00
165 lines
5.4 KiB
PHP
165 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
namespace OCA\Forum\Service;
|
|
|
|
use OCA\Forum\Db\GuestSession;
|
|
use OCA\Forum\Db\GuestSessionMapper;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
|
|
class GuestService {
|
|
private const ADJECTIVES = [
|
|
'Bright', 'Swift', 'Calm', 'Bold', 'Keen',
|
|
'Wise', 'Fair', 'Warm', 'Cool', 'Pure',
|
|
'Sharp', 'Brave', 'Clear', 'Quick', 'Glad',
|
|
'Kind', 'Free', 'Noble', 'Proud', 'True',
|
|
'Lucky', 'Merry', 'Jolly', 'Lively', 'Gentle',
|
|
'Vivid', 'Quiet', 'Eager', 'Happy', 'Witty',
|
|
'Daring', 'Clever', 'Humble', 'Cosmic', 'Sunny',
|
|
'Golden', 'Silver', 'Crystal', 'Mystic', 'Velvet',
|
|
'Amber', 'Azure', 'Coral', 'Ivory', 'Jade',
|
|
'Ruby', 'Sage', 'Teal', 'Rustic', 'Nimble',
|
|
'Polar', 'Lunar', 'Solar', 'Stellar', 'Radiant',
|
|
'Serene', 'Dapper', 'Frosty', 'Mossy', 'Dusty',
|
|
'Misty', 'Breezy', 'Stormy', 'Snowy', 'Rainy',
|
|
'Zesty', 'Peppy', 'Perky', 'Chipper', 'Plucky',
|
|
'Hardy', 'Sturdy', 'Steady', 'Loyal', 'Fierce',
|
|
'Savvy', 'Crafty', 'Nifty', 'Handy', 'Zippy',
|
|
'Glossy', 'Sleek', 'Crisp', 'Fresh', 'Rosy',
|
|
'Dusky', 'Ashen', 'Oaken', 'Marble', 'Pewter',
|
|
'Copper', 'Bronze', 'Cobalt', 'Scarlet', 'Indigo',
|
|
];
|
|
|
|
private const NOUNS = [
|
|
'Mountain', 'River', 'Forest', 'Meadow', 'Ocean',
|
|
'Valley', 'Desert', 'Island', 'Canyon', 'Prairie',
|
|
'Falcon', 'Eagle', 'Raven', 'Phoenix', 'Tiger',
|
|
'Dolphin', 'Panther', 'Wolf', 'Fox', 'Hawk',
|
|
'Storm', 'Thunder', 'Breeze', 'Frost', 'Aurora',
|
|
'Comet', 'Star', 'Nebula', 'Galaxy', 'Cosmos',
|
|
'Oak', 'Cedar', 'Willow', 'Maple', 'Birch',
|
|
'Spark', 'Flame', 'Wave', 'Stone', 'Crystal',
|
|
'Arrow', 'Shield', 'Crown', 'Compass', 'Lantern',
|
|
'Harbor', 'Summit', 'Ridge', 'Cliff', 'Shore',
|
|
'Heron', 'Otter', 'Panda', 'Lynx', 'Crane',
|
|
'Badger', 'Osprey', 'Wren', 'Finch', 'Condor',
|
|
'Glacier', 'Lagoon', 'Tundra', 'Steppe', 'Reef',
|
|
'Plateau', 'Ravine', 'Basin', 'Delta', 'Fjord',
|
|
'Pebble', 'Ember', 'Geyser', 'Prism', 'Quartz',
|
|
'Beacon', 'Anchor', 'Vessel', 'Scepter', 'Banner',
|
|
'Thistle', 'Clover', 'Orchid', 'Lotus', 'Fern',
|
|
'Ivy', 'Aspen', 'Cypress', 'Spruce', 'Sequoia',
|
|
'Dusk', 'Dawn', 'Zenith', 'Horizon', 'Eclipse',
|
|
'Cascade', 'Torrent', 'Zephyr', 'Tempest', 'Monsoon',
|
|
];
|
|
|
|
public function __construct(
|
|
private GuestSessionMapper $guestSessionMapper,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Resolve a guest token to a guest author ID.
|
|
* Creates a new guest session if the token does not exist yet.
|
|
*
|
|
* @param string $guestToken 32-character hex token
|
|
* @return string Author ID in the format "guest:<token>"
|
|
* @throws \InvalidArgumentException If token is invalid
|
|
*/
|
|
public function resolveGuestIdentity(string $guestToken): string {
|
|
// Validate token format: 32 hex characters
|
|
if (!preg_match('/^[0-9a-f]{32}$/', $guestToken)) {
|
|
throw new \InvalidArgumentException('Invalid guest token format');
|
|
}
|
|
|
|
try {
|
|
$this->guestSessionMapper->findByToken($guestToken);
|
|
} catch (DoesNotExistException $e) {
|
|
// Create new guest session
|
|
$session = new GuestSession();
|
|
$session->setSessionToken($guestToken);
|
|
$session->setDisplayName($this->generateGuestName());
|
|
$session->setCreatedAt(time());
|
|
$this->guestSessionMapper->insert($session);
|
|
}
|
|
|
|
return 'guest:' . $guestToken;
|
|
}
|
|
|
|
/**
|
|
* Get the display name for a guest author ID
|
|
*
|
|
* @param string $authorId Author ID in the format "guest:<token>"
|
|
* @return string|null Display name, or null if not found
|
|
*/
|
|
public function getGuestDisplayName(string $authorId): ?string {
|
|
if (!self::isGuestAuthor($authorId)) {
|
|
return null;
|
|
}
|
|
|
|
$token = substr($authorId, 6); // Remove "guest:" prefix
|
|
try {
|
|
$session = $this->guestSessionMapper->findByToken($token);
|
|
return $session->getDisplayName();
|
|
} catch (DoesNotExistException $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get guest session by token, creating one if it does not exist
|
|
*
|
|
* @param string $guestToken 32-character hex token
|
|
* @return GuestSession
|
|
* @throws \InvalidArgumentException If token is invalid
|
|
*/
|
|
public function getOrCreateSession(string $guestToken): GuestSession {
|
|
if (!preg_match('/^[0-9a-f]{32}$/', $guestToken)) {
|
|
throw new \InvalidArgumentException('Invalid guest token format');
|
|
}
|
|
|
|
try {
|
|
return $this->guestSessionMapper->findByToken($guestToken);
|
|
} catch (DoesNotExistException $e) {
|
|
$session = new GuestSession();
|
|
$session->setSessionToken($guestToken);
|
|
$session->setDisplayName($this->generateGuestName());
|
|
$session->setCreatedAt(time());
|
|
/** @var GuestSession */
|
|
return $this->guestSessionMapper->insert($session);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an author ID represents a guest user
|
|
*/
|
|
public static function isGuestAuthor(string $authorId): bool {
|
|
return str_starts_with($authorId, 'guest:');
|
|
}
|
|
|
|
/**
|
|
* Generate a unique guest display name
|
|
* Format: AdjectiveNounNN (e.g., "BrightMountain42")
|
|
*/
|
|
private function generateGuestName(): string {
|
|
$maxAttempts = 20;
|
|
for ($i = 0; $i < $maxAttempts; $i++) {
|
|
$adjective = self::ADJECTIVES[array_rand(self::ADJECTIVES)];
|
|
$noun = self::NOUNS[array_rand(self::NOUNS)];
|
|
$number = str_pad((string)random_int(0, 99), 2, '0', STR_PAD_LEFT);
|
|
$name = $adjective . $noun . $number;
|
|
|
|
if (!$this->guestSessionMapper->displayNameExists($name)) {
|
|
return $name;
|
|
}
|
|
}
|
|
|
|
// Fallback: use timestamp-based name
|
|
return 'Guest' . dechex(time()) . str_pad((string)random_int(0, 99), 2, '0', STR_PAD_LEFT);
|
|
}
|
|
}
|