Compare commits
7 commits
main
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 25cae3c0e9 | |||
| 58848c934e | |||
| 2087ca389e | |||
| f765d04d06 | |||
| cb276cf81d | |||
| e986f7871b | |||
| ed93dc3630 |
182 changed files with 6949 additions and 1282 deletions
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use Domains\Article\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class FetchNewArticlesCommand extends Command
|
class FetchNewArticlesCommand extends Command
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
use Domains\Platform\Jobs\SyncChannelPostsJob;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class SyncChannelPostsCommand extends Command
|
class SyncChannelPostsCommand extends Command
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ChannelException extends Exception
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Resources\ArticleResource;
|
use Domains\Article\Resources\ArticleResource;
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use Domains\Article\Jobs\ArticleDiscoveryJob;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
class AuthController extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Login user and create token
|
|
||||||
*/
|
|
||||||
public function login(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$request->validate([
|
|
||||||
'email' => 'required|email',
|
|
||||||
'password' => 'required',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::where('email', $request->email)->first();
|
|
||||||
|
|
||||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
|
||||||
return $this->sendError('Invalid credentials', [], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $user->createToken('api-token')->plainTextToken;
|
|
||||||
|
|
||||||
return $this->sendResponse([
|
|
||||||
'user' => [
|
|
||||||
'id' => $user->id,
|
|
||||||
'name' => $user->name,
|
|
||||||
'email' => $user->email,
|
|
||||||
],
|
|
||||||
'token' => $token,
|
|
||||||
'token_type' => 'Bearer',
|
|
||||||
], 'Login successful');
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
return $this->sendValidationError($e->errors());
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->sendError('Login failed: ' . $e->getMessage(), [], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new user
|
|
||||||
*/
|
|
||||||
public function register(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'email' => 'required|string|email|max:255|unique:users',
|
|
||||||
'password' => 'required|string|min:8|confirmed',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::create([
|
|
||||||
'name' => $validated['name'],
|
|
||||||
'email' => $validated['email'],
|
|
||||||
'password' => Hash::make($validated['password']),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$token = $user->createToken('api-token')->plainTextToken;
|
|
||||||
|
|
||||||
return $this->sendResponse([
|
|
||||||
'user' => [
|
|
||||||
'id' => $user->id,
|
|
||||||
'name' => $user->name,
|
|
||||||
'email' => $user->email,
|
|
||||||
],
|
|
||||||
'token' => $token,
|
|
||||||
'token_type' => 'Bearer',
|
|
||||||
], 'Registration successful', 201);
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
return $this->sendValidationError($e->errors());
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->sendError('Registration failed: ' . $e->getMessage(), [], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout user (revoke token)
|
|
||||||
*/
|
|
||||||
public function logout(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$request->user()->currentAccessToken()->delete();
|
|
||||||
|
|
||||||
return $this->sendResponse(null, 'Logged out successfully');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $this->sendError('Logout failed: ' . $e->getMessage(), [], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current authenticated user
|
|
||||||
*/
|
|
||||||
public function me(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
return $this->sendResponse([
|
|
||||||
'user' => [
|
|
||||||
'id' => $request->user()->id,
|
|
||||||
'name' => $request->user()->name,
|
|
||||||
'email' => $request->user()->email,
|
|
||||||
],
|
|
||||||
], 'User retrieved successfully');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Services\DashboardStatsService;
|
use Domains\Article\Services\DashboardStatsService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
|
@ -40,7 +40,6 @@ public function stats(Request $request): JsonResponse
|
||||||
'current_period' => $period,
|
'current_period' => $period,
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
throw $e;
|
|
||||||
return $this->sendError('Failed to fetch dashboard stats: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to fetch dashboard stats: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Requests\StoreFeedRequest;
|
use Domains\Feed\Requests\StoreFeedRequest;
|
||||||
use App\Http\Requests\UpdateFeedRequest;
|
use Domains\Feed\Requests\UpdateFeedRequest;
|
||||||
use App\Http\Resources\FeedResource;
|
use Domains\Feed\Resources\FeedResource;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -19,7 +19,7 @@ public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$perPage = min($request->get('per_page', 15), 100);
|
$perPage = min($request->get('per_page', 15), 100);
|
||||||
|
|
||||||
$feeds = Feed::with(['language'])
|
$feeds = Feed::with(['languages'])
|
||||||
->withCount('articles')
|
->withCount('articles')
|
||||||
->orderBy('is_active', 'desc')
|
->orderBy('is_active', 'desc')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
|
|
@ -49,15 +49,35 @@ public function store(StoreFeedRequest $request): JsonResponse
|
||||||
|
|
||||||
// Map provider to URL and set type
|
// Map provider to URL and set type
|
||||||
$providers = [
|
$providers = [
|
||||||
'vrt' => new \App\Services\Parsers\VrtHomepageParserAdapter(),
|
'vrt' => new \Domains\Article\Parsers\Vrt\VrtHomepageParserAdapter(),
|
||||||
'belga' => new \App\Services\Parsers\BelgaHomepageParserAdapter(),
|
'belga' => new \Domains\Article\Parsers\Belga\BelgaHomepageParserAdapter(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$adapter = $providers[$validated['provider']];
|
$adapter = $providers[$validated['provider']];
|
||||||
$validated['url'] = $adapter->getHomepageUrl();
|
$validated['url'] = $adapter->getHomepageUrl();
|
||||||
$validated['type'] = 'website';
|
$validated['type'] = 'website';
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'] ?? [];
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? $languageIds[0] ?? null;
|
||||||
|
$languageUrls = $validated['language_urls'] ?? [];
|
||||||
|
|
||||||
|
// Remove language fields from feed data
|
||||||
|
unset($validated['language_ids'], $validated['primary_language_id'], $validated['language_urls']);
|
||||||
|
|
||||||
$feed = Feed::create($validated);
|
$feed = Feed::create($validated);
|
||||||
|
|
||||||
|
// Attach languages to the feed
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$pivotData = [
|
||||||
|
'url' => $languageUrls[$languageId] ?? $validated['url'],
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $languageId == $primaryLanguageId,
|
||||||
|
];
|
||||||
|
$feed->languages()->attach($languageId, $pivotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed),
|
new FeedResource($feed),
|
||||||
|
|
@ -76,6 +96,8 @@ public function store(StoreFeedRequest $request): JsonResponse
|
||||||
*/
|
*/
|
||||||
public function show(Feed $feed): JsonResponse
|
public function show(Feed $feed): JsonResponse
|
||||||
{
|
{
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed),
|
new FeedResource($feed),
|
||||||
'Feed retrieved successfully.'
|
'Feed retrieved successfully.'
|
||||||
|
|
@ -91,10 +113,34 @@ public function update(UpdateFeedRequest $request, Feed $feed): JsonResponse
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'] ?? null;
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? null;
|
||||||
|
$languageUrls = $validated['language_urls'] ?? [];
|
||||||
|
|
||||||
|
// Remove language fields from feed data
|
||||||
|
unset($validated['language_ids'], $validated['primary_language_id'], $validated['language_urls']);
|
||||||
|
|
||||||
$feed->update($validated);
|
$feed->update($validated);
|
||||||
|
|
||||||
|
// Update languages if provided
|
||||||
|
if ($languageIds !== null) {
|
||||||
|
// Sync languages with the feed
|
||||||
|
$syncData = [];
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$syncData[$languageId] = [
|
||||||
|
'url' => $languageUrls[$languageId] ?? $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $primaryLanguageId ? $languageId == $primaryLanguageId : $index === 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$feed->languages()->sync($syncData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed->fresh()),
|
new FeedResource($feed),
|
||||||
'Feed updated successfully!'
|
'Feed updated successfully!'
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\Keyword;
|
use Domains\Article\Models\Keyword;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
|
||||||
146
backend/app/Http/Controllers/Api/V1/LanguagesController.php
Normal file
146
backend/app/Http/Controllers/Api/V1/LanguagesController.php
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
|
use Domains\Settings\Services\LanguageService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LanguagesController extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LanguageService $languageService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get languages available for route creation
|
||||||
|
* Returns languages that have both active feeds and active channels
|
||||||
|
*/
|
||||||
|
public function availableForRoutes(): JsonResponse
|
||||||
|
{
|
||||||
|
$languages = $this->languageService->getAvailableForRoutes();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$languages,
|
||||||
|
'Available languages for routes retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get feeds filtered by language
|
||||||
|
*/
|
||||||
|
public function feedsByLanguage(Request $request, int $languageId): JsonResponse
|
||||||
|
{
|
||||||
|
$fields = $this->parseFields($request->get('fields'), [
|
||||||
|
'feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$feeds = $this->languageService->getFeedsByLanguage(
|
||||||
|
$languageId,
|
||||||
|
$request->get('search'),
|
||||||
|
(int) $request->get('per_page', 15),
|
||||||
|
$fields
|
||||||
|
);
|
||||||
|
|
||||||
|
$language = Language::select(['name'])->findOrFail($languageId);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$feeds,
|
||||||
|
"Feeds for language '{$language->name}' retrieved successfully."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get channels filtered by language
|
||||||
|
*/
|
||||||
|
public function channelsByLanguage(Request $request, int $languageId): JsonResponse
|
||||||
|
{
|
||||||
|
$fields = $this->parseFields($request->get('fields'), [
|
||||||
|
'id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$channels = $this->languageService->getChannelsByLanguage(
|
||||||
|
$languageId,
|
||||||
|
$request->get('search'),
|
||||||
|
(int) $request->get('per_page', 15),
|
||||||
|
$fields
|
||||||
|
);
|
||||||
|
|
||||||
|
$language = Language::select(['name'])->findOrFail($languageId);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$channels,
|
||||||
|
"Channels for language '{$language->name}' retrieved successfully."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get language statistics
|
||||||
|
*/
|
||||||
|
public function statistics(int $languageId): JsonResponse
|
||||||
|
{
|
||||||
|
$stats = $this->languageService->getLanguageStatistics($languageId);
|
||||||
|
$language = Language::select(['name'])->findOrFail($languageId);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$stats,
|
||||||
|
"Statistics for language '{$language->name}' retrieved successfully."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get usage summary for all languages
|
||||||
|
*/
|
||||||
|
public function usageSummary(): JsonResponse
|
||||||
|
{
|
||||||
|
$summary = $this->languageService->getLanguageUsageSummary();
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$summary,
|
||||||
|
'Language usage summary retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common languages between feed and channel
|
||||||
|
*/
|
||||||
|
public function commonLanguages(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'channel_id' => 'required|exists:platform_channels,id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$commonLanguages = $this->languageService->getCommonLanguages(
|
||||||
|
$request->get('feed_id'),
|
||||||
|
$request->get('channel_id')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$commonLanguages,
|
||||||
|
'Common languages retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse field selection from request
|
||||||
|
*/
|
||||||
|
private function parseFields(?string $fieldsParam, array $defaultFields): array
|
||||||
|
{
|
||||||
|
if (!$fieldsParam) {
|
||||||
|
return $defaultFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestedFields = array_map('trim', explode(',', $fieldsParam));
|
||||||
|
$validFields = [];
|
||||||
|
|
||||||
|
foreach ($requestedFields as $field) {
|
||||||
|
if (in_array($field, $defaultFields) || str_contains($field, '.')) {
|
||||||
|
$validFields[] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($validFields) ? $defaultFields : $validFields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Models\Log;
|
use Domains\Logging\Models\Log;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,22 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Requests\StoreFeedRequest;
|
use Domains\Feed\Requests\StoreFeedRequest;
|
||||||
use App\Http\Resources\FeedResource;
|
use Domains\Feed\Resources\FeedResource;
|
||||||
use App\Http\Resources\PlatformAccountResource;
|
use Domains\Platform\Resources\PlatformAccountResource;
|
||||||
use App\Http\Resources\PlatformChannelResource;
|
use Domains\Platform\Resources\PlatformChannelResource;
|
||||||
use App\Http\Resources\RouteResource;
|
use Domains\Feed\Resources\RouteResource;
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use Domains\Article\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\Language;
|
use Domains\Settings\Models\Language;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use App\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use App\Services\Auth\LemmyAuthService;
|
use Domains\Platform\Services\Auth\Authenticators\LemmyAuthService;
|
||||||
|
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
||||||
|
use Domains\Platform\Exceptions\ChannelException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
@ -89,13 +91,19 @@ public function options(): JsonResponse
|
||||||
|
|
||||||
// Get existing feeds and channels for route creation
|
// Get existing feeds and channels for route creation
|
||||||
$feeds = Feed::where('is_active', true)
|
$feeds = Feed::where('is_active', true)
|
||||||
|
->with('languages')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name', 'url', 'type']);
|
->get(['id', 'name', 'url', 'type']);
|
||||||
|
|
||||||
$platformChannels = PlatformChannel::where('is_active', true)
|
$platformChannels = PlatformChannel::where('is_active', true)
|
||||||
->with(['platformInstance:id,name,url'])
|
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
|
||||||
|
|
||||||
|
// Get languages available for routes (have both feeds and channels)
|
||||||
|
$availableLanguages = Language::availableForRoutes()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'short_code', 'name', 'native_name']);
|
||||||
|
|
||||||
// Get feed providers from config
|
// Get feed providers from config
|
||||||
$feedProviders = collect(config('feed.providers', []))
|
$feedProviders = collect(config('feed.providers', []))
|
||||||
|
|
@ -107,6 +115,7 @@ public function options(): JsonResponse
|
||||||
'platform_instances' => $platformInstances,
|
'platform_instances' => $platformInstances,
|
||||||
'feeds' => $feeds,
|
'feeds' => $feeds,
|
||||||
'platform_channels' => $platformChannels,
|
'platform_channels' => $platformChannels,
|
||||||
|
'available_languages' => $availableLanguages,
|
||||||
'feed_providers' => $feedProviders,
|
'feed_providers' => $feedProviders,
|
||||||
], 'Onboarding options retrieved successfully.');
|
], 'Onboarding options retrieved successfully.');
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +183,7 @@ public function createPlatform(Request $request): JsonResponse
|
||||||
'Platform account created successfully.'
|
'Platform account created successfully.'
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (\App\Exceptions\PlatformAuthException $e) {
|
} catch (\Domains\Platform\Exceptions\PlatformAuthException $e) {
|
||||||
// Check if it's a rate limit error
|
// Check if it's a rate limit error
|
||||||
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
||||||
return $this->sendError($e->getMessage(), [], 429);
|
return $this->sendError($e->getMessage(), [], 429);
|
||||||
|
|
@ -204,7 +213,9 @@ public function createFeed(Request $request): JsonResponse
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'provider' => 'required|in:belga,vrt',
|
'provider' => 'required|in:belga,vrt',
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
'description' => 'nullable|string|max:1000',
|
'description' => 'nullable|string|max:1000',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -224,20 +235,34 @@ public function createFeed(Request $request): JsonResponse
|
||||||
$url = 'https://www.belganewsagency.eu/';
|
$url = 'https://www.belganewsagency.eu/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'];
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? $languageIds[0];
|
||||||
|
|
||||||
$feed = Feed::firstOrCreate(
|
$feed = Feed::firstOrCreate(
|
||||||
['url' => $url],
|
['url' => $url],
|
||||||
[
|
[
|
||||||
'name' => $validated['name'],
|
'name' => $validated['name'],
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'language_id' => $validated['language_id'],
|
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sync languages with the feed
|
||||||
|
$syncData = [];
|
||||||
|
foreach ($languageIds as $languageId) {
|
||||||
|
$syncData[$languageId] = [
|
||||||
|
'url' => $url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $languageId == $primaryLanguageId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$feed->languages()->sync($syncData);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed->load('language')),
|
new FeedResource($feed->load('languages')),
|
||||||
'Feed created successfully.'
|
'Feed created successfully.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +276,6 @@ public function createChannel(Request $request): JsonResponse
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'platform_instance_id' => 'required|exists:platform_instances,id',
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
'language_id' => 'required|exists:languages,id',
|
|
||||||
'description' => 'nullable|string|max:1000',
|
'description' => 'nullable|string|max:1000',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -277,6 +301,22 @@ public function createChannel(Request $request): JsonResponse
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect and validate channel languages
|
||||||
|
$languageDetectionService = app(ChannelLanguageDetectionService::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$languageInfo = $languageDetectionService->detectChannelLanguages(
|
||||||
|
$validated['name'],
|
||||||
|
$validated['platform_instance_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add detected language to validated data
|
||||||
|
$validated['language_id'] = $languageInfo['language_id'];
|
||||||
|
|
||||||
|
} catch (ChannelException $e) {
|
||||||
|
return $this->sendError($e->getMessage(), [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$channel = PlatformChannel::create([
|
$channel = PlatformChannel::create([
|
||||||
'platform_instance_id' => $validated['platform_instance_id'],
|
'platform_instance_id' => $validated['platform_instance_id'],
|
||||||
'channel_id' => $validated['name'], // For Lemmy, this is the community name
|
'channel_id' => $validated['name'], // For Lemmy, this is the community name
|
||||||
|
|
@ -296,9 +336,16 @@ public function createChannel(Request $request): JsonResponse
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$responseMessage = 'Channel created successfully and linked to platform account.';
|
||||||
|
|
||||||
|
// Add information about language detection if fallback was used
|
||||||
|
if (isset($languageInfo['fallback_used']) && $languageInfo['fallback_used']) {
|
||||||
|
$responseMessage .= ' Note: Used default language due to detection issue.';
|
||||||
|
}
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new PlatformChannelResource($channel->load(['platformInstance', 'language', 'platformAccounts'])),
|
new PlatformChannelResource($channel->load(['platformInstance', 'language', 'platformAccounts'])),
|
||||||
'Channel created successfully and linked to platform account.'
|
$responseMessage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,6 +359,7 @@ public function createRoute(Request $request): JsonResponse
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
'platform_channel_id' => 'required|exists:platform_channels,id',
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
'priority' => 'nullable|integer|min:1|max:100',
|
'priority' => 'nullable|integer|min:1|max:100',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -321,9 +369,22 @@ public function createRoute(Request $request): JsonResponse
|
||||||
|
|
||||||
$validated = $validator->validated();
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
// Validate language consistency
|
||||||
|
$feed = Feed::find($validated['feed_id']);
|
||||||
|
$channel = PlatformChannel::find($validated['platform_channel_id']);
|
||||||
|
|
||||||
|
if (!$feed || !$feed->languages()->where('languages.id', $validated['language_id'])->exists()) {
|
||||||
|
return $this->sendError('The selected feed does not support the chosen language.', [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$channel || $channel->language_id !== (int)$validated['language_id']) {
|
||||||
|
return $this->sendError('The selected channel does not support the chosen language.', [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$route = Route::create([
|
$route = Route::create([
|
||||||
'feed_id' => $validated['feed_id'],
|
'feed_id' => $validated['feed_id'],
|
||||||
'platform_channel_id' => $validated['platform_channel_id'],
|
'platform_channel_id' => $validated['platform_channel_id'],
|
||||||
|
'language_id' => $validated['language_id'],
|
||||||
'priority' => $validated['priority'] ?? 50,
|
'priority' => $validated['priority'] ?? 50,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
@ -333,7 +394,7 @@ public function createRoute(Request $request): JsonResponse
|
||||||
ArticleDiscoveryJob::dispatch();
|
ArticleDiscoveryJob::dispatch();
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->load(['feed', 'platformChannel'])),
|
new RouteResource($route->load(['feed', 'platformChannel', 'language'])),
|
||||||
'Route created successfully.'
|
'Route created successfully.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Http\Resources\PlatformAccountResource;
|
use Domains\Platform\Resources\PlatformAccountResource;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use App\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Resources\PlatformChannelResource;
|
use Domains\Platform\Resources\PlatformChannelResource;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
|
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
||||||
|
use Domains\Platform\Api\Lemmy\LemmyApiService;
|
||||||
|
use Domains\Platform\Exceptions\ChannelException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class PlatformChannelsController extends BaseController
|
class PlatformChannelsController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
@ -45,7 +50,7 @@ public function store(Request $request): JsonResponse
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
|
|
||||||
// Get the platform instance to check for active accounts
|
// Get the platform instance to check for active accounts
|
||||||
$platformInstance = \App\Models\PlatformInstance::findOrFail($validated['platform_instance_id']);
|
$platformInstance = \Domains\Platform\Models\PlatformInstance::findOrFail($validated['platform_instance_id']);
|
||||||
|
|
||||||
// Check if there are active platform accounts for this instance
|
// Check if there are active platform accounts for this instance
|
||||||
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
|
|
@ -60,6 +65,22 @@ public function store(Request $request): JsonResponse
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect and validate channel languages
|
||||||
|
$languageDetectionService = app(ChannelLanguageDetectionService::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$languageInfo = $languageDetectionService->detectChannelLanguages(
|
||||||
|
$validated['name'],
|
||||||
|
$validated['platform_instance_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add detected language to validated data
|
||||||
|
$validated['language_id'] = $languageInfo['language_id'];
|
||||||
|
|
||||||
|
} catch (ChannelException $e) {
|
||||||
|
return $this->sendError($e->getMessage(), [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$channel = PlatformChannel::create($validated);
|
$channel = PlatformChannel::create($validated);
|
||||||
|
|
||||||
// Automatically attach the first active account to the channel
|
// Automatically attach the first active account to the channel
|
||||||
|
|
@ -71,9 +92,16 @@ public function store(Request $request): JsonResponse
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$responseMessage = 'Platform channel created successfully and linked to platform account!';
|
||||||
|
|
||||||
|
// Add information about language detection if fallback was used
|
||||||
|
if (isset($languageInfo['fallback_used']) && $languageInfo['fallback_used']) {
|
||||||
|
$responseMessage .= ' Note: Used default language due to detection issue.';
|
||||||
|
}
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new PlatformChannelResource($channel->load(['platformInstance', 'platformAccounts'])),
|
new PlatformChannelResource($channel->load(['platformInstance', 'platformAccounts', 'language'])),
|
||||||
'Platform channel created successfully and linked to platform account!',
|
$responseMessage,
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
|
|
@ -246,4 +274,108 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
|
||||||
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available communities for a platform instance
|
||||||
|
*/
|
||||||
|
public function getCommunities(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
|
'type' => 'sometimes|string|in:Local,All,Subscribed',
|
||||||
|
'sort' => 'sometimes|string|in:Hot,Active,New,TopDay,TopWeek,TopMonth,TopYear,TopAll',
|
||||||
|
'limit' => 'sometimes|integer|min:1|max:100',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'show_nsfw' => 'sometimes|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$platformInstance = PlatformInstance::findOrFail($validated['platform_instance_id']);
|
||||||
|
|
||||||
|
// Check if there are active platform accounts for this instance to get auth token
|
||||||
|
$activeAccount = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$activeAccount) {
|
||||||
|
return $this->sendError(
|
||||||
|
'Cannot fetch communities: No active platform accounts found for this instance. Please create a platform account first.',
|
||||||
|
[],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache key based on instance and parameters
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'communities:%s:%s:%s:%d:%d:%s',
|
||||||
|
$platformInstance->id,
|
||||||
|
$validated['type'] ?? 'Local',
|
||||||
|
$validated['sort'] ?? 'Active',
|
||||||
|
$validated['limit'] ?? 50,
|
||||||
|
$validated['page'] ?? 1,
|
||||||
|
$validated['show_nsfw'] ?? false ? '1' : '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to get communities from cache first (cache for 10 minutes)
|
||||||
|
$communities = Cache::remember($cacheKey, 600, function () use ($platformInstance, $activeAccount, $validated) {
|
||||||
|
$apiService = app(LemmyApiService::class, ['instance' => $platformInstance->url]);
|
||||||
|
|
||||||
|
return $apiService->listCommunities(
|
||||||
|
$activeAccount->settings['api_token'] ?? null,
|
||||||
|
$validated['type'] ?? 'Local',
|
||||||
|
$validated['sort'] ?? 'Active',
|
||||||
|
$validated['limit'] ?? 50,
|
||||||
|
$validated['page'] ?? 1,
|
||||||
|
$validated['show_nsfw'] ?? false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform the response to include only relevant data and add helpful fields
|
||||||
|
$transformedCommunities = collect($communities['communities'] ?? [])->map(function ($item) {
|
||||||
|
$community = $item['community'] ?? [];
|
||||||
|
return [
|
||||||
|
'id' => $community['id'] ?? null,
|
||||||
|
'name' => $community['name'] ?? null,
|
||||||
|
'title' => $community['title'] ?? null,
|
||||||
|
'description' => $community['description'] ?? null,
|
||||||
|
'nsfw' => $community['nsfw'] ?? false,
|
||||||
|
'local' => $community['local'] ?? false,
|
||||||
|
'subscribers' => $item['counts']['subscribers'] ?? 0,
|
||||||
|
'posts' => $item['counts']['posts'] ?? 0,
|
||||||
|
'display_text' => sprintf(
|
||||||
|
'%s (%s subscribers)',
|
||||||
|
$community['title'] ?? $community['name'] ?? 'Unknown',
|
||||||
|
number_format($item['counts']['subscribers'] ?? 0)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'communities' => $transformedCommunities,
|
||||||
|
'total' => $transformedCommunities->count(),
|
||||||
|
'platform_instance' => [
|
||||||
|
'id' => $platformInstance->id,
|
||||||
|
'name' => $platformInstance->name,
|
||||||
|
'url' => $platformInstance->url,
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'type' => $validated['type'] ?? 'Local',
|
||||||
|
'sort' => $validated['sort'] ?? 'Active',
|
||||||
|
'limit' => $validated['limit'] ?? 50,
|
||||||
|
'page' => $validated['page'] ?? 1,
|
||||||
|
'show_nsfw' => $validated['show_nsfw'] ?? false,
|
||||||
|
]
|
||||||
|
], 'Communities retrieved successfully.');
|
||||||
|
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Clear cache on error to prevent serving stale data
|
||||||
|
if (isset($cacheKey)) {
|
||||||
|
Cache::forget($cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendError('Failed to fetch communities: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,22 +2,28 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Http\Resources\RouteResource;
|
use Domains\Feed\Resources\RouteResource;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
|
use Domains\Feed\Requests\StoreRouteRequest;
|
||||||
|
use Domains\Feed\Requests\UpdateRouteRequest;
|
||||||
|
use Domains\Settings\Services\LanguageService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class RoutingController extends BaseController
|
class RoutingController extends BaseController
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LanguageService $languageService
|
||||||
|
) {}
|
||||||
/**
|
/**
|
||||||
* Display a listing of routing configurations
|
* Display a listing of routing configurations
|
||||||
*/
|
*/
|
||||||
public function index(): JsonResponse
|
public function index(): JsonResponse
|
||||||
{
|
{
|
||||||
$routes = Route::with(['feed', 'platformChannel', 'keywords'])
|
$routes = Route::withAllRelationships()
|
||||||
->orderBy('is_active', 'desc')
|
->orderBy('is_active', 'desc')
|
||||||
->orderBy('priority', 'asc')
|
->orderBy('priority', 'asc')
|
||||||
->get();
|
->get();
|
||||||
|
|
@ -31,23 +37,27 @@ public function index(): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created routing configuration
|
* Store a newly created routing configuration
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(StoreRouteRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validate([
|
$validated = $request->validated();
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
|
||||||
'platform_channel_id' => 'required|exists:platform_channels,id',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'nullable|integer|min:0',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
$validated['priority'] = $validated['priority'] ?? 0;
|
$validated['priority'] = $validated['priority'] ?? 0;
|
||||||
|
|
||||||
$route = Route::create($validated);
|
$route = Route::create($validated);
|
||||||
|
|
||||||
|
// Load relationships efficiently
|
||||||
|
$route->load([
|
||||||
|
'feed:id,name,url,type,provider,is_active',
|
||||||
|
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
|
||||||
|
'platformChannel.platformInstance:id,name,url',
|
||||||
|
'language:id,short_code,name,native_name,is_active',
|
||||||
|
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
|
||||||
|
]);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->load(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route),
|
||||||
'Routing configuration created successfully!',
|
'Routing configuration created successfully!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
|
|
@ -69,7 +79,13 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
return $this->sendNotFound('Routing configuration not found.');
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$route->load(['feed', 'platformChannel', 'keywords']);
|
$route->load([
|
||||||
|
'feed:id,name,url,type,provider,is_active',
|
||||||
|
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
|
||||||
|
'platformChannel.platformInstance:id,name,url',
|
||||||
|
'language:id,short_code,name,native_name,is_active',
|
||||||
|
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
|
||||||
|
]);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route),
|
new RouteResource($route),
|
||||||
|
|
@ -80,7 +96,7 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Update the specified routing configuration
|
* Update the specified routing configuration
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Feed $feed, PlatformChannel $channel): JsonResponse
|
public function update(UpdateRouteRequest $request, Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$route = $this->findRoute($feed, $channel);
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
@ -89,17 +105,14 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel):
|
||||||
return $this->sendNotFound('Routing configuration not found.');
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validated();
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'nullable|integer|min:0',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Route::where('feed_id', $feed->id)
|
Route::where('feed_id', $feed->id)
|
||||||
->where('platform_channel_id', $channel->id)
|
->where('platform_channel_id', $channel->id)
|
||||||
->update($validated);
|
->update($validated);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
|
||||||
'Routing configuration updated successfully!'
|
'Routing configuration updated successfully!'
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
|
|
@ -154,7 +167,7 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
$status = $newStatus ? 'activated' : 'deactivated';
|
$status = $newStatus ? 'activated' : 'deactivated';
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
|
||||||
"Routing configuration {$status} successfully!"
|
"Routing configuration {$status} successfully!"
|
||||||
);
|
);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -162,6 +175,19 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common languages for feed and channel
|
||||||
|
*/
|
||||||
|
public function commonLanguages(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
|
{
|
||||||
|
$commonLanguages = $this->languageService->getCommonLanguages($feed->id, $channel->id);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$commonLanguages,
|
||||||
|
'Common languages for feed and channel retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a route by feed and channel
|
* Find a route by feed and channel
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreFeedRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'provider' => 'required|in:vrt,belga',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'is_active' => 'boolean'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use App\Models\Feed;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpdateFeedRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'url' => 'required|url|unique:feeds,url,' . ($this->route('feed') instanceof Feed ? (string)$this->route('feed')->id : (string)$this->route('feed')),
|
|
||||||
'type' => 'required|in:website,rss',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'is_active' => 'boolean'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class FeedResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Transform the resource into an array.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'name' => $this->name,
|
|
||||||
'url' => $this->url,
|
|
||||||
'type' => $this->type,
|
|
||||||
'provider' => $this->provider,
|
|
||||||
'language_id' => $this->language_id,
|
|
||||||
'is_active' => $this->is_active,
|
|
||||||
'description' => $this->description,
|
|
||||||
'created_at' => $this->created_at->toISOString(),
|
|
||||||
'updated_at' => $this->updated_at->toISOString(),
|
|
||||||
'articles_count' => $this->when(
|
|
||||||
$request->routeIs('api.feeds.*') && isset($this->articles_count),
|
|
||||||
$this->articles_count
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Database\Factories\LanguageFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class Language extends Model
|
|
||||||
{
|
|
||||||
/** @use HasFactory<LanguageFactory> */
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'short_code',
|
|
||||||
'name',
|
|
||||||
'native_name',
|
|
||||||
'is_active'
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_active' => 'boolean'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsToMany<PlatformInstance, $this>
|
|
||||||
*/
|
|
||||||
public function platformInstances(): BelongsToMany
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(PlatformInstance::class)
|
|
||||||
->withPivot(['platform_language_id', 'is_default'])
|
|
||||||
->withTimestamps();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return HasMany<PlatformChannel, $this>
|
|
||||||
*/
|
|
||||||
public function platformChannels(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(PlatformChannel::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return HasMany<Feed, $this>
|
|
||||||
*/
|
|
||||||
public function feeds(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(Feed::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Database\Factories\RouteFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property int $feed_id
|
|
||||||
* @property int $platform_channel_id
|
|
||||||
* @property bool $is_active
|
|
||||||
* @property int $priority
|
|
||||||
* @property Carbon $created_at
|
|
||||||
* @property Carbon $updated_at
|
|
||||||
*/
|
|
||||||
class Route extends Model
|
|
||||||
{
|
|
||||||
/** @use HasFactory<RouteFactory> */
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'routes';
|
|
||||||
|
|
||||||
// Laravel doesn't handle composite primary keys well, so we'll use regular queries
|
|
||||||
protected $primaryKey = null;
|
|
||||||
public $incrementing = false;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'feed_id',
|
|
||||||
'platform_channel_id',
|
|
||||||
'is_active',
|
|
||||||
'priority'
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_active' => 'boolean'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsTo<Feed, $this>
|
|
||||||
*/
|
|
||||||
public function feed(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Feed::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsTo<PlatformChannel, $this>
|
|
||||||
*/
|
|
||||||
public function platformChannel(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(PlatformChannel::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return HasMany<Keyword, $this>
|
|
||||||
*/
|
|
||||||
public function keywords(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
|
|
||||||
->where('platform_channel_id', $this->platform_channel_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use Domains\Logging\Enums\LogLevelEnum;
|
||||||
use App\Events\ExceptionOccurred;
|
use Domains\Logging\Events\ExceptionOccurred;
|
||||||
use App\Listeners\LogExceptionToDatabase;
|
use Domains\Logging\Listeners\LogExceptionToDatabase;
|
||||||
use Error;
|
use Error;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
@ -26,8 +26,8 @@ public function boot(): void
|
||||||
);
|
);
|
||||||
|
|
||||||
Event::listen(
|
Event::listen(
|
||||||
\App\Events\NewArticleFetched::class,
|
\Domains\Article\Events\NewArticleFetched::class,
|
||||||
\App\Listeners\ValidateArticleListener::class,
|
\Domains\Article\Listeners\ValidateArticleListener::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,13 @@
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
$exceptions->reportable(function (Throwable $e) {
|
$exceptions->reportable(function (Throwable $e) {
|
||||||
$level = match (true) {
|
$level = match (true) {
|
||||||
$e instanceof Error => \App\Enums\LogLevelEnum::CRITICAL,
|
$e instanceof Error => \Domains\Logging\Enums\LogLevelEnum::CRITICAL,
|
||||||
$e instanceof RuntimeException => \App\Enums\LogLevelEnum::ERROR,
|
$e instanceof RuntimeException => \Domains\Logging\Enums\LogLevelEnum::ERROR,
|
||||||
$e instanceof InvalidArgumentException => \App\Enums\LogLevelEnum::WARNING,
|
$e instanceof InvalidArgumentException => \Domains\Logging\Enums\LogLevelEnum::WARNING,
|
||||||
default => \App\Enums\LogLevelEnum::ERROR,
|
default => \Domains\Logging\Enums\LogLevelEnum::ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
App\Events\ExceptionOccurred::dispatch(
|
Domains\Logging\Events\ExceptionOccurred::dispatch(
|
||||||
$e,
|
$e,
|
||||||
$level,
|
$level,
|
||||||
$e->getMessage(),
|
$e->getMessage(),
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Database\\Factories\\": "database/factories/",
|
"Domains\\": "src/Domains/",
|
||||||
"Database\\Seeders\\": "database/seeders/"
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
'model' => env('AUTH_MODEL', Domains\User\Models\User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,20 @@
|
||||||
'description' => 'Belgian public broadcaster news',
|
'description' => 'Belgian public broadcaster news',
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'supported_languages' => [
|
||||||
|
'en' => [
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
|
'name' => 'English',
|
||||||
|
],
|
||||||
|
'nl' => [
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/nl/',
|
||||||
|
'name' => 'Dutch',
|
||||||
|
],
|
||||||
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \App\Services\Parsers\VrtHomepageParserAdapter::class,
|
'homepage' => \Domains\Article\Parsers\Vrt\VrtHomepageParserAdapter::class,
|
||||||
'article' => \App\Services\Parsers\VrtArticleParser::class,
|
'article' => \Domains\Article\Parsers\Vrt\VrtArticleParser::class,
|
||||||
'article_page' => \App\Services\Parsers\VrtArticlePageParser::class,
|
'article_page' => \Domains\Article\Parsers\Vrt\VrtArticlePageParser::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'belga' => [
|
'belga' => [
|
||||||
|
|
@ -31,10 +41,16 @@
|
||||||
'description' => 'Belgian national news agency',
|
'description' => 'Belgian national news agency',
|
||||||
'type' => 'rss',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'supported_languages' => [
|
||||||
|
'en' => [
|
||||||
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
|
'name' => 'English',
|
||||||
|
],
|
||||||
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class,
|
'homepage' => \Domains\Article\Parsers\Belga\BelgaHomepageParserAdapter::class,
|
||||||
'article' => \App\Services\Parsers\BelgaArticleParser::class,
|
'article' => \Domains\Article\Parsers\Belga\BelgaArticleParser::class,
|
||||||
'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class,
|
'article_page' => \Domains\Article\Parsers\Belga\BelgaArticlePageParser::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Create feed_languages pivot table for many-to-many relationship
|
||||||
|
Schema::create('feed_languages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('feed_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('language_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('url')->nullable(); // Optional: specific URL for this feed-language combination
|
||||||
|
$table->json('settings')->nullable(); // Optional: language-specific settings
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->boolean('is_primary')->default(false); // To indicate primary language for the feed
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Ensure unique combination of feed and language
|
||||||
|
$table->unique(['feed_id', 'language_id']);
|
||||||
|
|
||||||
|
// Index for performance
|
||||||
|
$table->index(['feed_id', 'is_active']);
|
||||||
|
$table->index(['language_id', 'is_active']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate existing data from feeds.language_id to feed_languages
|
||||||
|
// This will preserve existing language associations
|
||||||
|
DB::table('feeds')
|
||||||
|
->whereNotNull('language_id')
|
||||||
|
->orderBy('id')
|
||||||
|
->chunk(100, function ($feeds) {
|
||||||
|
foreach ($feeds as $feed) {
|
||||||
|
DB::table('feed_languages')->insert([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'language_id' => $feed->language_id,
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true, // Mark existing languages as primary
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('feed_languages');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('feeds', function (Blueprint $table) {
|
||||||
|
// Drop foreign key constraint first
|
||||||
|
$table->dropForeign(['language_id']);
|
||||||
|
// Then drop the column
|
||||||
|
$table->dropColumn('language_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('feeds', function (Blueprint $table) {
|
||||||
|
// Re-add the language_id column
|
||||||
|
$table->foreignId('language_id')->nullable()->after('provider');
|
||||||
|
$table->foreign('language_id')->references('id')->on('languages');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->foreignId('language_id')->nullable()->after('platform_channel_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->index(['language_id', 'is_active']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate existing routes to set language based on feed's primary language and channel language (where they match)
|
||||||
|
DB::statement("
|
||||||
|
UPDATE routes r
|
||||||
|
INNER JOIN platform_channels pc ON r.platform_channel_id = pc.id
|
||||||
|
INNER JOIN feed_languages fl ON r.feed_id = fl.feed_id AND fl.is_primary = 1 AND fl.is_active = 1
|
||||||
|
SET r.language_id = pc.language_id
|
||||||
|
WHERE pc.language_id = fl.language_id
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['language_id']);
|
||||||
|
$table->dropIndex(['language_id', 'is_active']);
|
||||||
|
$table->dropColumn('language_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Add composite index on feed_languages for language filtering queries
|
||||||
|
Schema::table('feed_languages', function (Blueprint $table) {
|
||||||
|
$table->index(['language_id', 'is_active', 'is_primary'], 'idx_feed_languages_filtering');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add index on platform_channels for language-based channel lookups
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->index(['language_id', 'is_active'], 'idx_platform_channels_language_active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add composite index on routes for efficient language-based queries
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->index(['language_id', 'is_active', 'priority'], 'idx_routes_language_active_priority');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add index for language consistency validation queries
|
||||||
|
Schema::table('feed_languages', function (Blueprint $table) {
|
||||||
|
$table->index(['feed_id', 'language_id', 'is_active'], 'idx_feed_languages_validation');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('feed_languages', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_feed_languages_filtering');
|
||||||
|
$table->dropIndex('idx_feed_languages_validation');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_platform_channels_language_active');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_routes_language_active_priority');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class PlatformInstanceSeeder extends Seeder
|
class PlatformInstanceSeeder extends Seeder
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<source>
|
<source>
|
||||||
<include>
|
<include>
|
||||||
<directory>app</directory>
|
<directory>app</directory>
|
||||||
|
<directory>src</directory>
|
||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Api\V1\ArticlesController;
|
use App\Http\Controllers\Api\V1\ArticlesController;
|
||||||
use App\Http\Controllers\Api\V1\AuthController;
|
|
||||||
use App\Http\Controllers\Api\V1\DashboardController;
|
use App\Http\Controllers\Api\V1\DashboardController;
|
||||||
use App\Http\Controllers\Api\V1\FeedsController;
|
use App\Http\Controllers\Api\V1\FeedsController;
|
||||||
|
use App\Http\Controllers\Api\V1\LanguagesController;
|
||||||
use App\Http\Controllers\Api\V1\LogsController;
|
use App\Http\Controllers\Api\V1\LogsController;
|
||||||
use App\Http\Controllers\Api\V1\OnboardingController;
|
use App\Http\Controllers\Api\V1\OnboardingController;
|
||||||
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
||||||
|
|
@ -25,16 +25,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Route::prefix('v1')->group(function () {
|
Route::prefix('v1')->group(function () {
|
||||||
// Public authentication routes
|
|
||||||
Route::post('/auth/login', [AuthController::class, 'login'])->name('api.auth.login');
|
|
||||||
Route::post('/auth/register', [AuthController::class, 'register'])->name('api.auth.register');
|
|
||||||
|
|
||||||
// Protected authentication routes
|
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
|
||||||
Route::post('/auth/logout', [AuthController::class, 'logout'])->name('api.auth.logout');
|
|
||||||
Route::get('/auth/me', [AuthController::class, 'me'])->name('api.auth.me');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Onboarding
|
// Onboarding
|
||||||
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
|
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
|
||||||
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');
|
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');
|
||||||
|
|
@ -67,6 +57,10 @@
|
||||||
->name('api.platform-accounts.set-active');
|
->name('api.platform-accounts.set-active');
|
||||||
|
|
||||||
// Platform Channels
|
// Platform Channels
|
||||||
|
// NOTE: Specific routes must come before resource routes to avoid parameter conflicts
|
||||||
|
Route::get('/platform-channels/communities', [PlatformChannelsController::class, 'getCommunities'])
|
||||||
|
->name('api.platform-channels.get-communities');
|
||||||
|
|
||||||
Route::apiResource('platform-channels', PlatformChannelsController::class)->names([
|
Route::apiResource('platform-channels', PlatformChannelsController::class)->names([
|
||||||
'index' => 'api.platform-channels.index',
|
'index' => 'api.platform-channels.index',
|
||||||
'store' => 'api.platform-channels.store',
|
'store' => 'api.platform-channels.store',
|
||||||
|
|
@ -112,6 +106,11 @@
|
||||||
Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index');
|
Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index');
|
||||||
Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update');
|
Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update');
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
Route::get('/languages/available-for-routes', [LanguagesController::class, 'availableForRoutes'])->name('api.languages.available-for-routes');
|
||||||
|
Route::get('/languages/{language}/feeds', [LanguagesController::class, 'feedsByLanguage'])->name('api.languages.feeds');
|
||||||
|
Route::get('/languages/{language}/channels', [LanguagesController::class, 'channelsByLanguage'])->name('api.languages.channels');
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index');
|
Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Jobs\ArticleDiscoveryJob;
|
use Domains\Article\Jobs\ArticleDiscoveryJob;
|
||||||
use App\Jobs\PublishNextArticleJob;
|
use Domains\Platform\Jobs\PublishNextArticleJob;
|
||||||
use App\Jobs\SyncChannelPostsJob;
|
use Domains\Platform\Jobs\SyncChannelPostsJob;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Schedule::call(function () {
|
Schedule::call(function () {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Contracts;
|
namespace Domains\Article\Contracts;
|
||||||
|
|
||||||
interface ArticleParserInterface
|
interface ArticleParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Contracts;
|
namespace Domains\Article\Contracts;
|
||||||
|
|
||||||
interface HomepageParserInterface
|
interface HomepageParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Events;
|
namespace Domains\Article\Events;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Events;
|
namespace Domains\Article\Events;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Article\Factories;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Domains\Article\Models\Article>
|
||||||
*/
|
*/
|
||||||
class ArticleFactory extends Factory
|
class ArticleFactory extends Factory
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The name of the factory's corresponding model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $model = \Domains\Article\Models\Article::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
*
|
*
|
||||||
|
|
@ -17,7 +24,7 @@ class ArticleFactory extends Factory
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'feed_id' => \App\Models\Feed::factory(),
|
'feed_id' => \Domains\Feed\Models\Feed::factory(),
|
||||||
'url' => $this->faker->url(),
|
'url' => $this->faker->url(),
|
||||||
'title' => $this->faker->sentence(),
|
'title' => $this->faker->sentence(),
|
||||||
'description' => $this->faker->paragraph(),
|
'description' => $this->faker->paragraph(),
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Article\Factories;
|
||||||
|
|
||||||
use App\Models\ArticlePublication;
|
use Domains\Article\Models\ArticlePublication;
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class ArticlePublicationFactory extends Factory
|
class ArticlePublicationFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = ArticlePublication::class;
|
protected $model = \Domains\Article\Models\ArticlePublication::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Article\Factories;
|
||||||
|
|
||||||
use App\Models\Keyword;
|
use Domains\Article\Models\Keyword;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class KeywordFactory extends Factory
|
class KeywordFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = Keyword::class;
|
protected $model = \Domains\Article\Models\Keyword::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'feed_id' => \App\Models\Feed::factory(),
|
'feed_id' => \Domains\Feed\Models\Feed::factory(),
|
||||||
'platform_channel_id' => \App\Models\PlatformChannel::factory(),
|
'platform_channel_id' => \Domains\Platform\Models\PlatformChannel::factory(),
|
||||||
'keyword' => $this->faker->word(),
|
'keyword' => $this->faker->word(),
|
||||||
'is_active' => $this->faker->boolean(70), // 70% chance of being active
|
'is_active' => $this->faker->boolean(70), // 70% chance of being active
|
||||||
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||||
|
|
@ -21,14 +21,14 @@ public function definition(): array
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function forFeed(\App\Models\Feed $feed): static
|
public function forFeed(\Domains\Feed\Models\Feed $feed): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function forChannel(\App\Models\PlatformChannel $channel): static
|
public function forChannel(\Domains\Platform\Models\PlatformChannel $channel): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'platform_channel_id' => $channel->id,
|
'platform_channel_id' => $channel->id,
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace Domains\Article\Jobs;
|
||||||
|
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use Domains\Article\Services\ArticleFetcher;
|
||||||
use App\Services\Log\LogSaver;
|
use Domains\Logging\Services\LogSaver;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace Domains\Article\Jobs;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use App\Services\Log\LogSaver;
|
use Domains\Logging\Services\LogSaver;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace Domains\Article\Listeners;
|
||||||
|
|
||||||
use App\Events\NewArticleFetched;
|
use Domains\Article\Events\NewArticleFetched;
|
||||||
use App\Events\ArticleApproved;
|
use Domains\Article\Events\ArticleApproved;
|
||||||
use App\Models\Setting;
|
use Domains\Settings\Models\Setting;
|
||||||
use App\Services\Article\ValidationService;
|
use Domains\Article\Services\ValidationService;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
class ValidateArticleListener implements ShouldQueue
|
class ValidateArticleListener implements ShouldQueue
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Article\Models;
|
||||||
|
|
||||||
use App\Events\ArticleApproved;
|
use Domains\Article\Events\ArticleApproved;
|
||||||
use App\Events\NewArticleFetched;
|
use Domains\Article\Events\NewArticleFetched;
|
||||||
use Database\Factories\ArticleFactory;
|
use Domains\Article\Factories\ArticleFactory;
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Settings\Models\Setting;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -12,9 +14,9 @@
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static firstOrCreate(array<string, mixed> $array)
|
* @method static firstOrCreate(array $array)
|
||||||
* @method static where(string $string, string $url)
|
* @method static where(string $string, string $url)
|
||||||
* @method static create(array<string, mixed> $array)
|
* @method static create(array $array)
|
||||||
* @property integer $id
|
* @property integer $id
|
||||||
* @property int $feed_id
|
* @property int $feed_id
|
||||||
* @property Feed $feed
|
* @property Feed $feed
|
||||||
|
|
@ -30,6 +32,11 @@ class Article extends Model
|
||||||
/** @use HasFactory<ArticleFactory> */
|
/** @use HasFactory<ArticleFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return ArticleFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'url',
|
'url',
|
||||||
|
|
@ -101,7 +108,7 @@ public function canBePublished(): bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// If approval system is disabled, auto-approve valid articles
|
// If approval system is disabled, auto-approve valid articles
|
||||||
if (!\App\Models\Setting::isPublishingApprovalsEnabled()) {
|
if (!Setting::isPublishingApprovalsEnabled()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Article\Models;
|
||||||
|
|
||||||
use Database\Factories\ArticlePublicationFactory;
|
use Domains\Article\Factories\ArticlePublicationFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -12,13 +12,18 @@
|
||||||
* @property integer $platform_channel_id
|
* @property integer $platform_channel_id
|
||||||
* @property integer $post_id
|
* @property integer $post_id
|
||||||
*
|
*
|
||||||
* @method static create(array<string, mixed> $array)
|
* @method static create(array $array)
|
||||||
*/
|
*/
|
||||||
class ArticlePublication extends Model
|
class ArticlePublication extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<ArticlePublicationFactory> */
|
/** @use HasFactory<ArticlePublicationFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Article\Factories\ArticlePublicationFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'article_id',
|
'article_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Article\Models;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -22,6 +24,11 @@ class Keyword extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Article\Factories\KeywordFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Belga;
|
||||||
|
|
||||||
class BelgaArticlePageParser
|
class BelgaArticlePageParser
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Belga;
|
||||||
|
|
||||||
use App\Contracts\ArticleParserInterface;
|
use Domains\Article\Contracts\ArticleParserInterface;
|
||||||
|
|
||||||
class BelgaArticleParser implements ArticleParserInterface
|
class BelgaArticleParser implements ArticleParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Belga;
|
||||||
|
|
||||||
class BelgaHomepageParser
|
class BelgaHomepageParser
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Belga;
|
||||||
|
|
||||||
use App\Contracts\HomepageParserInterface;
|
use Domains\Article\Contracts\HomepageParserInterface;
|
||||||
|
|
||||||
class BelgaHomepageParserAdapter implements HomepageParserInterface
|
class BelgaHomepageParserAdapter implements HomepageParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Factories;
|
namespace Domains\Article\Parsers\Factories;
|
||||||
|
|
||||||
use App\Contracts\ArticleParserInterface;
|
use Domains\Article\Contracts\ArticleParserInterface;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Services\Parsers\VrtArticleParser;
|
use Domains\Article\Parsers\Vrt\VrtArticleParser;
|
||||||
use App\Services\Parsers\BelgaArticleParser;
|
use Domains\Article\Parsers\Belga\BelgaArticleParser;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class ArticleParserFactory
|
class ArticleParserFactory
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Factories;
|
namespace Domains\Article\Parsers\Factories;
|
||||||
|
|
||||||
use App\Contracts\HomepageParserInterface;
|
use Domains\Article\Contracts\HomepageParserInterface;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Services\Parsers\VrtHomepageParserAdapter;
|
use Domains\Article\Parsers\Vrt\VrtHomepageParserAdapter;
|
||||||
use App\Services\Parsers\BelgaHomepageParserAdapter;
|
use Domains\Article\Parsers\Belga\BelgaHomepageParserAdapter;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class HomepageParserFactory
|
class HomepageParserFactory
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Vrt;
|
||||||
|
|
||||||
class VrtArticlePageParser
|
class VrtArticlePageParser
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Vrt;
|
||||||
|
|
||||||
use App\Contracts\ArticleParserInterface;
|
use Domains\Article\Contracts\ArticleParserInterface;
|
||||||
|
|
||||||
class VrtArticleParser implements ArticleParserInterface
|
class VrtArticleParser implements ArticleParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Vrt;
|
||||||
|
|
||||||
class VrtHomepageParser
|
class VrtHomepageParser
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
namespace Domains\Article\Parsers\Vrt;
|
||||||
|
|
||||||
use App\Contracts\HomepageParserInterface;
|
use Domains\Article\Contracts\HomepageParserInterface;
|
||||||
|
|
||||||
class VrtHomepageParserAdapter implements HomepageParserInterface
|
class VrtHomepageParserAdapter implements HomepageParserInterface
|
||||||
{
|
{
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace Domains\Article\Resources;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace Domains\Article\Resources;
|
||||||
|
|
||||||
|
use Domains\Feed\Resources\FeedResource;
|
||||||
|
use Domains\Article\Resources\ArticlePublicationResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Article;
|
namespace Domains\Article\Services;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Services\Http\HttpFetcher;
|
use Domains\Shared\Services\HttpFetcher;
|
||||||
use App\Services\Factories\ArticleParserFactory;
|
use Domains\Article\Parsers\Factories\ArticleParserFactory;
|
||||||
use App\Services\Factories\HomepageParserFactory;
|
use Domains\Article\Parsers\Factories\HomepageParserFactory;
|
||||||
use App\Services\Log\LogSaver;
|
use Domains\Logging\Services\LogSaver;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace Domains\Article\Services;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Models\ArticlePublication;
|
use Domains\Article\Models\ArticlePublication;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Article;
|
namespace Domains\Article\Services;
|
||||||
|
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
|
|
||||||
class ValidationService
|
class ValidationService
|
||||||
{
|
{
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace Domains\Feed\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace Domains\Feed\Exceptions;
|
||||||
|
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
|
||||||
class RoutingMismatchException extends RoutingException
|
class RoutingMismatchException extends RoutingException
|
||||||
{
|
{
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Feed\Factories;
|
||||||
|
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\Language;
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
*/
|
*/
|
||||||
class FeedFactory extends Factory
|
class FeedFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = Feed::class;
|
protected $model = \Domains\Feed\Models\Feed::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +20,6 @@ public function definition(): array
|
||||||
'url' => $this->faker->url(),
|
'url' => $this->faker->url(),
|
||||||
'type' => $this->faker->randomElement(['website', 'rss']),
|
'type' => $this->faker->randomElement(['website', 'rss']),
|
||||||
'provider' => $this->faker->randomElement(['vrt', 'belga']),
|
'provider' => $this->faker->randomElement(['vrt', 'belga']),
|
||||||
'language_id' => null,
|
|
||||||
'description' => $this->faker->optional()->sentence(),
|
'description' => $this->faker->optional()->sentence(),
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
|
@ -56,11 +55,30 @@ public function recentlyFetched(): static
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function language(Language $language): static
|
/**
|
||||||
|
* Attach languages to the feed after creation
|
||||||
|
*/
|
||||||
|
public function withLanguages(array $languageIds = [], bool $usePrimary = true): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->afterCreating(function (Feed $feed) use ($languageIds, $usePrimary) {
|
||||||
'language_id' => $language->id,
|
if (empty($languageIds)) {
|
||||||
]);
|
// Default to English if no languages specified
|
||||||
|
$language = Language::where('short_code', 'en')->first();
|
||||||
|
if ($language) {
|
||||||
|
$languageIds = [$language->id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$feed->languages()->attach($languageId, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $usePrimary && $index === 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function vrt(): static
|
public function vrt(): static
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Feed\Factories;
|
||||||
|
|
||||||
use App\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class RouteFactory extends Factory
|
class RouteFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = Route::class;
|
protected $model = \Domains\Feed\Models\Route::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Feed\Models;
|
||||||
|
|
||||||
use Database\Factories\FeedFactory;
|
use Domains\Article\Models\Article;
|
||||||
|
use Domains\Feed\Factories\FeedFactory;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -16,8 +19,6 @@
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $provider
|
* @property string $provider
|
||||||
* @property int $language_id
|
|
||||||
* @property Language|null $language
|
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property array<string, mixed> $settings
|
* @property array<string, mixed> $settings
|
||||||
* @property bool $is_active
|
* @property bool $is_active
|
||||||
|
|
@ -32,6 +33,11 @@ class Feed extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<FeedFactory> */
|
/** @use HasFactory<FeedFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Feed\Factories\FeedFactory::new();
|
||||||
|
}
|
||||||
private const RECENT_FETCH_THRESHOLD_HOURS = 2;
|
private const RECENT_FETCH_THRESHOLD_HOURS = 2;
|
||||||
private const DAILY_FETCH_THRESHOLD_HOURS = 24;
|
private const DAILY_FETCH_THRESHOLD_HOURS = 24;
|
||||||
|
|
||||||
|
|
@ -40,7 +46,6 @@ class Feed extends Model
|
||||||
'url',
|
'url',
|
||||||
'type',
|
'type',
|
||||||
'provider',
|
'provider',
|
||||||
'language_id',
|
|
||||||
'description',
|
'description',
|
||||||
'settings',
|
'settings',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
|
@ -112,10 +117,35 @@ public function articles(): HasMany
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Language, $this>
|
* @return BelongsToMany<Language, $this>
|
||||||
*/
|
*/
|
||||||
public function language(): BelongsTo
|
public function languages(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Language::class);
|
return $this->belongsToMany(Language::class, 'feed_languages')
|
||||||
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
|
->withTimestamps()
|
||||||
|
->wherePivot('is_active', true)
|
||||||
|
->orderByPivot('is_primary', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary language for this feed
|
||||||
|
* @return Language|null
|
||||||
|
*/
|
||||||
|
public function getPrimaryLanguageAttribute(): ?Language
|
||||||
|
{
|
||||||
|
return $this->languages()->wherePivot('is_primary', true)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all languages including inactive ones
|
||||||
|
* @return BelongsToMany<Language, $this>
|
||||||
|
*/
|
||||||
|
public function allLanguages(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Language::class, 'feed_languages')
|
||||||
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
|
->withTimestamps()
|
||||||
|
->orderByPivot('is_primary', 'desc');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
171
backend/src/Domains/Feed/Models/Route.php
Normal file
171
backend/src/Domains/Feed/Models/Route.php
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Models;
|
||||||
|
|
||||||
|
use Domains\Article\Models\Keyword;
|
||||||
|
use Domains\Feed\Factories\RouteFactory;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $feed_id
|
||||||
|
* @property int $platform_channel_id
|
||||||
|
* @property int|null $language_id
|
||||||
|
* @property bool $is_active
|
||||||
|
* @property int $priority
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
* @property Language|null $language
|
||||||
|
*/
|
||||||
|
class Route extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<RouteFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Feed\Factories\RouteFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected $table = 'routes';
|
||||||
|
|
||||||
|
// Laravel doesn't handle composite primary keys well, so we'll use regular queries
|
||||||
|
protected $primaryKey = null;
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'feed_id',
|
||||||
|
'platform_channel_id',
|
||||||
|
'language_id',
|
||||||
|
'is_active',
|
||||||
|
'priority'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Feed, $this>
|
||||||
|
*/
|
||||||
|
public function feed(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Feed::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<PlatformChannel, $this>
|
||||||
|
*/
|
||||||
|
public function platformChannel(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PlatformChannel::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Language, $this>
|
||||||
|
*/
|
||||||
|
public function language(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Language::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<Keyword, $this>
|
||||||
|
*/
|
||||||
|
public function keywords(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
|
||||||
|
->where('platform_channel_id', $this->platform_channel_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the feed has the given language
|
||||||
|
*/
|
||||||
|
public function feedHasLanguage(int $languageId): bool
|
||||||
|
{
|
||||||
|
if (!$this->feed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->feed->languages()->where('languages.id', $languageId)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the platform channel has the given language
|
||||||
|
*/
|
||||||
|
public function channelHasLanguage(int $languageId): bool
|
||||||
|
{
|
||||||
|
return $this->platformChannel && $this->platformChannel->language_id === $languageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the route's language is consistent with feed and channel
|
||||||
|
*/
|
||||||
|
public function hasConsistentLanguage(): bool
|
||||||
|
{
|
||||||
|
if (!$this->language_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->feedHasLanguage($this->language_id) && $this->channelHasLanguage($this->language_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common languages between feed and channel
|
||||||
|
*/
|
||||||
|
public function getCommonLanguages(): array
|
||||||
|
{
|
||||||
|
if (!$this->feed || !$this->platformChannel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedLanguageIds = $this->feed->languages()->pluck('languages.id')->toArray();
|
||||||
|
$channelLanguageId = $this->platformChannel->language_id;
|
||||||
|
|
||||||
|
return in_array($channelLanguageId, $feedLanguageIds) ? [$channelLanguageId] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for eager loading all route relationships
|
||||||
|
*/
|
||||||
|
public function scopeWithAllRelationships(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->with([
|
||||||
|
'feed:id,name,url,type,provider,is_active',
|
||||||
|
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
|
||||||
|
'platformChannel.platformInstance:id,name,url',
|
||||||
|
'language:id,short_code,name,native_name,is_active',
|
||||||
|
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for minimal route data
|
||||||
|
*/
|
||||||
|
public function scopeMinimal(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->select(['feed_id', 'platform_channel_id', 'language_id', 'is_active', 'priority']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for active routes only
|
||||||
|
*/
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for routes by language
|
||||||
|
*/
|
||||||
|
public function scopeByLanguage(Builder $query, int $languageId): Builder
|
||||||
|
{
|
||||||
|
return $query->where('language_id', $languageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/src/Domains/Feed/Requests/StoreFeedRequest.php
Normal file
44
backend/src/Domains/Feed/Requests/StoreFeedRequest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreFeedRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'provider' => 'required|in:vrt,belga',
|
||||||
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
|
'language_urls' => 'nullable|array',
|
||||||
|
'language_urls.*' => 'nullable|url',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_ids.required' => 'At least one language must be selected.',
|
||||||
|
'language_ids.*.exists' => 'The selected language is invalid.',
|
||||||
|
'primary_language_id.exists' => 'The selected primary language is invalid.',
|
||||||
|
'language_urls.*.url' => 'Each language URL must be a valid URL.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
86
backend/src/Domains/Feed/Requests/StoreRouteRequest.php
Normal file
86
backend/src/Domains/Feed/Requests/StoreRouteRequest.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Feed\Services\LanguageConsistencyValidator;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class StoreRouteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the validator instance.
|
||||||
|
*/
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator) {
|
||||||
|
$this->validateLanguageConsistency($validator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the language is consistent between feed and channel
|
||||||
|
*/
|
||||||
|
protected function validateLanguageConsistency(Validator $validator): void
|
||||||
|
{
|
||||||
|
$feedId = $this->input('feed_id');
|
||||||
|
$channelId = $this->input('platform_channel_id');
|
||||||
|
$languageId = $this->input('language_id');
|
||||||
|
|
||||||
|
if (!$feedId || !$channelId || !$languageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$consistencyValidator = app(LanguageConsistencyValidator::class);
|
||||||
|
$result = $consistencyValidator->validateConsistency($feedId, $channelId, $languageId);
|
||||||
|
|
||||||
|
if (!$result['is_valid']) {
|
||||||
|
$message = $consistencyValidator->getValidationMessage($result);
|
||||||
|
$validator->errors()->add('language_id', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'feed_id.required' => 'A feed must be selected.',
|
||||||
|
'feed_id.exists' => 'The selected feed is invalid.',
|
||||||
|
'platform_channel_id.required' => 'A platform channel must be selected.',
|
||||||
|
'platform_channel_id.exists' => 'The selected platform channel is invalid.',
|
||||||
|
'language_id.required' => 'A language must be selected.',
|
||||||
|
'language_id.exists' => 'The selected language is invalid.',
|
||||||
|
'priority.min' => 'Priority must be at least 0.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/src/Domains/Feed/Requests/UpdateFeedRequest.php
Normal file
46
backend/src/Domains/Feed/Requests/UpdateFeedRequest.php
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateFeedRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'url' => 'required|url|unique:feeds,url,' . ($this->route('feed') instanceof Feed ? (string)$this->route('feed')->id : (string)$this->route('feed')),
|
||||||
|
'type' => 'required|in:website,rss',
|
||||||
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
|
'language_urls' => 'nullable|array',
|
||||||
|
'language_urls.*' => 'nullable|url',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'is_active' => 'boolean'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_ids.required' => 'At least one language must be selected.',
|
||||||
|
'language_ids.*.exists' => 'The selected language is invalid.',
|
||||||
|
'primary_language_id.exists' => 'The selected primary language is invalid.',
|
||||||
|
'language_urls.*.url' => 'Each language URL must be a valid URL.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
86
backend/src/Domains/Feed/Requests/UpdateRouteRequest.php
Normal file
86
backend/src/Domains/Feed/Requests/UpdateRouteRequest.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Feed\Services\LanguageConsistencyValidator;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class UpdateRouteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_id' => 'nullable|exists:languages,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the validator instance.
|
||||||
|
*/
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator) {
|
||||||
|
$this->validateLanguageConsistency($validator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the language is consistent between feed and channel
|
||||||
|
*/
|
||||||
|
protected function validateLanguageConsistency(Validator $validator): void
|
||||||
|
{
|
||||||
|
$languageId = $this->input('language_id');
|
||||||
|
|
||||||
|
// If no language_id provided, skip validation (allowing null updates)
|
||||||
|
if (!$languageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get feed and channel from route parameters
|
||||||
|
$feed = $this->route('feed');
|
||||||
|
$channel = $this->route('channel');
|
||||||
|
|
||||||
|
if (!$feed || !$channel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$consistencyValidator = app(LanguageConsistencyValidator::class);
|
||||||
|
$result = $consistencyValidator->validateConsistency($feed->id, $channel->id, $languageId);
|
||||||
|
|
||||||
|
if (!$result['is_valid']) {
|
||||||
|
$message = $consistencyValidator->getValidationMessage($result);
|
||||||
|
$validator->errors()->add('language_id', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_id.exists' => 'The selected language is invalid.',
|
||||||
|
'priority.min' => 'Priority must be at least 0.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
56
backend/src/Domains/Feed/Resources/FeedResource.php
Normal file
56
backend/src/Domains/Feed/Resources/FeedResource.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class FeedResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'url' => $this->url,
|
||||||
|
'type' => $this->type,
|
||||||
|
'provider' => $this->provider,
|
||||||
|
'language_ids' => $this->whenLoaded('languages', function () {
|
||||||
|
return $this->languages->pluck('id');
|
||||||
|
}),
|
||||||
|
'languages' => $this->whenLoaded('languages', function () {
|
||||||
|
return $this->languages->map(function ($language) {
|
||||||
|
return [
|
||||||
|
'id' => $language->id,
|
||||||
|
'short_code' => $language->short_code,
|
||||||
|
'name' => $language->name,
|
||||||
|
'url' => $language->pivot->url,
|
||||||
|
'is_active' => $language->pivot->is_active,
|
||||||
|
'is_primary' => $language->pivot->is_primary,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
'primary_language' => $this->whenLoaded('languages', function () {
|
||||||
|
$primary = $this->languages->where('pivot.is_primary', true)->first();
|
||||||
|
return $primary ? [
|
||||||
|
'id' => $primary->id,
|
||||||
|
'short_code' => $primary->short_code,
|
||||||
|
'name' => $primary->name,
|
||||||
|
] : null;
|
||||||
|
}),
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
'description' => $this->description,
|
||||||
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
|
'articles_count' => $this->when(
|
||||||
|
$request->routeIs('api.feeds.*') && isset($this->articles_count),
|
||||||
|
$this->articles_count
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace Domains\Feed\Resources;
|
||||||
|
|
||||||
|
use Domains\Feed\Resources\FeedResource;
|
||||||
|
use Domains\Platform\Resources\PlatformChannelResource;
|
||||||
|
use Domains\Settings\Resources\LanguageResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
|
@ -18,12 +21,14 @@ public function toArray(Request $request): array
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'feed_id' => $this->feed_id,
|
'feed_id' => $this->feed_id,
|
||||||
'platform_channel_id' => $this->platform_channel_id,
|
'platform_channel_id' => $this->platform_channel_id,
|
||||||
|
'language_id' => $this->language_id,
|
||||||
'is_active' => $this->is_active,
|
'is_active' => $this->is_active,
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
'created_at' => $this->created_at->toISOString(),
|
'created_at' => $this->created_at->toISOString(),
|
||||||
'updated_at' => $this->updated_at->toISOString(),
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
'feed' => new FeedResource($this->whenLoaded('feed')),
|
'feed' => new FeedResource($this->whenLoaded('feed')),
|
||||||
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
|
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
|
||||||
|
'language' => new LanguageResource($this->whenLoaded('language')),
|
||||||
'keywords' => $this->whenLoaded('keywords', function () {
|
'keywords' => $this->whenLoaded('keywords', function () {
|
||||||
return $this->keywords->map(function ($keyword) {
|
return $this->keywords->map(function ($keyword) {
|
||||||
return [
|
return [
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Services;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class LanguageConsistencyValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate that a language is consistent between feed and channel in a single optimized query
|
||||||
|
*/
|
||||||
|
public function validateConsistency(int $feedId, int $channelId, int $languageId): array
|
||||||
|
{
|
||||||
|
// Single query to check both feed and channel language consistency
|
||||||
|
$result = DB::selectOne("
|
||||||
|
SELECT
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM feed_languages fl
|
||||||
|
JOIN feeds f ON f.id = fl.feed_id
|
||||||
|
WHERE fl.feed_id = ?
|
||||||
|
AND fl.language_id = ?
|
||||||
|
AND fl.is_active = 1
|
||||||
|
AND f.is_active = 1
|
||||||
|
) as feed_has_language,
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM platform_channels pc
|
||||||
|
WHERE pc.id = ?
|
||||||
|
AND pc.language_id = ?
|
||||||
|
AND pc.is_active = 1
|
||||||
|
) as channel_has_language
|
||||||
|
", [$feedId, $languageId, $channelId, $languageId]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_valid' => $result->feed_has_language && $result->channel_has_language,
|
||||||
|
'feed_supports_language' => (bool) $result->feed_has_language,
|
||||||
|
'channel_supports_language' => (bool) $result->channel_has_language,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation error message based on the validation result
|
||||||
|
*/
|
||||||
|
public function getValidationMessage(array $validationResult): ?string
|
||||||
|
{
|
||||||
|
if ($validationResult['is_valid']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$validationResult['feed_supports_language']) {
|
||||||
|
return 'The selected feed does not support this language.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$validationResult['channel_supports_language']) {
|
||||||
|
return 'The selected channel does not support this language.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Language consistency validation failed.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch validate multiple feed+channel+language combinations
|
||||||
|
*/
|
||||||
|
public function batchValidate(array $combinations): array
|
||||||
|
{
|
||||||
|
if (empty($combinations)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$conditions = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
foreach ($combinations as $index => $combo) {
|
||||||
|
$feedId = $combo['feed_id'];
|
||||||
|
$channelId = $combo['platform_channel_id'];
|
||||||
|
$languageId = $combo['language_id'];
|
||||||
|
|
||||||
|
$conditions[] = "(fl.feed_id = ? AND fl.language_id = ? AND pc.id = ? AND pc.language_id = ?)";
|
||||||
|
$params = array_merge($params, [$feedId, $languageId, $channelId, $languageId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
fl.feed_id,
|
||||||
|
pc.id as channel_id,
|
||||||
|
fl.language_id,
|
||||||
|
COUNT(*) as is_valid
|
||||||
|
FROM feed_languages fl
|
||||||
|
JOIN feeds f ON f.id = fl.feed_id AND f.is_active = 1
|
||||||
|
JOIN platform_channels pc ON pc.language_id = fl.language_id AND pc.is_active = 1
|
||||||
|
WHERE fl.is_active = 1
|
||||||
|
AND (" . implode(' OR ', $conditions) . ")
|
||||||
|
GROUP BY fl.feed_id, pc.id, fl.language_id
|
||||||
|
";
|
||||||
|
|
||||||
|
$results = DB::select($sql, $params);
|
||||||
|
|
||||||
|
// Convert results to associative array for easy lookup
|
||||||
|
$validCombinations = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$key = "{$result->feed_id}_{$result->channel_id}_{$result->language_id}";
|
||||||
|
$validCombinations[$key] = $result->is_valid > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map back to original combinations
|
||||||
|
$output = [];
|
||||||
|
foreach ($combinations as $index => $combo) {
|
||||||
|
$key = "{$combo['feed_id']}_{$combo['platform_channel_id']}_{$combo['language_id']}";
|
||||||
|
$output[$index] = [
|
||||||
|
'is_valid' => $validCombinations[$key] ?? false,
|
||||||
|
'feed_id' => $combo['feed_id'],
|
||||||
|
'channel_id' => $combo['platform_channel_id'],
|
||||||
|
'language_id' => $combo['language_id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common languages between a feed and channel efficiently
|
||||||
|
*/
|
||||||
|
public function getCommonLanguages(int $feedId, int $channelId): array
|
||||||
|
{
|
||||||
|
$results = DB::select("
|
||||||
|
SELECT DISTINCT fl.language_id, l.short_code, l.name
|
||||||
|
FROM feed_languages fl
|
||||||
|
JOIN feeds f ON f.id = fl.feed_id AND f.is_active = 1
|
||||||
|
JOIN platform_channels pc ON pc.language_id = fl.language_id AND pc.is_active = 1
|
||||||
|
JOIN languages l ON l.id = fl.language_id AND l.is_active = 1
|
||||||
|
WHERE fl.feed_id = ?
|
||||||
|
AND pc.id = ?
|
||||||
|
AND fl.is_active = 1
|
||||||
|
ORDER BY l.name
|
||||||
|
", [$feedId, $channelId]);
|
||||||
|
|
||||||
|
return array_map(function ($result) {
|
||||||
|
return [
|
||||||
|
'id' => $result->language_id,
|
||||||
|
'short_code' => $result->short_code,
|
||||||
|
'name' => $result->name,
|
||||||
|
];
|
||||||
|
}, $results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace Domains\Feed\Services;
|
||||||
|
|
||||||
use App\Exceptions\RoutingMismatchException;
|
use Domains\Feed\Exceptions\RoutingMismatchException;
|
||||||
use App\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class RoutingValidationService
|
class RoutingValidationService
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Enums;
|
namespace Domains\Logging\Enums;
|
||||||
|
|
||||||
enum LogLevelEnum: string
|
enum LogLevelEnum: string
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Events;
|
namespace Domains\Logging\Events;
|
||||||
|
|
||||||
use App\Models\Log;
|
use Domains\Logging\Models\Log;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Events;
|
namespace Domains\Logging\Events;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use Domains\Logging\Enums\LogLevelEnum;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Facades;
|
namespace Domains\Logging\Facades;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,6 @@ class LogSaver extends Facade
|
||||||
{
|
{
|
||||||
protected static function getFacadeAccessor()
|
protected static function getFacadeAccessor()
|
||||||
{
|
{
|
||||||
return \App\Services\Log\LogSaver::class;
|
return \Domains\Logging\Services\LogSaver::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Logging\Factories;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use Domains\Logging\Enums\LogLevelEnum;
|
||||||
use App\Models\Log;
|
use Domains\Logging\Models\Log;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
*/
|
*/
|
||||||
class LogFactory extends Factory
|
class LogFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = Log::class;
|
protected $model = \Domains\Logging\Models\Log::class;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace Domains\Logging\Listeners;
|
||||||
|
|
||||||
use App\Events\ExceptionLogged;
|
use Domains\Logging\Events\ExceptionLogged;
|
||||||
use App\Events\ExceptionOccurred;
|
use Domains\Logging\Events\ExceptionOccurred;
|
||||||
use App\Models\Log;
|
use Domains\Logging\Models\Log;
|
||||||
class LogExceptionToDatabase
|
class LogExceptionToDatabase
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Logging\Models;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use Domains\Logging\Enums\LogLevelEnum;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
@ -19,6 +19,11 @@ class Log extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Logging\Factories\LogFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $table = 'logs';
|
protected $table = 'logs';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Log;
|
namespace Domains\Logging\Services;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use Domains\Logging\Enums\LogLevelEnum;
|
||||||
use App\Models\Log;
|
use Domains\Logging\Models\Log;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
|
||||||
class LogSaver
|
class LogSaver
|
||||||
{
|
{
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\Lemmy\Services;
|
namespace Domains\Platform\Api\Lemmy;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Models\PlatformChannelPost;
|
use Domains\Platform\Models\PlatformChannelPost;
|
||||||
use App\Modules\Lemmy\LemmyRequest;
|
use Domains\Platform\Api\Lemmy\LemmyRequest;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class LemmyApiService
|
class LemmyApiService
|
||||||
|
|
@ -93,6 +93,47 @@ public function getCommunityId(string $communityName, string $token): int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full community details including language information
|
||||||
|
*
|
||||||
|
* @param string $communityName
|
||||||
|
* @param string $token
|
||||||
|
* @return array<string, mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getCommunityDetails(string $communityName, string $token): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request = new LemmyRequest($this->instance, $token);
|
||||||
|
$response = $request->get('community', ['name' => $communityName]);
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
$statusCode = $response->status();
|
||||||
|
$responseBody = $response->body();
|
||||||
|
|
||||||
|
if ($statusCode === 404) {
|
||||||
|
throw new Exception("Community '{$communityName}' not found on this instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Failed to fetch community details: {$statusCode} - {$responseBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if (!isset($data['community_view']['community'])) {
|
||||||
|
throw new Exception('Invalid community response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data['community_view'];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logger()->error('Community details lookup failed', [
|
||||||
|
'community_name' => $communityName,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function syncChannelPosts(string $token, int $platformChannelId, string $communityName): void
|
public function syncChannelPosts(string $token, int $platformChannelId, string $communityName): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -205,4 +246,70 @@ public function getLanguages(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List communities on the instance with optional filtering
|
||||||
|
*
|
||||||
|
* @param string|null $token
|
||||||
|
* @param string $type Local, All, or Subscribed
|
||||||
|
* @param string $sort Hot, Active, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
|
||||||
|
* @param int $limit Maximum number of communities to return (default: 50)
|
||||||
|
* @param int $page Page number for pagination (default: 1)
|
||||||
|
* @param bool $showNsfw Whether to include NSFW communities (default: false)
|
||||||
|
* @return array<string, mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function listCommunities(
|
||||||
|
?string $token = null,
|
||||||
|
string $type = 'Local',
|
||||||
|
string $sort = 'Active',
|
||||||
|
int $limit = 50,
|
||||||
|
int $page = 1,
|
||||||
|
bool $showNsfw = false
|
||||||
|
): array {
|
||||||
|
try {
|
||||||
|
$request = new LemmyRequest($this->instance, $token);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'type_' => $type,
|
||||||
|
'sort' => $sort,
|
||||||
|
'limit' => $limit,
|
||||||
|
'page' => $page,
|
||||||
|
'show_nsfw' => $showNsfw,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $request->get('community/list', $params);
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
$statusCode = $response->status();
|
||||||
|
$responseBody = $response->body();
|
||||||
|
|
||||||
|
logger()->warning('Failed to fetch communities list', [
|
||||||
|
'status' => $statusCode,
|
||||||
|
'response' => $responseBody,
|
||||||
|
'params' => $params
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new Exception("Failed to fetch communities list: {$statusCode} - {$responseBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if (!isset($data['communities'])) {
|
||||||
|
logger()->warning('Invalid communities list response format', ['response' => $data]);
|
||||||
|
return ['communities' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logger()->error('Exception while fetching communities list', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'type' => $type,
|
||||||
|
'sort' => $sort,
|
||||||
|
'limit' => $limit,
|
||||||
|
'page' => $page
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Modules\Lemmy;
|
namespace Domains\Platform\Api\Lemmy;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Http\Client\Response;
|
use Illuminate\Http\Client\Response;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Enums;
|
namespace Domains\Platform\Enums;
|
||||||
|
|
||||||
enum PlatformEnum: string
|
enum PlatformEnum: string
|
||||||
{
|
{
|
||||||
60
backend/src/Domains/Platform/Exceptions/ChannelException.php
Normal file
60
backend/src/Domains/Platform/Exceptions/ChannelException.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Platform\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ChannelException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Exception thrown when no languages match between channel and system
|
||||||
|
*/
|
||||||
|
public static function noMatchingLanguages(array $channelLanguages, array $systemLanguages): self
|
||||||
|
{
|
||||||
|
$channelLangNames = implode(', ', array_column($channelLanguages, 'name'));
|
||||||
|
$systemLangNames = implode(', ', array_column($systemLanguages, 'name'));
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
"No matching languages found. Channel supports: [{$channelLangNames}]. " .
|
||||||
|
"System supports: [{$systemLangNames}]. Please ensure the channel supports " .
|
||||||
|
"at least one system language."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when channel/community is not found
|
||||||
|
*/
|
||||||
|
public static function channelNotFound(string $channelName, string $instanceUrl): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
"Channel '{$channelName}' not found on instance '{$instanceUrl}'. " .
|
||||||
|
"Please verify the channel name exists on this platform instance."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when platform authentication fails
|
||||||
|
*/
|
||||||
|
public static function authenticationFailed(string $reason = ''): self
|
||||||
|
{
|
||||||
|
$message = 'Failed to authenticate with platform instance';
|
||||||
|
if ($reason) {
|
||||||
|
$message .= ": {$reason}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when platform API is unavailable
|
||||||
|
*/
|
||||||
|
public static function platformUnavailable(string $instanceUrl, string $reason = ''): self
|
||||||
|
{
|
||||||
|
$message = "Platform instance '{$instanceUrl}' is unavailable";
|
||||||
|
if ($reason) {
|
||||||
|
$message .= ": {$reason}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace Domains\Platform\Exceptions;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class PlatformAuthException extends Exception
|
class PlatformAuthException extends Exception
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace Domains\Platform\Exceptions;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Platform\Factories;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
*/
|
*/
|
||||||
class PlatformAccountFactory extends Factory
|
class PlatformAccountFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = PlatformAccount::class;
|
protected $model = \Domains\Platform\Models\PlatformAccount::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Platform\Factories;
|
||||||
|
|
||||||
use App\Models\Language;
|
use Domains\Settings\Models\Language;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
*/
|
*/
|
||||||
class PlatformChannelFactory extends Factory
|
class PlatformChannelFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = PlatformChannel::class;
|
protected $model = \Domains\Platform\Models\PlatformChannel::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Platform\Factories;
|
||||||
|
|
||||||
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
|
use Domains\Platform\Models\PlatformChannelPost;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<PlatformChannelPost>
|
||||||
|
*/
|
||||||
|
class PlatformChannelPostFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = PlatformChannelPost::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'platform' => $this->faker->randomElement(PlatformEnum::cases()),
|
||||||
|
'channel_id' => $this->faker->slug(2),
|
||||||
|
'channel_name' => $this->faker->words(2, true),
|
||||||
|
'post_id' => $this->faker->slug(3),
|
||||||
|
'url' => $this->faker->url(),
|
||||||
|
'title' => $this->faker->sentence(),
|
||||||
|
'posted_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lemmy(): static
|
||||||
|
{
|
||||||
|
return $this->state([
|
||||||
|
'platform' => PlatformEnum::LEMMY,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Factories;
|
namespace Domains\Platform\Factories;
|
||||||
|
|
||||||
use App\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
*/
|
*/
|
||||||
class PlatformInstanceFactory extends Factory
|
class PlatformInstanceFactory extends Factory
|
||||||
{
|
{
|
||||||
protected $model = PlatformInstance::class;
|
protected $model = \Domains\Platform\Models\PlatformInstance::class;
|
||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace Domains\Platform\Jobs;
|
||||||
|
|
||||||
use App\Exceptions\PublishException;
|
use Domains\Platform\Exceptions\PublishException;
|
||||||
use App\Models\Article;
|
use Domains\Article\Models\Article;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use Domains\Article\Services\ArticleFetcher;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use Domains\Platform\Services\Publishing\ArticlePublishingService;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace Domains\Platform\Jobs;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use App\Exceptions\PlatformAuthException;
|
use Domains\Platform\Exceptions\PlatformAuthException;
|
||||||
use App\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use App\Modules\Lemmy\Services\LemmyApiService;
|
use Domains\Platform\Api\Lemmy\LemmyApiService;
|
||||||
use App\Services\Log\LogSaver;
|
use Domains\Logging\Services\LogSaver;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Platform\Models;
|
||||||
|
|
||||||
use Database\Factories\PlatformAccountFactory;
|
use Domains\Platform\Factories\PlatformAccountFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
|
|
@ -27,13 +27,18 @@
|
||||||
* @property Collection<int, PlatformChannel> $activeChannels
|
* @property Collection<int, PlatformChannel> $activeChannels
|
||||||
* @method static where(string $string, PlatformEnum $platform)
|
* @method static where(string $string, PlatformEnum $platform)
|
||||||
* @method static orderBy(string $string)
|
* @method static orderBy(string $string)
|
||||||
* @method static create(array<string, mixed> $validated)
|
* @method static create(array $validated)
|
||||||
*/
|
*/
|
||||||
class PlatformAccount extends Model
|
class PlatformAccount extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<PlatformAccountFactory> */
|
/** @use HasFactory<PlatformAccountFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Platform\Factories\PlatformAccountFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'platform',
|
'platform',
|
||||||
'instance_url',
|
'instance_url',
|
||||||
|
|
@ -64,12 +69,12 @@ protected function password(): Attribute
|
||||||
if (is_null($value)) {
|
if (is_null($value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return empty string if value is empty
|
// Return empty string if value is empty
|
||||||
if (empty($value)) {
|
if (empty($value)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Crypt::decryptString($value);
|
return Crypt::decryptString($value);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
@ -82,12 +87,12 @@ protected function password(): Attribute
|
||||||
if (is_null($value)) {
|
if (is_null($value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store empty string as null
|
// Store empty string as null
|
||||||
if (empty($value)) {
|
if (empty($value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Crypt::encryptString($value);
|
return Crypt::encryptString($value);
|
||||||
},
|
},
|
||||||
)->withoutObjectCaching();
|
)->withoutObjectCaching();
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Platform\Models;
|
||||||
|
|
||||||
use Database\Factories\PlatformChannelFactory;
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Platform\Factories\PlatformChannelFactory;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -25,6 +27,11 @@ class PlatformChannel extends Model
|
||||||
/** @use HasFactory<PlatformChannelFactory> */
|
/** @use HasFactory<PlatformChannelFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return \Domains\Platform\Factories\PlatformChannelFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $table = 'platform_channels';
|
protected $table = 'platform_channels';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Platform\Models;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
|
use Domains\Platform\Factories\PlatformChannelPostFactory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static where(string $string, PlatformEnum $platform)
|
* @method static where(string $string, PlatformEnum $platform)
|
||||||
* @method static updateOrCreate(array<string, mixed> $array, array<string, mixed> $array1)
|
* @method static updateOrCreate(array $array, array $array1)
|
||||||
*/
|
*/
|
||||||
class PlatformChannelPost extends Model
|
class PlatformChannelPost extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory()
|
||||||
|
{
|
||||||
|
return PlatformChannelPostFactory::new();
|
||||||
|
}
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'platform',
|
'platform',
|
||||||
'channel_id',
|
'channel_id',
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Domains\Platform\Models;
|
||||||
|
|
||||||
use App\Enums\PlatformEnum;
|
use Domains\Platform\Enums\PlatformEnum;
|
||||||
use Database\Factories\PlatformInstanceFactory;
|
use Domains\Platform\Factories\PlatformInstanceFactory;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static updateOrCreate(array<string, mixed> $array, $instanceData)
|
* @method static updateOrCreate(array $array, $instanceData)
|
||||||
* @method static where(string $string, mixed $operator)
|
* @method static where(string $string, mixed $operator)
|
||||||
* @property PlatformEnum $platform
|
* @property PlatformEnum $platform
|
||||||
* @property string $url
|
* @property string $url
|
||||||
|
|
@ -22,7 +23,12 @@ class PlatformInstance extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<PlatformInstanceFactory> */
|
/** @use HasFactory<PlatformInstanceFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected static function newFactory(): PlatformInstanceFactory
|
||||||
|
{
|
||||||
|
return PlatformInstanceFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'platform',
|
'platform',
|
||||||
'url',
|
'url',
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue