A lot of things

This commit is contained in:
2026-06-16 16:21:43 +02:00
parent 4f9f6c63b3
commit 7e1e26f20b
126 changed files with 7917 additions and 204 deletions

View File

@@ -0,0 +1,223 @@
<?php
namespace App\Services;
use App\Models\Entry;
use App\Models\News;
use App\View\Components\EntryCard;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ActivityService
{
private const CACHE_ENTRIES = 300; // seconds.
private const CACHE_NEWS = 300; // seconds.
private const CACHE_MESSAGES = 300; // seconds.
private const CACHE_THREADS = 300; // seconds.
private const CACHE_CLUBS = 300; // seconds.
private const ITEMS_PER_TYPE = 15;
public function getActivities( array $activities = [ 'entries', 'news', 'messages', 'threads', 'clubs' ] ): Collection
{
$c = collect();
if( in_array( 'entries', $activities ) ) {
$c = $c->merge($this->getEntries());
}
if( in_array( 'news', $activities ) ) {
$c = $c->merge($this->getNews());
}
if( in_array( 'messages', $activities ) ) {
$c = $c->merge($this->getMessages());
}
if( in_array( 'threads', $activities ) ) {
$c = $c->merge($this->getThreads());
}
if( in_array( 'clubs', $activities ) ) {
$c = $c->merge($this->getClubs());
}
return $c->sortByDesc('date')
->values()
->take(30)
->map(function(array $item){
$obj = (object) $item;
$obj->date = Carbon::createFromTimestamp($obj->date);
return $obj;
});
}
private function formatEntry( Entry $entry ): array
{
return [
'type' => 'entry',
'title' => $entry->complete_title ?? $entry->title,
'url' => route('entries.show', ['section' => $entry->type, 'entry' => $entry]),
'image' => $entry->main_image ? \Storage::url($entry->main_image) : null,
'date' => $entry->created_at->timestamp,
'author' => $entry->authors->pluck('name')->implode(', '),
'user_id' => $entry->user_id,
'badge' => EntryCard::ENTRY_TYPES_BADGE[$entry->type],
'badge_class' => $entry->type,
'excerpt' => $entry->description ? \Str::limit(strip_tags($entry->description), 80) : null,
'meta' => $entry->getRealPlatform()?->name
];
}
private function formatNews( News $news ): array
{
return [
'type' => 'news',
'title' => $news->title,
'url' => route('news.show', ['news' => $news]),
'image' => $news->gallery()->first() ? \Storage::url($news->gallery()->first()->image) : null,
'date' => $news->created_at->timestamp,
'author' => null,
'user_id' => $news->user_id,
'badge' => 'News',
'badge_class' => 'news',
'excerpt' => $news->description ? \Str::limit(strip_tags($news->description), 80) : null,
'meta' => $news->category?->name
];
}
private function formatMessage( object $message ): array
{
return [
'type' => 'message',
'title' => $message->title,
'url' => xfRoute('threads/.' ) . $message->thread_id . '/post-' . $message->post_id,
'image' => null,
'date' => $message->post_date,
'author' => null,
'user_id' => $message->user_id,
'badge' => 'Post',
'badge_class' => 'message',
'excerpt' => $message->message ? \Str::limit(strip_tags($message->message), 80) : null,
'meta' => null
];
}
private function formatThread( object $thread ): array
{
return [
'type' => 'thread',
'title' => $thread->title,
'url' => xfRoute('threads/.' ) . $thread->thread_id,
'image' => null,
'date' => $thread->post_date,
'author' => null,
'user_id' => $thread->user_id,
'badge' => 'Thread',
'badge_class' => 'thread',
'excerpt' => $thread->message ? \Str::limit(strip_tags($thread->message), 80) : null,
'meta' => null
];
}
private function formatClub( object $club ): array
{
return [
'type' => 'club',
'title' => $club->title,
'url' => xfRoute('forums/.' ) . $club->node_id,
'image' => null, // TODO: Remplacer par banner_date
'date' => $club->club_creation_date,
'author' => null,
'user_id' => $club->user_id,
'badge' => 'Club',
'badge_class' => 'club',
'excerpt' => $club->description ? \Str::limit(strip_tags($club->description), 80) : null,
'meta' => null
];
}
private function getEntries(): array
{
return Cache::remember('activity_entries', self::CACHE_ENTRIES, function() {
return Entry::published()
->with(['authors', 'game.platform'])
->latest('created_at')
->limit(self::ITEMS_PER_TYPE)
->get()
->map($this->formatEntry(...))
->toArray();
});
}
private function getNews(): array
{
return Cache::remember('activity_news', self::CACHE_NEWS, function() {
return News::published()
->with('gallery')
->latest('created_at')
->limit(self::ITEMS_PER_TYPE)
->get()
->map($this->formatNews(...))
->toArray();
});
}
private function getMessages(): array
{
return Cache::remember('activity_messages', self::CACHE_MESSAGES, function() {
return DB::connection('xenforo')
->table('post')
->join('user', 'post.user_id', '=', 'user.user_id')
->join('thread', 'post.thread_id', '=', 'thread.thread_id')
->where('post.message_state', 'visible')
->where('thread.first_post_id', '!=', 'post.post_id')
->orderByDesc('post.post_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'thread.title', 'thread.thread_id', 'post.post_id', 'post.post_date',
'post.user_id', 'post.message'
])
->get()
->map($this->formatMessage(...))
->toArray();
});
}
private function getThreads(): array
{
return Cache::remember('activity_threads', self::CACHE_THREADS, function() {
return DB::connection('xenforo')
->table('thread')
->join('user', 'thread.user_id', '=', 'user.user_id')
->join('post', 'thread.first_post_id', '=', 'post.post_id')
->where('thread.discussion_state', 'visible')
->where('thread.discussion_type', '!=', 'redirect' )
->orderByDesc('thread.post_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'thread.title', 'thread.thread_id', 'thread.post_date', 'thread.user_id',
'post.message'
])
->get()
->map($this->formatThread(...))
->toArray();
});
}
private function getClubs(): array
{
return Cache::remember('activity_clubs', self::CACHE_CLUBS, function() {
return DB::connection('xenforo')
->table('club')
->where('club_state', 'visible')
->orderByDesc('club_creation_date')
->limit(self::ITEMS_PER_TYPE)
->select([
'club.title', 'club.description', 'club.node_id',
'club.club_creation_date', 'club.user_id'
])
->get()
->map($this->formatClub(...))
->toArray();
});
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Services;
use App\Models\EntryFile;
use App\Models\LogXfUser;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
@@ -110,7 +111,6 @@ class FileServersService {
'filename' => $filename,
'current_chunk' => $currentChunk,
'total_chunks' => $totalChunks,
// TODO : Must replace User ID
'zeus' => $this->generateZeusToken( \Auth::user()->user_id, $server['base_url'], "Uploadchunk" ),
]);
@@ -129,4 +129,43 @@ class FileServersService {
return $data;
}
public function deleteFile(
string $filePath,
string $fileName,
int $userId
): bool {
foreach( $this->servers as $serverKey => $server ){
$response = Http::withHeaders([])
->post( $server['delete_file'], [
'filepath' => $filePath,
'filename' => $fileName,
'zeus' => $this->generateZeusToken( $userId, $server['base_url'], "Deletefile" ),
]);
if (!$response->successful()) {
throw new \RuntimeException( $response->body() );
}
$data = $response->json();
if( isset( $data['status'] ) && $data['status'] === 'deleted' ){
continue;
} else {
return false;
}
}
activity('entry-file')
->causedBy(LogXfUser::find($userId))
->withProperties([
'filepath' => $filePath,
'filename' => $fileName,
])
->event('file_deletion')
->log('File deleted');
return true;
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace App\Services;
use App\Helpers\EntryHelpers;
use App\Jobs\CreateXenForoCommentsThread;
use App\Models\Entry;
use App\Models\News;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class NewsService
{
/**
* Request for store/edit.
* @var Request|null
*/
private ?Request $request = null;
/**
* Entry for edit.
* @var News|null
*/
private ?News $news = null;
public function storeNews(Request $request): News
{
$this->request = $request;
$user_id = \Auth::user()->user_id;
$news = DB::transaction(function () use ($user_id) {
// STEP 1 : Slug
$slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class );
$fields = [
'title' => $this->request->input('title'),
'slug' => $slug,
'category_id' => $this->request->input('category'),
'description' => $this->request->input('description'),
'state' => $this->request->input('submit-state'),
'entry_id' => $this->request->input('entry_id'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_link'),
'user_id' => $user_id,
];
$news = News::create( $fields );
// STEP 3 : Prepare Gallery images.
$this->Step3a_PrepareGalleryImages( $news );
return $news;
});
$this->Step3b_SaveGalleryImages( $news );
$this->Step4_CreateCommentsThread( $news );
return $news;
}
private function Step3a_PrepareGalleryImages(News $news): void
{
foreach( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ){
$news->gallery()->create([
'image' => $imagePath,
'order' => $i
]);
}
}
private function Step3b_SaveGalleryImages(News $news): void
{
foreach ( $news->gallery ?? [] as $galleryItem ) {
$newPath = 'news/gallery-images/' . $news->id . '/' . basename($galleryItem->image);
if( !Storage::disk('public')->move($galleryItem->image, $newPath) )
continue;
$galleryItem->update(['image' => $newPath]);
}
}
private function Step4_CreateCommentsThread( News $news ): void
{
if( $news->state !== 'published' )
return;
if( !$news->comments_thread_id )
CreateXenForoCommentsThread::dispatch( $news );
}
public function editNews(Request $request, News $news): News
{
$this->request = $request;
$this->news = $news;
if( \Auth::user()->can('moderate', $news) ){
$user_id = $this->request->input('owner_user_id');
} else {
$user_id = \Auth::user()->user_id;
}
$galleryPaths = [];
$news = DB::transaction(function () use ($user_id, &$galleryPaths) {
// STEP 1 : Refresh slug.
if( $this->request->input('title') !== $this->news->title ){
$this->news->slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class, $this->news->id );
}
$fields = [
'title' => $this->request->input('title'),
'slug' => $this->news->slug,
'category_id' => $this->request->input('category'),
'description' => $this->request->input('description'),
'state' => $this->request->input('submit-state'),
'entry_id' => $this->request->input('entry_id'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_link'),
'user_id' => $user_id,
];
if( \Auth::user()->can('moderate', $this->news) ){
$fields['staff_comment'] = $this->request->input('staff_comment');
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
}
$this->news->update( $fields );
$galleryPaths = $this->eStep3a_UpdateGalleryImages();
return $this->news;
});
$this->eStep3b_UpdateGalleryImages( $galleryPaths );
$this->step4_CreateCommentsThread( $news );
return $news;
}
private function eStep3a_UpdateGalleryImages(): array
{
$requestGallery = $this->request->input('gallery', [] ) ?? [];
$existingGalleryPaths = $this->news->gallery->pluck('image')->toArray();
$needDeletion = array_diff( $existingGalleryPaths, $requestGallery );
if( !empty( $needDeletion ) ){
$this->news->gallery()->whereIn('image', $needDeletion )->delete();
}
$needAddition = array_diff( $requestGallery, $existingGalleryPaths );
$images = [];
foreach( $needAddition as $imagePath ){
$images[] = $this->news->gallery()->create([
'image' => $imagePath,
]);
}
foreach( $requestGallery as $i => $imagePath ){
$this->news->gallery()->where('image', $imagePath )->update(['order' => $i]);
}
return [ 'addition' => $images, 'deletion' => $needDeletion ];
}
private function eStep3b_UpdateGalleryImages( array $pathsChanges ): void
{
foreach ( $pathsChanges['deletion'] as $deletePath ){
if( Storage::disk('public')->exists($deletePath) )
Storage::disk('public')->delete($deletePath);
}
foreach ( $pathsChanges['addition'] as $galleryItem ){
$newPath = 'news/gallery-images/' . $this->news->id . '/' . basename( $galleryItem->image );
if( !Storage::disk('public')->move( $galleryItem->image, $newPath ) ){
continue;
}
$galleryItem->update(['image' => $newPath]);
}
}
}

View File

@@ -4,9 +4,11 @@ namespace App\Services;
use App\Exceptions\SubmissionException;
use App\Helpers\EntryHelpers;
use App\Helpers\PlayOnlineHelpers;
use App\Helpers\XenForoHelpers;
use App\Http\Requests\StoreEntryRequest;
use App\Jobs\CreateXenForoCommentsThread;
use App\Jobs\DeleteFile;
use App\Models\Author;
use App\Models\Category;
use App\Models\Entry;
@@ -76,8 +78,12 @@ class SubmissionsService {
'error' => null,
'uuid' => $uuid,
'state' => $file->state,
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
'meta_online_patcher' => $file->online_patcher,
'meta_secondary_online_patcher' => $file->secondary_online_patcher,
'meta_play_online' => $file->playOnlineSetting()->exists() ? true : false,
'meta_play_online_core' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->core : '',
'meta_play_online_threads' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->threads : false,
];
$file = Cache::get("uploaded_file_{$uuid}");
@@ -92,8 +98,12 @@ class SubmissionsService {
'error' => null,
'uuid' => $uuid,
'state' => $file['state'],
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
'meta_online_patcher' => false,
'meta_secondary_online_patcher' => false,
'meta_play_online' => false,
'meta_play_online_core' => null,
'meta_play_online_threads' => false
];
return null;
@@ -298,15 +308,20 @@ class SubmissionsService {
foreach ( $uuidData ?? [] as $uuid ) {
$fileData = Cache::pull("uploaded_file_{$uuid}");
if( !$fileData )
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry." );
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all the new files and retry." );
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
if( !$onlinePatcher )
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension( $fileData['filename'] );
if( section_must_be( [ 'romhacks', 'translations' ], $entry->type ) ) {
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
if (!$onlinePatcher)
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension($fileData['filename']);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
} else {
$onlinePatcher = false;
$secondaryOnlinePatcher = false;
}
EntryFile::create([
$file = EntryFile::create([
'entry_id' => $entry->id,
'file_uuid' => $uuid,
'filename' => $fileData['filename'],
@@ -319,6 +334,26 @@ class SubmissionsService {
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $entry->type ) ) {
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
if (!$playOnline && $entry->getRealPlatform()?->play_online_core !== null) {
$playOnline = true;
$playOnlineCore = $entry->getRealPlatform()?->play_online_core;
}
if ($playOnline) {
$file->playOnlineSetting()->updateOrCreate(
['file_id' => $file->id],
[
'core' => $playOnlineCore,
'threads' => $playOnlineThreads,
]
);
}
}
}
}
@@ -452,9 +487,10 @@ class SubmissionsService {
private function Step12a_PrepareGalleryImages( Entry $entry ): void
{
foreach ( $this->request->input('gallery', [] ) ?? [] as $imagePath ) {
foreach ( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ) {
$entry->gallery()->create([
'image' => $imagePath,
'order' => $i
]);
}
}
@@ -556,6 +592,10 @@ class SubmissionsService {
if( \Auth::user()->can('moderate', $this->entry) ){
$fields['staff_comment'] = $this->request->input('staff_comment');
$fields['featured'] = $this->request->input('featured') ?? false;
if( $fields['featured'] == true && $this->entry->featured_at === null )
$fields['featured_at'] = now();
if( $fields['featured'] == false )
$fields['featured_at'] = null;
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
}
@@ -666,6 +706,10 @@ class SubmissionsService {
$needDeletion = array_diff( $existingUuids, $requestUuids );
if( !empty( $needDeletion ) ){
$userId = \Auth::user()->user_id;
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->get()->each( function ( $f ) use ( $userId ) {
DeleteFile::dispatch( $f->filepath, $f->filename, $userId);
});
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->delete();
}
@@ -680,15 +724,45 @@ class SubmissionsService {
foreach( $stateMap as $uuid => $state ){
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
if( section_must_be( ['romhacks', 'translations'], $this->entry->type ) ) {
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
} else {
$onlinePatcher = false;
$secondaryOnlinePatcher = false;
}
EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')
->update([
'state' => $state,
'online_patcher' => $onlinePatcher,
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
$entryFile = EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')->first();
if( !$entryFile )
continue;
$entryFile->update([
'state' => $state,
'online_patcher' => $onlinePatcher,
'secondary_online_patcher' => $secondaryOnlinePatcher,
]);
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $this->entry->type ) ) {
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
if ($playOnline) {
if ($playOnlineCore === null || !in_array($playOnlineCore, PlayOnlineHelpers::getCoreLists()))
$playOnlineCore = $this->entry->getRealPlatform()->play_online_core ? $this->entry->getRealPlatform()->play_online_core : 'nes';
$entryFile->playOnlineSetting()->updateOrCreate(
['file_id' => $entryFile->id],
[
'core' => $playOnlineCore,
'threads' => $playOnlineThreads,
]
);
} else {
$entryFile->playOnlineSetting()->delete();
}
}
}
}
@@ -860,6 +934,10 @@ class SubmissionsService {
]);
}
foreach ( $requestGallery as $i => $imagePath ){
$this->entry->gallery()->where('image', $imagePath )->update(['order' => $i]);
}
return [ 'addition' => $images, 'deletion' => $needDeletion ];
}

View File

@@ -92,7 +92,7 @@ class XenforoApiService {
return $response['success'] ?? false;
}
public function createCommentsThread( Entry $entry ): bool
public function createCommentsThread( Entry|News $entry ): bool
{
if( !$entry->comments_thread_id || $entry->comments_thread_id <= 0 ){
$data = [
@@ -134,6 +134,14 @@ class XenforoApiService {
return $response['success'] ?? false;
}
public function featuredRequest( Entry $entry ): bool
{
$response = $this->post("romhackplaza_entry/featured", data: [
'entry_id' => $entry->id, 'user_id' => $entry->user_id, 'entry_title' => $entry->complete_title ?? $entry->title,
]);
return $response['success'] ?? false;
}
public function deleteThreadWithEntry(int $threadId): bool
{
return (bool) $this->delete( "threads/{$threadId}", data: ['reason' => "Deletion with entry." ] );

View File

@@ -195,9 +195,9 @@ class XenforoService {
}
private function hashCSRFToken( string $token ): string
private function hashCSRFToken( string $token, int $timestamp ): string
{
return hash_hmac('md5', $token . time(), config('app.xf_salt') );
return hash_hmac('md5', $token . $timestamp, config('app.xf_salt') );
}
public function getCSRFToken(): string
{
@@ -207,6 +207,28 @@ class XenforoService {
Cookie::queue('xf_csrf', $token, 0, '/', config('session.domain'), 0, false, false );
}
return time() . ',' . $this->hashCSRFToken($token);
$timestamp = time();
return $timestamp . ',' . $this->hashCSRFToken($token, $timestamp);
}
public function verifyCSRFToken( string $requestToken ): bool
{
$token = Cookie::get('xf_csrf');
if( !$token ){
return false;
}
try {
[$timestamp, $hash] = explode(',', $requestToken);
} catch (\Throwable $th) {
return false;
}
$timestamp = intval($timestamp);
$currentTimestamp = time();
if( abs( $currentTimestamp - $timestamp ) > 3600 )
return false;
return $hash === $this->hashCSRFToken($token, $timestamp);
}
}