forked from w33b/hstream
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d1ec34c3 | |||
| 81639aaabf | |||
| 5dc1bff60c | |||
| 2f3f0edc30 | |||
| a71b2976af | |||
| 2c016274ab | |||
| 5ba0a55316 | |||
| a6fe34a0d1 | |||
| bb53e06c69 | |||
| 5cae5dc658 | |||
| 356d07365f | |||
| 3574d20fae | |||
| 9fc9e8ed10 | |||
| f5c706b587 | |||
| cbea71d9ae | |||
| 64a621173c | |||
| 839779b82e | |||
| 112cf9433e | |||
| 26a6500fca | |||
| d8cf70e747 | |||
| 0d4545c2ab | |||
| 900103e1c2 | |||
| d4c90976f8 | |||
| 72263127df | |||
| 6d3de59929 | |||
| ddb1bc2d14 | |||
| 5f3874a233 | |||
| ba3650899e | |||
| 904604fcfb | |||
| b7b34b503c | |||
| 4928733383 | |||
| 6340302ac6 | |||
| c1829ba7bd | |||
| 0b155bbb80 | |||
| 9f959efa14 | |||
| 38e3346dc3 | |||
| 09c08f3fea | |||
| 75f631c3e6 | |||
| fdf26604f3 | |||
| 59cb39ca77 | |||
| 62647be75c | |||
| de6efb877c | |||
| 2151d69791 | |||
| 05d4ef1bdb | |||
| 8ae9eaaadb | |||
| 361b511c3e | |||
| 1bc505057f | |||
| a78b1c41ac | |||
| 2480c5b309 | |||
| 4fc11d7329 | |||
| f3e5100d5d | |||
| 5b4d3d435e | |||
| 564f816fb9 | |||
| 3709e378c3 | |||
| 4a45dae593 | |||
| 2b0448d517 | |||
| 3bb6af73c3 | |||
| 57cf153560 | |||
| e45fd4b148 | |||
|
d479369770
|
|||
|
af739e3c88
|
|||
| 273ed65a8d | |||
| ccfd5b996b | |||
| e5ef197ed6 | |||
| c0be2e294a | |||
| b8ba17b33f | |||
| 5a8dd12cb8 | |||
| 3a77c4320d | |||
| 823a284fbc | |||
| 67e601d0c4 | |||
| 7e4ebd91ad | |||
| 4dc5dee2b9 | |||
| 5310908b0c | |||
| 4b05b3db6d | |||
| df47a926e4 | |||
| 1e9e95f35f | |||
| 2aa76baafd | |||
| aa50bb1f72 | |||
| dfedf4058e | |||
| 268e3eb4c2 | |||
| ab61574956 | |||
| 81038b6c26 | |||
| e949ba955a | |||
| 819e2fde27 | |||
| 3259e2197b | |||
| b133db0573 | |||
| 41c34e6d89 | |||
| db6da608aa | |||
| 13b70fdf23 | |||
| cfd6af59fb | |||
| 7810cd53fb | |||
| 871028930b | |||
| 6ce0255764 | |||
| e136e8e1b6 | |||
| a3b66b483b | |||
| 4c2a6024d7 | |||
| 5f575024e2 | |||
| 67f5d0db8b | |||
| 571bf4584c | |||
| d7dc96e11c | |||
| 58426b6e4e | |||
| 53b600daea | |||
| 224cdbcdc5 | |||
| 972d3d0aa4 | |||
| 8f7f012c14 | |||
| c0b068de58 | |||
| 51c67bb797 | |||
| 3d78f9e524 | |||
| 2d28a37463 | |||
| ac853920ee | |||
| fb3722036a | |||
| ab4e7c7999 | |||
| 8f99718058 | |||
| 2029af334c | |||
| b1c48830c4 | |||
| e100f3bf23 | |||
| c13d443696 | |||
| 8e7a56f559 | |||
| 30777a6968 | |||
| 256af435ad | |||
| e972f8db41 | |||
| 98d36d6018 | |||
| 7eea8285ca | |||
| 9e8efbbe05 | |||
| 5461606857 | |||
| 9ca2f73714 | |||
| 59d63abd79 | |||
| efb3e4197b | |||
| 735dd693ca | |||
| 36f0126a21 | |||
| 50d8704560 | |||
| 7e382ffe1d | |||
| 6a25fd2700 | |||
| 71bcf277f6 | |||
| 6c44d83e6b |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -28,7 +27,7 @@ class GetFileSize extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
foreach(Downloads::whereNull('size')->get() as $download) {
|
foreach (Downloads::whereNull('size')->get() as $download) {
|
||||||
GetFileSizeFromCDN::dispatch($download->id);
|
GetFileSizeFromCDN::dispatch($download->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum UserRole: string
|
||||||
|
{
|
||||||
|
case ADMINISTRATOR = 'admin';
|
||||||
|
case MODERATOR = 'moderator';
|
||||||
|
case SUPPORTER = 'supporter';
|
||||||
|
case BANNED = 'banned';
|
||||||
|
}
|
||||||
+20
-18
@@ -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,10 +64,11 @@ 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')
|
||||||
->limit(16)
|
->limit(16)
|
||||||
->get();
|
->get();
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
@@ -36,7 +36,7 @@ class SiteBackgroundController extends Controller
|
|||||||
'date_end' => 'required',
|
'date_end' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach($request->file('images') as $file) {
|
foreach ($request->file('images') as $file) {
|
||||||
// Initiating a database transaction in case something goes wrong.
|
// Initiating a database transaction in case something goes wrong.
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
|
|
||||||
@@ -44,24 +44,25 @@ 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),
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
|
|
||||||
$resolutions = [1440, 1080, 720, 640];
|
$resolutions = [1440, 1080, 720, 640];
|
||||||
foreach($resolutions as $resolution) {
|
foreach ($resolutions as $resolution) {
|
||||||
// /images/background/1-2560p.webp
|
// /images/background/1-2560p.webp
|
||||||
$targetPath = "/images/background/{$bg->id}-{$resolution}p.webp";
|
$targetPath = "/images/background/{$bg->id}-{$resolution}p.webp";
|
||||||
|
|
||||||
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,17 +106,18 @@ 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 {
|
||||||
foreach($resolutions as $resolution) {
|
foreach ($resolutions as $resolution) {
|
||||||
$targetPath = "/images/background/{$id}-{$resolution}p.webp";
|
$targetPath = "/images/background/{$id}-{$resolution}p.webp";
|
||||||
File::delete(public_path($targetPath));
|
File::delete(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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,20 +28,19 @@ 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:
|
||||||
alert()->error('Error','Invalid action provided');
|
alert()->error('Error', 'Invalid action provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -22,7 +20,7 @@ class StreamApiController extends Controller
|
|||||||
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
|
|
||||||
$subtitles = $episode->subtitles
|
$subtitles = $episode->subtitles
|
||||||
->mapWithKeys(fn($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
|
->mapWithKeys(fn ($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +31,9 @@ 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,9 +62,9 @@ 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,21 +98,19 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
$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,17 +118,23 @@ 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([
|
||||||
'message' => 'not-numeric',
|
'message' => 'not-numeric',
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
], 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 +122,15 @@ 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'));
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -54,15 +52,15 @@ class StreamController extends Controller
|
|||||||
// Increment Popular Count
|
// Increment Popular Count
|
||||||
$episode->incrementPopularCount();
|
$episode->incrementPopularCount();
|
||||||
|
|
||||||
if (!Auth::guest()) {
|
if (! Auth::guest()) {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Add to user watched list
|
// Add to user watched list
|
||||||
$time = Carbon::now()->subHour(1);
|
$time = Carbon::now()->subHour(1);
|
||||||
$alreadyWatched = Watched::where('user_id', $user->id)->where('episode_id', $episode->id)->where('created_at', '>=', $time)->exists();
|
$alreadyWatched = Watched::where('user_id', $user->id)->where('episode_id', $episode->id)->where('created_at', '>=', $time)->exists();
|
||||||
if (!$alreadyWatched) {
|
if (! $alreadyWatched) {
|
||||||
Watched::create(['user_id' => $user->id, 'episode_id' => $episode->id]);
|
Watched::create(['user_id' => $user->id, 'episode_id' => $episode->id]);
|
||||||
cache()->forget('user' . $user->id . 'watched' . $episode->id);
|
cache()->forget('user'.$user->id.'watched'.$episode->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +79,7 @@ class StreamController extends Controller
|
|||||||
$playlistEpisodes = $playlist->episodes()->orderBy('position')->get();
|
$playlistEpisodes = $playlist->episodes()->orderBy('position')->get();
|
||||||
|
|
||||||
// Check if authorized
|
// Check if authorized
|
||||||
if ($playlist->is_private && (Auth::guest() || (!Auth::guest() && Auth::user()->id != $playlist->user_id))) {
|
if ($playlist->is_private && (Auth::guest() || (! Auth::guest() && Auth::user()->id != $playlist->user_id))) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
class IsAdmin {
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class IsAdmin
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* 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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 . _ = -',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -35,8 +35,9 @@ class GetFileSizeFromCDN implements ShouldQueue
|
|||||||
{
|
{
|
||||||
// Retrieve the download record, return if not found
|
// Retrieve the download record, return if not found
|
||||||
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ class GetFileSizeFromCDN implements ShouldQueue
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Send HTTP request to the endpoint
|
// Send HTTP request to the endpoint
|
||||||
$response = Http::get($endpoint . '/getSize/' . $file . '/' . $expire);
|
$response = Http::get($endpoint.'/getSize/'.$file.'/'.$expire);
|
||||||
|
|
||||||
// Check if response is successful
|
// Check if response is successful
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
@@ -67,9 +68,9 @@ class GetFileSizeFromCDN implements ShouldQueue
|
|||||||
Log::error("Failed to retrieve size for download ID: {$this->downloadId}, HTTP status: {$response->status()}");
|
Log::error("Failed to retrieve size for download ID: {$this->downloadId}, HTTP status: {$response->status()}");
|
||||||
}
|
}
|
||||||
} catch (RequestException $e) {
|
} catch (RequestException $e) {
|
||||||
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: " . $e->getMessage());
|
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: ".$e->getMessage());
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("An error occurred for download ID: {$this->downloadId}, error: " . $e->getMessage());
|
Log::error("An error occurred for download ID: {$this->downloadId}, error: ".$e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,10 +21,29 @@ 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);
|
||||||
if (!$download) {
|
if (! $download) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +52,44 @@ 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);
|
||||||
if (!$download) {
|
if (! $download) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
@@ -72,7 +75,7 @@ class LiveSearch extends Component
|
|||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
// User blacklist
|
// User blacklist
|
||||||
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
|
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
|
||||||
$this->blacklist = auth()->user()->tag_blacklist;
|
$this->blacklist = auth()->user()->tag_blacklist;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -54,12 +53,12 @@ class Playlists extends Component
|
|||||||
$playlists = Playlist::where('is_private', 0)
|
$playlists = Playlist::where('is_private', 0)
|
||||||
->withCount('episodes')
|
->withCount('episodes')
|
||||||
->having('episodes_count', '>', 1)
|
->having('episodes_count', '>', 1)
|
||||||
->when($this->search != '', fn($query) => $query->where('name', 'like', '%' . $this->search . '%'))
|
->when($this->search != '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||||
->orderBy($orderby, $orderdirection)
|
->orderBy($orderby, $orderdirection)
|
||||||
->paginate($this->pagination);
|
->paginate($this->pagination);
|
||||||
|
|
||||||
return view('livewire.playlists', [
|
return view('livewire.playlists', [
|
||||||
'playlists' => $playlists
|
'playlists' => $playlists,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)]
|
||||||
@@ -72,7 +75,7 @@ class UserLikes extends Component
|
|||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
// User blacklist
|
// User blacklist
|
||||||
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
|
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
|
||||||
$this->blacklist = auth()->user()->tag_blacklist;
|
$this->blacklist = auth()->user()->tag_blacklist;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use App\Models\Episode;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class ViewCount extends Component
|
|
||||||
{
|
|
||||||
public $episodeId = 0;
|
|
||||||
|
|
||||||
public $viewCount = 0;
|
|
||||||
|
|
||||||
public function mount(Episode $episode)
|
|
||||||
{
|
|
||||||
$this->episodeId = $episode->id;
|
|
||||||
$this->viewCount = $episode->view_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update()
|
|
||||||
{
|
|
||||||
$this->viewCount = Episode::where('id', $this->episodeId)->firstOrFail()->view_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.view-count');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ class Downloads extends Model
|
|||||||
$bytes /= 1024;
|
$bytes /= 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
return round($bytes, 2) . ' ' . $units[$i];
|
return round($bytes, 2).' '.$units[$i];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+59
-29
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +118,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
*/
|
*/
|
||||||
public function viewCount(): int
|
public function viewCount(): int
|
||||||
{
|
{
|
||||||
return cache()->remember('episodeViews' . $this->id, 300, fn() => $this->view_count);
|
return cache()->remember('episodeViews'.$this->id, 300, fn () => $this->view_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,7 +134,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
$index = floor(log($this->viewCount(), 1000));
|
$index = floor(log($this->viewCount(), 1000));
|
||||||
$shortNumber = $this->viewCount() / pow(1000, $index);
|
$shortNumber = $this->viewCount() / pow(1000, $index);
|
||||||
|
|
||||||
return round($shortNumber, 0) . $units[$index - 1];
|
return round($shortNumber, 0).$units[$index - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,7 +142,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
*/
|
*/
|
||||||
public function likeCount(): int
|
public function likeCount(): int
|
||||||
{
|
{
|
||||||
return cache()->remember('episodeLikes' . $this->id, 300, fn() => $this->likes->count());
|
return cache()->remember('episodeLikes'.$this->id, 300, fn () => $this->likes->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +150,12 @@ class Episode extends Model implements Sitemapable
|
|||||||
*/
|
*/
|
||||||
public function commentCount(): int
|
public function commentCount(): int
|
||||||
{
|
{
|
||||||
return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count());
|
return cache()->remember('episodeComments'.$this->id, 300, fn () => $this->comments->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function comments()
|
||||||
|
{
|
||||||
|
return $this->morphMany(Comment::class, 'commentable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProblematicTags(): string
|
public function getProblematicTags(): string
|
||||||
@@ -145,6 +173,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
|
|
||||||
$problematicResults .= $pTag;
|
$problematicResults .= $pTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $problematicResults;
|
return $problematicResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,17 +196,17 @@ class Episode extends Model implements Sitemapable
|
|||||||
*/
|
*/
|
||||||
public function hasAutoTrans(): bool
|
public function hasAutoTrans(): bool
|
||||||
{
|
{
|
||||||
return cache()->remember('mt' . $this->id, 900, fn() => $this->subtitles()->exists());
|
return cache()->remember('mt'.$this->id, 900, fn () => $this->subtitles()->exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function is48Fps(): bool
|
public function is48Fps(): bool
|
||||||
{
|
{
|
||||||
return cache()->remember('48fps' . $this->id, 900, fn() => $this->interpolated);
|
return cache()->remember('48fps'.$this->id, 900, fn () => $this->interpolated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isUHD48Fps(): bool
|
public function isUHD48Fps(): bool
|
||||||
{
|
{
|
||||||
return cache()->remember('48fpsUHD' . $this->id, 900, fn() => $this->interpolated_uhd);
|
return cache()->remember('48fpsUHD'.$this->id, 900, fn () => $this->interpolated_uhd);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getResolution(): string
|
public function getResolution(): string
|
||||||
@@ -191,7 +220,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
|
|
||||||
public function userWatched(int $user_id): bool
|
public function userWatched(int $user_id): bool
|
||||||
{
|
{
|
||||||
return cache()->remember('user' . $user_id . 'watched' . $this->id, 300, fn() => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
|
return cache()->remember('user'.$user_id.'watched'.$this->id, 300, fn () => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function watched(): HasMany
|
public function watched(): HasMany
|
||||||
@@ -199,15 +228,16 @@ 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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toSitemapTag(): Url | string | array
|
public function toSitemapTag(): Url|string|array
|
||||||
{
|
{
|
||||||
return Url::create(route('hentai.index', $this->slug))
|
return Url::create(route('hentai.index', $this->slug))
|
||||||
->setLastModificationDate(Carbon::create($this->created_at));
|
->setLastModificationDate(Carbon::create($this->created_at));
|
||||||
|
|||||||
+13
-10
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -59,7 +62,7 @@ class Hentai extends Model implements Sitemapable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toSitemapTag(): Url | string | array
|
public function toSitemapTag(): Url|string|array
|
||||||
{
|
{
|
||||||
return Url::create(route('hentai.index', $this->slug))
|
return Url::create(route('hentai.index', $this->slug))
|
||||||
->setLastModificationDate(Carbon::create($this->created_at));
|
->setLastModificationDate(Carbon::create($this->created_at));
|
||||||
|
|||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PopularDaily extends Model
|
|||||||
*
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $fillable = [ 'episode_id' ];
|
protected $fillable = ['episode_id'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Episode.
|
* Get the Episode.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PopularMonthly extends Model
|
|||||||
*
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $fillable = [ 'episode_id' ];
|
protected $fillable = ['episode_id'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Episode.
|
* Get the Episode.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PopularWeekly extends Model
|
|||||||
*
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $fillable = [ 'episode_id' ];
|
protected $fillable = ['episode_id'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Episode.
|
* Get the Episode.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,29 +5,25 @@ 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;
|
||||||
|
|
||||||
if($request->hasFile('episodegallery'.$galleryInputNumber)) {
|
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
|
||||||
|
|
||||||
$this->deleteOldGallery($episode);
|
$this->deleteOldGallery($episode);
|
||||||
|
|
||||||
$this->createGalleryFolder($hentai);
|
$this->createGalleryFolder($hentai);
|
||||||
|
|
||||||
$counter = 0;
|
$counter = 0;
|
||||||
foreach($request->file('episodegallery'.$galleryInputNumber) as $file) {
|
foreach ($request->file('episodegallery'.$galleryInputNumber) as $file) {
|
||||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||||
$this->saveGalleryImage($gallery, $file);
|
$this->saveGalleryImage($gallery, $file);
|
||||||
$counter += 1;
|
$counter += 1;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user