135 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 af739e3c88 Remove old comments repo in composer.json 2026-01-18 18:49:59 +01:00
w33b 273ed65a8d Remove laravel sail 2026-01-18 18:44:59 +01:00
w33b ccfd5b996b Replace captcha system 2026-01-18 18:37:08 +01:00
w33b e5ef197ed6 Add user roles system 2026-01-16 23:14:47 +01:00
w33b c0be2e294a Refactor routes 2026-01-16 23:01:34 +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
w33b 823a284fbc Fix PHP 8.4 deprecation warning 2026-01-11 16:39:14 +01:00
w33b 67e601d0c4 Add ability to properly set locale (session/account) 2026-01-11 16:33:13 +01:00
w33b 7e4ebd91ad Remove vluzrmos/language-detector package 2026-01-11 16:31:16 +01:00
w33b 4dc5dee2b9 Update dependencies 2026-01-11 15:46:33 +01:00
w33b 5310908b0c Update login button on mobile 2026-01-11 00:21:05 +01:00
w33b 4b05b3db6d Fix cache not flushed after comment delete by admin 2026-01-10 23:24:31 +01:00
w33b df47a926e4 Fix comment mass delete 2026-01-10 23:21:29 +01:00
w33b 1e9e95f35f Fix admin comment moderation 2026-01-10 22:35:35 +01:00
w33b 2aa76baafd Fix account deletion anonymizing comments 2026-01-10 22:35:21 +01:00
w33b aa50bb1f72 Merge pull request 'Replace Comment System' (#4) from comment-system into main
Reviewed-on: w33b/hstream#4
2026-01-10 21:16:55 +00:00
w33b dfedf4058e Fix rate limit and make it more strict (1 message in 5 minutes) 2026-01-10 22:15:50 +01:00
w33b 268e3eb4c2 Add Notification for comments 2026-01-10 22:00:09 +01:00
w33b ab61574956 Fix comment depth chain check 2026-01-10 21:59:53 +01:00
w33b 81038b6c26 Add id to comment, so it can autoscroll to that notification 2026-01-10 21:59:08 +01:00
w33b e949ba955a Add rate limiter to comment system 2026-01-10 21:04:26 +01:00
w33b 819e2fde27 Misc changes 2026-01-10 20:33:35 +01:00
w33b 3259e2197b Update design comments home page 2026-01-10 19:45:19 +01:00
w33b b133db0573 Add likes to comments 2026-01-10 19:41:23 +01:00
w33b 41c34e6d89 Fix style 2026-01-10 19:15:32 +01:00
w33b db6da608aa Add comments to home page 2026-01-10 19:11:28 +01:00
w33b 13b70fdf23 Misc changes 2026-01-10 18:55:53 +01:00
w33b cfd6af59fb Add Profile Comment Search (Livewire) 2026-01-10 18:55:47 +01:00
w33b 7810cd53fb Add comments to Hentai 2026-01-10 18:54:48 +01:00
w33b 871028930b Migrate existing comments 2026-01-10 16:41:06 +01:00
w33b 6ce0255764 Remove ring offset 2026-01-10 15:45:52 +01:00
w33b e136e8e1b6 Refresh on delete 2026-01-10 15:45:41 +01:00
w33b a3b66b483b Add admin and donator badge 2026-01-10 15:34:05 +01:00
w33b 4c2a6024d7 Add dark mode 2026-01-10 15:27:37 +01:00
w33b 5f575024e2 Add Livewire comment system 2026-01-10 15:02:14 +01:00
w33b 67f5d0db8b Remove laravelista/comments 2026-01-10 14:06:00 +01:00
w33b 571bf4584c Remove view_count from meilisearch 2026-01-10 12:27:54 +01:00
w33b d7dc96e11c Don't trigger update on view_count increase 2026-01-10 12:27:24 +01:00
w33b 58426b6e4e Add studio filter on download page closes #1 2026-01-09 22:51:15 +01:00
w33b 53b600daea Fix certain livewire components not working 2026-01-09 22:32:16 +01:00
w33b 224cdbcdc5 Save mute state of player - fixes #2 2026-01-09 22:28:53 +01:00
w33b 972d3d0aa4 Add zhentube.com to footer 2026-01-09 22:20:02 +01:00
w33b 8f7f012c14 Merge pull request 'Replace Auth System' (#3) from auth-redo into main
Reviewed-on: w33b/hstream#3
2026-01-09 15:11:36 +00:00
w33b c0b068de58 Misc changes 2026-01-09 13:01:53 +01:00
w33b 51c67bb797 Improve Migrations & Fix Discord Avatars 2026-01-09 10:45:41 +01:00
w33b 3d78f9e524 Optionally update discord avatar on login 2026-01-08 22:17:00 +01:00
w33b 2d28a37463 Don't set password on new account with oauth 2026-01-08 20:03:24 +01:00
w33b ac853920ee Fix delete account function & delete modal 2026-01-08 19:28:44 +01:00
w33b fb3722036a Add ability to set custom avatar 2026-01-08 18:47:31 +01:00
w33b ab4e7c7999 Update Mail Design (Password Reset) 2026-01-08 17:21:03 +01:00
w33b 8f99718058 Allow changing email, username and password 2026-01-08 16:14:35 +01:00
w33b 2029af334c Fix Signup redirect 2026-01-08 16:13:54 +01:00
w33b b1c48830c4 Remove nickname 2026-01-08 16:13:43 +01:00
w33b e100f3bf23 Remove Public Profile Page (because usernames are not unique) 2026-01-07 20:28:40 +01:00
w33b c13d443696 Add discord patreon check 2026-01-07 19:04:51 +01:00
w33b 8e7a56f559 Add discord oauth 2026-01-07 18:17:46 +01:00
w33b 30777a6968 Login / Register Design 2026-01-07 17:03:57 +01:00
w33b 256af435ad Add Auth System (Breeze) 2026-01-07 16:04:23 +01:00
w33b e972f8db41 Rename column names 2026-01-07 12:54:10 +01:00
w33b 98d36d6018 Fix database structure 2026-01-07 12:41:11 +01:00
w33b 7eea8285ca Remove old breeze auth controllers 2026-01-07 12:10:49 +01:00
w33b 9e8efbbe05 Remove jakyeru/larascord 2026-01-07 12:02:02 +01:00
w33b 5461606857 DMCA 2025-12-19 22:13:50 +01:00
w33b 9ca2f73714 Admin: Add comments overview for moderation 2025-10-29 15:59:43 +01:00
w33b 59d63abd79 Admin: Add ability to delete all coments from user 2025-10-28 16:20:40 +01:00
w33b efb3e4197b Use cached values for view and like count in nav search 2025-10-26 21:25:50 +01:00
w33b 735dd693ca [Expiremental] Add ability to disable blur effects 2025-10-24 23:15:52 +02:00
w33b 36f0126a21 Only display last 28 days on stats page (excluding current day) 2025-10-23 21:13:07 +02:00
w33b 50d8704560 Fix mobile home page offset 2025-10-23 19:33:30 +02:00
w33b 7e382ffe1d Redesign nav search 2025-10-23 15:39:07 +02:00
w33b 6a25fd2700 Use meilisearch in nav search 2025-10-23 15:30:32 +02:00
w33b 71bcf277f6 Add type icons to downloads search page 2025-10-15 19:33:37 +02:00
w33b 6c44d83e6b Add type filter to download search page 2025-10-15 18:50:49 +02:00
323 changed files with 12411 additions and 7205 deletions
+5
View File
@@ -57,3 +57,8 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SCOUT_QUEUE=true
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=masterKey
+11 -7
View File
@@ -2,13 +2,13 @@
## hstream Website ## hstream Website
### Install ### Install (Ubuntu)
```bash ```bash
# Install PHP # Install PHP
sudo add-apt-repository ppa:ondrej/php sudo add-apt-repository ppa:ondrej/php
apt update && apt upgrade 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 # Install NodeJS
curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
@@ -22,12 +22,16 @@ mv composer.phar composer
# Install NGINX (skip for local dev) # Install NGINX (skip for local dev)
apt install nginx apt install nginx
apt install php8.3-fpm apt install php8.4-fpm
# Install MariaDB # Install MariaDB
apt install mariadb-server apt install mariadb-server
sudo mysql_secure_installation sudo mysql_secure_installation
# Install Meilisearch
echo "deb [trusted=yes] https://apt.fury.io/meilisearch/ /" | sudo tee /etc/apt/sources.list.d/fury.list
sudo apt update && sudo apt install meilisearch
# Clone Repo # Clone Repo
cd /var/www cd /var/www
git clone https://gitea.hstream.moe/w33b/hstream.git git clone https://gitea.hstream.moe/w33b/hstream.git
@@ -50,7 +54,7 @@ nano /etc/supervisor/conf.d/laravel-queue.conf :
[program:laravel-queue] [program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d 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 autostart=true
autorestart=true autorestart=true
stopasgroup=true stopasgroup=true
@@ -79,9 +83,9 @@ zip -r hstream_2023_11_30.zip hstream/
### Update ### Update
```bash ```bash
php artisan down php84 artisan down
git pull git pull
npm run build npm run build
php artisan view:clear && php artisan optimize:clear && php artisan cache:clear && service php8.4-fpm restart php84 artisan view:clear && php84 artisan optimize:clear && php84 artisan cache:clear && service php8.4-fpm restart
php artisan up php84 artisan up
``` ```
+5 -6
View File
@@ -3,11 +3,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\PopularDaily; use App\Models\PopularDaily;
use App\Models\PopularWeekly;
use App\Models\PopularMonthly; use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class AutoStats extends Command class AutoStats extends Command
{ {
@@ -30,9 +29,9 @@ class AutoStats extends Command
*/ */
public function handle() public function handle()
{ {
PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->forceDelete(); PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->delete();
PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->forceDelete(); PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->delete();
PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->forceDelete(); PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->delete();
$this->comment('Automated Purge Stats Complete'); $this->comment('Automated Purge Stats Complete');
} }
+1 -2
View File
@@ -4,7 +4,6 @@ namespace App\Console\Commands;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Hentai; use App\Models\Hentai;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Sitemap;
@@ -17,7 +16,7 @@ class GenerateSitemap extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'sitemap:generate'; protected $signature = 'app:generate-sitemap';
/** /**
* The console command description. * The console command description.
+1 -2
View File
@@ -2,9 +2,8 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Downloads;
use App\Jobs\GetFileSizeFromCDN; use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class GetFileSize extends Command class GetFileSize extends Command
+2 -3
View File
@@ -4,9 +4,8 @@ namespace App\Console\Commands;
use App\Models\User; use App\Models\User;
use App\Models\UserDownload; use App\Models\UserDownload;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class ResetUserDownloads extends Command class ResetUserDownloads extends Command
{ {
@@ -35,6 +34,6 @@ class ResetUserDownloads extends Command
// Clear old downloads which have expired // Clear old downloads which have expired
UserDownload::where('created_at', '<=', Carbon::now()->subHour(6)) UserDownload::where('created_at', '<=', Carbon::now()->subHour(6))
->forceDelete(); ->delete();
} }
} }
@@ -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);
}
}
});
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserRole: string
{
case ADMINISTRATOR = 'admin';
case MODERATOR = 'moderator';
case SUPPORTER = 'supporter';
case BANNED = 'banned';
}
+19 -17
View File
@@ -2,23 +2,23 @@
namespace App\Helpers; namespace App\Helpers;
use App\Models\Comment;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\PopularDaily;
use App\Models\PopularMonthly; use App\Models\PopularMonthly;
use App\Models\PopularWeekly; use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use Conner\Tagging\Model\Tag; use Conner\Tagging\Model\Tag;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class CacheHelper class CacheHelper
{ {
public static function getRecentlyReleased(bool $guest) public static function getRecentlyReleased(bool $guest)
{ {
$guestString = $guest ? 'guest' : 'authed'; $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') return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota'])) ->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('release_date', 'desc') ->orderBy('release_date', 'desc')
@@ -30,7 +30,8 @@ class CacheHelper
public static function getRecentlyUploaded(bool $guest) public static function getRecentlyUploaded(bool $guest)
{ {
$guestString = $guest ? 'guest' : 'authed'; $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') return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota'])) ->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
@@ -41,21 +42,21 @@ class CacheHelper
public static function getTotalViewCount() 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'); return Episode::sum('view_count');
}); });
} }
public static function getTotalEpisodeCount() 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(); return Episode::count();
}); });
} }
public static function getTotalHentaiCount() 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(); return Hentai::count();
}); });
} }
@@ -63,7 +64,8 @@ class CacheHelper
public static function getPopularAllTime(bool $guest) public static function getPopularAllTime(bool $guest)
{ {
$guestString = $guest ? 'guest' : 'authed'; $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') return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota'])) ->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('view_count', 'desc') ->orderBy('view_count', 'desc')
@@ -74,7 +76,7 @@ class CacheHelper
public static function getPopularMonthly() 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') return PopularMonthly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total')) ->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery') ->with('episode.gallery')
@@ -86,7 +88,7 @@ class CacheHelper
public static function getPopularWeekly() 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') return PopularWeekly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total')) ->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery') ->with('episode.gallery')
@@ -99,7 +101,7 @@ class CacheHelper
public static function getPopularDaily() 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') return PopularDaily::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total')) ->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery') ->with('episode.gallery')
@@ -111,22 +113,22 @@ class CacheHelper
public static function getMostLikes() 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(); 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() 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(); return Tag::where('count', '>', 0)->orderBy('slug', 'ASC')->get();
}); });
} }
public static function getLatestComments() public static function getLatestComments()
{ {
return Cache::remember("latest_comments", now()->addMinutes(60), function () { return Cache::remember('latest_comments', now()->addMinutes(60), function () {
return DB::table('comments')->latest()->take(10)->get(); 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() public static function shortCommit()
{ {
return Cache::remember("git_commit", now()->addMinutes(60), function () { return Cache::remember('git_commit', now()->addMinutes(60), function () {
try { try {
return trim(exec('git rev-parse --short HEAD')); return trim(exec('git rev-parse --short HEAD'));
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Alert; use App\Models\Alert;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View;
class AlertController extends Controller class AlertController extends Controller
{ {
/** /**
* Display alert index page * Display alert index page
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('admin.alert.index'); return view('admin.alert.index');
} }
@@ -19,7 +21,7 @@ class AlertController extends Controller
/** /**
* Create Alert. * Create Alert.
*/ */
public function store(Request $request): \Illuminate\Http\RedirectResponse public function store(Request $request): RedirectResponse
{ {
$validated = $request->validate([ $validated = $request->validate([
'message' => 'required|string|max:255', 'message' => 'required|string|max:255',
@@ -39,9 +41,9 @@ class AlertController extends Controller
/** /**
* Delete Alert. * Delete Alert.
*/ */
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse public function delete(int $alert_id): RedirectResponse
{ {
Alert::where('id', $alert_id)->forceDelete(); Alert::where('id', $alert_id)->delete();
cache()->forget('alerts'); cache()->forget('alerts');
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class CommentsController extends Controller
{
/**
* Display Comments Page.
*/
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\Http\Controllers\Controller;
use App\Models\Contact; use App\Models\Contact;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ContactController extends Controller class ContactController extends Controller
{ {
/** /**
* Display Contact Page. * Display Contact Page.
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
$contacts = Contact::orderBy('created_at', 'DESC')->get(); $contacts = Contact::orderBy('created_at', 'DESC')->get();
return view('admin.contact.index', [ return view('admin.contact.index', [
'contacts' => $contacts 'contacts' => $contacts,
]); ]);
} }
/** /**
* Delete Contact. * Delete Contact.
*/ */
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse public function delete(int $contact_id): RedirectResponse
{ {
Contact::where('id', $contact_id)->delete(); Contact::where('id', $contact_id)->delete();
@@ -2,19 +2,22 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Jobs\DiscordReleaseNotification; use App\Jobs\DiscordReleaseNotification;
use App\Models\Episode;
use App\Services\DownloadService; use App\Services\DownloadService;
use App\Services\EpisodeService; use App\Services\EpisodeService;
use App\Services\GalleryService; use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class EpisodeController extends Controller class EpisodeController extends Controller
{ {
protected EpisodeService $episodeService; protected EpisodeService $episodeService;
protected GalleryService $galleryService; protected GalleryService $galleryService;
protected DownloadService $downloadService; protected DownloadService $downloadService;
public function __construct( public function __construct(
@@ -30,7 +33,7 @@ class EpisodeController extends Controller
/** /**
* Add Episode to existing series * 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(); $referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1; $episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
@@ -43,7 +46,7 @@ class EpisodeController extends Controller
// Discord Alert // Discord Alert
if ($request->has('censored')) { if ($request->has('censored')) {
DiscordReleaseNotification::dispatch($referenceEpisode->title." - ".$episodeNumber, 'release-censored'); DiscordReleaseNotification::dispatch($referenceEpisode->title.' - '.$episodeNumber, 'release-censored');
} else { } else {
DiscordReleaseNotification::dispatch($episode->slug, 'release'); DiscordReleaseNotification::dispatch($episode->slug, 'release');
} }
@@ -51,16 +54,27 @@ class EpisodeController extends Controller
cache()->flush(); cache()->flush();
return to_route('hentai.index', [ return to_route('hentai.index', [
'title' => $episode->slug 'title' => $episode->slug,
]); ]);
} }
/** /**
* Edit Episode * 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(); $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); $studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
$oldinterpolated = $episode->interpolated; $oldinterpolated = $episode->interpolated;
@@ -87,7 +101,7 @@ class EpisodeController extends Controller
cache()->flush(); cache()->flush();
return to_route('hentai.index', [ return to_route('hentai.index', [
'title' => $episode->slug 'title' => $episode->slug,
]); ]);
} }
} }
@@ -3,18 +3,21 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Hentai;
use App\Jobs\DiscordReleaseNotification; use App\Jobs\DiscordReleaseNotification;
use App\Models\Hentai;
use App\Services\DownloadService; use App\Services\DownloadService;
use App\Services\EpisodeService; use App\Services\EpisodeService;
use App\Services\GalleryService; use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View;
class ReleaseController extends Controller class ReleaseController extends Controller
{ {
protected EpisodeService $episodeService; protected EpisodeService $episodeService;
protected GalleryService $galleryService; protected GalleryService $galleryService;
protected DownloadService $downloadService; protected DownloadService $downloadService;
public function __construct( public function __construct(
@@ -30,7 +33,7 @@ class ReleaseController extends Controller
/** /**
* Display release page * Display release page
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('admin.release.create'); return view('admin.release.create');
} }
@@ -38,7 +41,7 @@ class ReleaseController extends Controller
/** /**
* Upload New Hentai with One or Multipe Episodes * 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 // Create new Hentai or find existing one
$slug = $this->episodeService->generateSlug($request->input('title')); $slug = $this->episodeService->generateSlug($request->input('title'));
@@ -3,22 +3,22 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\SiteBackground; use App\Models\SiteBackground;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Intervention\Image\Laravel\Facades\Image; use Illuminate\View\View;
use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class SiteBackgroundController extends Controller class SiteBackgroundController extends Controller
{ {
/** /**
* Display admin index page * Display admin index page
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('admin.background.index', [ return view('admin.background.index', [
'images' => SiteBackground::all(), 'images' => SiteBackground::all(),
@@ -28,7 +28,7 @@ class SiteBackgroundController extends Controller
/** /**
* Create new site backgrounds * Create new site backgrounds
*/ */
public function create(Request $request): \Illuminate\Http\RedirectResponse public function create(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'images' => 'required', 'images' => 'required',
@@ -44,7 +44,7 @@ class SiteBackgroundController extends Controller
$bg = SiteBackground::create(array_merge( $bg = SiteBackground::create(array_merge(
$request->only(['date_start', 'date_end']), $request->only(['date_start', 'date_end']),
[ [
'default' => (bool) $request->input('default', false) 'default' => (bool) $request->input('default', false),
] ]
)); ));
@@ -55,13 +55,14 @@ class SiteBackgroundController extends Controller
Image::read($file->getRealPath()) Image::read($file->getRealPath())
->scaleDown(height: $resolution) ->scaleDown(height: $resolution)
->encode(new WebpEncoder()) ->encode(new WebpEncoder)
->save(public_path($targetPath)); ->save(public_path($targetPath));
} }
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error($e->getMessage()); Log::error($e->getMessage());
return redirect()->back(); return redirect()->back();
} }
@@ -74,7 +75,7 @@ class SiteBackgroundController extends Controller
return redirect()->back(); return redirect()->back();
} }
public function update(Request $request): \Illuminate\Http\RedirectResponse public function update(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'id' => 'required|exists:site_backgrounds,id', 'id' => 'required|exists:site_backgrounds,id',
@@ -85,7 +86,7 @@ class SiteBackgroundController extends Controller
SiteBackground::where('id', $request->input('id'))->update(array_merge( SiteBackground::where('id', $request->input('id'))->update(array_merge(
$request->only(['date_start', 'date_end']), $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 * Delete backround
*/ */
public function delete(Request $request): \Illuminate\Http\RedirectResponse public function delete(Request $request): RedirectResponse
{ {
$id = $request->input('id'); $id = $request->input('id');
@@ -105,7 +106,7 @@ class SiteBackgroundController extends Controller
DB::beginTransaction(); DB::beginTransaction();
$bg = SiteBackground::where('id', $id)->firstOrFail(); $bg = SiteBackground::where('id', $id)->firstOrFail();
$bg->forceDelete(); $bg->delete();
$resolutions = [1440, 1080, 720, 640]; $resolutions = [1440, 1080, 720, 640];
try { try {
@@ -116,6 +117,7 @@ class SiteBackgroundController extends Controller
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error($e->getMessage()); Log::error($e->getMessage());
return redirect()->back(); return redirect()->back();
} }
@@ -2,10 +2,11 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Episode; use App\Models\Episode;
use App\Models\EpisodeSubtitle; use App\Models\EpisodeSubtitle;
use App\Models\Subtitle; use App\Models\Subtitle;
use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class SubtitleController extends Controller class SubtitleController extends Controller
@@ -13,7 +14,7 @@ class SubtitleController extends Controller
/** /**
* Add new Subtitle. * Add new Subtitle.
*/ */
public function store(Request $request): \Illuminate\Http\RedirectResponse public function store(Request $request): RedirectResponse
{ {
$subtitle = Subtitle::create([ $subtitle = Subtitle::create([
'name' => $request->name, 'name' => $request->name,
@@ -32,13 +33,13 @@ class SubtitleController extends Controller
/** /**
* Update Episode Subtitles. * 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(); $episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
// Clear everything // Clear everything
foreach ($episode->subtitles as $sub) { foreach ($episode->subtitles as $sub) {
$sub->forceDelete(); $sub->delete();
} }
if (! $request->input('subtitles')) { if (! $request->input('subtitles')) {
@@ -2,16 +2,18 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Models\User; use App\Enums\UserRole;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller class UserController extends Controller
{ {
/** /**
* Display Users Page. * Display Users Page.
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('admin.users.index'); return view('admin.users.index');
} }
@@ -26,16 +28,15 @@ class UserController extends Controller
'action' => 'required', 'action' => 'required',
]); ]);
$user = User::findOrFail($validated['id']); $user = User::findOrFail($validated['id']);
switch ($validated['action']) { switch ($validated['action']) {
case 'ban': case 'ban':
$user->update(['is_banned' => 1]); $user->addRole(UserRole::BANNED);
alert()->success('Banned', 'User has been banned.'); alert()->success('Banned', 'User has been banned.');
break; break;
case 'unban': case 'unban':
$user->update(['is_banned' => 0]); $user->removeRole(UserRole::BANNED);
alert()->success('Unbanned', 'User has been unbanned.'); alert()->success('Unbanned', 'User has been unbanned.');
break; break;
default: default:
@@ -3,10 +3,10 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper; use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Studios; use App\Models\Studios;
use App\Models\Subtitle; use App\Models\Subtitle;
use App\Http\Controllers\Controller;
class AdminApiController extends Controller class AdminApiController extends Controller
{ {
@@ -2,11 +2,11 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Downloads; use App\Models\Downloads;
use App\Models\Episode; use App\Models\Episode;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class DownloadApiController extends Controller class DownloadApiController extends Controller
{ {
@@ -16,11 +16,12 @@ class DownloadApiController extends Controller
public function getDownload(Request $request) public function getDownload(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
'episode_id' => 'required', 'episode_id' => ['required'],
'captcha' => 'required|captcha' 'captcha' => ['required', new ValidCaptcha],
]); ]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail(); $episode = Episode::where('id', $request->input('episode_id'))
->firstOrFail();
// Increase download count, as we assume the user // Increase download count, as we assume the user
// downloads after submitting the captcha // downloads after submitting the captcha
@@ -2,11 +2,11 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\PopularMonthly; use App\Models\PopularMonthly;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class HentaiApiController extends Controller class HentaiApiController extends Controller
{ {
@@ -46,6 +46,8 @@ class HentaiApiController extends Controller
// Cache for 60 minutes // Cache for 60 minutes
$data = Cache::remember('api_monthly_views', now()->addMinutes(60), function () { $data = Cache::remember('api_monthly_views', now()->addMinutes(60), function () {
return PopularMonthly::selectRaw('DATE(created_at) as date, COUNT(*) as count') return PopularMonthly::selectRaw('DATE(created_at) as date, COUNT(*) as count')
->whereDate('created_at', '<', Carbon::today())
->whereDate('created_at', '>=', Carbon::today()->subDays(28))
->groupBy('date') ->groupBy('date')
->orderBy('date', 'asc') ->orderBy('date', 'asc')
->get(); ->get();
@@ -3,9 +3,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Episode; use App\Models\Episode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class StreamApiController extends Controller class StreamApiController extends Controller
@@ -30,10 +28,10 @@ class StreamApiController extends Controller
'poster' => $episode->gallery()->first()->image_url, 'poster' => $episode->gallery()->first()->image_url,
'interpolated' => $episode->interpolated, 'interpolated' => $episode->interpolated,
'interpolated_uhd' => $episode->interpolated_uhd, 'interpolated_uhd' => $episode->interpolated_uhd,
'stream_url' => $episode->url, 'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
'stream_domains' => config('hstream.stream_domain'), 'stream_domains' => config('hstream.stream_domain'),
'asia_stream_domains' => config('hstream.asia_stream_domain'), 'asia_stream_domains' => config('hstream.asia_stream_domain'),
'extra_subtitles' => $subtitles 'extra_subtitles' => $subtitles,
], 200); ], 200);
} }
} }
@@ -4,9 +4,8 @@ namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper; use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Conner\Tagging\Model\Tag; use Conner\Tagging\Model\Tag;
use Illuminate\Http\Request;
class UserApiController extends Controller class UserApiController extends Controller
{ {
@@ -33,11 +32,10 @@ class UserApiController extends Controller
} }
} }
return response()->json([ return response()->json([
'message' => 'success', 'message' => 'success',
'tags' => $tagWhiteList, 'tags' => $tagWhiteList,
'usertags' => $tagBlackList 'usertags' => $tagBlackList,
], 200); ], 200);
} }
} }
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest; use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -29,7 +28,7 @@ class AuthenticatedSessionController extends Controller
$request->session()->regenerate(); $request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME); return redirect()->intended(route('home.index', absolute: false));
} }
/** /**
@@ -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,7 +3,6 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -36,6 +35,6 @@ class ConfirmablePasswordController extends Controller
$request->session()->put('auth.password_confirmed_at', time()); $request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(RouteServiceProvider::HOME); return redirect()->intended(route('home.index', absolute: false));
} }
} }
@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\Facades\Socialite;
class DiscordAuthController extends Controller
{
/**
* Redirect to Discord
*/
public function redirect(): RedirectResponse
{
return Socialite::driver('discord')->redirect();
}
/**
* Callback received from Discord
*/
public function callback(): RedirectResponse
{
$discordUser = Socialite::driver('discord')->user();
$user = User::where('discord_id', $discordUser->id)->first();
if (! $user) {
// link by email if it already exists
$user = User::where('email', $discordUser->email)->first();
if ($user) {
$user->update([
'discord_id' => $discordUser->id,
'discord_avatar' => $discordUser->avatar,
]);
} else {
// Create new user
$user = User::create([
'name' => $discordUser->name,
'email' => $discordUser->email,
'discord_id' => $discordUser->id,
'discord_avatar' => $discordUser->avatar,
'password' => null,
]);
}
}
$this->checkDiscordAvatar($discordUser, $user);
$this->checkDiscordRoles($user);
Auth::login($user, true);
return redirect()->route('home.index');
}
/**
* Check if discord avatar changed
*/
private function checkDiscordAvatar(\Laravel\Socialite\Contracts\User $socialiteUser, User $user): void
{
if ($socialiteUser->avatar != $user->discord_avatar) {
$user->update(['discord_avatar' => $socialiteUser->avatar]);
}
}
/**
* Check Discord Roles if user is Patreon member
*/
private function checkDiscordRoles(User $user): void
{
// Should not ever happen
if (! $user->discord_id) {
return;
}
$guildId = config('discord.guild_id');
$response = Http::withToken(config('discord.discord_bot_token'), 'Bot')
->timeout(5)
->get("https://discord.com/api/v10/guilds/{$guildId}/members/{$user->discord_id}");
// User is not in the guild
if ($response->status() === 404) {
$user->removeRole(UserRole::SUPPORTER);
return;
}
// Something else failed
if ($response->failed()) {
Log::warning('Discord role check failed', [
'user_id' => $user->id,
'discord_id' => $user->discord_id,
'status' => $response->status(),
'body' => $response->body(),
]);
return;
}
$discordRoles = $response->json('roles', []);
$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));
if (! $hasSupporterRole) {
// Remove role if not found
$user->removeRole(UserRole::SUPPORTER);
return;
}
$user->addRole(UserRole::SUPPORTER);
}
}
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -15,7 +14,7 @@ class EmailVerificationNotificationController extends Controller
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
if ($request->user()->hasVerifiedEmail()) { if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME); return redirect()->intended(route('home.index', absolute: false));
} }
$request->user()->sendEmailVerificationNotification(); $request->user()->sendEmailVerificationNotification();
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
@@ -16,7 +15,7 @@ class EmailVerificationPromptController extends Controller
public function __invoke(Request $request): RedirectResponse|View public function __invoke(Request $request): RedirectResponse|View
{ {
return $request->user()->hasVerifiedEmail() return $request->user()->hasVerifiedEmail()
? redirect()->intended(RouteServiceProvider::HOME) ? redirect()->intended(route('home.index', absolute: false))
: view('auth.verify-email'); : view('auth.verify-email');
} }
} }
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -10,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rules; use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View; use Illuminate\View\View;
class NewPasswordController extends Controller class NewPasswordController extends Controller
@@ -25,7 +27,7 @@ class NewPasswordController extends Controller
/** /**
* Handle an incoming new password request. * Handle an incoming new password request.
* *
* @throws \Illuminate\Validation\ValidationException * @throws ValidationException
*/ */
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
@@ -40,7 +42,7 @@ class NewPasswordController extends Controller
// database. Otherwise we will parse the error and return the response. // database. Otherwise we will parse the error and return the response.
$status = Password::reset( $status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'), $request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) { function (User $user) use ($request) {
$user->forceFill([ $user->forceFill([
'password' => Hash::make($request->password), 'password' => Hash::make($request->password),
'remember_token' => Str::random(60), 'remember_token' => Str::random(60),
@@ -15,6 +15,19 @@ class PasswordController extends Controller
*/ */
public function update(Request $request): RedirectResponse 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)) {
$validated = $request->validateWithBag('updatePassword', [
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
$validated = $request->validateWithBag('updatePassword', [ $validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'], 'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'], 'password' => ['required', Password::defaults(), 'confirmed'],
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View; use Illuminate\View\View;
class PasswordResetLinkController extends Controller class PasswordResetLinkController extends Controller
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
/** /**
* Handle an incoming password reset link request. * Handle an incoming password reset link request.
* *
* @throws \Illuminate\Validation\ValidationException * @throws ValidationException
*/ */
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
@@ -4,36 +4,29 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules; use Illuminate\Validation\Rules;
use Illuminate\View\View; use Illuminate\Validation\ValidationException;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
{ {
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/** /**
* Handle an incoming registration request. * Handle an incoming registration request.
* *
* @throws \Illuminate\Validation\ValidationException * @throws ValidationException
*/ */
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidCaptcha],
]); ]);
$user = User::create([ $user = User::create([
@@ -46,6 +39,6 @@ class RegisteredUserController extends Controller
Auth::login($user); Auth::login($user);
return redirect(RouteServiceProvider::HOME); return redirect(route('home.index', absolute: false));
} }
} }
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified; use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -16,13 +15,13 @@ class VerifyEmailController extends Controller
public function __invoke(EmailVerificationRequest $request): RedirectResponse public function __invoke(EmailVerificationRequest $request): RedirectResponse
{ {
if ($request->user()->hasVerifiedEmail()) { if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); return redirect()->intended(route('home.index', absolute: false).'?verified=1');
} }
if ($request->user()->markEmailAsVerified()) { if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user())); event(new Verified($request->user()));
} }
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); return redirect()->intended(route('home.index', absolute: false).'?verified=1');
} }
} }
+7 -9
View File
@@ -3,14 +3,17 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Contact; use App\Models\Contact;
use App\Rules\ValidCaptcha;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View;
class ContactController extends Controller class ContactController extends Controller
{ {
/** /**
* Display Contact Page. * Display Contact Page.
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('contact.form'); return view('contact.form');
} }
@@ -18,17 +21,17 @@ class ContactController extends Controller
/** /**
* Store Contact Submission. * Store Contact Submission.
*/ */
public function store(Request $request): \Illuminate\Http\RedirectResponse public function store(Request $request): RedirectResponse
{ {
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|max:30', 'name' => 'required|max:30',
'email' => 'required|max:50', 'email' => 'required|max:50',
'message' => 'required|max:1000', 'message' => 'required|max:1000',
'subject' => 'required|max:50', 'subject' => 'required|max:50',
'captcha' => 'required|captcha', 'altcha' => ['required', new ValidCaptcha],
]); ]);
$contact = new Contact(); $contact = new Contact;
$contact->name = $request->input('name'); $contact->name = $request->input('name');
$contact->email = $request->input('email'); $contact->email = $request->input('email');
$contact->message = $request->input('message'); $contact->message = $request->input('message');
@@ -37,9 +40,4 @@ class ContactController extends Controller
return back()->with('status', 'contact-submitted'); return back()->with('status', 'contact-submitted');
} }
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
{
return response()->json(['captcha'=> captcha_img()]);
}
} }
+19 -17
View File
@@ -2,32 +2,32 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Episode;
use App\Helpers\CacheHelper; use App\Helpers\CacheHelper;
use App\Models\Episode;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cookie; use Illuminate\View\View;
class HomeController extends Controller class HomeController extends Controller
{ {
/** /**
* Display Home Page. * Display Home Page.
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
$guest = Auth::guest(); $guest = Auth::guest();
$guestString = $guest ? 'guest' : 'authed'; $guestString = $guest ? 'guest' : 'authed';
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () => $mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () => Episode::with('gallery')
Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota'])) ->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->whereIn('id', function ($query) { ->whereIn('id', function ($query) {
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray(); $mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
$query->selectRaw('id') $query->selectRaw('id')
->from('episodes') ->from('episodes')
->whereIn('id', $mostLikesIds) ->whereIn('id', $mostLikesIds)
->orderByRaw("FIELD(id, " . implode(',', $mostLikesIds) . ")"); ->orderByRaw('FIELD(id, '.implode(',', $mostLikesIds).')');
}) })
->get() ->get()
); );
@@ -47,7 +47,7 @@ class HomeController extends Controller
/** /**
* Display Banned Page. * Display Banned Page.
*/ */
public function banned(): \Illuminate\View\View public function banned(): View
{ {
return view('auth.banned'); return view('auth.banned');
} }
@@ -56,7 +56,7 @@ class HomeController extends Controller
* Redirects to a random Hentai episode * Redirects to a random Hentai episode
* Done due to performance reasons * Done due to performance reasons
*/ */
public function random(): \Illuminate\Http\RedirectResponse public function random(): RedirectResponse
{ {
$random = Episode::inRandomOrder() $random = Episode::inRandomOrder()
->limit(1) ->limit(1)
@@ -71,7 +71,7 @@ class HomeController extends Controller
/** /**
* Display Search Page. * Display Search Page.
*/ */
public function search(): \Illuminate\View\View public function search(): View
{ {
return view('search.index'); return view('search.index');
} }
@@ -79,7 +79,7 @@ class HomeController extends Controller
/** /**
* Display Download Search Page. * Display Download Search Page.
*/ */
public function downloadSearch(): \Illuminate\View\View public function downloadSearch(): View
{ {
return view('search.download'); return view('search.download');
} }
@@ -87,7 +87,7 @@ class HomeController extends Controller
/** /**
* Redirect POST Data to GET with Query String. * 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', [ return redirect()->route('hentai.search', [
'search' => $request->input('live-search'), 'search' => $request->input('live-search'),
@@ -97,7 +97,7 @@ class HomeController extends Controller
/** /**
* Display Stats Page. * Display Stats Page.
*/ */
public function stats(): \Illuminate\View\View public function stats(): View
{ {
return view('home.stats', [ return view('home.stats', [
'viewCount' => CacheHelper::getTotalViewCount(), 'viewCount' => CacheHelper::getTotalViewCount(),
@@ -109,13 +109,15 @@ class HomeController extends Controller
/** /**
* Manually set website language * Manually set website language
*/ */
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse public function updateLanguage(Request $request): RedirectResponse
{ {
if(! in_array($request->language, config('lang-detector.languages'))) { abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
return redirect()->back();
}
Cookie::queue(Cookie::forever('locale', $request->language)); session(['locale' => $request->language]);
if (Auth::check()) {
Auth::user()->update(['locale' => $request->language]);
}
return redirect()->back(); return redirect()->back();
} }
+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,15 +2,16 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\View\View;
class NotificationController extends Controller class NotificationController extends Controller
{ {
/** /**
* Display the user's notification page. * Display the user's notification page.
*/ */
public function index(Request $request): \Illuminate\View\View public function index(Request $request): View
{ {
return view('profile.notifications', [ return view('profile.notifications', [
'user' => $request->user(), 'user' => $request->user(),
@@ -21,7 +22,7 @@ class NotificationController extends Controller
/** /**
* Delete Notifcation * Delete Notifcation
*/ */
public function delete(Request $request): \Illuminate\Http\RedirectResponse public function delete(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'id' => 'required|exists:notifications,id', 'id' => 'required|exists:notifications,id',
@@ -32,7 +33,7 @@ class NotificationController extends Controller
->where('id', $request->input('id')) ->where('id', $request->input('id'))
->firstOrFail(); ->firstOrFail();
$notification->forceDelete(); $notification->delete();
return redirect()->back(); return redirect()->back();
} }
+31 -26
View File
@@ -6,8 +6,10 @@ use App\Models\Episode;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\PlaylistEpisode; use App\Models\PlaylistEpisode;
use App\Services\PlaylistService; use App\Services\PlaylistService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use RealRashid\SweetAlert\Facades\Alert; use Illuminate\View\View;
class PlaylistController extends Controller class PlaylistController extends Controller
{ {
@@ -21,7 +23,7 @@ class PlaylistController extends Controller
/** /**
* Display the public playlists page. * Display the public playlists page.
*/ */
public function index(): \Illuminate\View\View public function index(): View
{ {
return view('playlist.index'); return view('playlist.index');
} }
@@ -29,7 +31,7 @@ class PlaylistController extends Controller
/** /**
* Display public playlist. * 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); abort(404);
@@ -42,14 +44,13 @@ class PlaylistController extends Controller
]); ]);
} }
/** /**
* Display the user's playlists page. * Display the user's playlists page.
*/ */
public function playlists(Request $request): \Illuminate\View\View public function playlists(Request $request): View
{ {
$title = 'Delete Playlist!'; $title = 'Delete Playlist!';
$text = "Are you sure you want to delete?"; $text = 'Are you sure you want to delete?';
confirmDelete($title, $text); confirmDelete($title, $text);
return view('profile.playlists', [ return view('profile.playlists', [
@@ -61,7 +62,7 @@ class PlaylistController extends Controller
/** /**
* Display user's playlist. * 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); abort(404);
@@ -79,13 +80,13 @@ class PlaylistController extends Controller
/** /**
* Create user playlist (Form). * Create user playlist (Form).
*/ */
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse public function createPlaylist(Request $request): RedirectResponse
{ {
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|max:30', 'name' => 'required|max:30',
]); ]);
$playlist = new Playlist(); $playlist = new Playlist;
$playlist->user_id = $request->user()->id; $playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name'); $playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private'; $playlist->is_private = $request->input('visiblity') === 'private';
@@ -97,7 +98,7 @@ class PlaylistController extends Controller
/** /**
* Delete user playlist. * 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); abort(404);
@@ -105,13 +106,11 @@ class PlaylistController extends Controller
$user = $request->user(); $user = $request->user();
$playlist = Playlist::where('user_id', $user->id)->where('id', $playlist_id)->firstOrFail(); $playlist = Playlist::where('user_id', $user->id)
->where('id', $playlist_id)
->firstOrFail();
// Delete Playlist Episodes $playlist->delete();
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
// Delete Playlist
$playlist->forceDelete();
return to_route('profile.playlists'); return to_route('profile.playlists');
} }
@@ -119,7 +118,7 @@ class PlaylistController extends Controller
/** /**
* Delete episode from playlist. * 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([ return response()->json([
@@ -128,8 +127,14 @@ class PlaylistController extends Controller
], 404); ], 404);
} }
$playlist = Playlist::where('user_id', $request->user()->id)->where('id', (int) $request->input('playlist'))->firstOrFail(); $playlist = Playlist::where('user_id', $request->user()->id)
PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', (int) $request->input('episode'))->forceDelete(); ->where('id', (int) $request->input('playlist'))
->firstOrFail();
PlaylistEpisode::where('playlist_id', $playlist->id)
->where('episode_id', (int) $request->input('episode'))
->delete();
$this->playlistService->reorderPositions($playlist); $this->playlistService->reorderPositions($playlist);
return response()->json([ return response()->json([
@@ -141,13 +146,13 @@ class PlaylistController extends Controller
/** /**
* Add to user playlist (API). * Add to user playlist (API).
*/ */
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse public function addPlaylistApi(Request $request): JsonResponse
{ {
$user = $request->user(); $user = $request->user();
$validated = $request->validate([ $validated = $request->validate([
'playlist' => 'required|max:30', 'playlist' => 'required|max:30',
'episode_id' => 'required' 'episode_id' => 'required',
]); ]);
$playlist = Playlist::where('user_id', $user->id)->where('id', $request->input('playlist'))->firstOrFail(); $playlist = Playlist::where('user_id', $user->id)->where('id', $request->input('playlist'))->firstOrFail();
@@ -157,7 +162,7 @@ class PlaylistController extends Controller
$exists = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->exists(); $exists = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->exists();
if ($exists) { if ($exists) {
return response()->json([ return response()->json([
'message' => 'already-added' 'message' => 'already-added',
], 200); ], 200);
} }
@@ -171,20 +176,20 @@ class PlaylistController extends Controller
]); ]);
return response()->json([ return response()->json([
'message' => 'success' 'message' => 'success',
], 200); ], 200);
} }
/** /**
* Create user playlist (API). * Create user playlist (API).
*/ */
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse public function createPlaylistApi(Request $request): JsonResponse
{ {
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|max:30', 'name' => 'required|max:30',
]); ]);
$playlist = new Playlist(); $playlist = new Playlist;
$playlist->user_id = $request->user()->id; $playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name'); $playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private'; $playlist->is_private = $request->input('visiblity') === 'private';
@@ -192,7 +197,7 @@ class PlaylistController extends Controller
return response()->json([ return response()->json([
'message' => 'success', 'message' => 'success',
'playlist_id' => $playlist->id 'playlist_id' => $playlist->id,
], 200); ], 200);
} }
} }
+95 -16
View File
@@ -2,22 +2,27 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Episode;
use App\Http\Requests\ProfileUpdateRequest; use App\Http\Requests\ProfileUpdateRequest;
use App\Models\Episode;
use Illuminate\Http\Request; use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Conner\Tagging\Model\Tag; use Conner\Tagging\Model\Tag;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
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;
class ProfileController extends Controller class ProfileController extends Controller
{ {
/** /**
* Display the user page. * Display the user page.
*/ */
public function index(Request $request): \Illuminate\View\View public function index(Request $request): View
{ {
return view('profile.index', [ return view('profile.index', [
'user' => $request->user(), 'user' => $request->user(),
@@ -27,7 +32,7 @@ class ProfileController extends Controller
/** /**
* Display the user's settings form. * Display the user's settings form.
*/ */
public function settings(Request $request): \Illuminate\View\View public function settings(Request $request): View
{ {
$example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first(); $example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first();
@@ -37,10 +42,33 @@ class ProfileController extends Controller
]); ]);
} }
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$user = $request->user();
// Fill everything except the image
$user->fill($request->safe()->except('image'));
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
if ($request->hasFile('image')) {
$this->storeAvatar($request->file('image'), $user);
}
$user->save();
return Redirect::route('profile.settings')->with('status', 'profile-updated');
}
/** /**
* Display the user's watched page. * Display the user's watched page.
*/ */
public function watched(Request $request): \Illuminate\View\View public function watched(Request $request): View
{ {
return view('profile.watched', [ return view('profile.watched', [
'user' => $request->user(), 'user' => $request->user(),
@@ -50,7 +78,7 @@ class ProfileController extends Controller
/** /**
* Display the user's comments page. * Display the user's comments page.
*/ */
public function comments(Request $request): \Illuminate\View\View public function comments(Request $request): View
{ {
return view('profile.comments', [ return view('profile.comments', [
'user' => $request->user(), 'user' => $request->user(),
@@ -60,17 +88,27 @@ class ProfileController extends Controller
/** /**
* Display the user's likes page. * Display the user's likes page.
*/ */
public function likes(Request $request): \Illuminate\View\View public function likes(Request $request): View
{ {
return view('profile.likes', [ 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(),
]);
}
/** /**
* Update user settings. * Update user settings.
*/ */
public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse public function saveSettings(Request $request): RedirectResponse
{ {
$user = $request->user(); $user = $request->user();
$user->search_design = $request->input('searchDesign') == 'thumbnail'; $user->search_design = $request->input('searchDesign') == 'thumbnail';
@@ -84,7 +122,7 @@ class ProfileController extends Controller
/** /**
* Update user tag blacklist. * Update user tag blacklist.
*/ */
public function saveBlacklist(Request $request): \Illuminate\Http\RedirectResponse public function saveBlacklist(Request $request): RedirectResponse
{ {
$user = $request->user(); $user = $request->user();
$tags = json_decode($request->input('tags')); $tags = json_decode($request->input('tags'));
@@ -92,6 +130,7 @@ class ProfileController extends Controller
if (! $tags) { if (! $tags) {
$user->tag_blacklist = null; $user->tag_blacklist = null;
$user->save(); $user->save();
return Redirect::route('profile.settings')->with('status', 'blacklist-updated'); return Redirect::route('profile.settings')->with('status', 'blacklist-updated');
} }
@@ -110,21 +149,61 @@ class ProfileController extends Controller
/** /**
* Delete the user's account. * 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)) {
$request->validateWithBag('userDeletion', [ $request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'], 'password' => ['required', 'current_password'],
]); ]);
}
$user = $request->user(); // Update comments to deleted user
DB::table('comments')->where('user_id', '=', $user->id)->update(['user_id' => 1]);
// Delete Profile Picture
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
Auth::logout(); Auth::logout();
$user->delete(); $user->delete();
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
cache()->flush();
return Redirect::to('/'); return Redirect::to('/');
} }
/**
* Store custom user avatar.
*/
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');
}
// Delete old avatar if it exists
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
$filename = "images/avatars/{$user->id}.webp";
$image = Image::read($file->getRealPath())
->cover(128, 128)
->toWebp(quality: 85);
Storage::disk('public')->put($filename, $image);
$user->avatar = $filename;
}
} }
+4 -6
View File
@@ -2,26 +2,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\CacheHelper;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Gallery; use App\Models\Gallery;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\PlaylistEpisode; use App\Models\PlaylistEpisode;
use App\Models\Watched; use App\Models\Watched;
use App\Helpers\CacheHelper; use hisorange\BrowserDetect\Facade as Browser;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use hisorange\BrowserDetect\Facade as Browser;
class StreamController extends Controller class StreamController extends Controller
{ {
/** /**
* Display Stream Page. * Display Stream Page.
*/ */
public function index(Request $request, string $title): \Illuminate\View\View public function index(Request $request, string $title): View
{ {
$titleParts = explode('-', $title); $titleParts = explode('-', $title);
if (! is_numeric($titleParts[array_key_last($titleParts)])) { if (! is_numeric($titleParts[array_key_last($titleParts)])) {
@@ -37,7 +36,6 @@ class StreamController extends Controller
]); ]);
} }
$episode = Episode::where('slug', $title)->firstOrFail(); $episode = Episode::where('slug', $title)->firstOrFail();
$gallery = Gallery::where('episode_id', $episode->id)->get(); $gallery = Gallery::where('episode_id', $episode->id)->get();
$moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get(); $moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get();
-58
View File
@@ -1,58 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Models\Watched;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
/**
* Display User Page.
*/
public function index(string $username): \Illuminate\View\View
{
$user = User::where('username', $username)
->select('id', 'username', 'global_name', 'avatar', 'created_at', 'is_patreon')
->firstOrFail();
return view('user.index', [
'user' => $user,
]);
}
/**
* Delete User.
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
{
$user = User::where('id', $request->user()->id)->firstOrFail();
// Delete Playlist
$playlists = Playlist::where('user_id', $user->id)->get();
foreach($playlists as $playlist) {
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
$playlist->forceDelete();
}
// Update comments to deleted user
DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]);
$user->forceDelete();
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
cache()->flush();
return redirect('/');
}
}
+56 -27
View File
@@ -2,7 +2,34 @@
namespace App\Http; 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\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 class Kernel extends HttpKernel
{ {
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
*/ */
protected $middleware = [ protected $middleware = [
// \App\Http\Middleware\TrustHosts::class, // \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class, HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ConvertEmptyStringsToNull::class,
]; ];
/** /**
@@ -30,19 +57,20 @@ class Kernel extends HttpKernel
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
\App\Http\Middleware\EncryptCookies::class, EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
\App\Http\Middleware\IsBanned::class, IsBanned::class,
SetLocale::class,
], ],
'api' => [ 'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api', ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
], ],
]; ];
@@ -54,17 +82,18 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string> * @var array<string, class-string|string>
*/ */
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'auth.session' => AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class, 'auth.admin' => IsAdmin::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'auth.moderator' => IsModerator::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'cache.headers' => SetCacheHeaders::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'can' => Authorize::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'guest' => RedirectIfAuthenticated::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 'password.confirm' => RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class, 'precognitive' => HandlePrecognitiveRequests::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'signed' => ValidateSignature::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
]; ];
} }
+14 -30
View File
@@ -1,44 +1,28 @@
<?php namespace app\Http\Middleware; <?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin { class IsAdmin
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{ {
$this->auth = $auth;
}
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
if( ! $this->auth->user()->is_admin) if (Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR)) {
{
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
return $next($request); return $next($request);
} }
session()->flash('error_msg', 'This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
} }
+11 -10
View File
@@ -1,29 +1,30 @@
<?php namespace app\Http\Middleware; <?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Guard; use Symfony\Component\HttpFoundation\Response;
class IsBanned {
class IsBanned
{
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{
if(auth()->check() && auth()->user()->is_banned == 1)
{ {
if (Auth::check() && Auth::user()->hasRole(UserRole::BANNED)) {
Auth::logout(); Auth::logout();
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect()->route('home.banned'); return redirect()->route('home.banned');
} }
return $next($request); return $next($request);
} }
} }
+30
View File
@@ -0,0 +1,30 @@
<?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 IsModerator
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
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 Moderators!');
return redirect()->route('home.index');
}
}
@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
/** /**
* Handle an incoming request. * 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 public function handle(Request $request, Closure $next, string ...$guards): Response
{ {
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
/**
* Handle an incoming request.
*
* @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 ($request->session()->has('locale') &&
in_array(session('locale'), config('app.supported_locales'), true)) {
App::setLocale(session('locale'));
return $next($request);
}
// 3. Browser language
$locale = $request->getPreferredLanguage(config('app.supported_locales'));
if ($locale) {
App::setLocale($locale);
}
return $next($request);
}
}
+7 -4
View File
@@ -2,7 +2,9 @@
namespace App\Http\Requests\Auth; namespace App\Http\Requests\Auth;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout; use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
@@ -22,20 +24,21 @@ class LoginRequest extends FormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string> * @return array<string, ValidationRule|array<mixed>|string>
*/ */
public function rules(): array public function rules(): array
{ {
return [ return [
'email' => ['required', 'string', 'email'], 'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'], 'password' => ['required', 'string'],
'altcha' => ['required', new ValidCaptcha],
]; ];
} }
/** /**
* Attempt to authenticate the request's credentials. * Attempt to authenticate the request's credentials.
* *
* @throws \Illuminate\Validation\ValidationException * @throws ValidationException
*/ */
public function authenticate(): void public function authenticate(): void
{ {
@@ -55,7 +58,7 @@ class LoginRequest extends FormRequest
/** /**
* Ensure the login request is not rate limited. * Ensure the login request is not rate limited.
* *
* @throws \Illuminate\Validation\ValidationException * @throws ValidationException
*/ */
public function ensureIsNotRateLimited(): void public function ensureIsNotRateLimited(): void
{ {
@@ -80,6 +83,6 @@ class LoginRequest extends FormRequest
*/ */
public function throttleKey(): string public function throttleKey(): string
{ {
return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
} }
} }
@@ -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 . _ = -',
];
}
}
+17 -3
View File
@@ -3,6 +3,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@@ -11,13 +12,26 @@ class ProfileUpdateRequest extends FormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string> * @return array<string, ValidationRule|array<mixed>|string>
*/ */
public function rules(): array public function rules(): array
{ {
return [ return [
'name' => ['string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 'image' => [
'nullable',
'image',
'mimes:jpg,png,jpeg,webp,gif',
'max:8192',
],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
]; ];
} }
} }
+10 -12
View File
@@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Spatie\DiscordAlerts\Facades\DiscordAlert; use Spatie\DiscordAlerts\Facades\DiscordAlert;
class DiscordReleaseNotification implements ShouldQueue class DiscordReleaseNotification implements ShouldQueue
@@ -32,26 +31,25 @@ class DiscordReleaseNotification implements ShouldQueue
*/ */
public function handle(): void public function handle(): void
{ {
switch($this->messageType) switch ($this->messageType) {
{
case 'release': 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; break;
case 'release-censored': case 'release-censored':
# Because Discord TOS // Because Discord TOS
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release: ".$this->slug." - *No link here because of* :pLoli:"); DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release: '.$this->slug.' - *No link here because of* :pLoli:');
break; break;
case 'update': case 'update':
# 1080p 48fps added // 1080p 48fps added
DiscordAlert::to('update')->message("<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug); DiscordAlert::to('update')->message('<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break; break;
case 'updateUHD': case 'updateUHD':
# 4k 48fps added // 4k 48fps added
DiscordAlert::to('update')->message("<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug); DiscordAlert::to('update')->message('<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break; break;
case 'v2': case 'v2':
# v2 re-release // v2 re-release
DiscordAlert::to('rerelease')->message("<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/".$this->slug); DiscordAlert::to('rerelease')->message('<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break; break;
default: default:
break; break;
+3 -2
View File
@@ -7,12 +7,12 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GetFileSizeFromCDN implements ShouldQueue class GetFileSizeFromCDN implements ShouldQueue
{ {
@@ -37,6 +37,7 @@ class GetFileSizeFromCDN implements ShouldQueue
$download = Downloads::find($this->downloadId); $download = Downloads::find($this->downloadId);
if (! $download) { if (! $download) {
Log::error("Download not found for ID: {$this->downloadId}"); Log::error("Download not found for ID: {$this->downloadId}");
return; return;
} }
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
class AdminCommentSearch extends Component
{
use WithPagination;
public $search = '';
public $userSearch = '';
public function updatingSearch(): void
{
$this->resetPage();
}
public function updatingUserSearch(): void
{
$this->resetPage();
}
public function deleteComment($commentId)
{
$comment = Comment::where('id', (int) $commentId)->firstOrFail();
$comment->delete();
cache()->flush();
}
public function render()
{
$comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%"))
->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%")))
->orderBy('created_at', 'DESC')
->paginate(12);
return view('livewire.admin-comment-search', [
'comments' => $comments,
]);
}
}
+20 -11
View File
@@ -2,11 +2,12 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User; use App\Models\User;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url;
class AdminUserSearch extends Component class AdminUserSearch extends Component
{ {
@@ -16,7 +17,7 @@ class AdminUserSearch extends Component
public $search = ''; public $search = '';
#[Url(history: true)] #[Url(history: true)]
public $filtered = ['true']; public $discordId = '';
#[Url(history: true)] #[Url(history: true)]
public $patreon = []; public $patreon = [];
@@ -24,19 +25,27 @@ class AdminUserSearch extends Component
#[Url(history: true)] #[Url(history: true)]
public $banned = []; public $banned = [];
public function deleteUserComments(int $userID)
{
$user = User::where('id', $userID)
->firstOrFail();
Comment::where('user_id', $user->id)
->delete();
cache()->flush();
}
public function render() public function render()
{ {
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000)) $users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1)) ->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1)) ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->search !== '', fn ($query) => $query->where(function($query) { ->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
$query->where('username', 'like', '%'.$this->search.'%')
->orWhere('global_name', 'like', '%'.$this->search.'%');
}))
->paginate(20); ->paginate(20);
return view('livewire.admin-user-search', [ return view('livewire.admin-user-search', [
'users' => $users 'users' => $users,
]); ]);
} }
} }
+6 -9
View File
@@ -2,12 +2,11 @@
namespace App\Livewire; namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use App\Models\SiteBackground; use App\Models\SiteBackground;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class BackgroundImages extends Component class BackgroundImages extends Component
{ {
@@ -20,16 +19,14 @@ class BackgroundImages extends Component
{ {
$now = Carbon::now(); $now = Carbon::now();
$images = SiteBackground::when($this->filter === 'active', fn ($query) => $images = SiteBackground::when($this->filter === 'active', fn ($query) => $query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
$query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
) )
->when($this->filter === 'inactive', fn ($query) => ->when($this->filter === 'inactive', fn ($query) => $query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
$query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
) )
->paginate(10); ->paginate(10);
return view('livewire.background-images', [ return view('livewire.background-images', [
'images' => $images 'images' => $images,
]); ]);
} }
} }
+201
View File
@@ -0,0 +1,201 @@
<?php
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Episode;
use App\Models\ModLog;
use App\Models\User;
use App\Notifications\CommentNotification;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Livewire\Component;
use Maize\Markable\Models\Like;
class Comment extends Component
{
use AuthorizesRequests;
public $comment;
public $isReplying = false;
public $likeCount = 0;
public $liked = false;
public $replyState = [
'body' => '',
];
public $isEditing = false;
public $editState = [
'body' => '',
];
protected $listeners = [
'refresh' => '$refresh',
];
protected $validationAttributes = [
'replyState.body' => 'reply',
];
public function updatedIsEditing(bool $isEditing)
{
if (! $isEditing) {
return;
}
$this->editState = [
'body' => $this->comment->body,
];
}
public function editComment()
{
$this->authorize('update', $this->comment);
$this->comment->update($this->editState);
$this->isEditing = false;
}
public function deleteComment()
{
$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');
}
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.');
return;
}
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$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',
]);
$reply = $this->comment->children()->make($this->replyState);
$reply->user()->associate($user);
$reply->commentable()->associate($this->comment->commentable);
$reply->save();
// Notify if Episode and if not the same user
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),
"{$url}#comment-{$reply->id}"
)
);
}
$this->replyState = [
'body' => '',
];
$this->isReplying = false;
$this->dispatch('refresh')->self();
}
public function like()
{
if (! Auth::check()) {
return;
}
Like::toggle($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
Cache::forget('commentLikes'.$this->comment->id);
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
$this->liked = true;
$this->likeCount++;
}
public function mount()
{
if (Auth::check()) {
$this->likeCount = $this->comment->likeCount();
$this->liked = Like::has($this->comment, User::where('id', Auth::user()->id)->firstOrFail());
}
}
public function render()
{
return view('livewire.comment');
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Livewire;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
use Livewire\WithPagination;
class Comments extends Component
{
use WithPagination;
public $model;
public $newCommentState = [
'body' => '',
];
protected $validationAttributes = [
'newCommentState.body' => 'comment',
];
protected $listeners = [
'refresh' => '$refresh',
];
public function postComment()
{
$this->validate([
'newCommentState.body' => 'required',
]);
$user = auth()->user();
$rateLimitKey = "send-comment:{$user->id}";
$rateLimitMinutes = 60 * 5; // 5 minutes
if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$comment = $this->model->comments()->make($this->newCommentState);
$comment->user()->associate($user);
$comment->save();
$this->newCommentState = [
'body' => '',
];
$this->resetPage();
}
public function render()
{
$comments = $this->model
->comments()
->with('user', 'children.user', 'children.children')
->parent()
->latest()
->paginate(50);
return view('livewire.comments', [
'comments' => $comments,
]);
}
}
+19
View File
@@ -21,6 +21,25 @@ class DownloadButton extends Component
public $background = 'bg-rose-600'; 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) public function clicked($downloadId)
{ {
$download = Downloads::find($downloadId); $download = Downloads::find($downloadId);
+4 -5
View File
@@ -5,12 +5,9 @@ namespace App\Livewire;
use App\Models\Episode; use App\Models\Episode;
use App\Models\User; use App\Models\User;
use App\Models\UserDownload; use App\Models\UserDownload;
use Livewire\Component;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Livewire\Component;
class DownloadsFree extends Component class DownloadsFree extends Component
{ {
@@ -51,7 +48,8 @@ class DownloadsFree extends Component
// Check timestamp // Check timestamp
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) { if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
// Already expired // Already expired
$alreadyDownloaded->forceDelete(); $alreadyDownloaded->delete();
return; return;
} }
@@ -66,6 +64,7 @@ class DownloadsFree extends Component
if ($user->downloads_left <= 0) { if ($user->downloads_left <= 0) {
// Daily limit reached // Daily limit reached
$this->granted = 3; $this->granted = 3;
return; return;
} }
+78 -1
View File
@@ -2,7 +2,9 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Downloads; use App\Models\Downloads;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
@@ -10,10 +12,36 @@ class DownloadsSearch extends Component
{ {
use WithPagination; use WithPagination;
#[Url(history: true)]
public $fileSearch; public $fileSearch;
public $order = 'created_at_desc'; public $order = 'created_at_desc';
public $options = [
'FHD' => true,
'FHD 48fps' => true,
];
public $isOpen = false;
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
// To toggle individual option selection
public function toggleOption($option)
{
$this->options[$option] = ! $this->options[$option];
$this->resetPage();
}
// To toggle dropdown visibility
public function toggleDropdown()
{
$this->isOpen = ! $this->isOpen;
}
protected $queryString = [ protected $queryString = [
'fileSearch' => ['except' => '', 'as' => 'fS'], 'fileSearch' => ['except' => '', 'as' => 'fS'],
'order' => ['except' => '', 'as' => 'order'], 'order' => ['except' => '', 'as' => 'order'],
@@ -24,6 +52,40 @@ class DownloadsSearch extends Component
$this->resetPage(); $this->resetPage();
} }
public function applyFilters(): void
{
$this->studiosCopy = $this->studios;
$this->resetPage();
}
public function revertFilters(): void
{
$this->studios = $this->studiosCopy;
}
// Map the selected options to database types
private function getSelectedTypes()
{
$types = [];
// Map the options to their corresponding database values
foreach ($this->options as $label => $selected) {
if ($selected) {
if ($label === 'FHD') {
$types[] = 'FHD';
} elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHDi';
}
}
}
return $types;
}
public function clicked($downloadId) public function clicked($downloadId)
{ {
$download = Downloads::find($downloadId); $download = Downloads::find($downloadId);
@@ -36,6 +98,17 @@ class DownloadsSearch extends Component
cache()->forget("episode_{$download->episode->id}_download_{$download->type}"); cache()->forget("episode_{$download->episode->id}_download_{$download->type}");
} }
public function mount()
{
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
return;
}
// Add patreon options
$this->options['UHD'] = true;
$this->options['UHD 48fps'] = true;
}
public function render() public function render()
{ {
$orderby = 'created_at'; $orderby = 'created_at';
@@ -72,7 +145,10 @@ class DownloadsSearch extends Component
} }
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%')) $downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
->when(!auth()->user()->is_patreon, fn ($query) => $query->whereIn('type', ['FHD', 'FHDi'])) ->whereIn('type', $this->getSelectedTypes())
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
})))
->whereNotNull('size') ->whereNotNull('size')
->orderBy($orderby, $orderdirection) ->orderBy($orderby, $orderdirection)
->paginate(20); ->paginate(20);
@@ -80,6 +156,7 @@ class DownloadsSearch extends Component
return view('livewire.downloads-search', [ return view('livewire.downloads-search', [
'downloads' => $downloads, 'downloads' => $downloads,
'query' => $this->fileSearch, '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\Episode;
use App\Models\User; use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Livewire\Component;
use Maize\Markable\Models\Like; use Maize\Markable\Models\Like;
class LikeButton extends Component class LikeButton extends Component
@@ -52,6 +51,7 @@ class LikeButton extends Component
if ($this->liked) { if ($this->liked) {
$this->liked = false; $this->liked = false;
$this->likeCount--; $this->likeCount--;
return; return;
} }
+11 -4
View File
@@ -3,10 +3,10 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Episode; use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class LiveSearch extends Component class LiveSearch extends Component
{ {
@@ -20,14 +20,17 @@ class LiveSearch extends Component
#[Url(history: true)] #[Url(history: true)]
public $tags = []; public $tags = [];
public $tagsCopy = []; public $tagsCopy = [];
#[Url(history: true)] #[Url(history: true)]
public $studios = []; public $studios = [];
public $studiosCopy = []; public $studiosCopy = [];
#[Url(history: true)] #[Url(history: true)]
public $blacklist = []; public $blacklist = [];
public $blacklistCopy = []; public $blacklistCopy = [];
#[Url(history: true)] #[Url(history: true)]
@@ -118,10 +121,14 @@ class LiveSearch extends Component
} }
$user_id = Auth::check() ? auth()->user()->id : 0; $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.'%'); })) $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->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist)) ->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->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->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id); $query->where('user_id', $user_id);
})) }))
+4 -12
View File
@@ -3,9 +3,8 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Gallery;
use Livewire\Component;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class NavLiveSearch extends Component class NavLiveSearch extends Component
{ {
@@ -18,22 +17,15 @@ class NavLiveSearch extends Component
public function render() public function render()
{ {
$episodes = []; $episodes = [];
$randomimage = null;
if ($this->navSearch != '') { if ($this->navSearch != '') {
$episodes = Episode::with('gallery')->where('title', 'like', '%'.$this->navSearch.'%') $episodes = Episode::search($this->navSearch)
->orWhere('title_jpn', 'like', '%'.$this->navSearch.'%') ->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
->when(Auth::guest(), fn ($query) => $query->withoutTags(['loli', 'shota'])) ->take(7)
->take(10)
->get(); ->get();
$randomimage = Gallery::all()
->random(1)
->first();
} }
return view('livewire.nav-live-search', [ return view('livewire.nav-live-search', [
'episodes' => $episodes, 'episodes' => $episodes,
'randomimage' => $randomimage,
'query' => $this->navSearch, 'query' => $this->navSearch,
'hide' => empty($this->navSearch), 'hide' => empty($this->navSearch),
]); ]);
+3 -5
View File
@@ -5,13 +5,11 @@ namespace App\Livewire;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\PlaylistEpisode; use App\Models\PlaylistEpisode;
use App\Services\PlaylistService; use App\Services\PlaylistService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Collection;
class PlaylistOverview extends Component class PlaylistOverview extends Component
{ {
+2 -3
View File
@@ -3,10 +3,9 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Playlist; use App\Models\Playlist;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url;
class Playlists extends Component class Playlists extends Component
{ {
@@ -59,7 +58,7 @@ class Playlists extends Component
->paginate($this->pagination); ->paginate($this->pagination);
return view('livewire.playlists', [ return view('livewire.playlists', [
'playlists' => $playlists 'playlists' => $playlists,
]); ]);
} }
} }
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
class UserComments extends Component
{
use WithPagination;
public $model;
public $commentSearch;
public $order = 'created_at_desc';
public function render()
{
$orderby = 'created_at';
$orderdirection = 'desc';
switch ($this->order) {
case 'created_at_desc':
$orderby = 'created_at';
$orderdirection = 'desc';
break;
case 'created_at_asc':
$orderby = 'created_at';
$orderdirection = 'asc';
break;
default:
$orderby = 'created_at';
$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(6);
return view('livewire.user-comments', [
'comments' => $comments,
]);
}
}
+11 -4
View File
@@ -3,10 +3,10 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Episode; use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class UserLikes extends Component class UserLikes extends Component
{ {
@@ -20,14 +20,17 @@ class UserLikes extends Component
#[Url(history: true)] #[Url(history: true)]
public $tags = []; public $tags = [];
public $tagsCopy = []; public $tagsCopy = [];
#[Url(history: true)] #[Url(history: true)]
public $studios = []; public $studios = [];
public $studiosCopy = []; public $studiosCopy = [];
#[Url(history: true)] #[Url(history: true)]
public $blacklist = []; public $blacklist = [];
public $blacklistCopy = []; public $blacklistCopy = [];
#[Url(history: true)] #[Url(history: true)]
@@ -119,10 +122,14 @@ class UserLikes extends Component
$user_id = Auth::check() ? auth()->user()->id : 0; $user_id = Auth::check() ? auth()->user()->id : 0;
$episodes = Episode::whereHasLike(auth()->user()) $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->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->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist)) ->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->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->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id); $query->where('user_id', $user_id);
})) }))
+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');
}
}
-1
View File
@@ -3,7 +3,6 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Watched as UserWatched; use App\Models\Watched as UserWatched;
use App\Models\User;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace App\Models;
use App\Models\Presenters\CommentPresenter;
use Illuminate\Database\Eloquent\Builder;
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, Markable, SoftDeletes;
protected static $marks = [
Like::class,
];
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'body',
];
public function presenter()
{
return new CommentPresenter($this);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function scopeParent(Builder $builder)
{
$builder->whereNull('parent_id');
}
public function children()
{
return $this->hasMany(Comment::class, 'parent_id')->oldest();
}
public function commentable()
{
return $this->morphTo();
}
public function parent()
{
return $this->hasOne(Comment::class, 'id', 'parent_id');
}
// Recursevly calculates how deep the nesting is
public function depth(): int
{
return $this->parent
? $this->parent->depth() + 1
: 0;
}
/**
* Get cached like count
*/
public function likeCount(): int
{
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;
}
}
+50 -20
View File
@@ -2,35 +2,57 @@
namespace App\Models; namespace App\Models;
use App\Models\Downloads;
use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use Conner\Tagging\Taggable; use Conner\Tagging\Taggable;
use Laravelista\Comments\Commentable;
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\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; 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 class Episode extends Model implements Sitemapable
{ {
use Commentable, Markable, Taggable;
use HasFactory; use HasFactory;
use Markable, Taggable;
use Searchable;
protected static $marks = [ protected static $marks = [
Like::class Like::class,
]; ];
/**
* Get the name of the index associated with the model.
*/
public function searchableAs(): string
{
return 'episodes_index';
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
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,
];
}
/** /**
* Get the studio for the Hentai. * Get the studio for the Hentai.
*/ */
@@ -74,10 +96,11 @@ class Episode extends Model implements Sitemapable
/** /**
* Increment View Count. * Increment View Count.
*/ */
public function incrementViewCount(): bool public function incrementViewCount(): void
{ {
$this->view_count++; DB::table('episodes')
return $this->save(); ->where('id', $this->id)
->update(['view_count' => $this->view_count + 1]);
} }
/** /**
@@ -130,6 +153,11 @@ class Episode extends Model implements Sitemapable
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()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function getProblematicTags(): string public function getProblematicTags(): string
{ {
$problematicTags = ['Gore', 'Scat', 'Horror']; $problematicTags = ['Gore', 'Scat', 'Horror'];
@@ -145,6 +173,7 @@ class Episode extends Model implements Sitemapable
$problematicResults .= $pTag; $problematicResults .= $pTag;
} }
return $problematicResults; return $problematicResults;
} }
@@ -199,9 +228,10 @@ class Episode extends Model implements Sitemapable
return $this->hasMany(Watched::class); return $this->hasMany(Watched::class);
} }
public function getDownloadByType(string $type): Downloads | null public function getDownloadByType(string $type): ?Downloads
{ {
$cacheKey = "episode_{$this->id}_download_{$type}"; $cacheKey = "episode_{$this->id}_download_{$type}";
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) { return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) {
return $this->downloads()->where('type', $type)->first(); return $this->downloads()->where('type', $type)->first();
}); });
+12 -9
View File
@@ -2,20 +2,18 @@
namespace App\Models; 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\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url; 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;
use Laravelista\Comments\Commentable;
class Hentai extends Model implements Sitemapable class Hentai extends Model implements Sitemapable
{ {
use Commentable, Taggable;
use HasFactory; use HasFactory;
use Taggable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -32,11 +30,16 @@ class Hentai extends Model implements Sitemapable
return $this->hasMany(Episode::class, 'hentai_id'); return $this->hasMany(Episode::class, 'hentai_id');
} }
public function title(): String public function title(): string
{ {
return $this->episodes->first()->title; return $this->episodes->first()->title;
} }
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
/** /**
* Has a Gallery. * Has a Gallery.
*/ */
+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; namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Playlist extends Model class Playlist extends Model
{ {
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PlaylistEpisode extends Model class PlaylistEpisode extends Model
{ {
@@ -0,0 +1,28 @@
<?php
namespace App\Models\Presenters;
use App\Models\Comment;
use Illuminate\Support\Str;
class CommentPresenter
{
public $comment;
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
public function markdownBody()
{
return Str::of($this->comment->body)->markdown([
'html_input' => 'strip',
]);
}
public function relativeCreatedAt()
{
return $this->comment->created_at->diffForHumans();
}
}
+2 -2
View File
@@ -16,13 +16,13 @@ class SiteBackground extends Model
protected $fillable = [ protected $fillable = [
'date_start', 'date_start',
'date_end', 'date_end',
'default' 'default',
]; ];
/** /**
* Returns the current IDs of active wallpaper * Returns the current IDs of active wallpaper
*/ */
public function getImages(): ? \Illuminate\Support\Collection public function getImages(): ?Collection
{ {
$now = Carbon::now(); $now = Carbon::now();
+1 -1
View File
@@ -3,8 +3,8 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Studios extends Model class Studios extends Model
{ {
+95 -36
View File
@@ -2,20 +2,21 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Jakyeru\Larascord\Traits\InteractsWithDiscord;
use Laravelista\Comments\Commenter;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Support\Facades\DB; 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 HasApiTokens, HasFactory, Notifiable, InteractsWithDiscord, Commenter; use HasFactory, InteractsWithPasskeys, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -23,22 +24,14 @@ class User extends Authenticatable
* @var string[] * @var string[]
*/ */
protected $fillable = [ protected $fillable = [
'id', 'name',
'username',
'global_name',
'discriminator',
'email', 'email',
'avatar', 'password',
'verified',
'banner',
'banner_color',
'accent_color',
'locale', 'locale',
'mfa_enabled', // Discord
'premium_type', 'discord_id',
'public_flags', 'discord_avatar',
'roles', 'subscription_key',
'is_banned',
]; ];
/** /**
@@ -47,7 +40,9 @@ class User extends Authenticatable
* @var array * @var array
*/ */
protected $hidden = [ protected $hidden = [
'password',
'remember_token', 'remember_token',
'subscription_key',
]; ];
/** /**
@@ -56,22 +51,18 @@ class User extends Authenticatable
* @var array * @var array
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', // Laravel defaults
'username' => 'string', 'email_verified_at' => 'datetime',
'global_name' => 'string', 'password' => 'hashed',
'discriminator' => 'string', // Other
'name' => 'string',
'email' => 'string', 'email' => 'string',
'avatar' => 'string',
'verified' => 'boolean',
'banner' => 'string',
'banner_color' => 'string',
'accent_color' => 'string',
'locale' => 'string', 'locale' => 'string',
'mfa_enabled' => 'boolean', 'roles' => 'array',
'premium_type' => 'integer',
'public_flags' => 'integer',
'roles' => 'json',
'tag_blacklist' => 'array', 'tag_blacklist' => 'array',
// Discord
'discord_id' => 'integer',
'discord_avatar' => 'string',
]; ];
/** /**
@@ -101,8 +92,76 @@ class User extends Authenticatable
/** /**
* Has Many Comments. * Has Many Comments.
*/ */
public function comments()
{
return $this->hasMany(Comment::class, 'user_id');
}
/**
* Get Comment Count.
*/
public function commentCount(): int public function commentCount(): int
{ {
return DB::table('comments')->where('commenter_id', $this->id)->count(); return cache()->remember('userComments'.$this->id, 300, fn () => $this->comments->count());
}
/**
* Returns the user avatar image url.
*/
public function getAvatar(): string
{
if ($this->discord_id && $this->discord_avatar && ! $this->avatar) {
return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}";
}
if ($this->avatar) {
return Storage::url($this->avatar);
}
return asset('images/default-avatar.webp');
}
/**
* Check if user has a specific role
*/
public function hasRole(UserRole $role): bool
{
return in_array($role->value, $this->roles ?? [], true);
}
/**
* Add Role to User
*/
public function addRole(UserRole $role): void
{
if ($this->hasRole($role)) {
return;
}
// Get all current roles
$roles = $this->roles ?? [];
// Add new role
$roles[] = $role->value;
$this->roles = $roles;
$this->save();
}
/**
* Remove Role from User
*/
public function removeRole(UserRole $role): void
{
if (! $this->hasRole($role)) {
return;
}
$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; namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserDownload extends Model class UserDownload extends Model
{ {
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Watched extends Model class Watched extends Model
{ {
+2 -2
View File
@@ -3,8 +3,6 @@
namespace App\Notifications; namespace App\Notifications;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
class CommentNotification extends Notification class CommentNotification extends Notification
@@ -12,7 +10,9 @@ class CommentNotification extends Notification
use Queueable; use Queueable;
protected $type; protected $type;
protected $message; protected $message;
protected $url; protected $url;
/** /**
-56
View File
@@ -1,56 +0,0 @@
<?php
namespace Laravelista\Comments;
use Laravelista\Comments\Comment;
class CommentPolicy
{
/**
* Can user create the comment
*
* @param $user
* @return bool
*/
public function create($user) : bool
{
return true;
}
/**
* Can user delete the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function delete($user, Comment $comment) : bool
{
return ($user->getKey() == $comment->commenter_id) || $user->is_admin;
}
/**
* Can user update the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function update($user, Comment $comment) : bool
{
return $user->getKey() == $comment->commenter_id;
}
/**
* Can user reply to the comment
*
* @param $user
* @param Comment $comment
* @return bool
*/
public function reply($user, Comment $comment) : bool
{
return $user->getKey() != $comment->commenter_id;
}
}
-139
View File
@@ -1,139 +0,0 @@
<?php
namespace Laravelista\Comments;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use App\Notifications\CommentNotification;
use App\Models\User;
use App\Models\Episode;
class CommentService
{
/**
* Handles creating a new comment for given model.
* @return mixed the configured comment-model
*/
public function store(Request $request)
{
// If guest commenting is turned off, authorize this action.
if (Config::get('comments.guest_commenting') == false) {
Gate::authorize('create-comment', Comment::class);
}
// Define guest rules if user is not logged in.
if (!Auth::check()) {
$guest_rules = [
'guest_name' => 'required|string|max:255',
'guest_email' => 'required|string|email|max:255',
];
}
// Merge guest rules, if any, with normal validation rules.
Validator::make($request->all(), array_merge($guest_rules ?? [], [
'commentable_type' => 'required|string',
'commentable_id' => 'required|string|min:1',
'message' => 'required|string'
]))->validate();
$model = $request->commentable_type::findOrFail($request->commentable_id);
$commentClass = Config::get('comments.model');
$comment = new $commentClass;
if (!Auth::check()) {
$comment->guest_name = $request->guest_name;
$comment->guest_email = $request->guest_email;
} else {
$comment->commenter()->associate(Auth::user());
}
$comment->commentable()->associate($model);
$comment->comment = $request->message;
$comment->approved = !Config::get('comments.approval_required');
$comment->save();
return $comment;
}
/**
* Handles updating the message of the comment.
* @return mixed the configured comment-model
*/
public function update(Request $request, Comment $comment)
{
Gate::authorize('edit-comment', $comment);
Validator::make($request->all(), [
'message' => 'required|string'
])->validate();
$comment->update([
'comment' => $request->message
]);
return $comment;
}
/**
* Handles deleting a comment.
* @return mixed the configured comment-model
*/
public function destroy(Comment $comment): void
{
Gate::authorize('delete-comment', $comment);
if (Config::get('comments.soft_deletes') == true) {
$comment->delete();
} else {
$comment->forceDelete();
}
}
/**
* Handles creating a reply "comment" to a comment.
* @return mixed the configured comment-model
*/
public function reply(Request $request, Comment $comment)
{
Gate::authorize('reply-to-comment', $comment);
Validator::make($request->all(), [
'message' => 'required|string'
])->validate();
$commentClass = Config::get('comments.model');
$reply = new $commentClass;
$reply->commenter()->associate(Auth::user());
$reply->commentable()->associate($comment->commentable);
$reply->parent()->associate($comment);
$reply->comment = $request->message;
$reply->approved = !Config::get('comments.approval_required');
$reply->save();
// Notify
if ($comment->commentable_type == 'App\Models\Episode') {
$episode = Episode::where('id', $comment->commentable_id)->firstOrFail();
$url = '/hentai/' . $episode->slug . '#comment-' . $reply->id;
$user = Auth::user();
$username = $user->global_name ?? $user->username;
$parentCommentUser = User::where('id', $comment->commenter_id)->firstOrFail();
$parentCommentUser->notify(
new CommentNotification(
"{$username} replied to your comment.",
Str::limit($reply->comment, 50),
$url
)
);
}
return $reply;
}
}
-155
View File
@@ -1,155 +0,0 @@
<?php
namespace Jakyeru\Larascord\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Jakyeru\Larascord\Http\Requests\StoreUserRequest;
use Jakyeru\Larascord\Services\DiscordService;
use RealRashid\SweetAlert\Facades\Alert;
class DiscordController extends Controller
{
/**
* Handles the Discord OAuth2 login.
*/
public function handle(StoreUserRequest $request): RedirectResponse | JsonResponse
{
// Making sure the "guilds" scope was added to .env if there are any guilds specified in "larascord.guilds".
if (count(config('larascord.guilds'))) {
if (!in_array('guilds', explode('&', config('larascord.scopes')))) {
return $this->throwError('missing_guilds_scope');
}
}
// Getting the accessToken from the Discord API.
try {
$accessToken = (new DiscordService())->getAccessTokenFromCode($request->get('code'));
} catch (\Exception $e) {
return $this->throwError('invalid_code', $e);
}
// Get the user from the Discord API.
try {
$user = (new DiscordService())->getCurrentUser($accessToken);
$user->setAccessToken($accessToken);
} catch (\Exception $e) {
return $this->throwError('authorization_failed', $e);
}
// Making sure the user has an email if the email scope is set.
if (in_array('email', explode('&', config('larascord.scopes')))) {
if (empty($user->email)) {
return $this->throwError('missing_email');
}
}
if (auth()->check()) {
// Making sure the current logged-in user's ID is matching the ID retrieved from the Discord API.
if (auth()->id() !== (int)$user->id) {
auth()->logout();
return $this->throwError('invalid_user');
}
// Confirming the session in case the user was redirected from the password.confirm middleware.
$request->session()->put('auth.password_confirmed_at', time());
}
// Trying to create or update the user in the database.
// Initiating a database transaction in case something goes wrong.
DB::beginTransaction();
try {
$user = (new DiscordService())->createOrUpdateUser($user);
$user->accessToken()->updateOrCreate([], $accessToken->toArray());
} catch (\Exception $e) {
DB::rollBack();
return $this->throwError('database_error', $e);
}
// Verifying if the user is soft-deleted.
if (Schema::hasColumn('users', 'deleted_at')) {
if ($user->trashed()) {
DB::rollBack();
return $this->throwError('user_deleted');
}
}
// Patreon check
try {
if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) {
DB::rollBack();
return $this->throwError('missing_guilds_members_read_scope');
}
$guildMember = (new DiscordService())->getGuildMember($accessToken, config('discord.guild_id'));
$patreonroles = config('discord.patreon_roles');
$user->is_patreon = false;
if ((new DiscordService())->hasRoleInGuild($guildMember, $patreonroles)) {
$user->is_patreon = true;
}
$user->save();
} catch (\Exception $e) {
// Clearly not a patreon
$user->is_patreon = false;
$user->save();
}
// Committing the database transaction.
DB::commit();
// Authenticating the user if the user is not logged in.
if (!auth()->check()) {
auth()->login($user, config('larascord.remember_me', false));
}
// Redirecting the user to the intended page or to the home page.
return redirect()->intended(RouteServiceProvider::HOME);
}
/**
* Handles the throwing of an error.
*/
private function throwError(string $message, \Exception $exception = NULL): RedirectResponse | JsonResponse
{
if (app()->hasDebugModeEnabled()) {
return response()->json([
'larascord_message' => config('larascord.error_messages.' . $message),
'message' => $exception?->getMessage(),
'code' => $exception?->getCode()
]);
} else {
if (config('larascord.error_messages.' . $message . '.redirect')) {
Alert::error('Error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.'));
return redirect(config('larascord.error_messages.' . $message . '.redirect'))->with('error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.'));
} else {
return redirect('/')->with('error', config('larascord.error_messages.' . $message, 'An error occurred while trying to log you in.'));
}
}
}
/**
* Handles the deletion of the user.
*/
public function destroy(): RedirectResponse | JsonResponse
{
// Revoking the OAuth2 access token.
try {
(new DiscordService())->revokeAccessToken(auth()->user()->accessToken()->first()->refresh_token);
} catch (\Exception $e) {
return $this->throwError('revoke_token_failed', $e);
}
// Deleting the user from the database.
auth()->user()->delete();
// Showing the success message.
if (config('larascord.success_messages.user_deleted.redirect')) {
return redirect(config('larascord.success_messages.user_deleted.redirect'))->with('success', config('larascord.success_messages.user_deleted.message', 'Your account has been deleted.'));
} else {
return redirect('/')->with('success', config('larascord.success_messages.user_deleted', 'Your account has been deleted.'));
}
}
}
@@ -1,273 +0,0 @@
<?php
namespace Jakyeru\Larascord\Services;
use App\Models\User;
use App\Models\OldUser;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use Exception;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Jakyeru\Larascord\Types\AccessToken;
use Jakyeru\Larascord\Types\GuildMember;
use Illuminate\Support\Facades\DB;
class DiscordService
{
/**
* The Discord OAuth2 token URL.
*/
protected string $tokenURL = "https://discord.com/api/oauth2/token";
/**
* The Discord API base URL.
*/
protected string $baseApi = "https://discord.com/api";
/**
* The required data for the token request.
*/
protected array $tokenData = [
"client_id" => NULL,
"client_secret" => NULL,
"grant_type" => "authorization_code",
"code" => NULL,
"redirect_uri" => NULL,
"scope" => null
];
/**
* UserService constructor.
*/
public function __construct()
{
$this->tokenData['client_id'] = config('larascord.client_id');
$this->tokenData['client_secret'] = config('larascord.client_secret');
$this->tokenData['grant_type'] = config('larascord.grant_type');
$this->tokenData['redirect_uri'] = config('larascord.redirect_uri');
$this->tokenData['scope'] = config('larascord.scopes');
}
/**
* Handles the Discord OAuth2 callback and returns the access token.
*
* @throws RequestException
*/
public function getAccessTokenFromCode(string $code): AccessToken
{
$this->tokenData['code'] = $code;
$response = Http::asForm()->post($this->tokenURL, $this->tokenData);
$response->throw();
return new AccessToken(json_decode($response->body()));
}
/**
* Get access token from refresh token.
*
* @throws RequestException
*/
public function refreshAccessToken(string $refreshToken): AccessToken
{
$response = Http::asForm()->post($this->tokenURL, [
'client_id' => config('larascord.client_id'),
'client_secret' => config('larascord.client_secret'),
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
]);
$response->throw();
return new AccessToken(json_decode($response->body()));
}
/**
* Authenticates the user with the access token and returns the user data.
*
* @throws RequestException
*/
public function getCurrentUser(AccessToken $accessToken): \Jakyeru\Larascord\Types\User
{
$response = Http::withToken($accessToken->access_token)->get($this->baseApi . '/users/@me');
$response->throw();
return new \Jakyeru\Larascord\Types\User(json_decode($response->body()));
}
/**
* Get the user's guilds.
*
* @throws RequestException
* @throws Exception
*/
public function getCurrentUserGuilds(AccessToken $accessToken, bool $withCounts = false): array
{
if (!$accessToken->hasScope('guilds')) throw new Exception(config('larascord.error_messages.missing_guilds_scope.message'));
$endpoint = '/users/@me/guilds';
if ($withCounts) {
$endpoint .= '?with_counts=true';
}
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . $endpoint);
$response->throw();
return array_map(function ($guild) {
return new \Jakyeru\Larascord\Types\Guild($guild);
}, json_decode($response->body()));
}
/**
* Get the Guild Member object for a user.
*
* @throws RequestException
* @throws Exception
*/
public function getGuildMember(AccessToken $accessToken, string $guildId): GuildMember
{
if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) throw new Exception(config('larascord.error_messages.missing_guilds_members_read_scope.message'));
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/guilds/' . $guildId . '/member');
$response->throw();
return new GuildMember(json_decode($response->body()));
}
/**
* Get the User's connections.
*
* @throws RequestException
* @throws Exception
*/
public function getCurrentUserConnections(AccessToken $accessToken): array
{
if (!$accessToken->hasScope('connections')) throw new Exception('The "connections" scope is required.');
$response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/connections');
$response->throw();
return array_map(function ($connection) {
return new \Jakyeru\Larascord\Types\Connection($connection);
}, json_decode($response->body()));
}
/**
* Join a guild.
*
* @throws RequestException
* @throws Exception
*/
public function joinGuild(AccessToken $accessToken, User $user, string $guildId, array $options = []): GuildMember
{
if (!config('larascord.access_token')) throw new Exception(config('larascord.error_messages.missing_access_token.message'));
if (!$accessToken->hasScope('guilds.join')) throw new Exception('The "guilds" and "guilds.join" scopes are required.');
$response = Http::withToken(config('larascord.access_token'), 'Bot')->put($this->baseApi . '/guilds/' . $guildId . '/members/' . $user->id, array_merge([
'access_token' => $accessToken->access_token,
], $options));
$response->throw();
if ($response->status() === 204) return throw new Exception('User is already in the guild.');
return new GuildMember(json_decode($response->body()));
}
/**
* Create or update a user in the database.
*
* @throws Exception
*/
public function createOrUpdateUser(\Jakyeru\Larascord\Types\User $user): User
{
if (!$user->getAccessToken()) {
throw new Exception('User access token is missing.');
}
$forgottenUser = User::where('email', '=', $user->email)->where('id', '!=', $user->id)->first();
if ($forgottenUser) {
// This case should never happen (TM) - The discord id changed
// The user probably re-created their discord account with the same email
// Delete Playlist
$playlists = Playlist::where('user_id', $forgottenUser->id)->get();
foreach($playlists as $playlist) {
PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete();
$playlist->forceDelete();
}
// Update comments to deleted user
DB::table('comments')->where('commenter_id', '=', $forgottenUser->id)->update(['commenter_id' => 1]);
$forgottenUser->forceDelete();
}
return User::updateOrCreate(
[
'id' => $user->id,
],
$user->toArray(),
);
}
/**
* Verify if the user is in the specified guild(s).
*/
public function isUserInGuilds(array $guilds): bool
{
// Verify if the user is in all the specified guilds if strict mode is enabled.
if (config('larascord.guilds_strict')) {
return empty(array_diff(config('larascord.guilds'), array_column($guilds, 'id')));
}
// Verify if the user is in any of the specified guilds if strict mode is disabled.
return !empty(array_intersect(config('larascord.guilds'), array_column($guilds, 'id')));
}
/**
* Verify if the user has the specified role(s) in the specified guild.
*/
public function hasRoleInGuild(GuildMember $guildMember, array $roles): bool
{
// Verify if the user has any of the specified roles.
return !empty(array_intersect($roles, $guildMember->roles));
}
/**
* Updates the user's roles in the database.
*/
public function updateUserRoles(User $user, GuildMember $guildMember, int $guildId): void
{
// Updating the user's roles in the database.
$updatedRoles = $user->roles;
$updatedRoles[$guildId] = $guildMember->roles;
$user->roles = $updatedRoles;
$user->save();
}
/**
* Revoke the user's access token.
*
* @throws RequestException
*/
public function revokeAccessToken(string $accessToken): object
{
$response = Http::asForm()->post($this->tokenURL . '/revoke', [
'token' => $accessToken,
'client_id' => config('larascord.client_id'),
'client_secret' => config('larascord.client_secret'),
]);
$response->throw();
return json_decode($response->body());
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommentPolicy
{
use HandlesAuthorization;
public function update(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id;
}
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;
}
}
+6 -1
View File
@@ -2,7 +2,10 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use SocialiteProviders\Discord\Provider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -19,6 +22,8 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// 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.');
}
}
}
+1 -3
View File
@@ -2,11 +2,9 @@
namespace App\Services; namespace App\Services;
use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads; use App\Models\Downloads;
use App\Models\Episode; use App\Models\Episode;
use App\Jobs\GetFileSizeFromCDN;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class DownloadService class DownloadService
+94 -19
View File
@@ -5,14 +5,13 @@ namespace App\Services;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Hentai; use App\Models\Hentai;
use App\Models\Studios; use App\Models\Studios;
use App\Models\ModLog;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Laravel\Facades\Image;
use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class EpisodeService class EpisodeService
{ {
@@ -24,6 +23,7 @@ class EpisodeService
if (is_numeric($lastPart) && $lastPart < 1000) { if (is_numeric($lastPart) && $lastPart < 1000) {
$slugParts[array_key_last($slugParts)] = 's'.$lastPart; $slugParts[array_key_last($slugParts)] = 's'.$lastPart;
return implode('-', $slugParts); return implode('-', $slugParts);
} }
@@ -34,13 +34,12 @@ class EpisodeService
Request $request, Request $request,
Hentai $hentai, Hentai $hentai,
int $episodeNumber, int $episodeNumber,
Studios $studio = null, ?Studios $studio = null,
Episode $referenceEpisode = null ?Episode $referenceEpisode = null
): Episode ): Episode {
{ $episode = new Episode;
$episode = new Episode();
$episode->title = $referenceEpisode->title ?? $request->input('title'); $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->title_jpn = $referenceEpisode->title_jpn ?? $request->input('title_jpn');
$episode->slug = "{$hentai->slug}-{$episodeNumber}"; $episode->slug = "{$hentai->slug}-{$episodeNumber}";
$episode->hentai_id = $hentai->id; $episode->hentai_id = $hentai->id;
@@ -64,6 +63,68 @@ class EpisodeService
return $episode; 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 public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
{ {
$episode = Episode::where('id', $episodeId)->firstOrFail(); $episode = Episode::where('id', $episodeId)->firstOrFail();
@@ -74,19 +135,34 @@ class EpisodeService
$episode->interpolated = $request->input('interpolated') == 'yes'; $episode->interpolated = $request->input('interpolated') == 'yes';
$episode->interpolated_uhd = $request->input('downloadUHDi1') ? true : false; $episode->interpolated_uhd = $request->input('downloadUHDi1') ? true : false;
$episode->is_dvd_aspect = $request->input('dvd') == 'yes'; $episode->is_dvd_aspect = $request->input('dvd') == 'yes';
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
$episode->save(); $episode->save();
// Tagging $this->applyTags($request, $episode);
$tags = json_decode($request->input('tags')); $this->updateTitle($request, $episode);
$newtags = [];
foreach ($tags as $t) {
$newtags[] = $t->value;
}
$episode->retag($newtags);
return $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 public function getOrCreateStudio(string $studioName): Studios
{ {
return Studios::firstOrCreate( return Studios::firstOrCreate(
@@ -95,7 +171,6 @@ class EpisodeService
); );
} }
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
{ {
if (! $request->hasFile("episodecover{$episodeNumber}")) { if (! $request->hasFile("episodecover{$episodeNumber}")) {
@@ -110,7 +185,7 @@ class EpisodeService
// Encode and save cover image // Encode and save cover image
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath()) Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
->cover(268, 394) ->cover(268, 394)
->encode(new WebpEncoder()) ->encode(new WebpEncoder)
->save(Storage::disk('public')->path($episode->cover_url)); ->save(Storage::disk('public')->path($episode->cover_url));
} }
} }
+5 -9
View File
@@ -5,17 +5,13 @@ namespace App\Services;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Gallery; use App\Models\Gallery;
use App\Models\Hentai; use App\Models\Hentai;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Laravel\Facades\Image;
use Intervention\Image\Encoders\WebpEncoder; use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class GalleryService class GalleryService
{ {
public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void
{ {
$galleryInputNumber = $override ? 1 : $episodeNumber; $galleryInputNumber = $override ? 1 : $episodeNumber;
@@ -45,7 +41,7 @@ class GalleryService
private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery
{ {
$gallery = new Gallery(); $gallery = new Gallery;
$gallery->hentai_id = $hentai->id; $gallery->hentai_id = $hentai->id;
$gallery->episode_id = $episode->id; $gallery->episode_id = $episode->id;
$gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp"; $gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp";
@@ -59,12 +55,12 @@ class GalleryService
{ {
Image::read($sourceImage->getRealPath()) Image::read($sourceImage->getRealPath())
->cover(1920, 1080) ->cover(1920, 1080)
->encode(new WebpEncoder()) ->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->image_url)); ->save(Storage::disk('public')->path($gallery->image_url));
Image::read($sourceImage->getRealPath()) Image::read($sourceImage->getRealPath())
->cover(960, 540) ->cover(960, 540)
->encode(new WebpEncoder()) ->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->thumbnail_url)); ->save(Storage::disk('public')->path($gallery->thumbnail_url));
} }
@@ -74,7 +70,7 @@ class GalleryService
foreach ($oldGallery as $oldImage) { foreach ($oldGallery as $oldImage) {
Storage::disk('public')->delete($oldImage->image_url); Storage::disk('public')->delete($oldImage->image_url);
Storage::disk('public')->delete($oldImage->thumbnail_url); Storage::disk('public')->delete($oldImage->thumbnail_url);
$oldImage->forceDelete(); $oldImage->delete();
} }
} }
} }
@@ -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 <?php
use App\Exceptions\Handler;
use App\Http\Kernel;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Create The Application | Create The Application
@@ -11,7 +16,7 @@
| |
*/ */
$app = new Illuminate\Foundation\Application( $app = new Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
); );
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
$app->singleton( $app->singleton(
Illuminate\Contracts\Http\Kernel::class, Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class Kernel::class
); );
$app->singleton( $app->singleton(
@@ -37,8 +42,8 @@ $app->singleton(
); );
$app->singleton( $app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class, ExceptionHandler::class,
App\Exceptions\Handler::class Handler::class
); );
/* /*
+22 -34
View File
@@ -1,7 +1,7 @@
{ {
"name": "laravel/laravel", "name": "w33b/hstream",
"type": "project", "type": "project",
"description": "The skeleton application for the Laravel framework.", "description": "The website of hstream.moe",
"keywords": [ "keywords": [
"laravel", "laravel",
"framework" "framework"
@@ -9,59 +9,47 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"altcha-org/altcha": "^2.0",
"guzzlehttp/guzzle": "^7.8.1", "guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0", "hisorange/browser-detect": "^5.0",
"intervention/image": "^3.9", "http-interop/http-factory-guzzle": "^1.2",
"intervention/image-laravel": "^1.3", "intervention/image": "^3.11",
"jakyeru/larascord": "^6.0", "intervention/image-laravel": "^1.5",
"laravel/framework": "^11.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.2",
"laravel/scout": "^10.20",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.10", "laravel/tinker": "^2.10",
"laravelista/comments": "dev-l11-compatibility", "livewire/livewire": "^3.7.0",
"livewire/livewire": "^3.6.4",
"maize-tech/laravel-markable": "^2.3.0", "maize-tech/laravel-markable": "^2.3.0",
"mews/captcha": "3.4.4", "meilisearch/meilisearch-php": "^1.16",
"predis/predis": "^2.2", "predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2", "realrashid/sweet-alert": "^7.2",
"rtconner/laravel-tagging": "^4.1", "rtconner/laravel-tagging": "^5.0",
"spatie/laravel-discord-alerts": "^1.5", "socialiteproviders/discord": "^4.2",
"spatie/laravel-sitemap": "^7.3", "spatie/laravel-discord-alerts": "^1.8",
"vluzrmos/language-detector": "^2.3" "spatie/laravel-passkeys": "^1.7",
"spatie/laravel-sitemap": "^7.3"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.14.7", "barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.24.0", "fakerphp/faker": "^1.24.0",
"laravel/breeze": "^2.3",
"laravel/pint": "^1.18", "laravel/pint": "^1.18",
"laravel/sail": "^1.38",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1", "nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.4", "phpunit/phpunit": "^11.4",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },
"repositories": [ "repositories": [],
{
"type": "vcs",
"url": "https://github.com/renatokira/comments.git"
}
],
"autoload": { "autoload": {
"exclude-from-classmap": [ "exclude-from-classmap": [],
"vendor/jakyeru/larascord/src/Http/Services/DiscordService.php",
"vendor/jakyeru/larascord/src/Http/Controllers/DiscordController.php",
"vendor/laravelista/comments/src/CommentPolicy.php",
"vendor/laravelista/comments/src/CommentService.php"
],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",
"Database\\Factories\\": "database/factories/", "Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/" "Database\\Seeders\\": "database/seeders/"
}, },
"files": [ "files": []
"app/Override/Discord/Services/DiscordService.php",
"app/Override/Discord/DiscordController.php",
"app/Override/Comments/CommentPolicy.php",
"app/Override/Comments/CommentService.php"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
Generated
+3203 -1585
View File
File diff suppressed because it is too large Load Diff

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