Compare commits

63 Commits

Author SHA1 Message Date
w33b 57d1ec34c3 Show two covers side by side on mobile 2026-05-26 16:14:29 +02:00
w33b 81639aaabf Use laravel task scheduler 2026-05-26 15:17:57 +02:00
w33b 5dc1bff60c Add an indicator for v2 releases in the download popup #7 2026-05-26 14:36:21 +02:00
w33b 2f3f0edc30 Add ability to edit hentai title & Update design of edit modal 2026-05-26 14:18:48 +02:00
w33b a71b2976af Add ability for mods to edit episode & Refactor code 2026-05-26 13:45:13 +02:00
w33b 2c016274ab Update packages 2026-05-26 12:06:54 +02:00
w33b 5ba0a55316 Update series page design & Refactor 2026-05-26 12:01:36 +02:00
w33b a6fe34a0d1 Reduce queries on home page by eager loading into cache 2026-05-24 22:15:16 +02:00
w33b bb53e06c69 Smaller style fix 2026-05-24 21:39:46 +02:00
w33b 5cae5dc658 Fix more responsiveness issues 2026-05-24 19:39:20 +02:00
w33b 356d07365f Update episode card design 2026-05-24 19:38:50 +02:00
w33b 3574d20fae Update hentai info design & Remove livewire view count 2026-05-24 15:52:22 +02:00
w33b 9fc9e8ed10 Update comments design 2026-05-24 14:49:46 +02:00
w33b f5c706b587 Fix smaller design issues 2026-05-24 12:14:19 +02:00
w33b cbea71d9ae Make home more responsive depending on screen width 2026-05-24 12:06:44 +02:00
w33b 64a621173c Fix hover to preview once and for all 2026-05-23 14:48:25 +02:00
w33b 839779b82e Update cover/thumbnail design 2026-05-23 14:43:35 +02:00
w33b 112cf9433e Make background darker 2026-05-23 14:00:11 +02:00
w33b 26a6500fca Remove unused modals 2026-05-23 11:46:30 +02:00
w33b d8cf70e747 Update more modal designs 2026-05-23 11:46:20 +02:00
w33b 0d4545c2ab Update language selector modal design 2026-05-23 11:00:18 +02:00
w33b 900103e1c2 Fix setting locale for guest users 2026-05-23 10:59:30 +02:00
w33b d4c90976f8 Fix janky loading icon & weird offset of modal 2026-05-23 10:29:07 +02:00
w33b 72263127df Update filter modal designs & Refactor code 2026-05-23 00:01:02 +02:00
w33b 6d3de59929 Update search filter design 2026-05-22 23:20:00 +02:00
w33b ddb1bc2d14 Stream page sidebar fix lightmode 2026-05-22 23:13:00 +02:00
w33b 5f3874a233 Stream page sidebar make padding consistent 2026-05-22 23:10:41 +02:00
w33b ba3650899e Refactor home template 2026-05-22 23:07:19 +02:00
w33b 904604fcfb Update hover color of tabs on home page 2026-05-22 22:44:38 +02:00
w33b b7b34b503c Update categories design 2026-05-22 22:43:57 +02:00
w33b 4928733383 Refactor by using app layout 2026-05-22 22:34:03 +02:00
w33b 6340302ac6 Update Cover & Thumbnail Design & Refactor 2026-05-22 22:16:31 +02:00
w33b c1829ba7bd Fix offset of thumbnail as guest 2026-05-22 20:59:45 +02:00
w33b 0b155bbb80 Update new comments design 2026-05-22 20:34:06 +02:00
w33b 9f959efa14 Update footer design 2026-05-22 20:31:38 +02:00
w33b 38e3346dc3 Update stats design 2026-05-22 19:41:07 +02:00
w33b 09c08f3fea Add comment restore button 2026-05-06 21:15:15 +02:00
w33b 75f631c3e6 Add MogLog System 2026-05-06 21:08:51 +02:00
w33b fdf26604f3 Add ability to delete comments by moderators 2026-05-06 19:02:50 +02:00
w33b 59cb39ca77 Fix remove role function 2026-05-06 16:27:42 +02:00
w33b 62647be75c Fix active status not showing immediatly (livewire) 2026-05-05 16:02:00 +02:00
w33b de6efb877c Add job to sync subscription keys 2026-05-04 20:22:51 +02:00
w33b 2151d69791 Add external subscription system 2026-05-04 19:12:21 +02:00
w33b 05d4ef1bdb Add Passkey Support & Pint 2026-04-21 15:56:46 +02:00
w33b 8ae9eaaadb Replace captcha package with own implementation 2026-04-20 21:36:02 +02:00
w33b 361b511c3e Update npm packages 2026-04-18 19:17:30 +02:00
w33b 1bc505057f Fix adding theme switch event listener on login page 2026-04-18 19:15:43 +02:00
w33b a78b1c41ac Remove unused code & Move files to correct folder 2026-04-18 18:42:09 +02:00
w33b 2480c5b309 Redo user comments list 2026-04-18 16:28:50 +02:00
w33b 4fc11d7329 Redo notification list 2026-04-18 15:47:06 +02:00
w33b f3e5100d5d Pint 2026-04-18 14:18:52 +02:00
w33b 5b4d3d435e Display the correct file extension on download button 2026-03-06 22:06:20 +01:00
w33b 564f816fb9 Add matrix nsfw rooms 2026-03-04 14:34:16 +01:00
w33b 3709e378c3 Fix Admin Download Buttons 2026-03-03 21:17:43 +01:00
w33b 4a45dae593 Update Readme 2026-03-01 16:07:09 +01:00
w33b 2b0448d517 Add Disclaimer and Guide for Matrix 2026-03-01 16:06:58 +01:00
w33b 3bb6af73c3 Add matrix 2026-02-18 12:34:10 +01:00
w33b 57cf153560 Update Livewire to fix CorruptComponentPayloadException 2026-02-15 14:52:16 +01:00
w33b e45fd4b148 Update axios 2026-02-15 13:02:08 +01:00
w33b d479369770 Merge branch 'laravel-12' 2026-01-18 18:54:25 +01:00
w33b b8ba17b33f Fix patreon role check (again again) 2026-01-16 15:18:29 +00:00
w33b 5a8dd12cb8 Fix patreon role check (again)
Life without typecast is hard
2026-01-16 15:11:28 +00:00
w33b 3a77c4320d Fix patreon role check
We compare the roles using a strict type check, the json contents from the discord api are strings and not integers.
2026-01-16 14:42:39 +00:00
253 changed files with 7031 additions and 4279 deletions
+7 -7
View File
@@ -2,13 +2,13 @@
## hstream Website
### Install
### Install (Ubuntu)
```bash
# Install PHP
sudo add-apt-repository ppa:ondrej/php
apt update && apt upgrade
apt install php8.3 php8.3-xml php8.3-mysql php8.3-gd php8.3-zip php8.3-curl php8.3-mbstring
apt install php8.4 php8.4-xml php8.4-mysql php8.4-gd php8.4-zip php8.4-curl php8.4-mbstring
# Install NodeJS
curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
@@ -22,7 +22,7 @@ mv composer.phar composer
# Install NGINX (skip for local dev)
apt install nginx
apt install php8.3-fpm
apt install php8.4-fpm
# Install MariaDB
apt install mariadb-server
@@ -54,7 +54,7 @@ nano /etc/supervisor/conf.d/laravel-queue.conf :
[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
command=php84 /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
@@ -83,9 +83,9 @@ zip -r hstream_2023_11_30.zip hstream/
### Update
```bash
php artisan down
php84 artisan down
git pull
npm run build
php artisan view:clear && php artisan optimize:clear && php artisan cache:clear && service php8.4-fpm restart
php artisan up
php84 artisan view:clear && php84 artisan optimize:clear && php84 artisan cache:clear && service php8.4-fpm restart
php84 artisan up
```
+2 -3
View File
@@ -3,11 +3,10 @@
namespace App\Console\Commands;
use App\Models\PopularDaily;
use App\Models\PopularWeekly;
use App\Models\PopularMonthly;
use Illuminate\Support\Carbon;
use App\Models\PopularWeekly;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class AutoStats extends Command
{
+2 -3
View File
@@ -4,7 +4,6 @@ namespace App\Console\Commands;
use App\Models\Episode;
use App\Models\Hentai;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Spatie\Sitemap\Sitemap;
@@ -17,7 +16,7 @@ class GenerateSitemap extends Command
*
* @var string
*/
protected $signature = 'sitemap:generate';
protected $signature = 'app:generate-sitemap';
/**
* The console command description.
@@ -45,7 +44,7 @@ class GenerateSitemap extends Command
->setLastModificationDate(Carbon::create('2023', '8', '1')))
->add(Episode::all())
->add(Hentai::all());
$sitemap->writeToFile(public_path('sitemap.xml'));
}
}
+2 -3
View File
@@ -2,9 +2,8 @@
namespace App\Console\Commands;
use App\Models\Downloads;
use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads;
use Illuminate\Console\Command;
class GetFileSize extends Command
@@ -28,7 +27,7 @@ class GetFileSize extends Command
*/
public function handle()
{
foreach(Downloads::whereNull('size')->get() as $download) {
foreach (Downloads::whereNull('size')->get() as $download) {
GetFileSizeFromCDN::dispatch($download->id);
}
+1 -2
View File
@@ -4,9 +4,8 @@ namespace App\Console\Commands;
use App\Models\User;
use App\Models\UserDownload;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class ResetUserDownloads extends Command
{
@@ -0,0 +1,109 @@
<?php
namespace App\Console\Commands;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Log;
class SyncSubscriptionKeys extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:sync-subscription-keys';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync local users against active subscription keys';
/**
* Execute the console command.
*/
public function handle()
{
$endpoint = config('services.subscription_service_host');
if (!$endpoint) {
$this->error('Missing endpoint.');
return self::FAILURE;
}
$response = Http::asJson()
->acceptJson()
->timeout(20)
->retry(3, 1000)
->post($endpoint.'/api/membership/keys', [
'payload' => base64_encode(Crypt::encryptString('get-active-keys')),
]);
if (! $response->successful()) {
$this->error('Subscription API request failed: HTTP ' . $response->status());
return self::FAILURE;
}
try {
$decrypted = Crypt::decryptString(base64_decode($response->json('payload')));
$activeKeys = json_decode($decrypted, true, flags: JSON_THROW_ON_ERROR);
} catch (DecryptException $e) {
$this->error('Could not decrypt API response.');
return self::FAILURE;
} catch (\JsonException $e) {
$this->error('API returned invalid JSON.');
return self::FAILURE;
}
if (! is_array($activeKeys)) {
$this->error('API response payload was not an array.');
return self::FAILURE;
}
$activeKeys = collect($activeKeys)
->filter(fn ($key) => is_string($key) && $key !== '')
->values()
->all();
$this->markInactiveUsers($activeKeys);
$this->markActiveUsers($activeKeys);
return self::SUCCESS;
}
private function markInactiveUsers(array $activeKeys)
{
User::query()
->whereNotNull('subscription_key')
->whereNotIn('subscription_key', $activeKeys)
->chunk(100, function ($users) {
foreach($users as $user) {
if ($user->hasRole(UserRole::SUPPORTER)) {
Log::info("Removed Supporter Role from {$user->name}");
$user->removeRole(UserRole::SUPPORTER);
}
}
});
}
private function markActiveUsers(array $activeKeys)
{
User::query()
->whereNotNull('subscription_key')
->whereIn('subscription_key', $activeKeys)
->chunk(100, function ($users) {
foreach($users as $user) {
if (!$user->hasRole(UserRole::SUPPORTER)) {
Log::info("Added Supporter Role for {$user->name}");
$user->addRole(UserRole::SUPPORTER);
}
}
});
}
}
+19 -18
View File
@@ -5,21 +5,20 @@ namespace App\Helpers;
use App\Models\Comment;
use App\Models\Episode;
use App\Models\Hentai;
use App\Models\PopularDaily;
use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use Conner\Tagging\Model\Tag;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class CacheHelper
{
public static function getRecentlyReleased(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("recently_released_".$guestString, now()->addMinutes(60), function () use ($guest) {
return Cache::remember('recently_released_'.$guestString, now()->addMinutes(60), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('release_date', 'desc')
@@ -31,7 +30,8 @@ class CacheHelper
public static function getRecentlyUploaded(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("recently_uploaded".$guestString, now()->addMinutes(5), function () use ($guest) {
return Cache::remember('recently_uploaded'.$guestString, now()->addMinutes(5), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('created_at', 'desc')
@@ -42,21 +42,21 @@ class CacheHelper
public static function getTotalViewCount()
{
return Cache::remember("total_view_count", now()->addMinutes(60), function () {
return Cache::remember('total_view_count', now()->addMinutes(60), function () {
return Episode::sum('view_count');
});
}
public static function getTotalEpisodeCount()
{
return Cache::remember("total_episode_count", now()->addMinutes(60), function () {
return Cache::remember('total_episode_count', now()->addMinutes(60), function () {
return Episode::count();
});
}
public static function getTotalHentaiCount()
{
return Cache::remember("total_hentai_count", now()->addMinutes(60), function () {
return Cache::remember('total_hentai_count', now()->addMinutes(60), function () {
return Hentai::count();
});
}
@@ -64,10 +64,11 @@ class CacheHelper
public static function getPopularAllTime(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("top_hentai_alltime".$guestString, now()->addMinutes(360), function () use ($guest) {
return Cache::remember('top_hentai_alltime'.$guestString, now()->addMinutes(360), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('view_count','desc')
->orderBy('view_count', 'desc')
->limit(16)
->get();
});
@@ -75,7 +76,7 @@ class CacheHelper
public static function getPopularMonthly()
{
return Cache::remember("top_hentai_monthly", now()->addMinutes(360), function () {
return Cache::remember('top_hentai_monthly', now()->addMinutes(360), function () {
return PopularMonthly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -87,7 +88,7 @@ class CacheHelper
public static function getPopularWeekly()
{
return Cache::remember("top_hentai_weekly", now()->addMinutes(360), function () {
return Cache::remember('top_hentai_weekly', now()->addMinutes(360), function () {
return PopularWeekly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -100,7 +101,7 @@ class CacheHelper
public static function getPopularDaily()
{
return Cache::remember("top_hentai_daily", now()->addMinutes(30), function () {
return Cache::remember('top_hentai_daily', now()->addMinutes(30), function () {
return PopularDaily::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -112,22 +113,22 @@ class CacheHelper
public static function getMostLikes()
{
return Cache::remember("top_likes", now()->addMinutes(30), function () {
return Cache::remember('top_likes', now()->addMinutes(30), function () {
return DB::table('markable_likes')->groupBy('markable_id')->select('markable_id', DB::raw('count(*) as total'))->orderBy('total', 'desc')->limit(16)->get();
});
}
public static function getAllTags()
{
return Cache::remember("all_tags", now()->addMinutes(10080), function () {
return Cache::remember('all_tags', now()->addMinutes(10080), function () {
return Tag::where('count', '>', 0)->orderBy('slug', 'ASC')->get();
});
}
public static function getLatestComments()
{
return Cache::remember("latest_comments", now()->addMinutes(60), function () {
return Comment::latest()->take(10)->get();
return Cache::remember('latest_comments', now()->addMinutes(60), function () {
return Comment::with('user')->latest()->take(10)->get();
});
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Conner\Tagging\Model\Tag;
class FilterCategories
{
public static function getFilterCategories()
{
$taglist = Cache::remember(
'searchtags',
300,
fn () => Tag::where('count', '>', 0)
->orderBy('slug')
->get(),
);
$appearances = [
'Loli',
'Shota',
'Milf',
'Futanari',
'Big Boobs',
'Small Boobs',
'Dark Skin',
'Cosplay',
'Elf',
'Maid',
'Nekomimi',
'Nurse',
'School Girl',
'Succubus',
'Teacher',
'Trap',
'Pregnant',
'Glasses',
'Swim Suit',
'Ugly Bastard',
'Monster',
];
$types = [
'3D',
'4K',
'48Fps',
'4K 48Fps',
'Censored',
'Uncensored',
'Comedy',
'Fantasy',
'Horror',
'Vanilla',
'Ntr',
'Pov',
'Filmed',
'X-Ray',
];
$actions = [
'Anal',
'Bdsm',
'Facial',
'Blow Job',
'Boob Job',
'Foot Job',
'Hand Job',
'Rimjob',
'Inflation',
'Masturbation',
'Public Sex',
'Rape',
'Reverse Rape',
'Threesome',
'Orgy',
'Gangbang',
];
$excluded = [...$appearances, ...$types, ...$actions];
$categories = [
'Genres' => $taglist
->reject(fn ($tag) => in_array($tag->name, $excluded))
->pluck('name')
->toArray(),
'Actions' => $actions,
'Appearance' => collect($appearances)
->reject(function ($tag) {
return Auth::guest() && in_array($tag, ['Loli', 'Shota']);
})
->toArray(),
'Types' => $types,
];
return $categories;
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ class GitHelper
{
public static function shortCommit()
{
return Cache::remember("git_commit", now()->addMinutes(60), function () {
return Cache::remember('git_commit', now()->addMinutes(60), function () {
try {
return trim(exec('git rev-parse --short HEAD'));
} catch (\Exception $e) {
@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Alert;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AlertController extends Controller
{
/**
/**
* Display alert index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.alert.index');
}
@@ -19,7 +21,7 @@ class AlertController extends Controller
/**
* Create Alert.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'message' => 'required|string|max:255',
@@ -39,7 +41,7 @@ class AlertController extends Controller
/**
* Delete Alert.
*/
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
public function delete(int $alert_id): RedirectResponse
{
Alert::where('id', $alert_id)->delete();
@@ -3,13 +3,14 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class CommentsController extends Controller
{
/**
* Display Comments Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.comments.index');
}
@@ -4,25 +4,27 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Contact;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$contacts = Contact::orderBy('created_at', 'DESC')->get();
return view('admin.contact.index', [
'contacts' => $contacts
'contacts' => $contacts,
]);
}
/**
* Delete Contact.
*/
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse
public function delete(int $contact_id): RedirectResponse
{
Contact::where('id', $contact_id)->delete();
@@ -2,26 +2,29 @@
namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Jobs\DiscordReleaseNotification;
use App\Models\Episode;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EpisodeController extends Controller
{
protected EpisodeService $episodeService;
protected GalleryService $galleryService;
protected DownloadService $downloadService;
public function __construct(
EpisodeService $episodeService,
GalleryService $galleryService,
EpisodeService $episodeService,
GalleryService $galleryService,
DownloadService $downloadService
) {
) {
$this->episodeService = $episodeService;
$this->galleryService = $galleryService;
$this->downloadService = $downloadService;
@@ -30,7 +33,7 @@ class EpisodeController extends Controller
/**
* Add Episode to existing series
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
@@ -43,7 +46,7 @@ class EpisodeController extends Controller
// Discord Alert
if ($request->has('censored')) {
DiscordReleaseNotification::dispatch($referenceEpisode->title." - ".$episodeNumber, 'release-censored');
DiscordReleaseNotification::dispatch($referenceEpisode->title.' - '.$episodeNumber, 'release-censored');
} else {
DiscordReleaseNotification::dispatch($episode->slug, 'release');
}
@@ -51,16 +54,27 @@ class EpisodeController extends Controller
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug
'title' => $episode->slug,
]);
}
/**
* Edit Episode
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
if ($request->user()->hasRole(UserRole::MODERATOR)) {
$this->episodeService->updateEpisodeModerator($request, $episode->id);
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug,
]);
}
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
$oldinterpolated = $episode->interpolated;
@@ -87,7 +101,7 @@ class EpisodeController extends Controller
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug
'title' => $episode->slug,
]);
}
}
@@ -3,34 +3,37 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Hentai;
use App\Jobs\DiscordReleaseNotification;
use App\Models\Hentai;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ReleaseController extends Controller
{
protected EpisodeService $episodeService;
protected GalleryService $galleryService;
protected DownloadService $downloadService;
public function __construct(
EpisodeService $episodeService,
GalleryService $galleryService,
EpisodeService $episodeService,
GalleryService $galleryService,
DownloadService $downloadService
) {
) {
$this->episodeService = $episodeService;
$this->galleryService = $galleryService;
$this->downloadService = $downloadService;
}
/**
/**
* Display release page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.release.create');
}
@@ -38,7 +41,7 @@ class ReleaseController extends Controller
/**
* Upload New Hentai with One or Multipe Episodes
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
// Create new Hentai or find existing one
$slug = $this->episodeService->generateSlug($request->input('title'));
@@ -3,22 +3,22 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\SiteBackground;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\View\View;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class SiteBackgroundController extends Controller
{
/**
/**
* Display admin index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.background.index', [
'images' => SiteBackground::all(),
@@ -28,15 +28,15 @@ class SiteBackgroundController extends Controller
/**
* Create new site backgrounds
*/
public function create(Request $request): \Illuminate\Http\RedirectResponse
public function create(Request $request): RedirectResponse
{
$request->validate([
'images' => 'required',
'date_start' => 'required',
'date_end' => 'required',
'images' => 'required',
'date_start' => 'required',
'date_end' => 'required',
]);
foreach($request->file('images') as $file) {
foreach ($request->file('images') as $file) {
// Initiating a database transaction in case something goes wrong.
DB::beginTransaction();
@@ -44,24 +44,25 @@ class SiteBackgroundController extends Controller
$bg = SiteBackground::create(array_merge(
$request->only(['date_start', 'date_end']),
[
'default' => (bool) $request->input('default', false)
'default' => (bool) $request->input('default', false),
]
));
$resolutions = [1440, 1080, 720, 640];
foreach($resolutions as $resolution) {
foreach ($resolutions as $resolution) {
// /images/background/1-2560p.webp
$targetPath = "/images/background/{$bg->id}-{$resolution}p.webp";
Image::read($file->getRealPath())
->scaleDown(height: $resolution)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(public_path($targetPath));
}
} catch (\Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return redirect()->back();
}
@@ -74,18 +75,18 @@ class SiteBackgroundController extends Controller
return redirect()->back();
}
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$request->validate([
'id' => 'required|exists:site_backgrounds,id',
'date_start' => 'required',
'date_end' => 'required',
'id' => 'required|exists:site_backgrounds,id',
'date_start' => 'required',
'date_end' => 'required',
]);
SiteBackground::where('id', $request->input('id'))->update(array_merge(
$request->only(['date_start', 'date_end']),
[
'default' => (bool) $request->input('default', false)
'default' => (bool) $request->input('default', false),
]
));
@@ -97,7 +98,7 @@ class SiteBackgroundController extends Controller
/**
* Delete backround
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
$id = $request->input('id');
@@ -109,16 +110,17 @@ class SiteBackgroundController extends Controller
$resolutions = [1440, 1080, 720, 640];
try {
foreach($resolutions as $resolution) {
foreach ($resolutions as $resolution) {
$targetPath = "/images/background/{$id}-{$resolution}p.webp";
File::delete(public_path($targetPath));
}
} catch (\Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return redirect()->back();
}
// Committing the database transaction.
DB::commit();
@@ -2,10 +2,11 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\EpisodeSubtitle;
use App\Models\Subtitle;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class SubtitleController extends Controller
@@ -13,11 +14,11 @@ class SubtitleController extends Controller
/**
* Add new Subtitle.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$subtitle = Subtitle::create([
'name' => $request->name,
'slug' => $request->slug,
'name' => $request->name,
'slug' => $request->slug,
]);
// Add to Episode
@@ -32,12 +33,12 @@ class SubtitleController extends Controller
/**
* Update Episode Subtitles.
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
// Clear everything
foreach($episode->subtitles as $sub) {
foreach ($episode->subtitles as $sub) {
$sub->delete();
}
@@ -3,16 +3,17 @@
namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Models\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Display Users Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.users.index');
}
@@ -23,10 +24,9 @@ class UserController extends Controller
public function update(Request $request)
{
$validated = $request->validate([
'id' => 'required|exists:users,id',
'action' => 'required',
'id' => 'required|exists:users,id',
'action' => 'required',
]);
$user = User::findOrFail($validated['id']);
@@ -40,7 +40,7 @@ class UserController extends Controller
alert()->success('Unbanned', 'User has been unbanned.');
break;
default:
alert()->error('Error','Invalid action provided');
alert()->error('Error', 'Invalid action provided');
}
return redirect()->back();
@@ -3,10 +3,10 @@
namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\Studios;
use App\Models\Subtitle;
use App\Http\Controllers\Controller;
class AdminApiController extends Controller
{
@@ -2,13 +2,11 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Downloads;
use App\Models\Episode;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use GrantHolle\Altcha\Rules\ValidAltcha;
class DownloadApiController extends Controller
{
@@ -18,8 +16,8 @@ class DownloadApiController extends Controller
public function getDownload(Request $request)
{
$validated = $request->validate([
'episode_id' => ['required'],
'captcha' => ['required', new ValidAltcha],
'episode_id' => ['required'],
'captcha' => ['required', new ValidCaptcha],
]);
$episode = Episode::where('id', $request->input('episode_id'))
@@ -33,9 +31,9 @@ class DownloadApiController extends Controller
$download->save();
return response()->json([
'message' => 'success',
'download_url' => $download->url,
'download_count' => $oldCount,
'message' => 'success',
'download_url' => $download->url,
'download_count' => $oldCount,
], 200);
}
}
@@ -2,12 +2,11 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Hentai;
use App\Models\PopularMonthly;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class HentaiApiController extends Controller
{
@@ -23,13 +22,13 @@ class HentaiApiController extends Controller
->get()
->map(function ($hentai) {
return [
'title' => $hentai->episodes[0]->title,
'title' => $hentai->episodes[0]->title,
'title_jpn' => $hentai->episodes[0]->title_jpn,
'slug' => $hentai->slug,
'episodes' => $hentai->episodes->map(function ($ep) {
'slug' => $hentai->slug,
'episodes' => $hentai->episodes->map(function ($ep) {
return [
'episode' => $ep->episode,
'slug' => $ep->slug,
'episode' => $ep->episode,
'slug' => $ep->slug,
];
}),
];
@@ -3,9 +3,7 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use Illuminate\Http\Request;
class StreamApiController extends Controller
@@ -16,24 +14,24 @@ class StreamApiController extends Controller
public function getStream(Request $request)
{
$validated = $request->validate([
'episode_id' => 'required',
'episode_id' => 'required',
]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
$subtitles = $episode->subtitles
->mapWithKeys(fn($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
->mapWithKeys(fn ($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
->toArray();
return response()->json([
'title' => $episode->title.' - '.$episode->episode,
'poster' => $episode->gallery()->first()->image_url,
'interpolated' => $episode->interpolated,
'interpolated_uhd' => $episode->interpolated_uhd,
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
'stream_domains' => config('hstream.stream_domain'),
'asia_stream_domains' => config('hstream.asia_stream_domain'),
'extra_subtitles' => $subtitles
'title' => $episode->title.' - '.$episode->episode,
'poster' => $episode->gallery()->first()->image_url,
'interpolated' => $episode->interpolated,
'interpolated_uhd' => $episode->interpolated_uhd,
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
'stream_domains' => config('hstream.stream_domain'),
'asia_stream_domains' => config('hstream.asia_stream_domain'),
'extra_subtitles' => $subtitles,
], 200);
}
}
@@ -4,9 +4,8 @@ namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Conner\Tagging\Model\Tag;
use Illuminate\Http\Request;
class UserApiController extends Controller
{
@@ -33,11 +32,10 @@ class UserApiController extends Controller
}
}
return response()->json([
'message' => 'success',
'tags' => $tagWhiteList,
'usertags' => $tagBlackList
'message' => 'success',
'tags' => $tagWhiteList,
'usertags' => $tagBlackList,
], 200);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\CreateChallengeOptions;
use App\Http\Controllers\Controller;
class CaptchaController extends Controller
{
public function create(): array
{
$pbkdf2 = new Pbkdf2;
$altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
// Create challenge
$challenge = $altcha->createChallenge(new CreateChallengeOptions(
algorithm: $pbkdf2,
cost: 5000,
counter: random_int(5000, 10000),
expiresAt: time() + 600,
));
return get_object_vars($challenge);
}
}
@@ -3,11 +3,9 @@
namespace App\Http\Controllers\Auth;
use App\Enums\UserRole;
use App\Models\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -32,7 +30,7 @@ class DiscordAuthController extends Controller
$user = User::where('discord_id', $discordUser->id)->first();
if (!$user) {
if (! $user) {
// link by email if it already exists
$user = User::where('email', $discordUser->email)->first();
@@ -77,7 +75,7 @@ class DiscordAuthController extends Controller
private function checkDiscordRoles(User $user): void
{
// Should not ever happen
if (!$user->discord_id) {
if (! $user->discord_id) {
return;
}
@@ -90,6 +88,7 @@ class DiscordAuthController extends Controller
// User is not in the guild
if ($response->status() === 404) {
$user->removeRole(UserRole::SUPPORTER);
return;
}
@@ -109,11 +108,12 @@ class DiscordAuthController extends Controller
$patreonRoles = config('discord.patreon_roles', []);
// If intersect of array is empty, then the user doesn't have the role
$hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
$hasSupporterRole = ! empty(array_intersect($discordRoles, $patreonRoles));
if (!$hasSupporterRole) {
if (! $hasSupporterRole) {
// Remove role if not found
$user->removeRole(UserRole::SUPPORTER);
return;
}
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
@@ -26,7 +27,7 @@ class NewPasswordController extends Controller
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -16,8 +16,7 @@ class PasswordController extends Controller
public function update(Request $request): RedirectResponse
{
// If user logged in with Discord and has not yet a password, allow to set password
if ($request->user()->discord_id && is_null($request->user()->password))
{
if ($request->user()->discord_id && is_null($request->user()->password)) {
$validated = $request->validateWithBag('updatePassword', [
'password' => ['required', Password::defaults(), 'confirmed'],
]);
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -4,21 +4,21 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use GrantHolle\Altcha\Rules\ValidAltcha;
use Illuminate\Validation\ValidationException;
class RegisteredUserController extends Controller
{
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -26,7 +26,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$user = User::create([
+9 -8
View File
@@ -3,16 +3,17 @@
namespace App\Http\Controllers;
use App\Models\Contact;
use App\Rules\ValidCaptcha;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use GrantHolle\Altcha\Rules\ValidAltcha;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('contact.form');
}
@@ -20,17 +21,17 @@ class ContactController extends Controller
/**
* Store Contact Submission.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'email' => 'required|max:50',
'name' => 'required|max:30',
'email' => 'required|max:50',
'message' => 'required|max:1000',
'subject' => 'required|max:50',
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$contact = new Contact();
$contact = new Contact;
$contact->name = $request->input('name');
$contact->email = $request->input('email');
$contact->message = $request->input('message');
+32 -32
View File
@@ -2,52 +2,52 @@
namespace App\Http\Controllers;
use App\Models\Episode;
use App\Helpers\CacheHelper;
use App\Models\Episode;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cookie;
use Illuminate\View\View;
class HomeController extends Controller
{
/**
* Display Home Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$guest = Auth::guest();
$guestString = $guest ? 'guest' : 'authed';
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () =>
Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->whereIn('id', function($query) {
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
$query->selectRaw('id')
->from('episodes')
->whereIn('id', $mostLikesIds)
->orderByRaw("FIELD(id, " . implode(',', $mostLikesIds) . ")");
})
->get()
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () => Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->whereIn('id', function ($query) {
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
$query->selectRaw('id')
->from('episodes')
->whereIn('id', $mostLikesIds)
->orderByRaw('FIELD(id, '.implode(',', $mostLikesIds).')');
})
->get()
);
return view('home.index', [
'recentlyReleased' => CacheHelper::getRecentlyReleased($guest),
'recentlyUploaded' => CacheHelper::getRecentlyUploaded($guest),
'popularAllTime' => CacheHelper::getPopularAllTime($guest),
'popularMonthly' => CacheHelper::getPopularMonthly(),
'popularWeekly' => CacheHelper::getPopularWeekly(),
'popularDaily' => CacheHelper::getPopularDaily(),
'mostLikes' => $mostLikes,
'latestComments' => CacheHelper::getLatestComments(),
'recentlyReleased' => CacheHelper::getRecentlyReleased($guest),
'recentlyUploaded' => CacheHelper::getRecentlyUploaded($guest),
'popularAllTime' => CacheHelper::getPopularAllTime($guest),
'popularMonthly' => CacheHelper::getPopularMonthly(),
'popularWeekly' => CacheHelper::getPopularWeekly(),
'popularDaily' => CacheHelper::getPopularDaily(),
'mostLikes' => $mostLikes,
'latestComments' => CacheHelper::getLatestComments(),
]);
}
/**
* Display Banned Page.
*/
public function banned(): \Illuminate\View\View
public function banned(): View
{
return view('auth.banned');
}
@@ -56,7 +56,7 @@ class HomeController extends Controller
* Redirects to a random Hentai episode
* Done due to performance reasons
*/
public function random(): \Illuminate\Http\RedirectResponse
public function random(): RedirectResponse
{
$random = Episode::inRandomOrder()
->limit(1)
@@ -71,7 +71,7 @@ class HomeController extends Controller
/**
* Display Search Page.
*/
public function search(): \Illuminate\View\View
public function search(): View
{
return view('search.index');
}
@@ -79,7 +79,7 @@ class HomeController extends Controller
/**
* Display Download Search Page.
*/
public function downloadSearch(): \Illuminate\View\View
public function downloadSearch(): View
{
return view('search.download');
}
@@ -87,7 +87,7 @@ class HomeController extends Controller
/**
* Redirect POST Data to GET with Query String.
*/
public function searchRedirect(Request $request): \Illuminate\Http\RedirectResponse
public function searchRedirect(Request $request): RedirectResponse
{
return redirect()->route('hentai.search', [
'search' => $request->input('live-search'),
@@ -97,19 +97,19 @@ class HomeController extends Controller
/**
* Display Stats Page.
*/
public function stats(): \Illuminate\View\View
public function stats(): View
{
return view('home.stats', [
'viewCount' => CacheHelper::getTotalViewCount(),
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
'viewCount' => CacheHelper::getTotalViewCount(),
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
]);
}
/**
* Manually set website language
*/
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
public function updateLanguage(Request $request): RedirectResponse
{
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\MatrixRegisterRequest;
use App\Services\MatrixRegistrationService;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MatrixController extends Controller
{
/**
* Display the user page.
*/
public function index(Request $request): View
{
$rooms = [
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],
['name' => '📡 Releases', 'description' => 'Were we @everyone for new releases.', 'alias' => 'https://matrix.to/#/#releases:hstream.moe'],
['name' => '👗 NSFW 2D', 'description' => 'Channel for R18 2D Media.', 'alias' => 'https://matrix.to/#/#nsfw:hstream.moe'],
['name' => '👗 NSFW IRL', 'description' => 'Channel for R18 IRL Media.', 'alias' => 'https://matrix.to/#/#nsfw-irl:hstream.moe'],
];
return view('matrix.index', [
'user' => $request->user(),
'rooms' => $rooms,
]);
}
/**
* Create matrix user
*/
public function store(
MatrixRegisterRequest $request,
MatrixRegistrationService $matrixService
) {
try {
$result = $matrixService->registerUser(
$request->username,
$request->password
);
$user = $request->user();
$user->matrix_id = $result['user_id'];
$user->save();
return redirect()
->back()
->with('success', 'Matrix user created successfully.');
} catch (\Exception $e) {
return back()
->withErrors([
'username' => $e->getMessage(),
])
->withInput();
}
}
}
@@ -2,18 +2,19 @@
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class NotificationController extends Controller
{
/**
* Display the user's notification page.
*/
public function index(Request $request): \Illuminate\View\View
public function index(Request $request): View
{
return view('profile.notifications', [
'user' => $request->user(),
'user' => $request->user(),
'notifications' => $request->user()->unreadNotifications,
]);
}
@@ -21,10 +22,10 @@ class NotificationController extends Controller
/**
* Delete Notifcation
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
$request->validate([
'id' => 'required|exists:notifications,id',
'id' => 'required|exists:notifications,id',
]);
$notification = $request->user()
+31 -30
View File
@@ -6,8 +6,10 @@ use App\Models\Episode;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Services\PlaylistService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use RealRashid\SweetAlert\Facades\Alert;
use Illuminate\View\View;
class PlaylistController extends Controller
{
@@ -21,7 +23,7 @@ class PlaylistController extends Controller
/**
* Display the public playlists page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('playlist.index');
}
@@ -29,9 +31,9 @@ class PlaylistController extends Controller
/**
* Display public playlist.
*/
public function show($playlist_id): \Illuminate\View\View
public function show($playlist_id): View
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -42,18 +44,17 @@ class PlaylistController extends Controller
]);
}
/**
* Display the user's playlists page.
*/
public function playlists(Request $request): \Illuminate\View\View
public function playlists(Request $request): View
{
$title = 'Delete Playlist!';
$text = "Are you sure you want to delete?";
$text = 'Are you sure you want to delete?';
confirmDelete($title, $text);
return view('profile.playlists', [
'user' => $request->user(),
'user' => $request->user(),
'playlists' => $request->user()->playlists,
]);
}
@@ -61,9 +62,9 @@ class PlaylistController extends Controller
/**
* Display user's playlist.
*/
public function showPlaylist(Request $request, $playlist_id): \Illuminate\View\View
public function showPlaylist(Request $request, $playlist_id): View
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -79,13 +80,13 @@ class PlaylistController extends Controller
/**
* Create user playlist (Form).
*/
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse
public function createPlaylist(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'name' => 'required|max:30',
]);
$playlist = new Playlist();
$playlist = new Playlist;
$playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private';
@@ -97,9 +98,9 @@ class PlaylistController extends Controller
/**
* Delete user playlist.
*/
public function deletePlaylist(Request $request, $playlist_id): \Illuminate\Http\RedirectResponse
public function deletePlaylist(Request $request, $playlist_id): RedirectResponse
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -117,12 +118,12 @@ class PlaylistController extends Controller
/**
* Delete episode from playlist.
*/
public function deleteEpisodeFromPlaylist(Request $request): \Illuminate\Http\JsonResponse
public function deleteEpisodeFromPlaylist(Request $request): JsonResponse
{
if (!is_numeric($request->input('playlist')) || !is_numeric($request->input('episode'))) {
if (! is_numeric($request->input('playlist')) || ! is_numeric($request->input('episode'))) {
return response()->json([
'message' => 'not-numeric',
'user' => $request->user(),
'user' => $request->user(),
], 404);
}
@@ -133,25 +134,25 @@ class PlaylistController extends Controller
PlaylistEpisode::where('playlist_id', $playlist->id)
->where('episode_id', (int) $request->input('episode'))
->delete();
$this->playlistService->reorderPositions($playlist);
return response()->json([
'message' => 'success',
'user' => $request->user(),
'user' => $request->user(),
], 200);
}
/**
* Add to user playlist (API).
*/
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function addPlaylistApi(Request $request): JsonResponse
{
$user = $request->user();
$validated = $request->validate([
'playlist' => 'required|max:30',
'episode_id' => 'required'
'playlist' => 'required|max:30',
'episode_id' => 'required',
]);
$playlist = Playlist::where('user_id', $user->id)->where('id', $request->input('playlist'))->firstOrFail();
@@ -161,7 +162,7 @@ class PlaylistController extends Controller
$exists = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->exists();
if ($exists) {
return response()->json([
'message' => 'already-added'
'message' => 'already-added',
], 200);
}
@@ -175,28 +176,28 @@ class PlaylistController extends Controller
]);
return response()->json([
'message' => 'success'
'message' => 'success',
], 200);
}
/**
* Create user playlist (API).
*/
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function createPlaylistApi(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'name' => 'required|max:30',
]);
$playlist = new Playlist();
$playlist = new Playlist;
$playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private';
$playlist->save();
return response()->json([
'message' => 'success',
'playlist_id' => $playlist->id
'message' => 'success',
'playlist_id' => $playlist->id,
], 200);
}
}
+24 -15
View File
@@ -2,22 +2,21 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use App\Models\Episode;
use App\Models\User;
use App\Http\Requests\ProfileUpdateRequest;
use Conner\Tagging\Model\Tag;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Intervention\Image\Laravel\Facades\Image;
use Conner\Tagging\Model\Tag;
class ProfileController extends Controller
{
/**
@@ -38,8 +37,8 @@ class ProfileController extends Controller
$example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first();
return view('profile.settings', [
'user' => $request->user(),
'example' => $example,
'user' => $request->user(),
'example' => $example,
]);
}
@@ -92,7 +91,17 @@ class ProfileController extends Controller
public function likes(Request $request): View
{
return view('profile.likes', [
'user' => $request->user(),
'user' => $request->user(),
]);
}
/**
* Display the user's subscription page.
*/
public function subscription(Request $request): View
{
return view('profile.subscription', [
'user' => $request->user(),
]);
}
@@ -118,9 +127,10 @@ class ProfileController extends Controller
$user = $request->user();
$tags = json_decode($request->input('tags'));
if (!$tags) {
if (! $tags) {
$user->tag_blacklist = null;
$user->save();
return Redirect::route('profile.settings')->with('status', 'blacklist-updated');
}
@@ -139,12 +149,12 @@ class ProfileController extends Controller
/**
* Delete the user's account.
*/
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
public function destroy(Request $request): RedirectResponse
{
$user = $request->user();
// Verify password if user has password
if (!is_null($user->password)) {
if (! is_null($user->password)) {
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
@@ -174,11 +184,11 @@ class ProfileController extends Controller
/**
* Store custom user avatar.
*/
protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void
protected function storeAvatar(UploadedFile $file, User $user): void
{
// Create Folder for Image Upload
if (! Storage::disk('public')->exists("/images/avatars")) {
Storage::disk('public')->makeDirectory("/images/avatars");
if (! Storage::disk('public')->exists('/images/avatars')) {
Storage::disk('public')->makeDirectory('/images/avatars');
}
// Delete old avatar if it exists
@@ -196,5 +206,4 @@ class ProfileController extends Controller
$user->avatar = $filename;
}
}
+23 -25
View File
@@ -2,26 +2,25 @@
namespace App\Http\Controllers;
use App\Helpers\CacheHelper;
use App\Models\Episode;
use App\Models\Gallery;
use App\Models\Hentai;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Models\Watched;
use App\Helpers\CacheHelper;
use hisorange\BrowserDetect\Facade as Browser;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use hisorange\BrowserDetect\Facade as Browser;
use Illuminate\View\View;
class StreamController extends Controller
{
/**
* Display Stream Page.
*/
public function index(Request $request, string $title): \Illuminate\View\View
public function index(Request $request, string $title): View
{
$titleParts = explode('-', $title);
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
@@ -32,12 +31,11 @@ class StreamController extends Controller
}
return view('series.index', [
'hentai' => $hentai,
'hentai' => $hentai,
'popularWeekly' => CacheHelper::getPopularWeekly(),
]);
}
$episode = Episode::where('slug', $title)->firstOrFail();
$gallery = Gallery::where('episode_id', $episode->id)->get();
$moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get();
@@ -54,15 +52,15 @@ class StreamController extends Controller
// Increment Popular Count
$episode->incrementPopularCount();
if (!Auth::guest()) {
if (! Auth::guest()) {
$user = Auth::user();
// Add to user watched list
$time = Carbon::now()->subHour(1);
$alreadyWatched = Watched::where('user_id', $user->id)->where('episode_id', $episode->id)->where('created_at', '>=', $time)->exists();
if (!$alreadyWatched) {
if (! $alreadyWatched) {
Watched::create(['user_id' => $user->id, 'episode_id' => $episode->id]);
cache()->forget('user' . $user->id . 'watched' . $episode->id);
cache()->forget('user'.$user->id.'watched'.$episode->id);
}
}
@@ -81,29 +79,29 @@ class StreamController extends Controller
$playlistEpisodes = $playlist->episodes()->orderBy('position')->get();
// Check if authorized
if ($playlist->is_private && (Auth::guest() || (!Auth::guest() && Auth::user()->id != $playlist->user_id))) {
if ($playlist->is_private && (Auth::guest() || (! Auth::guest() && Auth::user()->id != $playlist->user_id))) {
abort(404);
}
return view('stream.index', [
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'playlist' => $playlist,
'playlistEpisodes' => $playlistEpisodes,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'playlist' => $playlist,
'playlistEpisodes' => $playlistEpisodes,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
]);
}
return view('stream.index', [
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
]);
}
}
+56 -29
View File
@@ -2,7 +2,34 @@
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsBanned;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SetLocale;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
TrustProxies::class,
HandleCors::class,
PreventRequestsDuringMaintenance::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
/**
@@ -30,20 +57,20 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\IsBanned::class,
\App\Http\Middleware\SetLocale::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
IsBanned::class,
SetLocale::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
ThrottleRequests::class.':api',
SubstituteBindings::class,
],
];
@@ -55,18 +82,18 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => AuthenticateSession::class,
'auth.admin' => IsAdmin::class,
'auth.moderator' => IsModerator::class,
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'password.confirm' => RequirePassword::class,
'precognitive' => HandlePrecognitiveRequests::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
];
}
+9 -10
View File
@@ -1,29 +1,28 @@
<?php namespace app\Http\Middleware;
<?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin {
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
{
if (Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR)) {
return $next($request);
}
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
session()->flash('error_msg', 'This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
}
+7 -9
View File
@@ -1,32 +1,30 @@
<?php namespace app\Http\Middleware;
<?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsBanned {
class IsBanned
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
{
if (Auth::check() && Auth::user()->hasRole(UserRole::BANNED)) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('home.banned');
}
return $next($request);
}
}
+6 -5
View File
@@ -3,7 +3,6 @@
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -14,16 +13,18 @@ class IsModerator
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
{
if (Auth::check() && (
Auth::user()->hasRole(UserRole::MODERATOR) ||
Auth::user()->hasRole(UserRole::ADMINISTRATOR))) {
return $next($request);
}
session()->flash('error_msg','This resource is restricted to Administrators!');
session()->flash('error_msg', 'This resource is restricted to Moderators!');
return redirect()->route('home.index');
}
}
@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
+6 -2
View File
@@ -13,19 +13,23 @@ class SetLocale
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// 1. Logged-in user preference
if (Auth::check() && Auth::user()->locale) {
App::setLocale(Auth::user()->locale);
return $next($request);
}
// 2. Session (guest or user override)
if (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
if ($request->session()->has('locale') &&
in_array(session('locale'), config('app.supported_locales'), true)) {
App::setLocale(session('locale'));
return $next($request);
}
+6 -6
View File
@@ -2,15 +2,15 @@
namespace App\Http\Requests\Auth;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use GrantHolle\Altcha\Rules\ValidAltcha;
class LoginRequest extends FormRequest
{
/**
@@ -24,21 +24,21 @@ class LoginRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function authenticate(): void
{
@@ -58,7 +58,7 @@ class LoginRequest extends FormRequest
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class MatrixRegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$isOldEnough = $this->user()->created_at->lt(now()->subMonth());
$noAccount = ! $this->user()->matrix_id;
return $isOldEnough && $noAccount;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'username' => [
'required',
'string',
'min:3',
'max:32',
'regex:/^[a-z0-9._=-]+$/', // Valid Matrix localpart
],
'password' => [
'required',
'string',
'min:8',
'confirmed',
],
];
}
public function messages(): array
{
return [
'username.regex' => 'Username may only contain lowercase letters, numbers and . _ = -',
];
}
}
+3 -2
View File
@@ -3,6 +3,7 @@
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
@@ -21,7 +22,7 @@ class ProfileUpdateRequest extends FormRequest
'nullable',
'image',
'mimes:jpg,png,jpeg,webp,gif',
'max:8192'
'max:8192',
],
'email' => [
'required',
+10 -12
View File
@@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Spatie\DiscordAlerts\Facades\DiscordAlert;
class DiscordReleaseNotification implements ShouldQueue
@@ -32,26 +31,25 @@ class DiscordReleaseNotification implements ShouldQueue
*/
public function handle(): void
{
switch($this->messageType)
{
switch ($this->messageType) {
case 'release':
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'release-censored':
# Because Discord TOS
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release: ".$this->slug." - *No link here because of* :pLoli:");
// Because Discord TOS
DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release: '.$this->slug.' - *No link here because of* :pLoli:');
break;
case 'update':
# 1080p 48fps added
DiscordAlert::to('update')->message("<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// 1080p 48fps added
DiscordAlert::to('update')->message('<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'updateUHD':
# 4k 48fps added
DiscordAlert::to('update')->message("<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// 4k 48fps added
DiscordAlert::to('update')->message('<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'v2':
# v2 re-release
DiscordAlert::to('rerelease')->message("<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// v2 re-release
DiscordAlert::to('rerelease')->message('<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
default:
break;
+7 -6
View File
@@ -7,12 +7,12 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GetFileSizeFromCDN implements ShouldQueue
{
@@ -35,8 +35,9 @@ class GetFileSizeFromCDN implements ShouldQueue
{
// Retrieve the download record, return if not found
$download = Downloads::find($this->downloadId);
if (!$download) {
if (! $download) {
Log::error("Download not found for ID: {$this->downloadId}");
return;
}
@@ -52,7 +53,7 @@ class GetFileSizeFromCDN implements ShouldQueue
try {
// Send HTTP request to the endpoint
$response = Http::get($endpoint . '/getSize/' . $file . '/' . $expire);
$response = Http::get($endpoint.'/getSize/'.$file.'/'.$expire);
// Check if response is successful
if ($response->successful()) {
@@ -67,9 +68,9 @@ class GetFileSizeFromCDN implements ShouldQueue
Log::error("Failed to retrieve size for download ID: {$this->downloadId}, HTTP status: {$response->status()}");
}
} catch (RequestException $e) {
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: " . $e->getMessage());
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: ".$e->getMessage());
} catch (\Exception $e) {
Log::error("An error occurred for download ID: {$this->downloadId}, error: " . $e->getMessage());
Log::error("An error occurred for download ID: {$this->downloadId}, error: ".$e->getMessage());
}
}
}
+1 -2
View File
@@ -5,7 +5,6 @@ namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\DB;
class AdminCommentSearch extends Component
{
@@ -41,7 +40,7 @@ class AdminCommentSearch extends Component
->paginate(12);
return view('livewire.admin-comment-search', [
'comments' => $comments
'comments' => $comments,
]);
}
}
+3 -4
View File
@@ -5,10 +5,9 @@ namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class AdminUserSearch extends Component
{
@@ -41,12 +40,12 @@ class AdminUserSearch extends Component
{
$users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
->paginate(20);
return view('livewire.admin-user-search', [
'users' => $users
'users' => $users,
]);
}
}
+8 -11
View File
@@ -2,12 +2,11 @@
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use App\Models\SiteBackground;
use Illuminate\Support\Carbon;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class BackgroundImages extends Component
{
@@ -19,17 +18,15 @@ class BackgroundImages extends Component
public function render()
{
$now = Carbon::now();
$images = SiteBackground::when($this->filter === 'active', fn ($query) =>
$query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
)
->when($this->filter === 'inactive', fn ($query) =>
$query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
$images = SiteBackground::when($this->filter === 'active', fn ($query) => $query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
)
->when($this->filter === 'inactive', fn ($query) => $query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
)
->paginate(10);
return view('livewire.background-images', [
'images' => $images
'images' => $images,
]);
}
}
+59 -24
View File
@@ -1,20 +1,18 @@
<?php
namespace App\Livewire;
use App\Models\User;
use App\Enums\UserRole;
use App\Models\Episode;
use App\Models\ModLog;
use App\Models\User;
use App\Notifications\CommentNotification;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str;
use Livewire\Component;
use Maize\Markable\Models\Like;
class Comment extends Component
@@ -30,31 +28,31 @@ class Comment extends Component
public $liked = false;
public $replyState = [
'body' => ''
'body' => '',
];
public $isEditing = false;
public $editState = [
'body' => ''
'body' => '',
];
protected $listeners = [
'refresh' => '$refresh'
'refresh' => '$refresh',
];
protected $validationAttributes = [
'replyState.body' => 'reply'
'replyState.body' => 'reply',
];
public function updatedIsEditing($isEditing)
public function updatedIsEditing(bool $isEditing)
{
if (! $isEditing) {
return;
}
$this->editState = [
'body' => $this->comment->body
'body' => $this->comment->body,
];
}
@@ -71,15 +69,50 @@ class Comment extends Component
{
$this->authorize('destroy', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Deleted comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = $user->id;
$this->comment->save();
$this->dispatch('refresh');
return;
}
$this->comment->delete();
$this->dispatch('refresh');
$this->dispatch('refresh');
}
public function restoreComment()
{
$this->authorize('restore', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Restored comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = null;
$this->comment->save();
$this->dispatch('refresh');
}
}
public function postReply()
{
if (!($this->comment->depth() < 2)) {
$this->addError('replyState.body', "Too many sub comments.");
if (! ($this->comment->depth() < 2)) {
$this->addError('replyState.body', 'Too many sub comments.');
return;
}
@@ -91,13 +124,14 @@ class Comment extends Component
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$this->validate([
'replyState.body' => 'required'
'replyState.body' => 'required',
]);
$reply = $this->comment->children()->make($this->replyState);
@@ -110,25 +144,25 @@ class Comment extends Component
if ($reply->commentable_type == Episode::class && $user->id !== $reply->parent->user->id) {
$episode = Episode::where('id', $reply->commentable_id)
->firstOrFail();
$url = route('hentai.index', ['title' => $episode->slug]);
$reply->parent->user->notify(
new CommentNotification(
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$url}#comment-{$reply->id}"
)
);
}
$this->replyState = [
'body' => ''
'body' => '',
];
$this->isReplying = false;
$this->dispatch('refresh')->self();
$this->dispatch('refresh')->self();
}
public function like()
@@ -144,6 +178,7 @@ class Comment extends Component
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
@@ -163,4 +198,4 @@ class Comment extends Component
{
return view('livewire.comment');
}
}
}
+9 -9
View File
@@ -2,11 +2,10 @@
namespace App\Livewire;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\RateLimiter;
class Comments extends Component
{
use WithPagination;
@@ -14,21 +13,21 @@ class Comments extends Component
public $model;
public $newCommentState = [
'body' => ''
'body' => '',
];
protected $validationAttributes = [
'newCommentState.body' => 'comment'
'newCommentState.body' => 'comment',
];
protected $listeners = [
'refresh' => '$refresh'
'refresh' => '$refresh',
];
public function postComment()
{
$this->validate([
'newCommentState.body' => 'required'
'newCommentState.body' => 'required',
]);
$user = auth()->user();
@@ -39,6 +38,7 @@ class Comments extends Component
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
@@ -49,7 +49,7 @@ class Comments extends Component
$comment->save();
$this->newCommentState = [
'body' => ''
'body' => '',
];
$this->resetPage();
@@ -65,7 +65,7 @@ class Comments extends Component
->paginate(50);
return view('livewire.comments', [
'comments' => $comments
'comments' => $comments,
]);
}
}
}
+21 -2
View File
@@ -18,13 +18,32 @@ class DownloadButton extends Component
public $fillNumbers;
public $fileSize;
public $background = 'bg-rose-600';
public $fileExtension = 'HEVC';
public $version = '';
public function mount()
{
if (str_contains($this->downloadUrl, 'AV1')) {
$this->fileExtension = 'AV1';
}
if (str_contains($this->downloadUrl, 'v2')) {
$this->version = 'v2';
}
if (str_contains($this->downloadUrl, 'v3')) {
$this->version = 'v3';
}
}
public function clicked($downloadId)
{
$download = Downloads::find($downloadId);
if (!$download) {
if (! $download) {
return;
}
+6 -6
View File
@@ -5,11 +5,9 @@ namespace App\Livewire;
use App\Models\Episode;
use App\Models\User;
use App\Models\UserDownload;
use Livewire\Component;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class DownloadsFree extends Component
{
@@ -51,6 +49,7 @@ class DownloadsFree extends Component
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
// Already expired
$alreadyDownloaded->delete();
return;
}
@@ -65,6 +64,7 @@ class DownloadsFree extends Component
if ($user->downloads_left <= 0) {
// Daily limit reached
$this->granted = 3;
return;
}
@@ -75,9 +75,9 @@ class DownloadsFree extends Component
$this->granted = 1;
UserDownload::create([
'user_id' => $user->id,
'episode_id' => $this->episodeId,
'interpolated' => $this->interpolated,
'user_id' => $user->id,
'episode_id' => $this->episodeId,
'interpolated' => $this->interpolated,
]);
}
+19 -15
View File
@@ -2,10 +2,11 @@
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Downloads;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class DownloadsSearch extends Component
{
@@ -17,7 +18,7 @@ class DownloadsSearch extends Component
public $order = 'created_at_desc';
public $options = [
'FHD' => true,
'FHD' => true,
'FHD 48fps' => true,
];
@@ -25,24 +26,25 @@ class DownloadsSearch extends Component
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
// To toggle individual option selection
public function toggleOption($option)
{
$this->options[$option] = !$this->options[$option];
$this->options[$option] = ! $this->options[$option];
$this->resetPage();
}
// To toggle dropdown visibility
public function toggleDropdown()
{
$this->isOpen = !$this->isOpen;
$this->isOpen = ! $this->isOpen;
}
protected $queryString = [
'fileSearch' => ['except' => '', 'as' => 'fS'],
'order' => ['except' => '', 'as' => 'order'],
'fileSearch' => ['except' => '', 'as' => 'fS'],
'order' => ['except' => '', 'as' => 'order'],
];
public function updatingFileSearch()
@@ -73,9 +75,9 @@ class DownloadsSearch extends Component
$types[] = 'FHD';
} elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHDi';
}
}
@@ -87,7 +89,7 @@ class DownloadsSearch extends Component
public function clicked($downloadId)
{
$download = Downloads::find($downloadId);
if (!$download) {
if (! $download) {
return;
}
@@ -96,9 +98,9 @@ class DownloadsSearch extends Component
cache()->forget("episode_{$download->episode->id}_download_{$download->type}");
}
public function mount()
public function mount()
{
if (!auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
return;
}
@@ -144,15 +146,17 @@ class DownloadsSearch extends Component
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
->whereIn('type', $this->getSelectedTypes())
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); })))
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
})))
->whereNotNull('size')
->orderBy($orderby, $orderdirection)
->paginate(20);
return view('livewire.downloads-search', [
'downloads' => $downloads,
'query' => $this->fileSearch,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'downloads' => $downloads,
'query' => $this->fileSearch,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
]);
}
}
+2 -2
View File
@@ -4,10 +4,9 @@ namespace App\Livewire;
use App\Models\Episode;
use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
use Maize\Markable\Models\Like;
class LikeButton extends Component
@@ -52,6 +51,7 @@ class LikeButton extends Component
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
+24 -17
View File
@@ -3,10 +3,10 @@
namespace App\Livewire;
use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class LiveSearch extends Component
{
@@ -20,14 +20,17 @@ class LiveSearch extends Component
#[Url(history: true)]
public $tags = [];
public $tagsCopy = [];
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
#[Url(history: true)]
public $blacklist = [];
public $blacklistCopy = [];
#[Url(history: true)]
@@ -72,7 +75,7 @@ class LiveSearch extends Component
public function mount()
{
// User blacklist
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
$this->blacklist = auth()->user()->tag_blacklist;
}
@@ -118,11 +121,15 @@ class LiveSearch extends Component
}
$user_id = Auth::check() ? auth()->user()->id : 0;
$episodes = Episode::with('gallery')->when($this->search != '', fn ($query) => $query->where(function($query) { $query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%'); }))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); }))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$episodes = Episode::with('gallery')->when($this->search != '', fn ($query) => $query->where(function ($query) {
$query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%');
}))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
}))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id);
}))
->when(Auth::guest(), fn ($query) => $query->withoutTags(['loli', 'shota']))
@@ -140,16 +147,16 @@ class LiveSearch extends Component
$this->dispatch('contentChanged');
return view('livewire.live-search', [
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'selectedblacklist' => $this->blacklist,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
]);
}
}
+4 -4
View File
@@ -3,8 +3,8 @@
namespace App\Livewire;
use App\Models\Episode;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class NavLiveSearch extends Component
{
@@ -25,9 +25,9 @@ class NavLiveSearch extends Component
}
return view('livewire.nav-live-search', [
'episodes' => $episodes,
'query' => $this->navSearch,
'hide' => empty($this->navSearch),
'episodes' => $episodes,
'query' => $this->navSearch,
'hide' => empty($this->navSearch),
]);
}
}
+3 -5
View File
@@ -5,13 +5,11 @@ namespace App\Livewire;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Services\PlaylistService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Collection;
class PlaylistOverview extends Component
{
+3 -4
View File
@@ -3,10 +3,9 @@
namespace App\Livewire;
use App\Models\Playlist;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class Playlists extends Component
{
@@ -54,12 +53,12 @@ class Playlists extends Component
$playlists = Playlist::where('is_private', 0)
->withCount('episodes')
->having('episodes_count', '>', 1)
->when($this->search != '', fn($query) => $query->where('name', 'like', '%' . $this->search . '%'))
->when($this->search != '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->orderBy($orderby, $orderdirection)
->paginate($this->pagination);
return view('livewire.playlists', [
'playlists' => $playlists
'playlists' => $playlists,
]);
}
}
+2 -4
View File
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
@@ -36,14 +35,13 @@ class UserComments extends Component
$orderdirection = 'desc';
}
$comments = Comment::where('user_id', $this->model->id)
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
->orderBy($orderby, $orderdirection)
->paginate(10);
->paginate(6);
return view('livewire.user-comments', [
'comments' => $comments
'comments' => $comments,
]);
}
}
+24 -17
View File
@@ -3,10 +3,10 @@
namespace App\Livewire;
use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class UserLikes extends Component
{
@@ -20,14 +20,17 @@ class UserLikes extends Component
#[Url(history: true)]
public $tags = [];
public $tagsCopy = [];
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
#[Url(history: true)]
public $blacklist = [];
public $blacklistCopy = [];
#[Url(history: true)]
@@ -72,7 +75,7 @@ class UserLikes extends Component
public function mount()
{
// User blacklist
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
$this->blacklist = auth()->user()->tag_blacklist;
}
@@ -119,11 +122,15 @@ class UserLikes extends Component
$user_id = Auth::check() ? auth()->user()->id : 0;
$episodes = Episode::whereHasLike(auth()->user())
->when($this->search !== '', fn ($query) => $query->where(function($query) { $query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%'); }))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); }))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
->when($this->search !== '', fn ($query) => $query->where(function ($query) {
$query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%');
}))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
}))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id);
}))
->orderBy($orderby, $orderdirection)
@@ -137,16 +144,16 @@ class UserLikes extends Component
}
return view('livewire.user-likes', [
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'selectedblacklist' => $this->blacklist,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
]);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\User;
use App\Services\SubscriptionService;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Illuminate\Support\Facades\RateLimiter;
class UserSubscription extends Component
{
public $userId = 0;
public $subscriptionKey = '';
public $isActive = false;
protected $rules = [
'subscriptionKey' => 'required|string|size:48',
];
public function mount(User $user)
{
$this->userId = $user ? $user->id : auth()->user()->id;
$this->subscriptionKey = $user->subscription_key ?? '';
$this->isActive = $user->hasRole(UserRole::SUPPORTER) ?? false;
}
public function applyKey(SubscriptionService $subscriptionService)
{
$this->validate();
$rateLimitKey = "apply-subscription:{$this->userId}";
$rateLimitMinutes = 60 * 5; // 5 minutes
// Rate Limit to prevent users trying random keys
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('subscriptionKey', "Too many attempts. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
// Check if token is already being used
$alreadyUsed = User::where('subscription_key', $this->subscriptionKey)
->whereNot('id', $this->userId)
->exists();
if ($alreadyUsed) {
$this->addError('subscriptionKey', 'Key already used!');
return;
}
$user = User::where('id', $this->userId)->firstOrFail();
// Verify token
$success = $subscriptionService->checkSubscriptionStatus($user, $this->subscriptionKey);
if (!$success) {
$this->addError('subscriptionKey', 'Invalid Key! If you believe this is a bug, please report this to the admin!');
return;
}
$user->subscription_key = $this->subscriptionKey;
$user->save();
$this->isActive = true;
}
public function render()
{
return view('livewire.user-subscription');
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Episode;
use Livewire\Component;
class ViewCount extends Component
{
public $episodeId = 0;
public $viewCount = 0;
public function mount(Episode $episode)
{
$this->episodeId = $episode->id;
$this->viewCount = $episode->view_count;
}
public function update()
{
$this->viewCount = Episode::where('id', $this->episodeId)->firstOrFail()->view_count;
}
public function render()
{
return view('livewire.view-count');
}
}
+3 -4
View File
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\Watched as UserWatched;
use App\Models\User;
use Illuminate\Support\Carbon;
use Livewire\Component;
use Livewire\WithPagination;
@@ -14,7 +13,7 @@ class Watched extends Component
public $userId;
public function mount($user)
public function mount($user)
{
$this->userId = $user ? $user->id : auth()->user()->id;
}
@@ -27,8 +26,8 @@ class Watched extends Component
});
return view('livewire.watched', [
'watched' => $watched,
'watchedGrouped' => $watchedGrouped,
'watched' => $watched,
'watchedGrouped' => $watchedGrouped,
]);
}
}
+14 -7
View File
@@ -4,19 +4,18 @@ namespace App\Models;
use App\Models\Presenters\CommentPresenter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
class Comment extends Model
{
use HasFactory, SoftDeletes, Markable;
use HasFactory, Markable, SoftDeletes;
protected static $marks = [
Like::class
Like::class,
];
/**
@@ -25,7 +24,7 @@ class Comment extends Model
* @var string[]
*/
protected $fillable = [
'body'
'body',
];
public function presenter()
@@ -71,6 +70,14 @@ class Comment extends Model
*/
public function likeCount(): int
{
return cache()->remember('commentLikes' . $this->id, 300, fn() => $this->likes->count());
return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count());
}
/**
* Returns wether or not comment has been removed by moderation
*/
public function isDeletedByModerator(): bool
{
return $this->deleted_by_moderator_id !== null;
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ class Downloads extends Model
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
return round($bytes, 2).' '.$units[$i];
}
/**
+30 -35
View File
@@ -2,35 +2,28 @@
namespace App\Models;
use App\Models\Downloads;
use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use Conner\Tagging\Taggable;
use Laravel\Scout\Searchable;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Laravel\Scout\Searchable;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
class Episode extends Model implements Sitemapable
{
use Markable, Taggable;
use HasFactory;
use Markable, Taggable;
use Searchable;
protected static $marks = [
Like::class
Like::class,
];
/**
@@ -49,14 +42,14 @@ class Episode extends Model implements Sitemapable
public function toSearchableArray()
{
return [
'title' => $this->title,
'title_search' => $this->title_search,
'title_jpn' => $this->title_jpn,
'slug' => $this->slug,
'description' => $this->description,
'tags' => $this->tagNames(),
'release_date' => $this->release_date,
'created_at' => $this->created_at,
'title' => $this->title,
'title_search' => $this->title_search,
'title_jpn' => $this->title_jpn,
'slug' => $this->slug,
'description' => $this->description,
'tags' => $this->tagNames(),
'release_date' => $this->release_date,
'created_at' => $this->created_at,
];
}
@@ -125,7 +118,7 @@ class Episode extends Model implements Sitemapable
*/
public function viewCount(): int
{
return cache()->remember('episodeViews' . $this->id, 300, fn() => $this->view_count);
return cache()->remember('episodeViews'.$this->id, 300, fn () => $this->view_count);
}
/**
@@ -141,7 +134,7 @@ class Episode extends Model implements Sitemapable
$index = floor(log($this->viewCount(), 1000));
$shortNumber = $this->viewCount() / pow(1000, $index);
return round($shortNumber, 0) . $units[$index - 1];
return round($shortNumber, 0).$units[$index - 1];
}
/**
@@ -149,7 +142,7 @@ class Episode extends Model implements Sitemapable
*/
public function likeCount(): int
{
return cache()->remember('episodeLikes' . $this->id, 300, fn() => $this->likes->count());
return cache()->remember('episodeLikes'.$this->id, 300, fn () => $this->likes->count());
}
/**
@@ -157,7 +150,7 @@ class Episode extends Model implements Sitemapable
*/
public function commentCount(): int
{
return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count());
return cache()->remember('episodeComments'.$this->id, 300, fn () => $this->comments->count());
}
public function comments()
@@ -180,6 +173,7 @@ class Episode extends Model implements Sitemapable
$problematicResults .= $pTag;
}
return $problematicResults;
}
@@ -202,17 +196,17 @@ class Episode extends Model implements Sitemapable
*/
public function hasAutoTrans(): bool
{
return cache()->remember('mt' . $this->id, 900, fn() => $this->subtitles()->exists());
return cache()->remember('mt'.$this->id, 900, fn () => $this->subtitles()->exists());
}
public function is48Fps(): bool
{
return cache()->remember('48fps' . $this->id, 900, fn() => $this->interpolated);
return cache()->remember('48fps'.$this->id, 900, fn () => $this->interpolated);
}
public function isUHD48Fps(): bool
{
return cache()->remember('48fpsUHD' . $this->id, 900, fn() => $this->interpolated_uhd);
return cache()->remember('48fpsUHD'.$this->id, 900, fn () => $this->interpolated_uhd);
}
public function getResolution(): string
@@ -226,7 +220,7 @@ class Episode extends Model implements Sitemapable
public function userWatched(int $user_id): bool
{
return cache()->remember('user' . $user_id . 'watched' . $this->id, 300, fn() => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
return cache()->remember('user'.$user_id.'watched'.$this->id, 300, fn () => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
}
public function watched(): HasMany
@@ -234,15 +228,16 @@ class Episode extends Model implements Sitemapable
return $this->hasMany(Watched::class);
}
public function getDownloadByType(string $type): Downloads | null
public function getDownloadByType(string $type): ?Downloads
{
$cacheKey = "episode_{$this->id}_download_{$type}";
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) {
return $this->downloads()->where('type', $type)->first();
});
}
public function toSitemapTag(): Url | string | array
public function toSitemapTag(): Url|string|array
{
return Url::create(route('hentai.index', $this->slug))
->setLastModificationDate(Carbon::create($this->created_at));
+8 -9
View File
@@ -2,19 +2,18 @@
namespace App\Models;
use Conner\Tagging\Taggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Conner\Tagging\Taggable;
class Hentai extends Model implements Sitemapable
{
use Taggable;
use HasFactory;
use Taggable;
/**
* The attributes that are mass assignable.
@@ -31,7 +30,7 @@ class Hentai extends Model implements Sitemapable
return $this->hasMany(Episode::class, 'hentai_id');
}
public function title(): String
public function title(): string
{
return $this->episodes->first()->title;
}
@@ -63,7 +62,7 @@ class Hentai extends Model implements Sitemapable
);
}
public function toSitemapTag(): Url | string | array
public function toSitemapTag(): Url|string|array
{
return Url::create(route('hentai.index', $this->slug))
->setLastModificationDate(Carbon::create($this->created_at));
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ModLog extends Model
{
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'moderator',
'data',
];
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Playlist extends Model
{
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PlaylistEpisode extends Model
{
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularDaily extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularMonthly extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularWeekly extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -25,4 +25,4 @@ class CommentPresenter
{
return $this->comment->created_at->diffForHumans();
}
}
}
+3 -3
View File
@@ -16,19 +16,19 @@ class SiteBackground extends Model
protected $fillable = [
'date_start',
'date_end',
'default'
'default',
];
/**
* Returns the current IDs of active wallpaper
*/
public function getImages(): ? \Illuminate\Support\Collection
public function getImages(): ?Collection
{
$now = Carbon::now();
$byDates = $this->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)->get()->pluck('id');
$default = $this->where('default', true)->get()->pluck('id');
return $byDates->isEmpty() ? $default : $byDates;
}
}
+2 -2
View File
@@ -3,13 +3,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Studios extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
+18 -14
View File
@@ -2,20 +2,21 @@
namespace App\Models;
//use Illuminate\Contracts\Auth\MustVerifyEmail;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys;
class User extends Authenticatable
class User extends Authenticatable implements HasPasskeys
{
use HasFactory, Notifiable;
use HasFactory, InteractsWithPasskeys, Notifiable;
/**
* The attributes that are mass assignable.
@@ -30,6 +31,7 @@ class User extends Authenticatable
// Discord
'discord_id',
'discord_avatar',
'subscription_key',
];
/**
@@ -40,6 +42,7 @@ class User extends Authenticatable
protected $hidden = [
'password',
'remember_token',
'subscription_key',
];
/**
@@ -62,7 +65,6 @@ class User extends Authenticatable
'discord_avatar' => 'string',
];
/**
* Has Many Playlists.
*/
@@ -100,7 +102,7 @@ class User extends Authenticatable
*/
public function commentCount(): int
{
return cache()->remember('userComments' . $this->id, 300, fn() => $this->comments->count());
return cache()->remember('userComments'.$this->id, 300, fn () => $this->comments->count());
}
/**
@@ -108,13 +110,11 @@ class User extends Authenticatable
*/
public function getAvatar(): string
{
if ($this->discord_id && $this->discord_avatar && !$this->avatar)
{
if ($this->discord_id && $this->discord_avatar && ! $this->avatar) {
return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}";
}
if ($this->avatar)
{
if ($this->avatar) {
return Storage::url($this->avatar);
}
@@ -153,11 +153,15 @@ class User extends Authenticatable
*/
public function removeRole(UserRole $role): void
{
if (!$this->hasRole($role)) {
if (! $this->hasRole($role)) {
return;
}
$this->roles = array_diff($this->roles, [$role->value]);
$this->roles = collect($this->roles)
->reject(fn ($value) => $value === $role->value)
->values()
->all();
$this->save();
}
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserDownload extends Model
{
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Watched extends Model
{
+5 -5
View File
@@ -3,8 +3,6 @@
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class CommentNotification extends Notification
@@ -12,7 +10,9 @@ class CommentNotification extends Notification
use Queueable;
protected $type;
protected $message;
protected $url;
/**
@@ -38,9 +38,9 @@ class CommentNotification extends Notification
public function toDatabase($notifiable)
{
return [
'type' => $this->type,
'message' => $this->message,
'url' => $this->url,
'type' => $this->type,
'message' => $this->message,
'url' => $this->url,
];
}
}
+23 -2
View File
@@ -2,8 +2,9 @@
namespace App\Policies;
use App\Models\User;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommentPolicy
@@ -17,6 +18,26 @@ class CommentPolicy
public function destroy(User $user, Comment $comment): bool
{
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return $user->id === $comment->user_id;
}
}
public function restore(User $user, Comment $comment): bool
{
// Comment not deleted
if ($comment->deleted_by_moderator_id === null) {
return false;
}
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return false;
}
}
+4 -2
View File
@@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use SocialiteProviders\Discord\Provider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class AppServiceProvider extends ServiceProvider
{
@@ -20,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite('discord', Provider::class);
});
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Rules;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\Challenge;
use AltchaOrg\Altcha\ChallengeParameters;
use AltchaOrg\Altcha\Payload;
use AltchaOrg\Altcha\Solution;
use AltchaOrg\Altcha\VerifySolutionOptions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;
/**
* Validation rule to verify captcha solution.
*/
class ValidCaptcha implements ValidationRule
{
/**
* Altcha instance.
*/
protected Altcha $altcha;
/**
* Pbkdf2 algorithm instance.
*/
protected Pbkdf2 $pbkdf2;
/**
* Class constructor.
*/
public function __construct()
{
$this->pbkdf2 = new Pbkdf2;
$this->altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
}
/**
* Parse payload and return the decoded data as an array.
*/
private function parsePayload(string $value): ?array
{
$decoded = base64_decode($value, true);
if ($decoded === false) {
return null;
}
$payload = json_decode($decoded, true);
if (! is_array($payload)) {
return null;
}
return $payload;
}
/**
* Verify if payload has required fields.
*/
private function verifyFields(array $payload): bool
{
if (! isset($payload['challenge'], $payload['solution'])) {
return false;
}
if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
return false;
}
return true;
}
/**
* Create a Challenge object from challenge data.
*/
private function createChallenge(array $challengeData): Challenge
{
return new Challenge(
ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
$challengeData['signature'] ?? null,
);
}
/**
* Create a Solution object from solution data.
*/
private function createSolution(array $solutionData): Solution
{
return new Solution(
counter: (int) ($solutionData['counter'] ?? 0),
derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
);
}
/**
* Run the validation rule.
*
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$payload = $this->parsePayload($value);
if (! $payload) {
$fail('Invalid captcha.');
return;
}
if (! $this->verifyFields($payload)) {
$fail('Invalid captcha.');
return;
}
$challenge = $this->createChallenge($payload['challenge']);
$solution = $this->createSolution($payload['solution']);
$result = $this->altcha->verifySolution(new VerifySolutionOptions(
algorithm: $this->pbkdf2,
payload: new Payload($challenge, $solution),
));
if (! $result->verified) {
$fail('Invalid captcha.');
}
}
}
+7 -9
View File
@@ -2,11 +2,9 @@
namespace App\Services;
use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads;
use App\Models\Episode;
use App\Jobs\GetFileSizeFromCDN;
use Illuminate\Http\Request;
class DownloadService
@@ -14,10 +12,10 @@ class DownloadService
public function createOrUpdateDownloads(Request $request, Episode $episode, int $index): void
{
$downloadTypes = [
'episodedlurl' => 'FHD',
'episodedlurlinterpolated' => 'FHDi',
'episodedlurl4k' => 'UHD',
'downloadUHDi' => 'UHDi',
'episodedlurl' => 'FHD',
'episodedlurlinterpolated' => 'FHDi',
'episodedlurl4k' => 'UHD',
'downloadUHDi' => 'UHDi',
];
foreach ($downloadTypes as $inputField => $type) {
@@ -25,9 +23,9 @@ class DownloadService
if ($request->filled($fieldName)) {
$download = Downloads::updateOrCreate([
'episode_id' => $episode->id,
'type' => $type,
'type' => $type,
], [
'url' => $request->input($fieldName),
'url' => $request->input($fieldName),
]);
// Dispatch Job to get File Size from CDN
+92 -18
View File
@@ -5,14 +5,13 @@ namespace App\Services;
use App\Models\Episode;
use App\Models\Hentai;
use App\Models\Studios;
use App\Models\ModLog;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\Support\Str;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class EpisodeService
{
@@ -24,6 +23,7 @@ class EpisodeService
if (is_numeric($lastPart) && $lastPart < 1000) {
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
return implode('-', $slugParts);
}
@@ -31,16 +31,15 @@ class EpisodeService
}
public function createEpisode(
Request $request,
Request $request,
Hentai $hentai,
int $episodeNumber,
?Studios $studio = null,
?Episode $referenceEpisode = null
): Episode
{
$episode = new Episode();
): Episode {
$episode = new Episode;
$episode->title = $referenceEpisode->title ?? $request->input('title');
$episode->title_search = preg_replace("/[^A-Za-z0-9 ]/", '', $episode->title);
$episode->title_search = preg_replace('/[^A-Za-z0-9 ]/', '', $episode->title);
$episode->title_jpn = $referenceEpisode->title_jpn ?? $request->input('title_jpn');
$episode->slug = "{$hentai->slug}-{$episodeNumber}";
$episode->hentai_id = $hentai->id;
@@ -64,6 +63,68 @@ class EpisodeService
return $episode;
}
private function applyTags(Request $request, Episode $episode): void
{
$tags = json_decode($request->input('tags'));
$newtags = [];
foreach ($tags as $t) {
$newtags[] = $t->value;
}
$newTagsTemp = $newtags;
$oldTagsTemp = $episode->tagNames();
sort($newTagsTemp);
sort($oldTagsTemp);
if ($newTagsTemp !== $oldTagsTemp) {
ModLog::create([
'moderator' => $request->user()->name,
'data' => sprintf(
'Updated Episode tags from %s to %s',
implode(', ', $oldTagsTemp),
implode(', ', $newTagsTemp),
),
]);
}
$episode->retag($newtags);
}
private function updateTitle(Request $request, Episode $episode): void
{
$updates = [];
if ($episode->title !== $request->input('title')) {
$updates['title'] = $request->input('title');
$updates['title_search'] = preg_replace(
'/[^A-Za-z0-9 ]/',
'',
$request->input('title')
);
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updating Hentai Title from {$episode->title} to {$request->input('title')}",
]);
}
if ($episode->title_jpn !== $request->input('title_jpn')) {
$updates['title_jpn'] = $request->input('title_jpn');
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updating Hentai Title from {$episode->title_jpn} to {$request->input('title_jpn')}",
]);
}
if (! empty($updates)) {
$episode->hentai->episodes()->update($updates);
}
}
public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
{
$episode = Episode::where('id', $episodeId)->firstOrFail();
@@ -77,17 +138,31 @@ class EpisodeService
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
$episode->save();
// Tagging
$tags = json_decode($request->input('tags'));
$newtags = [];
foreach ($tags as $t) {
$newtags[] = $t->value;
}
$episode->retag($newtags);
$this->applyTags($request, $episode);
$this->updateTitle($request, $episode);
return $episode;
}
public function updateEpisodeModerator(Request $request, int $episodeId): void
{
$episode = Episode::where('id', $episodeId)->firstOrFail();
$oldDescription = $episode->description;
$episode->description = $request->input('description');
$episode->save();
if ($episode->description !== $oldDescription) {
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updated Episode description from {$oldDescription} to {$episode->description}",
]);
}
$this->applyTags($request, $episode);
$this->updateTitle($request, $episode);
}
public function getOrCreateStudio(string $studioName): Studios
{
return Studios::firstOrCreate(
@@ -96,7 +171,6 @@ class EpisodeService
);
}
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
{
if (! $request->hasFile("episodecover{$episodeNumber}")) {
@@ -111,7 +185,7 @@ class EpisodeService
// Encode and save cover image
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
->cover(268, 394)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($episode->cover_url));
}
}
+6 -10
View File
@@ -5,29 +5,25 @@ namespace App\Services;
use App\Models\Episode;
use App\Models\Gallery;
use App\Models\Hentai;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Laravel\Facades\Image;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class GalleryService
{
public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void
{
$galleryInputNumber = $override ? 1 : $episodeNumber;
if($request->hasFile('episodegallery'.$galleryInputNumber)) {
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
$this->deleteOldGallery($episode);
$this->createGalleryFolder($hentai);
$counter = 0;
foreach($request->file('episodegallery'.$galleryInputNumber) as $file) {
foreach ($request->file('episodegallery'.$galleryInputNumber) as $file) {
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
$this->saveGalleryImage($gallery, $file);
$counter += 1;
@@ -45,7 +41,7 @@ class GalleryService
private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery
{
$gallery = new Gallery();
$gallery = new Gallery;
$gallery->hentai_id = $hentai->id;
$gallery->episode_id = $episode->id;
$gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp";
@@ -59,12 +55,12 @@ class GalleryService
{
Image::read($sourceImage->getRealPath())
->cover(1920, 1080)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->image_url));
Image::read($sourceImage->getRealPath())
->cover(960, 540)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->thumbnail_url));
}
@@ -0,0 +1,49 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class MatrixRegistrationService
{
public function registerUser(string $username, string $password)
{
$server = config('services.matrix.server');
$secret = config('services.matrix.shared_secret');
// Get nonce from Synapse
$nonceResponse = Http::get("$server/_synapse/admin/v1/register");
if (! $nonceResponse->ok()) {
throw new \Exception('Could not fetch nonce from Matrix.');
}
$nonce = $nonceResponse->json()['nonce'];
// Generate MAC
$mac = hash_hmac(
'sha1',
$nonce."\0".
$username."\0".
$password."\0".
'notadmin',
$secret
);
// Send registration request
$response = Http::post("$server/_synapse/admin/v1/register", [
'nonce' => $nonce,
'username' => $username,
'password' => $password,
'admin' => false,
'mac' => $mac,
]);
if ($response->failed()) {
$error = $response->json()['error'] ?? $response->body();
throw new \Exception($error);
}
return $response->json();
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Services;
use App\Enums\UserRole;
use App\Models\User;
use Exception;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class SubscriptionService
{
private function generateEncryptedPayload(string $subscriptionKey): string
{
return base64_encode(Crypt::encryptString(json_encode([
'subscription_access_key' => $subscriptionKey,
'timestamp' => now()->timestamp,
'nonce' => Str::uuid()->toString(),
], JSON_THROW_ON_ERROR)));
}
/**
* Gets the subscription status from the subscription service.
*/
private function getSubscriptionStatus(string $subscriptionKey): array | null
{
try {
$payload = $this->generateEncryptedPayload($subscriptionKey);
$response = Http::post(config('services.subscription_service_host').'/api/membership/verify', [
'payload' => $payload,
]);
if (! $response->successful()) {
logger()->error('Subscription Service API error', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
$encryptedResponse = $response->json('payload');
$json = json_decode(
Crypt::decryptString($encryptedResponse),
true,
flags: JSON_THROW_ON_ERROR
);
return $json;
} catch (Exception $e) {
logger()->error('getSubscriptionStatus Exception', [
'details' => $e,
]);
}
return null;
}
public function checkSubscriptionStatus(User $user, string $subscriptionKey): bool
{
$subscriptionStatus = $this->getSubscriptionStatus($subscriptionKey);
if (!$subscriptionStatus) {
return false;
}
if ($subscriptionStatus['valid'] === true &&
$subscriptionStatus['active'] === true) {
$user->addRole(UserRole::SUPPORTER);
return true;
}
$user->removeRole(UserRole::SUPPORTER);
return true;
}
}
+9 -4
View File
@@ -1,5 +1,10 @@
<?php
use App\Exceptions\Handler;
use App\Http\Kernel;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application;
/*
|--------------------------------------------------------------------------
| Create The Application
@@ -11,7 +16,7 @@
|
*/
$app = new Illuminate\Foundation\Application(
$app = new Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
Kernel::class
);
$app->singleton(
@@ -37,8 +42,8 @@ $app->singleton(
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
ExceptionHandler::class,
Handler::class
);
/*
+2 -1
View File
@@ -9,7 +9,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"grantholle/laravel-altcha": "^2.1",
"altcha-org/altcha": "^2.0",
"guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2",
@@ -28,6 +28,7 @@
"rtconner/laravel-tagging": "^5.0",
"socialiteproviders/discord": "^4.2",
"spatie/laravel-discord-alerts": "^1.8",
"spatie/laravel-passkeys": "^1.7",
"spatie/laravel-sitemap": "^7.3"
},
"require-dev": {
Generated
+1806 -752
View File
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -1,7 +1,12 @@
<?php
use App\Providers\AppServiceProvider;
use App\Providers\AuthServiceProvider;
use App\Providers\EventServiceProvider;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
use Mews\Captcha\Facades\Captcha;
return [
@@ -116,7 +121,7 @@ return [
| Supported Locales
|--------------------------------------------------------------------------
|
| This is used to display the supported locales by this app, it also is
| This is used to display the supported locales by this app, it also is
| used to verify session data and requests in the SetLocale Middleware
|
*/
@@ -175,11 +180,11 @@ return [
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
AppServiceProvider::class,
AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
EventServiceProvider::class,
RouteServiceProvider::class,
])->toArray(),
/*
@@ -195,7 +200,7 @@ return [
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
'Captcha' => Mews\Captcha\Facades\Captcha::class,
'Captcha' => Captcha::class,
])->toArray(),
];
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
use App\Models\User;
return [
/*
@@ -62,7 +64,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
'model' => User::class,
],
// 'users' => [
+9
View File
@@ -0,0 +1,9 @@
<?php
/**
* Altcha Captcha System
*/
return [
'hmac_key' => env('ALTCHA_HMAC_KEY'),
];
+5 -3
View File
@@ -1,12 +1,14 @@
<?php
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;
return [
/*
* The webhook URLs that we'll use to send a message to Discord.
*/
'webhook_urls' => [
'default' => env('DISCORD_ALERT_WEBHOOK'),
'update' => env('DISCORD_ALERT_UPDATE_WEBHOOK'),
'default' => env('DISCORD_ALERT_WEBHOOK'),
'update' => env('DISCORD_ALERT_UPDATE_WEBHOOK'),
'rerelease' => env('DISCORD_ALERT_RERELEASE_WEBHOOK'),
],
@@ -14,5 +16,5 @@ return [
* This job will send the message to Discord. You can extend this
* job to set timeouts, retries, etc...
*/
'job' => Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob::class,
'job' => SendToDiscordChannelJob::class,
];
+3 -3
View File
@@ -1,9 +1,9 @@
<?php
return [
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
'guild_id' => 802233383710228550,
'guild_id' => 802233383710228550,
'patreon_roles' => [
'841798154999169054', // ????
@@ -12,7 +12,7 @@ return [
'803325441942356059', // Tier-3
'803322725576736858', // Tier-2
'802270568912519198', // Tier-1
'802234830384267315' // admin
'802234830384267315', // admin
],
'discord_bot_token' => env('DISCORD_BOT_TOKEN'),
+18 -18
View File
@@ -2,42 +2,42 @@
return [
// Download Domain
'download_domain' => [
'https://imoto-ddl.ane-h.xyz',
'https://chibi-ddl.imoto-h.xyz',
'https://koneko-ddl.musume-h.xyz',
'download_domain' => [
'https://imoto-ddl.ane-h.xyz',
'https://chibi-ddl.imoto-h.xyz',
'https://koneko-ddl.musume-h.xyz',
'https://shinobu-ddl.rorikon-h.xyz',
'https://oppai-ddl.shoujo-h.org',
],
// 4k Download Domain
'download_domain_4k' => [
'https://imoto-ddlp.ane-h.xyz',
'https://chibi-ddlp.imoto-h.xyz',
'https://koneko-ddlp.musume-h.xyz',
'download_domain_4k' => [
'https://imoto-ddlp.ane-h.xyz',
'https://chibi-ddlp.imoto-h.xyz',
'https://koneko-ddlp.musume-h.xyz',
'https://shinobu-ddlp.rorikon-h.xyz',
'https://oppai-ddlp.shoujo-h.org',
],
// Stream Domain
'stream_domain' => [
'https://imoto-str.ane-h.xyz',
'https://chibi-str.imoto-h.xyz',
'https://koneko-str.musume-h.xyz',
'stream_domain' => [
'https://imoto-str.ane-h.xyz',
'https://chibi-str.imoto-h.xyz',
'https://koneko-str.musume-h.xyz',
'https://shinobu-str.rorikon-h.xyz',
'https://oppai-str.shoujo-h.org',
],
// Asia Fallback (HTTP)
'asia_download_domain' => [
'https://komako-b-ddl.musume-h.xyz'
'asia_download_domain' => [
'https://komako-b-ddl.musume-h.xyz',
],
'asia_stream_domain' => [
'https://komako-b-str.musume-h.xyz'
'asia_stream_domain' => [
'https://komako-b-str.musume-h.xyz',
],
// Free 4k Downloads
'free_downloads' => true,
'free_downloads_count' => 10,
'free_downloads' => true,
'free_downloads_count' => 10,
];
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
@@ -16,7 +18,7 @@ return [
|
*/
'driver' => \Intervention\Image\Drivers\Gd\Driver::class,
'driver' => Driver::class,
/*
|--------------------------------------------------------------------------
@@ -38,5 +40,5 @@ return [
'autoOrientation' => true,
'decodeAnimation' => true,
'blendingColor' => 'ffffff',
]
],
];
+49
View File
@@ -0,0 +1,49 @@
<?php
use App\Models\User;
use Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction;
use Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction;
use Spatie\LaravelPasskeys\Actions\StorePasskeyAction;
use Spatie\LaravelPasskeys\Models\Passkey;
return [
/*
* After a successful authentication attempt using a passkey
* we'll redirect to this URL.
*/
'redirect_to_after_login' => '/',
/*
* These class are responsible for performing core tasks regarding passkeys.
* You can customize them by creating a class that extends the default, and
* by specifying your custom class name here.
*/
'actions' => [
'generate_passkey_register_options' => GeneratePasskeyRegisterOptionsAction::class,
'store_passkey' => StorePasskeyAction::class,
'generate_passkey_authentication_options' => GeneratePasskeyAuthenticationOptionsAction::class,
'find_passkey' => FindPasskeyToAuthenticateAction::class,
'configure_ceremony_step_manager_factory' => ConfigureCeremonyStepManagerFactoryAction::class,
],
/*
* These properties will be used to generate the passkey.
*/
'relying_party' => [
'name' => config('app.name'),
'id' => parse_url(config('app.url'), PHP_URL_HOST),
'icon' => null,
],
/*
* The models used by the package.
*
* You can override this by specifying your own models
*/
'models' => [
'passkey' => Passkey::class,
'authenticatable' => env('AUTH_MODEL', User::class),
],
];

Some files were not shown because too many files have changed in this diff Show More