mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-14 20:40:49 +08:00
Merge branch '1.7' into php8
This commit is contained in:
@@ -41,7 +41,7 @@ class AttendanceMigrate extends Command
|
||||
{
|
||||
$rep = new AttendanceRepository();
|
||||
$result = $rep->migrateAttendance();
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', REQUEST_ID, __METHOD__, var_export($result, true), last_query());
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', nexus() ? nexus()->getRequestId() : 'NO_REQUEST_ID', __METHOD__, var_export($result, true), last_query());
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -42,7 +42,7 @@ class BackuAll extends Command
|
||||
$result = $rep->backupAll();
|
||||
$log = sprintf(
|
||||
'[%s], %s, result: %s',
|
||||
REQUEST_ID, __METHOD__, var_export($result, true)
|
||||
nexus()->getRequestId(), __METHOD__, var_export($result, true)
|
||||
);
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
|
||||
@@ -42,7 +42,7 @@ class BackupCronjob extends Command
|
||||
$result = $rep->cronjobBackup();
|
||||
$log = sprintf(
|
||||
'[%s], %s, result: %s',
|
||||
REQUEST_ID, __METHOD__, var_export($result, true)
|
||||
nexus()->getRequestId(), __METHOD__, var_export($result, true)
|
||||
);
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
|
||||
@@ -40,7 +40,7 @@ class BackupDatabase extends Command
|
||||
{
|
||||
$rep = new ToolRepository();
|
||||
$result = $rep->backupDatabase();
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class BackupWeb extends Command
|
||||
{
|
||||
$rep = new ToolRepository();
|
||||
$result = $rep->backupWeb();
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class DeleteExpiredToken extends Command
|
||||
|
||||
$query->where('last_used_at', '<', Carbon::now()->subDays($days));
|
||||
$result = $query->delete();
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', REQUEST_ID, __METHOD__, var_export($result, true), last_query());
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true), last_query());
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -45,7 +45,7 @@ class ExamAssign extends Command
|
||||
$end = $this->option('end');
|
||||
$this->info(sprintf('uid: %s, examId: %s, begin: %s, end: %s', $uid, $examId, $begin, $end));
|
||||
$result = $examRep->assignToUser($uid, $examId, $begin, $end);
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -40,7 +40,7 @@ class ExamAssignCronjob extends Command
|
||||
{
|
||||
$examRep = new ExamRepository();
|
||||
$result = $examRep->cronjonAssign();
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExamCheckoutCronjob extends Command
|
||||
$ignoreTimeRange = $this->option('ignore-time-range');
|
||||
$this->info('ignore-time-range: ' . var_export($ignoreTimeRange, true));
|
||||
$result = $examRep->cronjobCheckout($ignoreTimeRange);
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExamUpdateProgress extends Command
|
||||
$uid = $this->argument('uid');
|
||||
$examRep = new ExamRepository();
|
||||
$result = $examRep->updateProgress($uid);
|
||||
$this->info(REQUEST_ID . ", result: " . var_export($result, true));
|
||||
$this->info(nexus()->getRequestId() . ", result: " . var_export($result, true));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ class HitAndRunUpdateStatus extends Command
|
||||
$rep = new HitAndRunRepository();
|
||||
$result = $rep->cronjobUpdateStatus($uid, $torrentId, $ignoreTime);
|
||||
$log = sprintf(
|
||||
'[%s], %s, uid: %s, torrentId: %s, ignoreTime: %s, result: %s',
|
||||
REQUEST_ID, __METHOD__, $uid, $torrentId, $ignoreTime, var_export($result, true)
|
||||
'[%s], %s, uid: %s, torrentId: %s, result: %s',
|
||||
nexus()->getRequestId(), __METHOD__, $uid, $torrentId, var_export($result, true)
|
||||
);
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
|
||||
@@ -43,7 +43,7 @@ class MigrateTorrentTag extends Command
|
||||
{
|
||||
$rep = new TagRepository();
|
||||
$result = $rep->migrateTorrentTag();
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', REQUEST_ID, __METHOD__, var_export($result, true), last_query());
|
||||
$log = sprintf('[%s], %s, result: %s, query: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true), last_query());
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
return 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\TorrentUpdated;
|
||||
use App\Http\Resources\TagResource;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Exam;
|
||||
@@ -9,15 +10,18 @@ use App\Models\ExamProgress;
|
||||
use App\Models\ExamUser;
|
||||
use App\Models\HitAndRun;
|
||||
use App\Models\Medal;
|
||||
use App\Models\Peer;
|
||||
use App\Models\SearchBox;
|
||||
use App\Models\Snatch;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use App\Repositories\AgentAllowRepository;
|
||||
use App\Repositories\AttendanceRepository;
|
||||
use App\Repositories\ExamRepository;
|
||||
use App\Repositories\HitAndRunRepository;
|
||||
use App\Repositories\SearchBoxRepository;
|
||||
use App\Repositories\SearchRepository;
|
||||
use App\Repositories\TagRepository;
|
||||
use App\Repositories\TorrentRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
@@ -27,7 +31,10 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use JeroenG\Explorer\Domain\Syntax\Matching;
|
||||
use JeroenG\Explorer\Infrastructure\Scout\ElasticEngine;
|
||||
use Rhilip\Bencode\Bencode;
|
||||
|
||||
class Test extends Command
|
||||
@@ -63,9 +70,55 @@ class Test extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$rep = new AttendanceRepository();
|
||||
$r = $rep->migrateAttendance();
|
||||
$searchRep = new SearchRepository();
|
||||
$r = $searchRep->deleteIndex();
|
||||
$r = $searchRep->createIndex();
|
||||
$r = $searchRep->import();
|
||||
|
||||
$arr = [
|
||||
'cat' => 'category',
|
||||
'source' => 'source',
|
||||
'medium' => 'medium',
|
||||
'codec' => 'codec',
|
||||
'audiocodec' => 'audiocodec',
|
||||
'standard' => 'standard',
|
||||
'processing' => 'processing',
|
||||
'team' => 'team',
|
||||
];
|
||||
$queryString = 'cat401=1&cat404=1&source2=1&medium2=1&medium3=1&codec3=1&audiocodec3=1&standard2=1&standard3=1&processing2=1&team3=1&team4=1&incldead=1&spstate=0&inclbookmarked=0&search=&search_area=0&search_mode=0';
|
||||
$userSetting = '[cat401][cat404][sou1][med1][cod1][sta2][sta3][pro2][tea2][aud2][incldead=0][spstate=3][inclbookmarked=2]';
|
||||
// foreach ($arr as $queryField => $value) {
|
||||
//// $pattern = sprintf("/\[%s([\d]+)\]/", substr($queryField, 0, 3));
|
||||
// $pattern = "/{$queryField}([\d]+)=/";
|
||||
// if (preg_match_all($pattern, $queryString, $matches)) {
|
||||
// dump($matches);
|
||||
// echo '----------------------' . PHP_EOL;
|
||||
// }
|
||||
// }
|
||||
// $r = preg_match("/\[incldead=([\d]+)\]/", $userSetting, $matches);
|
||||
// dump($matches);
|
||||
|
||||
$params = [
|
||||
'tag_id' => 1,
|
||||
// 'incldead' => 0,
|
||||
// 'spstate' => 0,
|
||||
// 'inclbookmarked' => 0,
|
||||
// 'search' => '5034',
|
||||
// 'search_area' => 4,
|
||||
// 'search_mode' => 0,
|
||||
];
|
||||
$queryString = "cat401=1&cat404=1&cat405=1&cat402=1&cat403=1&cat406=1&cat407=1&cat409=1&cat408=1&incldead=0&spstate=0&inclbookmarked=0&search=5034838&search_area=4&search_mode=0";
|
||||
// $r = $searchRep->listTorrentFromEs($params, 1, '');
|
||||
|
||||
// $r = $searchRep->updateTorrent(1);
|
||||
// $r = $searchRep->updateUser(1);
|
||||
// $r = $searchRep->addTorrent(1);
|
||||
// $r = $searchRep->deleteBookmark(1);
|
||||
// $r = $searchRep->addBookmark(1);
|
||||
|
||||
dd($r);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class UserResetPassword extends Command
|
||||
|
||||
$rep = new UserRepository();
|
||||
$result = $rep->resetPassword($uid, $password, $passwordConfirmation);
|
||||
$log = sprintf('[%s], %s, result: %s', REQUEST_ID, __METHOD__, var_export($result, true));
|
||||
$log = sprintf('[%s], %s, result: %s', nexus()->getRequestId(), __METHOD__, var_export($result, true));
|
||||
$this->info($log);
|
||||
do_log($log);
|
||||
}
|
||||
|
||||
38
app/Events/TorrentUpdated.php
Normal file
38
app/Events/TorrentUpdated.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TorrentUpdated
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public int $torrentId;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(int $torrentId)
|
||||
{
|
||||
$this->torrentId = $torrentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('channel-name');
|
||||
}
|
||||
}
|
||||
8
app/Exceptions/ClientNotAllowedException.php
Normal file
8
app/Exceptions/ClientNotAllowedException.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class ClientNotAllowedException extends NexusException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -61,7 +61,8 @@ class Handler extends ExceptionHandler
|
||||
|
||||
$this->renderable(function (NotFoundHttpException $e) {
|
||||
if ($e->getPrevious() && $e->getPrevious() instanceof ModelNotFoundException) {
|
||||
return response()->json(fail('No query result.', request()->all()), 404);
|
||||
$exception = $e->getPrevious();
|
||||
return response()->json(fail($exception->getMessage(), request()->all()), 404);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -97,7 +98,12 @@ class Handler extends ExceptionHandler
|
||||
|
||||
protected function getHttpStatusCode(Throwable $e)
|
||||
{
|
||||
if ($e instanceof \InvalidArgumentException || $e instanceof NexusException) {
|
||||
if (
|
||||
$e instanceof NexusException
|
||||
|| $e instanceof \InvalidArgumentException
|
||||
|| $e instanceof \LogicException
|
||||
|| $e instanceof \RuntimeException
|
||||
) {
|
||||
return 200;
|
||||
}
|
||||
if ($this->isHttpException($e)) {
|
||||
|
||||
8
app/Exceptions/TrackerException.php
Normal file
8
app/Exceptions/TrackerException.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class TrackerException extends NexusException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -4,10 +4,20 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\CommentResource;
|
||||
use App\Models\Comment;
|
||||
use App\Repositories\CommentRepository;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
private $repository;
|
||||
|
||||
public function __construct(CommentRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -20,7 +30,6 @@ class CommentController extends Controller
|
||||
$comments = Comment::query()
|
||||
->with($with)
|
||||
->where('torrent', $torrentId)
|
||||
->whereHas('create_user')
|
||||
->paginate();
|
||||
$resource = CommentResource::collection($comments);
|
||||
$resource->additional([
|
||||
@@ -30,6 +39,35 @@ class CommentController extends Controller
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
private function prepareData(Request $request)
|
||||
{
|
||||
$allTypes = array_keys(Comment::TYPE_MAPS);
|
||||
$request->validate([
|
||||
'type' => ['required', Rule::in($allTypes)],
|
||||
'torrent_id' => 'nullable|integer',
|
||||
'text' => 'required',
|
||||
'offer_id' => 'nullable|integer',
|
||||
'request_id' => 'nullable|integer',
|
||||
'anonymous' => 'nullable',
|
||||
]);
|
||||
$data = [
|
||||
'type' => $request->type,
|
||||
'torrent' => $request->torrent_id,
|
||||
'text' => $request->text,
|
||||
'ori_text' => $request->text,
|
||||
'offer' => $request->offer_id,
|
||||
'request' => $request->request_id,
|
||||
'anonymous' => $request->anonymous,
|
||||
];
|
||||
$data = array_filter($data);
|
||||
foreach ($allTypes as $type) {
|
||||
if ($data['type'] == $type && empty($data[$type])) {
|
||||
throw new \InvalidArgumentException("require {$type}_id");
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
@@ -38,7 +76,10 @@ class CommentController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
$user = Auth::user();
|
||||
$comment = $this->repository->store($this->prepareData($request), $user);
|
||||
$resource = new CommentResource($comment);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,11 +18,14 @@ class MessageController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$messages = Message::query()
|
||||
->where('receiver', $user->id)
|
||||
$query = $user->receive_messages()
|
||||
->with(['send_user'])
|
||||
->orderBy('id', 'desc')
|
||||
->paginate();
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
if ($request->unread) {
|
||||
$query->where('unread', 'yes');
|
||||
}
|
||||
$messages = $query->paginate();
|
||||
$resource = MessageResource::collection($messages);
|
||||
return $this->success($resource);
|
||||
|
||||
@@ -48,7 +51,11 @@ class MessageController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$message = Message::query()->with(['send_user'])->findOrFail($id);
|
||||
$message->update(['unread' => 'no']);
|
||||
$resource = new MessageResource($message);
|
||||
$resource->additional([
|
||||
'page_title' => nexus_trans('message.show.page_title'),
|
||||
]);
|
||||
|
||||
return $this->success($resource);
|
||||
}
|
||||
@@ -75,4 +82,27 @@ class MessageController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function listUnread(Request $request): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
$query = $user->receive_messages()
|
||||
->with(['send_user'])
|
||||
->orderBy('id', 'desc')
|
||||
->where('unread', 'yes');
|
||||
|
||||
$messages = $query->paginate();
|
||||
$resource = MessageResource::collection($messages);
|
||||
$resource->additional([
|
||||
'site_info' => site_info(),
|
||||
]);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
public function countUnread()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$count = $user->receive_messages()->where('unread', 'yes')->count();
|
||||
return $this->success(['unread' => $count]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ namespace App\Http\Controllers;
|
||||
use App\Http\Resources\NewsResource;
|
||||
use App\Models\News;
|
||||
use App\Repositories\NewsRepository;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class NewsController extends Controller
|
||||
{
|
||||
@@ -93,4 +96,24 @@ class NewsController extends Controller
|
||||
$result = $this->repository->delete($id);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function latest()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$result = News::query()->orderBy('id', 'desc')->first();
|
||||
$resource = new NewsResource($result);
|
||||
$resource->additional([
|
||||
'site_info' => site_info(),
|
||||
]);
|
||||
/**
|
||||
* Visiting the home page is the same as viewing the latest news
|
||||
* @see functions.php line 2590
|
||||
*/
|
||||
$user->update(['last_home' => Carbon::now()]);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
162
app/Http/Controllers/PollController.php
Normal file
162
app/Http/Controllers/PollController.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\PollResource;
|
||||
use App\Models\Poll;
|
||||
use App\Repositories\PollRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PollController extends Controller
|
||||
{
|
||||
private $repository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private function getRules(): array
|
||||
{
|
||||
return [
|
||||
'family_id' => 'required|numeric',
|
||||
'name' => 'required|string',
|
||||
'peer_id' => 'required|string',
|
||||
'agent' => 'required|string',
|
||||
'comment' => 'required|string',
|
||||
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$result = $this->repository->getList($request->all());
|
||||
$resource = PollResource::collection($result);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate($this->getRules());
|
||||
$result = $this->repository->store($request->all());
|
||||
$resource = new PollResource($result);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$result = Poll::query()->findOrFail($id);
|
||||
$resource = new PollResource($result);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return array
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate($this->getRules());
|
||||
$result = $this->repository->update($request->all(), $id);
|
||||
$resource = new PollResource($result);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$result = $this->repository->delete($id);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo 弃权选项
|
||||
* @return array
|
||||
*/
|
||||
public function latest()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$poll = Poll::query()->orderBy('id', 'desc')->first();
|
||||
$selection = null;
|
||||
$answerStats = [];
|
||||
if ($poll) {
|
||||
$baseAnswerQuery = $poll->answers()->where('selection', '<=', Poll::MAX_OPTION_INDEX);
|
||||
$poll->answers_count = (clone $baseAnswerQuery)->count();
|
||||
$answer = $poll->answers()->where('userid', $user->id)->first();
|
||||
$options = [];
|
||||
for ($i = 0; $i <= Poll::MAX_OPTION_INDEX; $i++) {
|
||||
$field = "option{$i}";
|
||||
$value = $poll->{$field};
|
||||
if ($value !== '') {
|
||||
$options[$i] = $value;
|
||||
}
|
||||
}
|
||||
if ($answer) {
|
||||
$selection = $answer->selection;
|
||||
} else {
|
||||
$options["255"] = "弃权(我想偷看结果!)";
|
||||
}
|
||||
$poll->options = $options;
|
||||
|
||||
$answerStats = (clone $baseAnswerQuery)
|
||||
->selectRaw("selection, count(*) as count")->groupBy("selection")
|
||||
->get()->pluck('count', 'selection')->toArray();
|
||||
foreach ($answerStats as $index => &$value) {
|
||||
$value = number_format(($value / $poll->answers_count) * 100, 2) . '%';
|
||||
}
|
||||
}
|
||||
|
||||
$resource = new PollResource($poll);
|
||||
$resource->additional([
|
||||
'selection' => $selection,
|
||||
'answer_stats' => $answerStats,
|
||||
'site_info' => site_info(),
|
||||
]);
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
public function vote(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'poll_id' => 'required',
|
||||
'selection' => 'required|integer|min:0|max:255',
|
||||
]);
|
||||
$pollId = $request->poll_id;
|
||||
$selection = $request->selection;
|
||||
$user = Auth::user();
|
||||
$poll = Poll::query()->findOrFail($pollId);
|
||||
$data = [
|
||||
'userid' => $user->id,
|
||||
'selection' => $selection,
|
||||
];
|
||||
$answer = $poll->answers()->create($data);
|
||||
return $this->success($answer->toArray());
|
||||
}
|
||||
|
||||
}
|
||||
92
app/Http/Controllers/RewardController.php
Normal file
92
app/Http/Controllers/RewardController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\RewardResource;
|
||||
use App\Http\Resources\PeerResource;
|
||||
use App\Http\Resources\SnatchResource;
|
||||
use App\Models\Peer;
|
||||
use App\Models\Snatch;
|
||||
use App\Repositories\RewardRepository;
|
||||
use App\Repositories\TorrentRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class RewardController extends Controller
|
||||
{
|
||||
private $repository;
|
||||
|
||||
public function __construct(RewardRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'torrent_id' => 'required',
|
||||
]);
|
||||
$result = $this->repository->getList($request->all());
|
||||
$resource = RewardResource::collection($result);
|
||||
$resource->additional([
|
||||
'page_title' => nexus_trans('reward.index.page_title'),
|
||||
]);
|
||||
|
||||
return $this->success($resource);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'torrent_id' => 'required',
|
||||
'value' => 'required',
|
||||
]);
|
||||
$result = $this->repository->store($request->torrent_id, $request->value, Auth::user());
|
||||
$resource = new RewardResource($result);
|
||||
return $this->success($resource, '赠魔成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,7 @@ class TagController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate($this->getRules());
|
||||
$data = $request->all();
|
||||
if (isset($data['priority'])) {
|
||||
$data['priority'] = intval($data['priority']);
|
||||
}
|
||||
$data = array_filter($request->all());
|
||||
$result = $this->repository->store($data);
|
||||
$resource = new TagResource($result);
|
||||
return $this->success($resource);
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\ThankResource;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Thank;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ThankController extends Controller
|
||||
{
|
||||
@@ -37,7 +42,48 @@ class ThankController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
$user = Auth::user();
|
||||
$request->validate(['torrent_id' => 'required']);
|
||||
$torrentId = $request->torrent_id;
|
||||
$torrent = Torrent::query()->findOrFail($torrentId, Torrent::$commentFields);
|
||||
$torrent->checkIsNormal();
|
||||
$torrentOwner = User::query()->findOrFail($torrent->owner);
|
||||
if ($user->id == $torrentOwner->id) {
|
||||
throw new \LogicException("you can't thank to yourself");
|
||||
}
|
||||
$torrentOwner->checkIsNormal();
|
||||
if ($user->thank_torrent_logs()->where('torrentid', $torrentId)->exists()) {
|
||||
throw new \LogicException("you already thank this torrent");
|
||||
}
|
||||
|
||||
$result = DB::transaction(function () use ($user, $torrentOwner, $torrent) {
|
||||
$thank = $user->thank_torrent_logs()->create(['torrentid' => $torrent->id]);
|
||||
$sayThanksBonus = Setting::get('bonus.saythanks');
|
||||
$receiveThanksBonus = Setting::get('bonus.receivethanks');
|
||||
if ($sayThanksBonus > 0) {
|
||||
$affectedRows = User::query()
|
||||
->where('id', $user->id)
|
||||
->where('seedbonus', $user->seedbonus)
|
||||
->increment('seedbonus', $sayThanksBonus);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment user bonus fail.");
|
||||
}
|
||||
}
|
||||
if ($receiveThanksBonus > 0) {
|
||||
$affectedRows = User::query()
|
||||
->where('id', $torrentOwner->id)
|
||||
->where('seedbonus', $torrentOwner->seedbonus)
|
||||
->increment('seedbonus', $receiveThanksBonus);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment owner bonus fail.");
|
||||
}
|
||||
}
|
||||
return $thank;
|
||||
});
|
||||
$resource = new ThankResource($result);
|
||||
return $this->success($resource, '说谢谢成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Repositories\ToolRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ToolController extends Controller
|
||||
{
|
||||
@@ -14,4 +15,11 @@ class ToolController extends Controller
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function notifications(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
$result = $this->repository->getNotificationCount($user);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\RewardResource;
|
||||
use App\Http\Resources\TorrentResource;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use App\Repositories\TorrentRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -51,16 +53,19 @@ class TorrentController extends Controller
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
|
||||
$result = $this->repository->getDetail($id, Auth::user());
|
||||
|
||||
$isBookmarked = Auth::user()->bookmarks()->where('torrentid', $id)->exists();
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
$user = Auth::user();
|
||||
$result = $this->repository->getDetail($id, $user);
|
||||
$isBookmarked = $user->bookmarks()->where('torrentid', $id)->exists();
|
||||
|
||||
$resource = new TorrentResource($result);
|
||||
$resource->additional([
|
||||
'page_title' => nexus_trans('torrent.show.page_title'),
|
||||
'field_labels' => Torrent::getFieldLabels(),
|
||||
'is_bookmarked' => (int)$isBookmarked,
|
||||
'bonus_reward_values' => Torrent::BONUS_REWARD_VALUES,
|
||||
]);
|
||||
|
||||
return $this->success($resource);
|
||||
|
||||
34
app/Http/Controllers/TrackerController.php
Normal file
34
app/Http/Controllers/TrackerController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Repositories\TrackerRepository;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrackerController extends Controller
|
||||
{
|
||||
private TrackerRepository $repository;
|
||||
|
||||
public function __construct(TrackerRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function announce(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
return $this->repository->announce($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function scrape(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
return $this->repository->scrape($request);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
// \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\BootNexus::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
29
app/Http/Middleware/BootNexus.php
Normal file
29
app/Http/Middleware/BootNexus.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Nexus\Nexus;
|
||||
|
||||
class BootNexus
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
Nexus::boot();
|
||||
do_log(sprintf(
|
||||
"Nexus booted. request.server: %s, request.header: %s, request.query: %s, request.input: %s",
|
||||
nexus_json_encode($request->server()), nexus_json_encode($request->header()), nexus_json_encode($request->query()), nexus_json_encode($request->input())
|
||||
));
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class Permission
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
if (!$user || (IS_PLATFORM_ADMIN && !$user->canAccessAdmin())) {
|
||||
if (!$user || (nexus()->isPlatformAdmin() && !$user->canAccessAdmin())) {
|
||||
do_log("denied!");
|
||||
throw new UnauthorizedException('Unauthorized!');
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ class Platform
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (empty(CURRENT_PLATFORM)) {
|
||||
if (empty(nexus()->getPlatform())) {
|
||||
throw new \InvalidArgumentException("Require platform header.");
|
||||
}
|
||||
if (!in_array(CURRENT_PLATFORM, PLATFORMS)) {
|
||||
if (!nexus()->isPlatformValid()) {
|
||||
throw new \InvalidArgumentException("Invalid platform: " . CURRENT_PLATFORM);
|
||||
}
|
||||
return $next($request);
|
||||
|
||||
@@ -15,5 +15,7 @@ class TrimStrings extends Middleware
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'peer_id',
|
||||
'info_hash',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class MessageResource extends JsonResource
|
||||
'subject' => $this->subject,
|
||||
'msg' => strip_all_tags($this->msg),
|
||||
'added_human' => $this->added->diffForHumans(),
|
||||
'added' => format_datetime($this->added),
|
||||
'send_user' => new UserResource($this->whenLoaded('send_user')),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ class NewsResource extends JsonResource
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$descriptionArr = format_description($this->body);
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'added' => $this->added,
|
||||
'body' => $descriptionArr,
|
||||
'images' => get_image_from_description($descriptionArr),
|
||||
'added' => format_datetime($this->added, 'Y.m.d'),
|
||||
'user' => new UserResource($this->whenLoaded('user'))
|
||||
];
|
||||
}
|
||||
|
||||
28
app/Http/Resources/PollResource.php
Normal file
28
app/Http/Resources/PollResource.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PollResource extends JsonResource
|
||||
{
|
||||
public $preserveKeys = true;
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$out = [
|
||||
'id' => $this->id,
|
||||
'added' => format_datetime($this->added),
|
||||
'question' => $this->question,
|
||||
'answers_count' => $this->answers_count,
|
||||
'options' => $this->options,
|
||||
];
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
27
app/Http/Resources/RewardResource.php
Normal file
27
app/Http/Resources/RewardResource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class RewardResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user_id' => $this->userid,
|
||||
'torrent_id' => $this->torrentid,
|
||||
'value' => $this->value,
|
||||
'created_at' => format_datetime($this->created_at),
|
||||
'updated_at' => format_datetime($this->updated_at),
|
||||
'user' => new UserResource($this->whenLoaded('user'))
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ class TagResource extends JsonResource
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'color' => $this->color,
|
||||
'font_color' => $this->font_color,
|
||||
'font_size' => $this->font_size,
|
||||
'padding' => $this->padding,
|
||||
'margin' => $this->margin,
|
||||
'border_radius' => $this->border_radius,
|
||||
'priority' => $this->priority,
|
||||
'created_at' => format_datetime($this->created_at),
|
||||
'updated_at' => format_datetime($this->updated_at),
|
||||
|
||||
@@ -16,6 +16,8 @@ class ThankResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'torrent_id' => $this->torrentid,
|
||||
'user_id' => $this->userid,
|
||||
'user' => new UserResource($this->whenLoaded('user')),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,14 +35,18 @@ class TorrentResource extends JsonResource
|
||||
'numfiles' => $this->numfiles,
|
||||
'sp_state' => $this->sp_state,
|
||||
'sp_state_real' => $this->sp_state_real,
|
||||
'sp_state_real_text' => $this->spStateRealText,
|
||||
'promotion_info' => $this->promotionInfo,
|
||||
'hr' => $this->hr,
|
||||
'pick_type' => $this->picktype,
|
||||
'pick_time' => $this->picktime,
|
||||
'pick_info' => $this->pickInfo,
|
||||
'download_url' => $this->download_url,
|
||||
'user' => new UserResource($this->whenLoaded('user')),
|
||||
'anonymous' => $this->anonymous,
|
||||
'basic_category' => new CategoryResource($this->whenLoaded('basic_category')),
|
||||
'tags' => TagResource::collection($this->whenLoaded('tags')),
|
||||
'thanks' => ThankResource::collection($this->whenLoaded('thanks')),
|
||||
'reward_logs' => RewardResource::collection($this->whenLoaded('reward_logs')),
|
||||
];
|
||||
$descriptionArr = format_description($this->descr);
|
||||
$out['cover'] = get_image_from_description($descriptionArr, true);
|
||||
@@ -63,6 +67,7 @@ class TorrentResource extends JsonResource
|
||||
|
||||
$out['thank_users_count'] = $this->thank_users_count;
|
||||
$out['peers_count'] = $this->peers_count;
|
||||
$out['reward_logs_count'] = $this->reward_logs_count;
|
||||
}
|
||||
// $out['upload_peers_count'] = $this->upload_peers_count;
|
||||
// $out['download_peers_count'] = $this->download_peers_count;
|
||||
|
||||
21
app/Listeners/ResetNexus.php
Normal file
21
app/Listeners/ResetNexus.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Nexus\Nexus;
|
||||
|
||||
class ResetNexus
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param mixed $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event): void
|
||||
{
|
||||
Nexus::flush();
|
||||
}
|
||||
}
|
||||
62
app/Listeners/SyncTorrentToEs.php
Normal file
62
app/Listeners/SyncTorrentToEs.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Repositories\SearchRepository;
|
||||
use App\Repositories\ToolRepository;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class SyncTorrentToEs implements ShouldQueue
|
||||
{
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
$id = $event->torrentId;
|
||||
$searchRep = new SearchRepository();
|
||||
$result = $searchRep->updateTorrent($id);
|
||||
do_log("result: " . var_export($result, true));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* handle failed
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function failed($event, \Throwable $exception)
|
||||
{
|
||||
$toolRep = new ToolRepository();
|
||||
$to = Setting::get('main.SITEEMAIL');
|
||||
$subject = sprintf('Event: %s listener: %s handle error', get_class($event), __CLASS__);
|
||||
$body = sprintf("%s\n%s", $exception->getMessage(), $exception->getTraceAsString());
|
||||
try {
|
||||
$result = $toolRep->sendMail($to, $subject, $body);
|
||||
if ($result === false) {
|
||||
do_log("$subject send mail fail", 'alert');
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
do_log("$subject send mail fail: " . $exception->getMessage() . $exception->getTraceAsString(), 'alert');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class NexusFormatter
|
||||
|
||||
protected function formatter()
|
||||
{
|
||||
$format = "[%datetime%] [" . REQUEST_ID . "] %channel%.%level_name%: %message% %context% %extra%\n";
|
||||
$format = "[%datetime%] [" . nexus()->getRequestId() . "] %channel%.%level_name%: %message% %context% %extra%\n";
|
||||
return tap(new LineFormatter($format, 'Y-m-d H:i:s', true, true), function ($formatter) {
|
||||
$formatter->includeStacktraces();
|
||||
});
|
||||
|
||||
@@ -8,4 +8,14 @@ class Bookmark extends NexusModel
|
||||
protected $table = 'bookmarks';
|
||||
|
||||
protected $fillable = ['userid', 'torrentid'];
|
||||
|
||||
public function torrent()
|
||||
{
|
||||
return $this->belongsTo(Torrent::class, 'torrentid');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(Torrent::class, 'userid');
|
||||
}
|
||||
}
|
||||
|
||||
12
app/Models/Cheater.php
Normal file
12
app/Models/Cheater.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class Cheater extends NexusModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'added', 'userid', 'torrentid', 'uploaded', 'downloaded', 'anctime', 'seeders', 'leechers', 'hit',
|
||||
'dealtby', 'dealtwith', 'comment',
|
||||
];
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Comment extends NexusModel
|
||||
{
|
||||
protected $casts = [
|
||||
@@ -10,6 +12,45 @@ class Comment extends NexusModel
|
||||
'editdate' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = ['user', 'torrent', 'added', 'text', 'ori_text', 'editedby', 'editdate', 'offer', 'request', 'anonymous'];
|
||||
|
||||
const TYPE_TORRENT = 'torrent';
|
||||
const TYPE_REQUEST = 'request';
|
||||
const TYPE_OFFER = 'offer';
|
||||
|
||||
const TYPE_MAPS = [
|
||||
self::TYPE_TORRENT => [
|
||||
'model' => Torrent::class,
|
||||
'foreign_key' => 'torrent',
|
||||
'target_name_field' => 'name',
|
||||
'target_script' => 'details.php?id=%s'
|
||||
],
|
||||
self::TYPE_REQUEST => [
|
||||
'model' => Request::class,
|
||||
'foreign_key' => 'request',
|
||||
'target_name_field' => 'request',
|
||||
'target_script' => 'viewrequests.php?id=%s&req_details=1'
|
||||
],
|
||||
self::TYPE_OFFER => [
|
||||
'model' => Offer::class,
|
||||
'foreign_key' => 'offer',
|
||||
'target_name_field' => 'name',
|
||||
'target_script' => 'offers.php?id=%s&off_details=1'
|
||||
],
|
||||
];
|
||||
|
||||
public function scopeType(Builder $query, string $type, int $typeValue)
|
||||
{
|
||||
foreach (self::TYPE_MAPS as $key => $value) {
|
||||
if ($type != $key) {
|
||||
$query->where($value['foreign_key'], 0);
|
||||
} else {
|
||||
$query->where($value['foreign_key'], $typeValue);
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function related_torrent()
|
||||
{
|
||||
return $this->belongsTo(Torrent::class, 'torrent');
|
||||
|
||||
@@ -21,4 +21,20 @@ class NexusModel extends Model
|
||||
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is valid date string
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/19271381/correctly-determine-if-date-string-is-a-valid-date-in-that-format
|
||||
* @param $name
|
||||
* @param string $format
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidDate($name, $format = 'Y-m-d H:i:s'): bool
|
||||
{
|
||||
$date = $this->getRawOriginal($name);
|
||||
$d = \DateTime::createFromFormat($format, $date);
|
||||
// The Y ( 4 digits year ) returns TRUE for any integer with any number of digits so changing the comparison from == to === fixes the issue.
|
||||
return $d && $d->format($format) === $date;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
app/Models/Offer.php
Normal file
19
app/Models/Offer.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class Offer extends NexusModel
|
||||
{
|
||||
protected $fillable = ['userid', 'name', 'descr', 'comments', 'added'];
|
||||
|
||||
protected $casts = [
|
||||
'added' => 'datetime'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,9 +4,15 @@ namespace App\Models;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class Peer extends NexusModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'torrent', 'peer_id', 'ip', 'port', 'uploaded', 'downloaded', 'to_go', 'seeder', 'started', 'last_action',
|
||||
'prev_action', 'connectable', 'userid', 'agent', 'finishedat', 'downloadoffset', 'uploadedoffset', 'passkey',
|
||||
];
|
||||
|
||||
const CONNECTABLE_YES = 'yes';
|
||||
|
||||
const CONNECTABLE_NO = 'no';
|
||||
@@ -71,4 +77,32 @@ class Peer extends NexusModel
|
||||
{
|
||||
return $this->belongsTo(Torrent::class, 'torrent');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function updateConnectableStateIfNeeded()
|
||||
{
|
||||
$tmp_ip = $this->ip;
|
||||
// IPv6 Check
|
||||
if (filter_var($tmp_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$tmp_ip = '['.$tmp_ip.']';
|
||||
}
|
||||
$cacheKey = 'peers:connectable:'.$tmp_ip.'-'.$this->port.'-'.$this->agent;
|
||||
$log = "cacheKey: $cacheKey";
|
||||
if (!Cache::has($cacheKey)) {
|
||||
$con = @fsockopen($tmp_ip, $this->port, $error_code, $error_message, 1);
|
||||
if (is_resource($con)) {
|
||||
$this->connectable = self::CONNECTABLE_YES;
|
||||
fclose($con);
|
||||
} else {
|
||||
$this->connectable = self::CONNECTABLE_NO;
|
||||
}
|
||||
Cache::put($cacheKey, $this->connectable, 600);
|
||||
$log .= ", do check, connectable: " . $this->connectable;
|
||||
} else {
|
||||
$log .= ", don't do check";
|
||||
}
|
||||
do_log($log);
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Models/Poll.php
Normal file
21
app/Models/Poll.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class Poll extends NexusModel
|
||||
{
|
||||
protected $fillable = ['added', 'question', 'option0', 'option1', 'option2', 'option3', 'option4', 'option5'];
|
||||
|
||||
protected $casts = [
|
||||
'added' => 'datetime'
|
||||
];
|
||||
|
||||
const MAX_OPTION_INDEX = 19;
|
||||
|
||||
public function answers()
|
||||
{
|
||||
return $this->hasMany(PollAnswer::class, 'pollid');
|
||||
}
|
||||
|
||||
}
|
||||
22
app/Models/PollAnswer.php
Normal file
22
app/Models/PollAnswer.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class PollAnswer extends NexusModel
|
||||
{
|
||||
protected $table = 'pollanswers';
|
||||
|
||||
protected $fillable = ['pollid', 'userid', 'selection',];
|
||||
|
||||
public function poll()
|
||||
{
|
||||
return $this->belongsTo(Poll::class, 'pollid');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
|
||||
}
|
||||
19
app/Models/Request.php
Normal file
19
app/Models/Request.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class Request extends NexusModel
|
||||
{
|
||||
protected $fillable = ['userid', 'request', 'descr', 'comments', 'hits', 'added'];
|
||||
|
||||
protected $casts = [
|
||||
'added' => 'datetime'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
|
||||
}
|
||||
18
app/Models/Reward.php
Normal file
18
app/Models/Reward.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
class Reward extends NexusModel
|
||||
{
|
||||
protected $table = 'magic';
|
||||
|
||||
protected $fillable = ['torrentid', 'userid', 'value', ];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class Setting extends NexusModel
|
||||
{
|
||||
@@ -10,9 +12,9 @@ class Setting extends NexusModel
|
||||
|
||||
public static function get($name = null)
|
||||
{
|
||||
static $settings;
|
||||
if (is_null($settings)) {
|
||||
$settings = NexusDB::remember("nexus_settings_in_laravel", 10, function () {
|
||||
$rows = self::query()->get(['name', 'value']);
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
$value = $row->value;
|
||||
if (!is_null($value)) {
|
||||
@@ -21,9 +23,10 @@ class Setting extends NexusModel
|
||||
$value = $arr;
|
||||
}
|
||||
}
|
||||
Arr::set($settings, $row->name, $value);
|
||||
Arr::set($result, $row->name, $value);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
});
|
||||
if (is_null($name)) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ class Snatch extends NexusModel
|
||||
{
|
||||
protected $table = 'snatched';
|
||||
|
||||
protected $fillable = [
|
||||
'torrentid', 'userid', 'ip', 'port', 'uploaded', 'downloaded', 'to_go', 'seedtime', 'leechtime',
|
||||
'last_action', 'startdat', 'completedat', 'finished'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_action' => 'datetime',
|
||||
'startdat' => 'datetime',
|
||||
|
||||
@@ -7,7 +7,7 @@ class Tag extends NexusModel
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'id', 'name', 'color', 'priority', 'created_at', 'updated_at'
|
||||
'id', 'name', 'color', 'priority', 'created_at', 'updated_at', 'font_size', 'font_color', 'padding', 'margin', 'border_radius'
|
||||
];
|
||||
|
||||
const DEFAULTS = [
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Models;
|
||||
|
||||
class Thank extends NexusModel
|
||||
{
|
||||
protected $fillable = ['torrentid', 'userid'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Repositories\TagRepository;
|
||||
use JeroenG\Explorer\Application\Explored;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Torrent extends NexusModel
|
||||
{
|
||||
@@ -26,6 +28,11 @@ class Torrent extends NexusModel
|
||||
'added' => 'datetime'
|
||||
];
|
||||
|
||||
public static $commentFields = [
|
||||
'id', 'name', 'added', 'visible', 'banned', 'owner', 'sp_state', 'pos_state', 'hr', 'picktype', 'picktime',
|
||||
'last_action', 'leechers', 'seeders', 'times_completed', 'views', 'size'
|
||||
];
|
||||
|
||||
public static $basicRelations = [
|
||||
'basic_category', 'basic_audio_codec', 'basic_codec', 'basic_media',
|
||||
'basic_source', 'basic_standard', 'basic_team',
|
||||
@@ -61,16 +68,79 @@ class Torrent extends NexusModel
|
||||
const PROMOTION_HALF_DOWN_TWO_TIMES_UP = 6;
|
||||
const PROMOTION_ONE_THIRD_DOWN = 7;
|
||||
|
||||
public static $promotionTypes = [
|
||||
self::PROMOTION_NORMAL => ['text' => 'Normal', 'up_multiplier' => 1, 'down_multiplier' => 1],
|
||||
self::PROMOTION_FREE => ['text' => 'Free', 'up_multiplier' => 1, 'down_multiplier' => 0],
|
||||
self::PROMOTION_TWO_TIMES_UP => ['text' => '2X', 'up_multiplier' => 2, 'down_multiplier' => 1],
|
||||
self::PROMOTION_FREE_TWO_TIMES_UP => ['text' => '2X Free', 'up_multiplier' => 2, 'down_multiplier' => 0],
|
||||
self::PROMOTION_HALF_DOWN => ['text' => '50%', 'up_multiplier' => 1, 'down_multiplier' => 0.5],
|
||||
self::PROMOTION_HALF_DOWN_TWO_TIMES_UP => ['text' => '2X 50%', 'up_multiplier' => 2, 'down_multiplier' => 0.5],
|
||||
self::PROMOTION_ONE_THIRD_DOWN => ['text' => '30%', 'up_multiplier' => 1, 'down_multiplier' => 0.3],
|
||||
public static array $promotionTypes = [
|
||||
self::PROMOTION_NORMAL => [
|
||||
'text' => 'Normal',
|
||||
'up_multiplier' => 1,
|
||||
'down_multiplier' => 1,
|
||||
'color' => ''
|
||||
],
|
||||
self::PROMOTION_FREE => [
|
||||
'text' => 'Free',
|
||||
'up_multiplier' => 1,
|
||||
'down_multiplier' => 0,
|
||||
'color' => 'linear-gradient(to right, rgba(0,52,206,0.5), rgba(0,52,206,1), rgba(0,52,206,0.5))'
|
||||
],
|
||||
self::PROMOTION_TWO_TIMES_UP => [
|
||||
'text' => '2X',
|
||||
'up_multiplier' => 2,
|
||||
'down_multiplier' => 1,
|
||||
'color' => 'linear-gradient(to right, rgba(0,153,0,0.5), rgba(0,153,0,1), rgba(0,153,0,0.5))'
|
||||
],
|
||||
self::PROMOTION_FREE_TWO_TIMES_UP => [
|
||||
'text' => '2X Free',
|
||||
'up_multiplier' => 2,
|
||||
'down_multiplier' => 0,
|
||||
'color' => 'linear-gradient(to right, rgba(0,153,0,1), rgba(0,52,206,1)'
|
||||
],
|
||||
self::PROMOTION_HALF_DOWN => [
|
||||
'text' => '50%',
|
||||
'up_multiplier' => 1,
|
||||
'down_multiplier' => 0.5,
|
||||
'color' => 'linear-gradient(to right, rgba(220,0,3,0.5), rgba(220,0,3,1), rgba(220,0,3,0.5))'
|
||||
],
|
||||
self::PROMOTION_HALF_DOWN_TWO_TIMES_UP => [
|
||||
'text' => '2X 50%',
|
||||
'up_multiplier' => 2,
|
||||
'down_multiplier' => 0.5,
|
||||
'color' => 'linear-gradient(to right, rgba(0,153,0,1), rgba(220,0,3,1)'
|
||||
],
|
||||
self::PROMOTION_ONE_THIRD_DOWN => [
|
||||
'text' => '30%',
|
||||
'up_multiplier' => 1,
|
||||
'down_multiplier' => 0.3,
|
||||
'color' => 'linear-gradient(to right, rgba(65,23,73,0.5), rgba(65,23,73,1), rgba(65,23,73,0.5))'
|
||||
],
|
||||
];
|
||||
|
||||
const PICK_NORMAL = 'normal';
|
||||
const PICK_HOT = 'hot';
|
||||
const PICK_CLASSIC = 'classic';
|
||||
const PICK_RECOMMENDED = 'recommended';
|
||||
|
||||
public static array $pickTypes = [
|
||||
self::PICK_NORMAL => ['text' => self::PICK_NORMAL, 'color' => ''],
|
||||
self::PICK_HOT => ['text' => self::PICK_HOT, 'color' => '#e78d0f'],
|
||||
self::PICK_CLASSIC => ['text' => self::PICK_CLASSIC, 'color' => '#77b300'],
|
||||
self::PICK_RECOMMENDED => ['text' => self::PICK_RECOMMENDED, 'color' => '#820084'],
|
||||
];
|
||||
|
||||
const BONUS_REWARD_VALUES = [50, 100, 200, 500, 1000];
|
||||
|
||||
public function getPickInfoAttribute()
|
||||
{
|
||||
$info = self::$pickTypes[$this->picktype] ?? null;
|
||||
if ($info) {
|
||||
$info['text'] = nexus_trans('torrent.pick_info.' . $this->picktype);
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPromotionInfoAttribute()
|
||||
{
|
||||
return self::$promotionTypes[$this->sp_state_real] ?? null;
|
||||
}
|
||||
|
||||
public function getSpStateRealTextAttribute()
|
||||
{
|
||||
$spStateReal = $this->sp_state_real;
|
||||
@@ -79,6 +149,9 @@ class Torrent extends NexusModel
|
||||
|
||||
public function getSpStateRealAttribute()
|
||||
{
|
||||
if ($this->getRawOriginal('sp_state') === null) {
|
||||
throw new \RuntimeException('no select sp_state field');
|
||||
}
|
||||
$spState = $this->sp_state;
|
||||
$global = self::getGlobalPromotionState();
|
||||
$log = sprintf('torrent: %s sp_state: %s, global sp state: %s', $this->id, $spState, $global);
|
||||
@@ -143,7 +216,10 @@ class Torrent extends NexusModel
|
||||
|
||||
public static function getFieldLabels(): array
|
||||
{
|
||||
$fields = ['comments', 'times_completed', 'peers_count', 'thank_users_count', 'numfiles', 'bookmark_yes', 'bookmark_no'];
|
||||
$fields = [
|
||||
'comments', 'times_completed', 'peers_count', 'thank_users_count', 'numfiles', 'bookmark_yes', 'bookmark_no',
|
||||
'reward_yes', 'reward_no', 'reward_logs', 'download', 'thanks_yes', 'thanks_no'
|
||||
];
|
||||
$result = [];
|
||||
foreach($fields as $field) {
|
||||
$result[$field] = nexus_trans("torrent.show.{$field}_label");
|
||||
@@ -163,6 +239,11 @@ class Torrent extends NexusModel
|
||||
return true;
|
||||
}
|
||||
|
||||
public function bookmarks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Bookmark::class, 'torrentid');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner')->withDefault(User::getDefaultUserAttributes());
|
||||
@@ -263,9 +344,19 @@ class Torrent extends NexusModel
|
||||
$query->where('visible', $visible);
|
||||
}
|
||||
|
||||
public function torrent_tags(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(TorrentTag::class, 'torrent_id');
|
||||
}
|
||||
|
||||
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'torrent_tags', 'torrent_id', 'tag_id')
|
||||
->orderByRaw(sprintf("field(`tags`.`id`,%s)", TagRepository::getOrderByFieldIdString()));
|
||||
}
|
||||
|
||||
public function reward_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Reward::class, 'torrentid');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,32 @@ class User extends Authenticatable
|
||||
'invites' => '邀请',
|
||||
];
|
||||
|
||||
public function mappableAs(): array
|
||||
{
|
||||
return [
|
||||
'id' => 'long',
|
||||
'username' => [
|
||||
'type' => 'text',
|
||||
'analyzer' => 'ik_max_word',
|
||||
],
|
||||
'email' => [
|
||||
'type' => 'text',
|
||||
'analyzer' => 'ik_max_word',
|
||||
],
|
||||
'added' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'username' => $this->username,
|
||||
'email' => $this->email,
|
||||
'added' => $this->added,
|
||||
];
|
||||
}
|
||||
|
||||
public function getClassTextAttribute(): string
|
||||
{
|
||||
return self::$classes[$this->class]['text'] ?? '';
|
||||
@@ -116,7 +142,7 @@ class User extends Authenticatable
|
||||
*/
|
||||
protected $fillable = [
|
||||
'username', 'email', 'passhash', 'secret', 'stylesheet', 'editsecret', 'added', 'modcomment', 'enabled', 'status',
|
||||
'leechwarn', 'leechwarnuntil', 'page', 'class'
|
||||
'leechwarn', 'leechwarnuntil', 'page', 'class', 'uploaded', 'downloaded', 'clientselect', 'showclienterror',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -320,8 +346,10 @@ class User extends Authenticatable
|
||||
public function medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Medal::class, 'user_medals', 'uid', 'medal_id')
|
||||
->withPivot(['id', 'expire_at'])
|
||||
->withTimestamps();
|
||||
->withPivot(['id', 'expire_at', 'status'])
|
||||
->withTimestamps()
|
||||
->orderByPivot('id', 'desc')
|
||||
;
|
||||
}
|
||||
|
||||
public function valid_medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
@@ -331,6 +359,26 @@ class User extends Authenticatable
|
||||
});
|
||||
}
|
||||
|
||||
public function wearing_medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
{
|
||||
return $this->valid_medals()->where('user_medals.status', UserMedal::STATUS_WEARING);
|
||||
}
|
||||
|
||||
public function reward_torrent_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Reward::class, 'userid');
|
||||
}
|
||||
|
||||
public function thank_torrent_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Thank::class, 'userid');
|
||||
}
|
||||
|
||||
public function poll_answers(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(PollAnswer::class, 'userid');
|
||||
}
|
||||
|
||||
public function getAvatarAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
@@ -348,7 +396,7 @@ class User extends Authenticatable
|
||||
public function updateWithModComment(array $update, $modComment)
|
||||
{
|
||||
if (!$this->exists) {
|
||||
throw new \RuntimeException('This mehtod only works when user exists!');
|
||||
throw new \RuntimeException('This method only works when user exists!');
|
||||
}
|
||||
//@todo how to do prepare bindings here ?
|
||||
$modComment = addslashes($modComment);
|
||||
|
||||
@@ -4,5 +4,10 @@ namespace App\Models;
|
||||
|
||||
class UserMedal extends NexusModel
|
||||
{
|
||||
protected $fillable = ['uid', 'medal_id', 'expire_at'];
|
||||
protected $fillable = ['uid', 'medal_id', 'expire_at', 'status'];
|
||||
|
||||
const STATUS_NOT_WEARING = 0;
|
||||
const STATUS_WEARING = 1;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Nexus\Nexus;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -28,5 +29,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
// JsonResource::withoutWrapping();
|
||||
DB::connection(config('database.default'))->enableQueryLog();
|
||||
// Nexus::boot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\TorrentUpdated;
|
||||
use App\Listeners\SyncTorrentToEs;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
@@ -18,6 +20,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
TorrentUpdated::class => [
|
||||
SyncTorrentToEs::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,10 @@ class RouteServiceProvider extends ServiceProvider
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
|
||||
Route::prefix('api')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/tracker.php'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Exceptions\NexusException;
|
||||
use App\Exceptions\ClientNotAllowedException;
|
||||
use App\Models\AgentAllow;
|
||||
use App\Models\AgentDeny;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class AgentAllowRepository extends BaseRepository
|
||||
{
|
||||
@@ -52,31 +53,40 @@ class AgentAllowRepository extends BaseRepository
|
||||
public function getPatternMatches($pattern, $start, $matchNum)
|
||||
{
|
||||
if (!preg_match($pattern, $start, $matches)) {
|
||||
throw new NexusException(sprintf('pattern: %s can not match start: %s', $pattern, $start));
|
||||
throw new ClientNotAllowedException(sprintf('pattern: %s can not match start: %s', $pattern, $start));
|
||||
}
|
||||
$matchCount = count($matches) - 1;
|
||||
//due to old data may be matchNum > matchCount
|
||||
if ($matchNum > $matchCount && !IN_NEXUS) {
|
||||
throw new NexusException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum.");
|
||||
}
|
||||
// if ($matchNum > $matchCount && !IN_NEXUS) {
|
||||
// throw new ClientNotAllowedException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum.");
|
||||
// }
|
||||
return array_slice($matches, 1, $matchNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $peerId
|
||||
* @param $agent
|
||||
* @param false $debug
|
||||
* @return \App\Models\NexusModel|mixed
|
||||
* @throws ClientNotAllowedException
|
||||
*/
|
||||
public function checkClient($peerId, $agent, $debug = false)
|
||||
{
|
||||
//check from high version to low version, if high version allow, stop!
|
||||
$allows = AgentAllow::query()
|
||||
->orderBy('peer_id_start', 'desc')
|
||||
->orderBy('agent_start', 'desc')
|
||||
->get();
|
||||
$allows = NexusDB::remember("all_agent_allows", 600, function () {
|
||||
return AgentAllow::query()
|
||||
->orderBy('peer_id_start', 'desc')
|
||||
->orderBy('agent_start', 'desc')
|
||||
->get();
|
||||
});
|
||||
$agentAllowPassed = null;
|
||||
$versionTooLowStr = '';
|
||||
foreach ($allows as $agentAllow) {
|
||||
$agentAllowId = $agentAllow->id;
|
||||
$logPrefix = "[ID: $agentAllowId]";
|
||||
$isPeerIdAllowed = $isAgentAllowed = $isPeerIdTooLow = $isAgentTooLow = false;
|
||||
//check peer_id
|
||||
if ($agentAllow->peer_id_pattern == '') {
|
||||
//check peer_id, when handle scrape request, no peer_id, so let it pass
|
||||
if ($agentAllow->peer_id_pattern == '' || $peerId === null) {
|
||||
$isPeerIdAllowed = true;
|
||||
} else {
|
||||
$pattern = $agentAllow->peer_id_pattern;
|
||||
@@ -93,7 +103,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
do_log("$logPrefix, check peer_id error: " . $exception->getMessage(), 'error');
|
||||
throw new NexusException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this");
|
||||
throw new ClientNotAllowedException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this");
|
||||
}
|
||||
if ($peerIdResult == 1) {
|
||||
$isPeerIdAllowed = true;
|
||||
@@ -121,7 +131,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
do_log("$logPrefix, check agent error: " . $exception->getMessage(), 'error');
|
||||
throw new NexusException("regular expression err for agent: " . $start . ", please ask sysop to fix this");
|
||||
throw new ClientNotAllowedException("regular expression err for agent: " . $start . ", please ask sysop to fix this");
|
||||
}
|
||||
if ($agentResult == 1) {
|
||||
$isAgentAllowed = true;
|
||||
@@ -142,11 +152,11 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
|
||||
if ($versionTooLowStr) {
|
||||
throw new NexusException($versionTooLowStr);
|
||||
throw new ClientNotAllowedException($versionTooLowStr);
|
||||
}
|
||||
|
||||
if (!$agentAllowPassed) {
|
||||
throw new NexusException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
|
||||
throw new ClientNotAllowedException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
@@ -160,14 +170,14 @@ class AgentAllowRepository extends BaseRepository
|
||||
if ($debug) {
|
||||
do_log("agentDeny: " . $agentDeny->toJson());
|
||||
}
|
||||
throw new NexusException(sprintf(
|
||||
throw new ClientNotAllowedException(sprintf(
|
||||
"[%s-%s]Client: %s is banned due to: %s",
|
||||
$agentAllowPassed->id, $agentDeny->id, $agentDeny->name, $agentDeny->comment
|
||||
));
|
||||
}
|
||||
}
|
||||
if (isHttps() && $agentAllowPassed->allowhttps != 'yes') {
|
||||
throw new NexusException(sprintf(
|
||||
throw new ClientNotAllowedException(sprintf(
|
||||
"[%s]This client does not support https well, Please goto %s/faq.php#id29 for a list of proper clients",
|
||||
$agentAllowPassed->id, getSchemeAndHttpHost()
|
||||
));
|
||||
@@ -202,7 +212,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
* @param bool $debug
|
||||
* @param string $logPrefix
|
||||
* @return int
|
||||
* @throws NexusException
|
||||
* @throws ClientNotAllowedException
|
||||
*/
|
||||
private function isAllowed($pattern, $start, $matchNum, $matchType, $value, $debug = false, $logPrefix = ''): int
|
||||
{
|
||||
@@ -234,7 +244,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
$matchBench[$i] = hexdec($matchBench[$i]);
|
||||
$matchTarget[$i] = hexdec($matchTarget[$i]);
|
||||
} else {
|
||||
throw new NexusException(sprintf("Invalid match type: %s", $matchType));
|
||||
throw new ClientNotAllowedException(sprintf("Invalid match type: %s", $matchType));
|
||||
}
|
||||
if ($matchTarget[$i] > $matchBench[$i]) {
|
||||
//higher, pass directly
|
||||
|
||||
@@ -75,8 +75,8 @@ class AttendanceRepository extends BaseRepository
|
||||
->where('uid', $uid)
|
||||
->orderBy('id', 'desc');
|
||||
if (!empty($date)) {
|
||||
$query->where('added', '>=', Carbon::today())
|
||||
->where('added', '<', Carbon::tomorrow());
|
||||
$query->where('added', '>=', Carbon::parse($date)->startOfDay())
|
||||
->where('added', '<=', Carbon::parse($date)->endOfDay());
|
||||
}
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class AuthenticateRepository extends BaseRepository
|
||||
if (!$user || md5($user->secret . $password . $user->secret) != $user->passhash) {
|
||||
throw new \InvalidArgumentException('Username or password invalid.');
|
||||
}
|
||||
if (IS_PLATFORM_ADMIN && !$user->canAccessAdmin()) {
|
||||
if (nexus()->isPlatformAdmin() && !$user->canAccessAdmin()) {
|
||||
throw new UnauthorizedException('Unauthorized!');
|
||||
}
|
||||
$user->checkIsNormal();
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\HitAndRun;
|
||||
use App\Models\Medal;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\UserMedal;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Nexus\Database\NexusDB;
|
||||
@@ -66,7 +67,7 @@ class BonusRepository extends BaseRepository
|
||||
if ($medal->duration > 0) {
|
||||
$expireAt = Carbon::now()->addDays($medal->duration)->toDateTimeString();
|
||||
}
|
||||
$user->medals()->attach([$medal->id => ['expire_at' => $expireAt]]);
|
||||
$user->medals()->attach([$medal->id => ['expire_at' => $expireAt, 'status' => UserMedal::STATUS_NOT_WEARING]]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
111
app/Repositories/CommentRepository.php
Normal file
111
app/Repositories/CommentRepository.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Message;
|
||||
use App\Models\NexusModel;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Hamcrest\Core\Set;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class CommentRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Comment::query()->with(['create_user', 'update_user']);
|
||||
if (!empty($params['torrent_id'])) {
|
||||
$query->where('torrent', $params['torrent_id']);
|
||||
}
|
||||
if (!empty($params['offer_id'])) {
|
||||
$query->where('offer', $params['offer_id']);
|
||||
}
|
||||
if (!empty($params['request_id'])) {
|
||||
$query->where('request', $params['request_id']);
|
||||
}
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store(array $params, User $user)
|
||||
{
|
||||
$type = $params['type'];
|
||||
$modelName = Comment::TYPE_MAPS[$params['type']]['model'];
|
||||
/**
|
||||
* @var NexusModel $model
|
||||
*/
|
||||
$model = new $modelName;
|
||||
$target = $model->newQuery()->with('user')->find($params[$type]);
|
||||
return DB::transaction(function () use ($params, $user, $target) {
|
||||
$params['added'] = Carbon::now();
|
||||
$comment = $user->comments()->create($params);
|
||||
$commentCount = Comment::query()->type($params['type'], $params[$params['type']])->count();
|
||||
$target->comments = $commentCount;
|
||||
$target->save();
|
||||
|
||||
$userUpdate = [
|
||||
'seedbonus' => DB::raw('seedbonus + ' . Setting::get('bonus.addcomment')),
|
||||
'last_comment' => Carbon::now(),
|
||||
];
|
||||
$user->update($userUpdate);
|
||||
|
||||
//message
|
||||
if ($target->user->commentpm == 'yes' && $user->id != $target->user->id) {
|
||||
$messageInfo = $this->getNoticeMessage($target, $params['type']);
|
||||
$insert = [
|
||||
'sender' => 0,
|
||||
'receiver' => $target->user->id,
|
||||
'subject' => $messageInfo['subject'],
|
||||
'msg' => $messageInfo['body'],
|
||||
'added' => $params['added'],
|
||||
];
|
||||
Message::query()->insert($insert);
|
||||
NexusDB::cache_del('user_'.$target->user->id.'_unread_message_count');
|
||||
NexusDB::cache_del('user_'.$target->user->id.'_inbox_count');
|
||||
}
|
||||
|
||||
return $comment;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getNoticeMessage($target, $type): array
|
||||
{
|
||||
$allTrans = require_once base_path('lang/_target/lang_comment.php');
|
||||
$lang = $target->user->language->site_lang_folder ?? 'en';
|
||||
$trans = $allTrans[$lang];
|
||||
$subject = $trans['msg_new_comment'];
|
||||
$targetScript = Comment::TYPE_MAPS[$type]['target_script'];
|
||||
$targetNameField = Comment::TYPE_MAPS[$type]['target_name_field'];
|
||||
$body = sprintf(
|
||||
'%s [url=%s]%s[/url]',
|
||||
$trans['msg_torrent_receive_comment'],
|
||||
sprintf($targetScript, $target->id),
|
||||
$target->{$targetNameField}
|
||||
);
|
||||
return compact('subject', 'body');
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class DashboardRepository extends BaseRepository
|
||||
$result[$name] = [
|
||||
'name' => $name,
|
||||
'text' => nexus_trans("dashboard.system_info.$name"),
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'],
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'] ?? '',
|
||||
];
|
||||
$name = 'load_average';
|
||||
$result[$name] = [
|
||||
|
||||
@@ -66,7 +66,23 @@ class MedalRepository extends BaseRepository
|
||||
if ($duration > 0) {
|
||||
$expireAt = Carbon::now()->addDays($duration)->toDateTimeString();
|
||||
}
|
||||
return $user->medals()->attach([$medal->id => ['expire_at' => $expireAt]]);
|
||||
return $user->medals()->attach([$medal->id => ['expire_at' => $expireAt, 'status' => UserMedal::STATUS_NOT_WEARING]]);
|
||||
}
|
||||
|
||||
function toggleUserMedalStatus($id, $userId)
|
||||
{
|
||||
$userMedal = UserMedal::query()->findOrFail($id);
|
||||
if ($userMedal->uid != $userId) {
|
||||
throw new \LogicException("no privilege");
|
||||
}
|
||||
$current = $userMedal->status;
|
||||
if ($current == UserMedal::STATUS_NOT_WEARING) {
|
||||
$userMedal->status = UserMedal::STATUS_WEARING;
|
||||
} elseif ($current == UserMedal::STATUS_WEARING) {
|
||||
$userMedal->status = UserMedal::STATUS_NOT_WEARING;
|
||||
}
|
||||
$userMedal->save();
|
||||
return $userMedal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
72
app/Repositories/PollRepository.php
Normal file
72
app/Repositories/PollRepository.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Poll;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PollRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Poll::query();
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store($torrentId, $value, User $user)
|
||||
{
|
||||
if ($user->seedbonus < $value) {
|
||||
throw new \LogicException("user bonus not enough.");
|
||||
}
|
||||
if ($user->reward_torrent_logs()->where('torrentid', $torrentId)->exists()) {
|
||||
throw new \LogicException("user already reward this torrent.");
|
||||
}
|
||||
$torrent = Torrent::query()->findOrFail($torrentId, ['owner']);
|
||||
$torrentOwner = User::query()->findOrFail($torrent->owner, ['id', 'seedbonus']);
|
||||
return DB::transaction(function () use ($torrentId, $value, $user, $torrentOwner) {
|
||||
$model = $user->reward_torrent_logs()->create([
|
||||
'torrentid' => $torrentId,
|
||||
'value' => $value,
|
||||
]);
|
||||
$affectedRows = $user->where('seedbonus', $user->seedbonus)->decrement('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("decrement user bonus fail.");
|
||||
}
|
||||
$affectedRows = $torrentOwner->where('seedbonus', $torrentOwner->seedbonus)->increment('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment owner bonus fail.");
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function vote($selection, User $user)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
82
app/Repositories/RewardRepository.php
Normal file
82
app/Repositories/RewardRepository.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Reward;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Google\Service\ToolResults\StepLabelsEntry;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RewardRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Reward::query()->with(['user']);
|
||||
if (!empty($params['torrent_id'])) {
|
||||
$query->where('torrentid', $params['torrent_id']);
|
||||
}
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store($torrentId, $value, User $user)
|
||||
{
|
||||
if ($user->seedbonus < $value) {
|
||||
throw new \LogicException("your bonus not enough.");
|
||||
}
|
||||
if ($user->reward_torrent_logs()->where('torrentid', $torrentId)->exists()) {
|
||||
throw new \LogicException("you already reward this torrent.");
|
||||
}
|
||||
$torrent = Torrent::query()->findOrFail($torrentId, Torrent::$commentFields);
|
||||
$torrent->checkIsNormal();
|
||||
$torrentOwner = User::query()->findOrFail($torrent->owner);
|
||||
if ($user->id == $torrentOwner->id) {
|
||||
throw new \LogicException("you can't reward to yourself.");
|
||||
}
|
||||
$torrentOwner->checkIsNormal();
|
||||
return DB::transaction(function () use ($torrentId, $value, $user, $torrentOwner) {
|
||||
$model = $user->reward_torrent_logs()->create([
|
||||
'torrentid' => $torrentId,
|
||||
'value' => $value,
|
||||
]);
|
||||
$affectedRows = User::query()
|
||||
->where('id', $user->id)
|
||||
->where('seedbonus', $user->seedbonus)
|
||||
->decrement('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("decrement user bonus fail.");
|
||||
}
|
||||
$affectedRows = User::query()
|
||||
->where('id', $torrentOwner->id)
|
||||
->where('seedbonus', $torrentOwner->seedbonus)
|
||||
->increment('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment owner bonus fail.");
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
879
app/Repositories/SearchRepository.php
Normal file
879
app/Repositories/SearchRepository.php
Normal file
@@ -0,0 +1,879 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Bookmark;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\TorrentTag;
|
||||
use App\Models\User;
|
||||
use Elasticsearch\Client;
|
||||
use Elasticsearch\ClientBuilder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class SearchRepository extends BaseRepository
|
||||
{
|
||||
private Client $es;
|
||||
|
||||
private bool $enabled = false;
|
||||
|
||||
const INDEX_NAME = 'nexus_torrents';
|
||||
|
||||
const DOC_TYPE_TORRENT = 'torrent';
|
||||
const DOC_TYPE_TAG = 'tag';
|
||||
const DOC_TYPE_BOOKMARK = 'bookmark';
|
||||
const DOC_TYPE_USER = 'user';
|
||||
|
||||
const SEARCH_MODE_AND = '0';
|
||||
const SEARCH_MODE_OR = '1';
|
||||
const SEARCH_MODE_EXACT = '2';
|
||||
|
||||
const SEARCH_MODES = [
|
||||
self::SEARCH_MODE_AND => ['text' => 'and'],
|
||||
self::SEARCH_MODE_OR => ['text' => 'or'],
|
||||
self::SEARCH_MODE_EXACT => ['text' => 'exact'],
|
||||
];
|
||||
|
||||
const SEARCH_AREA_TITLE = '0';
|
||||
const SEARCH_AREA_DESC = '1';
|
||||
const SEARCH_AREA_OWNER = '3';
|
||||
const SEARCH_AREA_IMDB = '4';
|
||||
|
||||
const SEARCH_AREAS = [
|
||||
self::SEARCH_AREA_TITLE => ['text' => 'title'],
|
||||
self::SEARCH_AREA_DESC => ['text' => 'desc'],
|
||||
self::SEARCH_AREA_OWNER => ['text' => 'owner'],
|
||||
self::SEARCH_AREA_IMDB => ['text' => 'imdb'],
|
||||
];
|
||||
|
||||
|
||||
|
||||
private array $indexSetting = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => [
|
||||
'settings' => [
|
||||
'number_of_shards' => 1,
|
||||
'number_of_replicas' => 0,
|
||||
],
|
||||
'mappings' => [
|
||||
'properties' => [
|
||||
'_doc_type' => ['type' => 'keyword'],
|
||||
|
||||
//torrent
|
||||
'torrent_id' => ['type' => 'long', ],
|
||||
|
||||
//user
|
||||
'username' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
|
||||
//bookmark + user + tag
|
||||
'user_id' => ['type' => 'long', ],
|
||||
|
||||
//tag
|
||||
'tag_id' => ['type' => 'long', ],
|
||||
|
||||
//relations
|
||||
'torrent_relations' => [
|
||||
'type' => 'join',
|
||||
'eager_global_ordinals' => true,
|
||||
'relations' => [
|
||||
'user' => ['torrent'],
|
||||
'torrent' => ['bookmark', 'tag'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
//cat401=1&source1=1&medium1=1&codec1=1&audiocodec1=1&standard1=1&processing1=1&team1=1&incldead=1&spstate=2&inclbookmarked=1&search=tr&search_area=1&search_mode=1
|
||||
private static array $queryFieldToTorrentFieldMaps = [
|
||||
'cat' => 'category',
|
||||
'source' => 'source',
|
||||
'medium' => 'medium',
|
||||
'codec' => 'codec',
|
||||
'audiocodec' => 'audiocodec',
|
||||
'standard' => 'standard',
|
||||
'processing' => 'processing',
|
||||
'team' => 'team',
|
||||
];
|
||||
|
||||
private static array $sortFieldMaps = [
|
||||
'1' => 'name',
|
||||
'2' => 'numfiles',
|
||||
'3' => 'comments',
|
||||
'4' => 'added',
|
||||
'5' => 'size',
|
||||
'6' => 'times_completed',
|
||||
'7' => 'seeders',
|
||||
'8' => 'leechers',
|
||||
'9' => 'owner',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$elasticsearchEnabled = nexus_env('ELASTICSEARCH_ENABLED');
|
||||
if ($elasticsearchEnabled) {
|
||||
$this->enabled = true;
|
||||
$this->es = $this->getEs();
|
||||
} else {
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getEs(): Client
|
||||
{
|
||||
$config = nexus_config('nexus.elasticsearch');
|
||||
$es = ClientBuilder::create()->setHosts($config['hosts']);
|
||||
if (!empty($config['ssl_verification'])) {
|
||||
$es->setSSLVerification($config['ssl_verification']);
|
||||
}
|
||||
return $es->build();
|
||||
}
|
||||
|
||||
private function getTorrentRawMappingFields(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'descr' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'small_descr' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'category' => ['type' => 'long', ],
|
||||
'source' => ['type' => 'long', ],
|
||||
'medium' => ['type' => 'long', ],
|
||||
'codec' => ['type' => 'long', ],
|
||||
'standard' => ['type' => 'long', ],
|
||||
'processing' => ['type' => 'long', ],
|
||||
'team' => ['type' => 'long', ],
|
||||
'audiocodec' => ['type' => 'long', ],
|
||||
'size' => ['type' => 'long', ],
|
||||
'added' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
|
||||
'numfiles' => ['type' => 'long', ],
|
||||
'comments' => ['type' => 'long', ],
|
||||
'views' => ['type' => 'long', ],
|
||||
'hits' => ['type' => 'long', ],
|
||||
'times_completed' => ['type' => 'long', ],
|
||||
'leechers' => ['type' => 'long', ],
|
||||
'seeders' => ['type' => 'long', ],
|
||||
'last_action' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
|
||||
'visible' => ['type' => 'keyword', ],
|
||||
'banned' => ['type' => 'keyword', ],
|
||||
'owner' => ['type' => 'long', ],
|
||||
'sp_state' => ['type' => 'long', ],
|
||||
'url' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'pos_state' => ['type' => 'keyword', ],
|
||||
'picktype' => ['type' => 'keyword', ],
|
||||
'hr' => ['type' => 'long', ],
|
||||
];
|
||||
}
|
||||
|
||||
public function getEsInfo(): callable|array
|
||||
{
|
||||
return $this->es->info();
|
||||
}
|
||||
|
||||
public function createIndex()
|
||||
{
|
||||
$params = $this->indexSetting;
|
||||
$properties = $params['body']['mappings']['properties'];
|
||||
$properties = array_merge($properties, $this->getTorrentRawMappingFields());
|
||||
$params['body']['mappings']['properties'] = $properties;
|
||||
return $this->es->indices()->create($params);
|
||||
}
|
||||
|
||||
public function deleteIndex()
|
||||
{
|
||||
$params = ['index' => self::INDEX_NAME];
|
||||
return $this->es->indices()->delete($params);
|
||||
}
|
||||
|
||||
public function import($torrentId = null)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$page = 1;
|
||||
$size = 1000;
|
||||
$fields = $this->getTorrentBaseFields();
|
||||
array_unshift($fields, 'id');
|
||||
$query = Torrent::query()
|
||||
->with(['user', 'torrent_tags', 'bookmarks'])
|
||||
->select($fields);
|
||||
if (!is_null($torrentId)) {
|
||||
$idArr = preg_split('/[,\s]+/', $torrentId);
|
||||
$query->whereIn('id', $idArr);
|
||||
}
|
||||
while (true) {
|
||||
$log = "page: $page, size: $size";
|
||||
$torrentResults = (clone $query)->forPage($page, $size)->get();
|
||||
if ($torrentResults->isEmpty()) {
|
||||
do_log("$log, no more data...", 'info', true);
|
||||
break;
|
||||
}
|
||||
do_log("$log, get counts: " . $torrentResults->count(), 'info', true);
|
||||
|
||||
$torrentBodyBulk = $userBodyBulk = $tagBodyBulk = $bookmarkBodyBulk = ['body' => []];
|
||||
foreach ($torrentResults as $torrent) {
|
||||
$body = $this->buildUserBody($torrent->user, true);
|
||||
$userBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$userBodyBulk['body'][] = $body['body'];
|
||||
|
||||
$body = $this->buildTorrentBody($torrent, true);
|
||||
$torrentBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$torrentBodyBulk['body'][] = $body['body'];
|
||||
|
||||
foreach ($torrent->torrent_tags as $torrentTag) {
|
||||
$body = $this->buildTorrentTagBody($torrent, $torrentTag, true);
|
||||
$tagBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$tagBodyBulk['body'][] = $body['body'];
|
||||
}
|
||||
|
||||
foreach ($torrent->bookmarks as $bookmark) {
|
||||
$body = $this->buildBookmarkBody($torrent, $bookmark, true);
|
||||
$bookmarkBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$bookmarkBodyBulk['body'][] = $body['body'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//index user
|
||||
$result = $this->es->bulk($userBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index user done!", $result);
|
||||
|
||||
//index torrent
|
||||
$result = $this->es->bulk($torrentBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index torrent done!", $result);
|
||||
|
||||
//index tag
|
||||
$result = $this->es->bulk($tagBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index tag done!", $result);
|
||||
|
||||
//index bookmark
|
||||
$result = $this->es->bulk($bookmarkBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index bookmark done!", $result);
|
||||
|
||||
$page++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function buildUserBody(User $user, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_USER;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getUserId($user->id),
|
||||
'routing' => $user->id,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
private function buildTorrentBody($torrent, bool $underlinePrefix = false): array
|
||||
{
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
if (!$torrent instanceof Torrent) {
|
||||
$torrent = Torrent::query()->findOrFail((int)$torrent, array_merge(['id'], $baseFields));
|
||||
}
|
||||
$docType = self::DOC_TYPE_TORRENT;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getTorrentId($torrent->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$data = Arr::only($torrent->toArray(), $baseFields);
|
||||
$body = array_merge($data, [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $torrent->id,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'user_' . $torrent->owner,
|
||||
],
|
||||
]);
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function buildTorrentTagBody(Torrent $torrent, TorrentTag $torrentTag, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_TAG;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getTorrentTagId($torrentTag->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $torrentTag->torrent_id,
|
||||
'tag_id' => $torrentTag->tag_id,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'torrent_' . $torrent->id,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
private function buildBookmarkBody(Torrent $torrent, Bookmark $bookmark, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_BOOKMARK;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getBookmarkId($bookmark->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $bookmark->torrentid,
|
||||
'user_id' => $bookmark->userid,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'torrent_' . $torrent->id,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
private function logEsResponse($msg, $response)
|
||||
{
|
||||
if (isset($response['errors']) && $response['errors'] == true) {
|
||||
$msg .= var_export($response, true);
|
||||
}
|
||||
do_log($msg, 'info', app()->runningInConsole());
|
||||
}
|
||||
|
||||
private function getTorrentId($id): string
|
||||
{
|
||||
return "torrent_" . intval($id);
|
||||
}
|
||||
|
||||
private function getTorrentTagId($id): string
|
||||
{
|
||||
return "torrent_tag_" . intval($id);
|
||||
}
|
||||
|
||||
private function getUserId($id): string
|
||||
{
|
||||
return "user_" . intval($id);
|
||||
}
|
||||
|
||||
private function getBookmarkId($id): string
|
||||
{
|
||||
return "bookmark_" . intval($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* detect elastic response has error or not
|
||||
*
|
||||
* @param $esResponse
|
||||
* @return bool
|
||||
*/
|
||||
private function isEsResponseError($esResponse)
|
||||
{
|
||||
if (isset($esResponse['error'])) {
|
||||
return true;
|
||||
}
|
||||
//bulk insert
|
||||
if (isset($esResponse['errors']) && $esResponse['errors']) {
|
||||
return true;
|
||||
}
|
||||
//update by query
|
||||
if (!empty($esResponse['failures'])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* build es query
|
||||
*
|
||||
* @param array $params
|
||||
* @param $user
|
||||
* @param string $queryString cat401=1&cat404=1&source2=1&medium2=1&medium3=1&codec3=1&audiocodec3=1&standard2=1&standard3=1&processing2=1&team3=1&team4=1&incldead=1&spstate=0&inclbookmarked=0&search=&search_area=0&search_mode=0
|
||||
* @return array
|
||||
*/
|
||||
public function buildQuery(array $params, $user, string $queryString)
|
||||
{
|
||||
if (!($user instanceof User) || !$user->torrentsperpage || !$user->notifs) {
|
||||
$user = User::query()->findOrFail(intval($user));
|
||||
}
|
||||
//[cat401][cat404][sou1][med1][cod1][sta2][sta3][pro2][tea2][aud2][incldead=0][spstate=3][inclbookmarked=2]
|
||||
$userSetting = $user->notifs;
|
||||
$must = $must_not = [];
|
||||
$mustBoolShould = [];
|
||||
$must[] = ['match' => ['_doc_type' => self::DOC_TYPE_TORRENT]];
|
||||
|
||||
foreach (self::$queryFieldToTorrentFieldMaps as $queryField => $torrentField) {
|
||||
if (isset($params[$queryField]) && $params[$queryField] !== '') {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $params[$queryField]]];
|
||||
do_log("get mustBoolShould for $torrentField from params through $queryField: {$params[$queryField]}");
|
||||
} elseif (preg_match_all("/{$queryField}([\d]+)=/", $queryString, $matches)) {
|
||||
if (count($matches) == 2 && !empty($matches[1])) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $match]];
|
||||
do_log("get mustBoolShould for $torrentField from params through $queryField: $match");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//get user setting
|
||||
$pattern = sprintf("/\[%s([\d]+)\]/", substr($queryField, 0, 3));
|
||||
if (preg_match($pattern, $userSetting, $matches)) {
|
||||
if (count($matches) == 2 && !empty($matches[1])) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $match]];
|
||||
do_log("get mustBoolShould for $torrentField from user setting through $queryField: $match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$includeDead = 1;
|
||||
if (isset($params['incldead'])) {
|
||||
$includeDead = (int)$params['incldead'];
|
||||
do_log("maybe get must for visible from params");
|
||||
} elseif (preg_match("/\[incldead=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$includeDead = $matches[1];
|
||||
do_log("maybe get must for visible from user setting");
|
||||
}
|
||||
if ($includeDead == 1) {
|
||||
//active torrent
|
||||
$must[] = ['match' => ['visible' => 'yes']];
|
||||
do_log("get must for visible = yes through incldead: $includeDead");
|
||||
} elseif ($includeDead == 2) {
|
||||
//dead torrent
|
||||
$must[] = ['match' => ['visible' => 'no']];
|
||||
do_log("get must for visible = no through incldead: $includeDead");
|
||||
}
|
||||
|
||||
|
||||
$includeBookmarked = 0;
|
||||
if (isset($params['inclbookmarked'])) {
|
||||
$includeBookmarked = (int)$params['inclbookmarked'];
|
||||
do_log("maybe get must or must_not for has_child.bookmark from params");
|
||||
} elseif (preg_match("/\[inclbookmarked=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$includeBookmarked = $matches[1];
|
||||
do_log("maybe get must or must_not for has_child.bookmark from user setting");
|
||||
}
|
||||
if ($includeBookmarked == 1) {
|
||||
//only bookmark
|
||||
$must[] = ['has_child' => ['type' => 'bookmark', 'query' => ['match' => ['user_id' => $user->id]]]];
|
||||
do_log("get must for has_child.bookmark through inclbookmarked: $includeBookmarked");
|
||||
} elseif ($includeBookmarked == 2) {
|
||||
//only not bookmark
|
||||
$must_not[] = ['has_child' => ['type' => 'bookmark', 'query' => ['match' => ['user_id' => $user->id]]]];
|
||||
do_log("get must_not for has_child.bookmark through inclbookmarked: $includeBookmarked");
|
||||
}
|
||||
|
||||
|
||||
$spState = 0;
|
||||
if (isset($params['spstate'])) {
|
||||
$spState = (int)$params['spstate'];
|
||||
do_log("maybe get must for spstate from params");
|
||||
} elseif (preg_match("/\[spstate=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$spState = $matches[1];
|
||||
do_log("maybe get must for spstate from user setting");
|
||||
}
|
||||
if ($spState > 0) {
|
||||
$must[] = ['match' => ['sp_state' => $spState]];
|
||||
do_log("get must for sp_state = $spState through spstate: $spState");
|
||||
}
|
||||
|
||||
if (!empty($params['tag_id'])) {
|
||||
$must[] = ['has_child' => ['type' => 'tag', 'query' => ['match' => ['tag_id' => $params['tag_id']]]]];
|
||||
do_log("get must for has_child.tag through params.tag_id: {$params['tag_id']}");
|
||||
}
|
||||
|
||||
|
||||
if (!empty($params['search'])) {
|
||||
$searchMode = isset($params['search_mode']) && isset(self::SEARCH_MODES[$params['search_mode']]) ? $params['search_mode'] : self::SEARCH_MODE_AND;
|
||||
if (in_array($searchMode, [self::SEARCH_MODE_AND, self::SEARCH_MODE_OR])) {
|
||||
//and, or
|
||||
$keywordsArr = preg_split("/[\.\s]+/", trim($params['search']));
|
||||
} else {
|
||||
$keywordsArr = [trim($params['search'])];
|
||||
}
|
||||
$keywordsArr = array_slice($keywordsArr, 0, 10);
|
||||
$searchArea = isset($params['search_area']) && isset(self::SEARCH_AREAS[$params['search_area']]) ? $params['search_area'] : self::SEARCH_AREA_TITLE;
|
||||
if ($searchMode == self::SEARCH_MODE_AND || $searchMode == self::SEARCH_MODE_EXACT) {
|
||||
$keywordFlag = $searchMode == self::SEARCH_MODE_EXACT ? ".keyword" : "";
|
||||
if ($searchArea == self::SEARCH_AREA_TITLE) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould = [];
|
||||
$tmpMustBoolShould[] = ['match' => ["name{$keywordFlag}" => $keyword]];
|
||||
$tmpMustBoolShould[] = ['match' => ["small_descr{$keywordFlag}" => $keyword]];
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
do_log("get must bool should [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for name+small_descr match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_DESC) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['match' => ["descr{$keywordFlag}" => $keyword]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for descr match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_IMDB) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['match' => ["url{$keywordFlag}" => $keyword]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for url match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_OWNER) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['has_parent' => ['parent_type' => 'user', 'query' => ['match' => ["username{$keywordFlag}" => $keyword]]]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] has_parent.user match '$keyword' through search");
|
||||
}
|
||||
}
|
||||
} elseif ($searchMode == self::SEARCH_MODE_OR) {
|
||||
if ($searchArea == self::SEARCH_AREA_TITLE) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['name' => $keyword]];
|
||||
$tmpMustBoolShould[] = ['match' => ['small_descr' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for name+small_descr match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_DESC) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['descr' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for descr match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_IMDB) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['url' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for url match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_OWNER) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['has_parent' => ['parent_type' => 'user', 'query' => ['match' => ['username' => $keyword]]]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] has_parent.user match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
}
|
||||
}
|
||||
}
|
||||
$query = [
|
||||
'bool' => [
|
||||
'must' => $must
|
||||
]
|
||||
];
|
||||
foreach ($mustBoolShould as $torrentField => $boolShoulds) {
|
||||
$query['bool']['must'][]['bool']['should'] = $boolShoulds;
|
||||
}
|
||||
if (!empty($must_not)) {
|
||||
$query['bool']['must_not'] = $must_not;
|
||||
}
|
||||
|
||||
|
||||
$sort = [];
|
||||
$sort[] = ['pos_state' => ['order' => 'desc']];
|
||||
$hasAddSetSortField = false;
|
||||
if (!empty($params['sort'])) {
|
||||
$direction = isset($params['type']) && in_array($params['type'], ['asc', 'desc']) ? $params['type'] : 'desc';
|
||||
foreach (self::$sortFieldMaps as $key => $value) {
|
||||
if ($key == $params['sort']) {
|
||||
$hasAddSetSortField = true;
|
||||
$sort[] = [$value => ['order' => $direction]];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$hasAddSetSortField) {
|
||||
$sort[] = ['torrent_id' => ['order' => 'desc']];
|
||||
}
|
||||
|
||||
$page = isset($params['page']) && is_numeric($params['page']) ? $params['page'] : 0;
|
||||
if ($user->torrentsperpage) {
|
||||
$size = $user->torrentsperpage;
|
||||
} elseif (($sizeFromConfig = Setting::get('main.torrentsperpage')) > 0) {
|
||||
$size = $sizeFromConfig;
|
||||
} else {
|
||||
$size = 50;
|
||||
}
|
||||
$size = min($size, 200);
|
||||
$offset = $page * $size;
|
||||
|
||||
$result = [
|
||||
'query' => $query,
|
||||
'sort' => $sort,
|
||||
'from' => $offset,
|
||||
'size' => $size,
|
||||
'_source' => ['torrent_id', 'name', 'small_descr', 'owner']
|
||||
];
|
||||
do_log(sprintf(
|
||||
"params: %s, user: %s, queryString: %s, result: %s",
|
||||
nexus_json_encode($params), $user->id, $queryString, nexus_json_encode($result)
|
||||
));
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
public function listTorrentFromEs(array $params, $user, string $queryString)
|
||||
{
|
||||
$query = $this->buildQuery($params, $user, $queryString);
|
||||
$esParams = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => $query,
|
||||
];
|
||||
$response = $this->es->search($esParams);
|
||||
$result = [
|
||||
'total' => 0,
|
||||
'data' => [],
|
||||
];
|
||||
if ($this->isEsResponseError($response)) {
|
||||
do_log("error response: " . nexus_json_encode($response), 'error');
|
||||
return $result;
|
||||
}
|
||||
if (empty($response['hits'])) {
|
||||
do_log("empty response hits: " . nexus_json_encode($response), 'error');
|
||||
return $result;
|
||||
}
|
||||
if ($response['hits']['total']['value'] == 0) {
|
||||
do_log("total = 0, " . nexus_json_encode($response));
|
||||
return $result;
|
||||
}
|
||||
$result['total'] = $response['hits']['total']['value'];
|
||||
$torrentIdArr = [];
|
||||
foreach ($response['hits']['hits'] as $value) {
|
||||
$torrentIdArr[] = $value['_source']['torrent_id'];
|
||||
}
|
||||
$fieldStr = 'id, sp_state, promotion_time_type, promotion_until, banned, picktype, pos_state, category, source, medium, codec, standard, processing, team, audiocodec, leechers, seeders, name, small_descr, times_completed, size, added, comments,anonymous,owner,url,cache_stamp, pt_gen, hr';
|
||||
$idStr = implode(',', $torrentIdArr);
|
||||
$result['data'] = Torrent::query()
|
||||
->selectRaw($fieldStr)
|
||||
->whereIn('id', $torrentIdArr)
|
||||
->orderByRaw("field(id,$idStr)")
|
||||
->get()
|
||||
->toArray()
|
||||
;
|
||||
|
||||
return $result;
|
||||
|
||||
|
||||
}
|
||||
|
||||
private function getTorrentBaseFields()
|
||||
{
|
||||
return array_keys($this->getTorrentRawMappingFields());
|
||||
}
|
||||
|
||||
public function updateTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[UPDATE_TORRENT]: $id";
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
$torrent = Torrent::query()->findOrFail($id, array_merge(['id'], $baseFields));
|
||||
$data = $this->buildTorrentBody($torrent);
|
||||
$params = $data['index'];
|
||||
$params['body']['doc'] = $data['body'];
|
||||
$result = $this->es->update($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($torrent);
|
||||
}
|
||||
|
||||
public function addTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[ADD_TORRENT]: $id";
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
$torrent = Torrent::query()->findOrFail($id, array_merge(['id'], $baseFields));
|
||||
$data = $this->buildTorrentBody($torrent, true);
|
||||
$params = ['body' => []];
|
||||
$params['body'][] = ['index' => $data['index']];
|
||||
$params['body'][] = $data['body'];
|
||||
$result = $this->es->bulk($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($torrent);
|
||||
}
|
||||
|
||||
public function deleteTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[DELETE_TORRENT]: $id";
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'id' => $this->getTorrentId($id),
|
||||
];
|
||||
$result = $this->es->delete($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($id, true);
|
||||
}
|
||||
|
||||
public function syncTorrentTags($torrent, $onlyDelete = false): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$torrent instanceof Torrent) {
|
||||
$torrent = Torrent::query()->findOrFail((int)$torrent, ['id']);
|
||||
}
|
||||
$log = "sync torrent tags, torrent: " . $torrent->id;
|
||||
//remove first
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['match' => ['_doc_type' => self::DOC_TYPE_TAG]],
|
||||
['has_parent' => ['parent_type' => 'torrent', 'query' => ['match' => ['torrent_id' => $torrent->id]]]]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
$result = $this->es->deleteByQuery($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, delete torrent tag fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, delete torrent tag success: " . nexus_json_encode($result));
|
||||
if ($onlyDelete) {
|
||||
do_log("$log, only delete, return true");
|
||||
return true;
|
||||
}
|
||||
|
||||
//then insert new
|
||||
$bulk = ['body' => []];
|
||||
foreach ($torrent->torrent_tags as $torrentTag) {
|
||||
$body = $this->buildTorrentTagBody($torrent, $torrentTag, true);
|
||||
$bulk['body'][] = ['index' => $body['index']];
|
||||
$bulk['body'][] = $body['body'];
|
||||
}
|
||||
if (empty($bulk['body'])) {
|
||||
do_log("$log, no tags, return true");
|
||||
return true;
|
||||
}
|
||||
$result = $this->es->bulk($bulk);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, insert torrent tag fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, insert torrent tag success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateUser($user): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$user instanceof User) {
|
||||
$user = User::query()->findOrFail((int)$user, ['id', 'username']);
|
||||
}
|
||||
$log = "[UPDATE_USER]: " . $user->id;
|
||||
$data = $this->buildUserBody($user);
|
||||
$params = $data['index'];
|
||||
$params['body']['doc'] = $data['body'];
|
||||
$result = $this->es->update($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addBookmark($bookmark): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$bookmark instanceof Bookmark) {
|
||||
$bookmark = Bookmark::query()->with([
|
||||
'torrent' => function ($query) {$query->select(['id', 'owner']);}
|
||||
])->findOrFail((int)$bookmark);
|
||||
}
|
||||
$log = "[ADD_BOOKMARK]: " . $bookmark->toJson();
|
||||
$bulk = ['body' => []];
|
||||
$body = $this->buildBookmarkBody($bookmark->torrent, $bookmark, true);
|
||||
$bulk['body'][] = ['index' => $body['index']];
|
||||
$bulk['body'][] = $body['body'];
|
||||
$result = $this->es->bulk($bulk);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteBookmark(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[DELETE_BOOKMARK]: $id";
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'id' => $this->getBookmarkId($id),
|
||||
];
|
||||
$result = $this->es->delete($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -67,7 +67,10 @@ class TagRepository extends BaseRepository
|
||||
foreach ($renderIdArr as $tagId) {
|
||||
$value = $tagKeyById->get($tagId);
|
||||
if ($value) {
|
||||
$item = "<span style=\"background-color:{$value->color};color:white;padding: 1px 2px\">{$value->name}</span> ";
|
||||
$item = sprintf(
|
||||
"<span style=\"background-color:%s;color:%s;border-radius:%s;font-size:%s;margin:%s;padding:%s\">%s</span>",
|
||||
$value->color, $value->font_color, $value->border_radius, $value->font_size, $value->margin, $value->padding, $value->name
|
||||
);
|
||||
if ($withFilterLink) {
|
||||
$html .= sprintf('<a href="torrents.php?tag_id=%s">%s</a>', $tagId, $item);
|
||||
} else {
|
||||
@@ -129,7 +132,7 @@ class TagRepository extends BaseRepository
|
||||
return count($values);
|
||||
}
|
||||
|
||||
public static function getOrderByFieldIdString()
|
||||
public static function getOrderByFieldIdString(): string
|
||||
{
|
||||
if (is_null(self::$orderByFieldIdString)) {
|
||||
$results = self::createBasicQuery()->get(['id']);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Message;
|
||||
use App\Models\News;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollAnswer;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -148,4 +153,72 @@ class ToolRepository extends BaseRepository
|
||||
{
|
||||
return new Encrypter($key, 'AES-256-CBC');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $to
|
||||
* @param $subject
|
||||
* @param $body
|
||||
* @return bool
|
||||
*/
|
||||
public function sendMail($to, $subject, $body): bool
|
||||
{
|
||||
do_log("to: $to, subject: $subject, body: $body");
|
||||
$smtp = Setting::get('smtp');
|
||||
// Create the Transport
|
||||
$encryption = null;
|
||||
if (isset($smtp['encryption']) && in_array($smtp['encryption'], ['ssl', 'tls'])) {
|
||||
$encryption = $smtp['encryption'];
|
||||
}
|
||||
$transport = (new \Swift_SmtpTransport($smtp['smtpaddress'], $smtp['smtpport'], $encryption))
|
||||
->setUsername($smtp['accountname'])
|
||||
->setPassword($smtp['accountpassword'])
|
||||
;
|
||||
|
||||
// Create the Mailer using your created Transport
|
||||
$mailer = new \Swift_Mailer($transport);
|
||||
|
||||
// Create a message
|
||||
$message = (new \Swift_Message($subject))
|
||||
->setFrom($smtp['accountname'], Setting::get('basic.SITENAME'))
|
||||
->setTo([$to])
|
||||
->setBody($body, 'text/html')
|
||||
;
|
||||
|
||||
// Send the message
|
||||
try {
|
||||
$result = $mailer->send($message);
|
||||
if ($result == 0) {
|
||||
do_log("send mail fail, unknown error", 'error');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
do_log("send email fail: " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getNotificationCount(User $user): array
|
||||
{
|
||||
$result = [];
|
||||
//attend or not
|
||||
$attendRep = new AttendanceRepository();
|
||||
$attendance = $attendRep->getAttendance($user->id, date('Ymd'));
|
||||
$result['attendance'] = $attendance ? 0 : 1;
|
||||
|
||||
//unread news
|
||||
$count = News::query()->where('added', '>', $user->last_home)->count();
|
||||
$result['news'] = $count;
|
||||
|
||||
//unread messages
|
||||
$count = Message::query()->where('receiver', $user->id)->where('unread', 'yes')->count();
|
||||
$result['message'] = $count;
|
||||
|
||||
//un-vote poll
|
||||
$total = Poll::query()->count();
|
||||
$userVoteCount = PollAnswer::query()->where('userid', $user->id)->selectRaw('count(distinct(pollid)) as counts')->first()->counts;
|
||||
$result['poll'] = $total - $userVoteCount;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use Carbon\Carbon;
|
||||
use Hashids\Hashids;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class TorrentRepository extends BaseRepository
|
||||
{
|
||||
@@ -93,8 +94,12 @@ class TorrentRepository extends BaseRepository
|
||||
|
||||
public function getDetail($id, User $user)
|
||||
{
|
||||
$with = ['user', 'basic_audio_codec', 'basic_category', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team'];
|
||||
$result = Torrent::query()->with($with)->withCount(['peers', 'thank_users'])->visible()->findOrFail($id);
|
||||
$with = [
|
||||
'user', 'basic_audio_codec', 'basic_category', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team',
|
||||
'thanks' => function ($query) use ($user) {$query->where('userid', $user->id);},
|
||||
'reward_logs' => function ($query) use ($user) {$query->where('userid', $user->id);},
|
||||
];
|
||||
$result = Torrent::query()->with($with)->withCount(['peers', 'thank_users', 'reward_logs'])->visible()->findOrFail($id);
|
||||
$result->download_url = $this->getDownloadUrl($id, $user->toArray());
|
||||
return $result;
|
||||
}
|
||||
@@ -368,12 +373,14 @@ class TorrentRepository extends BaseRepository
|
||||
|
||||
private function getTrackerReportAuthKeySecret($id, $uid, $initializeIfNotExists = false)
|
||||
{
|
||||
$secret = TorrentSecret::query()
|
||||
->where('uid', $uid)
|
||||
->whereIn('torrent_id', [0, $id])
|
||||
->orderBy('torrent_id', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
$secret = NexusDB::remember("tracker_report_authkey_secret:$id:$uid", 3600*24, function () use ($id, $uid) {
|
||||
return TorrentSecret::query()
|
||||
->where('uid', $uid)
|
||||
->whereIn('torrent_id', [0, $id])
|
||||
->orderBy('torrent_id', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
});
|
||||
if ($secret) {
|
||||
return $secret->secret;
|
||||
}
|
||||
|
||||
873
app/Repositories/TrackerRepository.php
Normal file
873
app/Repositories/TrackerRepository.php
Normal file
@@ -0,0 +1,873 @@
|
||||
<?php
|
||||
/**
|
||||
* Handle announce and scrape
|
||||
*
|
||||
* @link https://github.com/HDInnovations/UNIT3D-Community-Edition/blob/master/app/Http/Controllers/AnnounceController.php
|
||||
* @link https://github.com/Rhilip/RidPT/blob/master/application/Controllers/Tracker/AnnounceController.php
|
||||
*/
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Exceptions\ClientNotAllowedException;
|
||||
use App\Models\Cheater;
|
||||
use App\Models\Peer;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Snatch;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Exceptions\TrackerException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Rhilip\Bencode\Bencode;
|
||||
|
||||
class TrackerRepository extends BaseRepository
|
||||
{
|
||||
const MIN_ANNOUNCE_WAIT_SECOND = 300;
|
||||
|
||||
const MAX_PEER_NUM_WANT = 50;
|
||||
|
||||
const MUST_BE_CHEATER_SPEED = 1024 * 1024 * 1024; //1024 MB/s
|
||||
const MAY_BE_CHEATER_SPEED = 1024 * 1024 * 100; //100 MB/s
|
||||
|
||||
// Port Blacklist
|
||||
protected const BLACK_PORTS = [
|
||||
22, // SSH Port
|
||||
53, // DNS queries
|
||||
80, 81, 8080, 8081, // Hyper Text Transfer Protocol (HTTP) - port used for web traffic
|
||||
411, 412, 413, // Direct Connect Hub (unofficial)
|
||||
443, // HTTPS / SSL - encrypted web traffic, also used for VPN tunnels over HTTPS.
|
||||
1214, // Kazaa - peer-to-peer file sharing, some known vulnerabilities, and at least one worm (Benjamin) targeting it.
|
||||
3389, // IANA registered for Microsoft WBT Server, used for Windows Remote Desktop and Remote Assistance connections
|
||||
4662, // eDonkey 2000 P2P file sharing service. http://www.edonkey2000.com/
|
||||
6346, 6347, // Gnutella (FrostWire, Limewire, Shareaza, etc.), BearShare file sharing app
|
||||
6699, // Port used by p2p software, such as WinMX, Napster.
|
||||
];
|
||||
|
||||
private array $userUpdates = [];
|
||||
|
||||
public function announce(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("queryString: " . $request->getQueryString());
|
||||
try {
|
||||
$withPeers = false;
|
||||
$queries = $this->checkAnnounceFields($request);
|
||||
$user = $this->checkUser($request);
|
||||
$clientAllow = $this->checkClient($request);
|
||||
$torrent = $this->checkTorrent($queries, $user);
|
||||
if ($this->isReAnnounce($queries) === false) {
|
||||
$withPeers = true;
|
||||
$peerSelf = $this->checkMinInterval($torrent, $queries, $user);
|
||||
if (!$peerSelf) {
|
||||
$this->checkPeer($torrent, $queries, $user);
|
||||
$this->checkPermission($torrent, $queries, $user);
|
||||
$peerSelf = new Peer([
|
||||
'torrent' => $torrent->id,
|
||||
'peer_id' => $queries['peer_id'],
|
||||
'userid' => $user->id,
|
||||
'passkey' => $user->passkey,
|
||||
]);
|
||||
} else {
|
||||
$this->checkCheater($torrent, $queries, $user, $peerSelf);
|
||||
}
|
||||
/**
|
||||
* Note: Must get before update peer!
|
||||
*/
|
||||
$dataTraffic = $this->getDataTraffic($torrent, $queries, $user, $peerSelf);
|
||||
|
||||
$this->updatePeer($peerSelf, $queries);
|
||||
$this->updateSnatch($peerSelf, $queries, $dataTraffic);
|
||||
$this->updateTorrent($torrent, $queries);
|
||||
|
||||
if ($dataTraffic['uploaded_increment_for_user'] > 0) {
|
||||
$this->userUpdates['uploaded'] = DB::raw('uploaded + ' . $dataTraffic['uploaded_increment_for_user']);
|
||||
}
|
||||
if ($dataTraffic['downloaded_increment_for_user'] > 0) {
|
||||
$this->userUpdates['downloaded'] = DB::raw('downloaded + ' . $dataTraffic['downloaded_increment_for_user']);
|
||||
}
|
||||
if ($user->clientselect != $clientAllow->id) {
|
||||
$this->userUpdates['clientselect'] = $clientAllow->id;
|
||||
}
|
||||
if ($user->showclienterror == 'yes') {
|
||||
$this->userUpdates['showclienterror'] = 'no';
|
||||
}
|
||||
}
|
||||
$repDict = $this->generateSuccessAnnounceResponse($torrent, $queries, $user, $withPeers);
|
||||
} catch (ClientNotAllowedException $exception) {
|
||||
do_log("[ClientNotAllowedException] " . $exception->getMessage());
|
||||
if (isset($user) && $user->showclienterror == 'no') {
|
||||
$this->userUpdates['showclienterror'] = 'yes';
|
||||
}
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (TrackerException $exception) {
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
//other system exception
|
||||
do_log("[" . get_class($exception) . "] " . $exception->getMessage() . "\n" . $exception->getTraceAsString(), 'error');
|
||||
$repDict = $this->generateFailedAnnounceResponse("system error, report to sysop please, hint: " . nexus()->getRequestId());
|
||||
} finally {
|
||||
if (isset($user) && count($this->userUpdates)) {
|
||||
$user->update($this->userUpdates);
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
return $this->sendFinalAnnounceResponse($repDict);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @throws ClientNotAllowedException
|
||||
* @throws TrackerException
|
||||
* @refs
|
||||
*/
|
||||
protected function checkClient(Request $request)
|
||||
{
|
||||
// Miss Header User-Agent is not allowed.
|
||||
if (! $request->header('User-Agent')) {
|
||||
throw new TrackerException('Invalid user-agent !');
|
||||
}
|
||||
|
||||
// Block Other Browser, Crawler (May Cheater or Faker Client) by check Requests headers
|
||||
if ($request->header('accept-language') || $request->header('referer')
|
||||
|| $request->header('accept-charset')
|
||||
|
||||
/**
|
||||
* This header check may block Non-bittorrent client `Aria2` to access tracker,
|
||||
* Because they always add this header which other clients don't have.
|
||||
*
|
||||
* @see https://blog.rhilip.info/archives/1010/ ( in Chinese )
|
||||
*/
|
||||
|| $request->header('want-digest')
|
||||
) {
|
||||
throw new TrackerException('Abnormal access blocked !');
|
||||
}
|
||||
|
||||
$userAgent = $request->header('User-Agent');
|
||||
|
||||
// Should also block User-Agent strings that are to long. (For Database reasons)
|
||||
if (\strlen((string) $userAgent) > 64) {
|
||||
throw new TrackerException('The User-Agent of this client is too long!');
|
||||
}
|
||||
|
||||
// Block Browser by checking it's User-Agent
|
||||
if (\preg_match('/(Mozilla|Browser|Chrome|Safari|AppleWebKit|Opera|Links|Lynx|Bot|Unknown)/i', (string) $userAgent)) {
|
||||
throw new TrackerException('Browser, Crawler or Cheater is not Allowed.');
|
||||
}
|
||||
|
||||
$agentAllowRep = new AgentAllowRepository();
|
||||
|
||||
return $agentAllowRep->checkClient($request->peer_id, $userAgent, config('app.debug'));
|
||||
|
||||
}
|
||||
|
||||
protected function checkPasskey($passkey)
|
||||
{
|
||||
// If Passkey Lenght Is Wrong
|
||||
if (\strlen((string) $passkey) !== 32) {
|
||||
throw new TrackerException('Invalid passkey ! the length of passkey must be 32');
|
||||
}
|
||||
|
||||
// If Passkey Format Is Wrong
|
||||
if (\strspn(\strtolower($passkey), 'abcdef0123456789') !== 32) { // MD5 char limit
|
||||
throw new TrackerException("Invalid passkey ! The format of passkey is not correct");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function checkAuthkey($authkey)
|
||||
{
|
||||
$arr = explode('|', $authkey);
|
||||
if (count($arr) != 3) {
|
||||
throw new TrackerException('Invalid authkey');
|
||||
}
|
||||
$torrentId = $arr[0];
|
||||
$uid = $arr[1];
|
||||
$torrentRep = new TorrentRepository();
|
||||
try {
|
||||
$decrypted = $torrentRep->checkTrackerReportAuthKey($authkey);
|
||||
} catch (\Exception $exception) {
|
||||
throw new TrackerException($exception->getMessage());
|
||||
}
|
||||
if (empty($decrypted)) {
|
||||
throw new TrackerException('Invalid authkey');
|
||||
}
|
||||
return compact('torrentId', 'uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws TrackerException
|
||||
*/
|
||||
protected function checkAnnounceFields(Request $request): array
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
// Part.1 check Announce **Need** Fields
|
||||
foreach (['info_hash', 'peer_id', 'port', 'uploaded', 'downloaded', 'left'] as $item) {
|
||||
$itemData = $request->query->get($item);
|
||||
if (! \is_null($itemData)) {
|
||||
$queries[$item] = $itemData;
|
||||
} else {
|
||||
throw new TrackerException("key: $item is Missing !");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['info_hash', 'peer_id'] as $item) {
|
||||
if (($length = \strlen((string) $queries[$item])) !== 20) {
|
||||
throw new TrackerException("Invalid $item ! $item is not 20 bytes long($length)");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['uploaded', 'downloaded', 'left'] as $item) {
|
||||
$itemData = $queries[$item];
|
||||
if (! \is_numeric($itemData) || $itemData < 0) {
|
||||
throw new TrackerException("Invalid $item ! $item Must be a number greater than or equal to 0");
|
||||
}
|
||||
}
|
||||
|
||||
// Part.2 check Announce **Option** Fields
|
||||
foreach (['event' => '', 'no_peer_id' => 1, 'compact' => 0, 'numwant' => 50, 'corrupt' => 0, 'key' => ''] as $item => $value) {
|
||||
$queries[$item] = $request->query->get($item, $value);
|
||||
if ($queries[$item] && $item == 'event') {
|
||||
$queries[$item] = strtolower($queries[$item]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['numwant', 'corrupt', 'no_peer_id', 'compact'] as $item) {
|
||||
if (! \is_numeric($queries[$item]) || $queries[$item] < 0) {
|
||||
throw new TrackerException("Invalid $item ! $item Must be a number greater than or equal to 0");
|
||||
}
|
||||
}
|
||||
|
||||
if (! \in_array(\strtolower($queries['event']), ['started', 'completed', 'stopped', 'paused', ''])) {
|
||||
throw new TrackerException("Unsupported Event type {$queries['event']} .");
|
||||
}
|
||||
|
||||
// Part.3 check Port is Valid and Allowed
|
||||
/**
|
||||
* Normally , the port must in 1 - 65535 , that is ( $port > 0 && $port < 0xffff )
|
||||
* However, in some case , When `&event=stopped` the port may set to 0.
|
||||
*/
|
||||
if ($queries['port'] === 0 && \strtolower($queries['event']) !== 'stopped') {
|
||||
throw new TrackerException("Illegal port 0 under Event type {$queries['event']} .");
|
||||
}
|
||||
|
||||
if (! \is_numeric($queries['port']) || $queries['port'] < 0 || $queries['port'] > 0xFFFF || \in_array($queries['port'], self::BLACK_PORTS,
|
||||
true)) {
|
||||
throw new TrackerException("Illegal port {$queries['port']} . Port should between 6881-64999");
|
||||
}
|
||||
|
||||
// Part.4 Get User Ip Address
|
||||
$queries['ip'] = $request->getClientIp();
|
||||
|
||||
// Part.5 Get Users Agent
|
||||
$queries['user_agent'] = $request->headers->get('user-agent');
|
||||
|
||||
// Part.6 info_hash, binary
|
||||
$queries['info_hash'] = $queries['info_hash'];
|
||||
|
||||
// Part.7
|
||||
$queries['peer_id'] = $queries['peer_id'];
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
protected function checkUser(Request $request)
|
||||
{
|
||||
if ($authkey = $request->query->get('authkey')) {
|
||||
$checkResult = $this->checkAuthkey($authkey);
|
||||
$field = 'id';
|
||||
$value = $checkResult['uid'];
|
||||
} elseif ($passkey = $request->query->get('passkey')) {
|
||||
$this->checkPasskey($passkey);
|
||||
$field = 'passkey';
|
||||
$value = $passkey;
|
||||
} else {
|
||||
throw new TrackerException("Require authkey or passkey.");
|
||||
}
|
||||
/**
|
||||
* @var $user User
|
||||
*/
|
||||
$user = Cache::remember("user:$field:$value:" . __METHOD__, 60, function () use ($field, $value) {
|
||||
return User::query()->where($field, $value)->first();
|
||||
});
|
||||
if (!$user) {
|
||||
throw new TrackerException("Invalid $field: $value.");
|
||||
}
|
||||
$user->checkIsNormal();
|
||||
|
||||
if ($user->parked == 'yes') {
|
||||
throw new TrackerException("Your account is parked! (Read the FAQ)");
|
||||
}
|
||||
if ($user->downloadpos == 'no') {
|
||||
throw new TrackerException("Your downloading privilege have been disabled! (Read the rules)");
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function checkTorrent($queries, User $user)
|
||||
{
|
||||
// Check Info Hash Against Torrents Table
|
||||
$torrent = $this->getTorrentByInfoHash($queries['info_hash']);
|
||||
|
||||
// If Torrent Doesnt Exists Return Error to Client
|
||||
if ($torrent === null) {
|
||||
throw new TrackerException('Torrent not registered with this tracker.');
|
||||
}
|
||||
|
||||
if ($torrent->banned == 'yes' && $user->class < Setting::get('authority.seebanned')) {
|
||||
throw new TrackerException("torrent banned");
|
||||
}
|
||||
|
||||
return $torrent;
|
||||
}
|
||||
|
||||
protected function checkPeer(Torrent $torrent, array $queries, User $user): void
|
||||
{
|
||||
if ($queries['event'] === 'completed') {
|
||||
throw new TrackerException("Torrent being announced as complete but no record found.");
|
||||
}
|
||||
|
||||
$counts = Peer::query()
|
||||
->where('torrent', '=', $torrent->id)
|
||||
->where('userid', $user->id)
|
||||
->count();
|
||||
if ($queries['left'] == 0 && $counts >= 3) {
|
||||
throw new TrackerException("You cannot seed the same torrent from more than 3 locations.");
|
||||
}
|
||||
if ($queries['left'] > 0 && $counts >= 1) {
|
||||
throw new TrackerException("You already are downloading the same torrent. You may only leech from one location at a time.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkPermission(Torrent $torrent, $queries, User $user)
|
||||
{
|
||||
if ($user->class >= User::CLASS_VIP) {
|
||||
return;
|
||||
}
|
||||
$gigs = $user->downloaded / (1024*1024*1024);
|
||||
if ($gigs < 10) {
|
||||
return;
|
||||
}
|
||||
$ratio = ($user->downloaded > 0) ? ($user->uploaded / $user->downloaded) : 1;
|
||||
$settingsMain = Setting::get('main');
|
||||
if ($settingsMain['waitsystem'] == 'yes') {
|
||||
$elapsed = Carbon::now()->diffInHours($torrent->added);
|
||||
if ($ratio < 0.4) $wait = 24;
|
||||
elseif ($ratio < 0.5) $wait = 12;
|
||||
elseif ($ratio < 0.6) $wait = 6;
|
||||
elseif ($ratio < 0.8) $wait = 3;
|
||||
else $wait = 0;
|
||||
|
||||
if ($elapsed < $wait) {
|
||||
$msg = "Your ratio is too low! You need to wait " . mkprettytime($wait * 3600 - $elapsed) . " to start";
|
||||
throw new TrackerException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
if ($settingsMain['maxdlsystem'] == 'yes') {
|
||||
if ($ratio < 0.5) $max = 1;
|
||||
elseif ($ratio < 0.65) $max = 2;
|
||||
elseif ($ratio < 0.8) $max = 3;
|
||||
elseif ($ratio < 0.95) $max = 4;
|
||||
else $max = 0;
|
||||
|
||||
if ($max > 0) {
|
||||
$counts = Peer::query()->where('userid', $user->id)->where('seeder', 'no')->count();
|
||||
if ($counts > $max) {
|
||||
$msg = "Your slot limit is reached! You may at most download $max torrents at the same time";
|
||||
throw new TrackerException($msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Torrent $torrent
|
||||
* @param $queries
|
||||
* @param User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder|Model|object|null
|
||||
* @throws TrackerException
|
||||
*/
|
||||
protected function checkMinInterval(Torrent $torrent, $queries, User $user)
|
||||
{
|
||||
$peer = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('peer_id', $queries['peer_id'])
|
||||
->first();
|
||||
|
||||
if (
|
||||
$peer
|
||||
&& $queries['event'] == ''
|
||||
&& $peer->isValidDate('prev_action')
|
||||
&& Carbon::now()->diffInSeconds($peer->prev_action) < self::MIN_ANNOUNCE_WAIT_SECOND
|
||||
) {
|
||||
throw new TrackerException('There is a minimum announce time of ' . self::MIN_ANNOUNCE_WAIT_SECOND . ' seconds');
|
||||
}
|
||||
return $peer;
|
||||
}
|
||||
|
||||
protected function checkCheater(Torrent $torrent, $queries, User $user, Peer $peer)
|
||||
{
|
||||
$settingSecurity = Setting::get('security');
|
||||
$level = $settingSecurity['cheaterdet'];
|
||||
if ($level == 0) {
|
||||
//don't do check
|
||||
return;
|
||||
}
|
||||
if ($user->class >= $settingSecurity['nodetect']) {
|
||||
//forever trust
|
||||
return;
|
||||
}
|
||||
if (!$peer->isValidDate('last_action')) {
|
||||
//no last action
|
||||
return;
|
||||
}
|
||||
$duration = Carbon::now()->diffInSeconds($peer->last_action);
|
||||
$upSpeed = $queries['uploaded'] > 0 ? ($queries['uploaded'] / $duration) : 0;
|
||||
$oneGB = 1024 * 1024 * 1024;
|
||||
$tenMB = 1024 * 1024 * 10;
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
$cheaterBaseData = [
|
||||
'added' => $nowStr,
|
||||
'userid' => $user->id,
|
||||
'torrentid' => $torrent->id,
|
||||
'uploaded' => $queries['uploaded'],
|
||||
'downloaded' => $queries['downloaded'],
|
||||
'anctime' => $duration,
|
||||
'seeders' => $torrent->seeders,
|
||||
'leechers' => $torrent->leechers,
|
||||
];
|
||||
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > self::MUST_BE_CHEATER_SPEED / $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 1024 MByte/S (For Consertive level). This is no doubt cheating.
|
||||
$comment = "User account was automatically disabled by system";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
Cheater::query()->insert($data);
|
||||
$modComment = "We believe you're trying to cheat. And your account is disabled.";
|
||||
$user->updateWithModComment(['enabled' => User::ENABLED_NO], $modComment);
|
||||
throw new TrackerException($modComment);
|
||||
}
|
||||
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > self::MAY_BE_CHEATER_SPEED / $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 100 MByte/S (For Consertive level). This is likely cheating.
|
||||
$comment = "Abnormally high uploading rate";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
|
||||
if ($level > 1) {
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > 1024 * 1024) && ($queries['leechers'] < 2 * $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 1 MByte/S when there is less than 8 leechers (For Consertive level). This is likely cheating.
|
||||
$comment = "User is uploading fast when there is few leechers";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
|
||||
if ($queries['uploaded'] > $tenMB && ($upSpeed > 1024 * 100) && ($queries['leechers'] == 0)) {
|
||||
///Uploaded more than 10 MB with uploading speed faster than 100 KByte/S when there is no leecher. This is likely cheating.
|
||||
$comment = "User is uploading when there is no leecher";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function createOrUpdateCheater(Torrent $torrent, User $user, array $createData)
|
||||
{
|
||||
$existsCheater = Cheater::query()
|
||||
->where('torrentid', $torrent->id)
|
||||
->where('userid', $user->id)
|
||||
->where('added', '>', Carbon::now()->subHours(24))
|
||||
->first();
|
||||
if ($existsCheater) {
|
||||
$existsCheater->increment('hit');
|
||||
} else {
|
||||
$createData['hit'] = 1;
|
||||
Cheater::query()->insert($createData);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isReAnnounce(array $queries): bool
|
||||
{
|
||||
unset($queries['key']);
|
||||
$lockKey = md5(http_build_query($queries));
|
||||
$redis = Redis::connection()->client();
|
||||
if (!$redis->set($lockKey, nexus()->getStartTimestamp(), ['nx', 'ex' => 5])) {
|
||||
do_log('[RE_ANNOUNCE]');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function generateSuccessAnnounceResponse($torrent, $queries, $user, $withPeers = true): array
|
||||
{
|
||||
// Build Response For Bittorrent Client
|
||||
$minInterval = self::MIN_ANNOUNCE_WAIT_SECOND;
|
||||
$interval = max($this->getRealAnnounceInterval($torrent), $minInterval);
|
||||
$repDict = [
|
||||
'interval' => $interval + random_int(10, 100),
|
||||
'min interval' => $minInterval + random_int(1, 10),
|
||||
'complete' => (int) $torrent->seeders,
|
||||
'incomplete' => (int) $torrent->leechers,
|
||||
'peers' => [],
|
||||
'peers6' => [],
|
||||
];
|
||||
do_log("[REP_DICT_BASE] " . json_encode($repDict));
|
||||
|
||||
/**
|
||||
* For non `stopped` event only
|
||||
* We query peers from database and send peer list, otherwise just quick return.
|
||||
*/
|
||||
if (\strtolower($queries['event']) !== 'stopped' && $withPeers) {
|
||||
$limit = ($queries['numwant'] <= self::MAX_PEER_NUM_WANT ? $queries['numwant'] : self::MAX_PEER_NUM_WANT);
|
||||
$baseQuery = Peer::query()
|
||||
->select(['peer_id', 'ip', 'port'])
|
||||
->where('torrent', $torrent->id)
|
||||
->where('userid', '!=', $user->id)
|
||||
->limit($limit)
|
||||
->orderByRaw('rand()')
|
||||
;
|
||||
|
||||
// Get Torrents Peers
|
||||
if ($queries['left'] == 0) {
|
||||
// Only include leechers in a seeder's peerlist
|
||||
$peers = $baseQuery->where('seeder', 'no')->get()->toArray();
|
||||
} else {
|
||||
$peers = $baseQuery->get()->toArray();
|
||||
}
|
||||
do_log("[REP_DICT_PEER_QUERY] " . last_query());
|
||||
$repDict['peers'] = $this->givePeers($peers, $queries['compact'], $queries['no_peer_id']);
|
||||
$repDict['peers6'] = $this->givePeers($peers, $queries['compact'], $queries['no_peer_id'], FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
return $repDict;
|
||||
}
|
||||
|
||||
private function getRealAnnounceInterval(Torrent $torrent)
|
||||
{
|
||||
$settingMain = Setting::get('main');
|
||||
$announce_wait = self::MIN_ANNOUNCE_WAIT_SECOND;
|
||||
$real_annnounce_interval = $settingMain['announce_interval'];
|
||||
$torrentSurvivalDays = Carbon::now()->diffInDays($torrent->added);
|
||||
if (
|
||||
$settingMain['anninterthreeage']
|
||||
&& ($settingMain['anninterthree'] > $announce_wait)
|
||||
&& ($torrentSurvivalDays >= $settingMain['anninterthreeage'])
|
||||
) {
|
||||
$real_annnounce_interval = $settingMain['anninterthree'];
|
||||
} elseif (
|
||||
$settingMain['annintertwoage']
|
||||
&& ($settingMain['annintertwo'] > $announce_wait)
|
||||
&& ($torrentSurvivalDays >= $settingMain['annintertwoage'])
|
||||
) {
|
||||
$real_annnounce_interval = $settingMain['annintertwo'];
|
||||
}
|
||||
do_log(sprintf(
|
||||
'torrent: %s, survival days: %s, real_announce_interval: %s',
|
||||
$torrent->id, $torrentSurvivalDays, $real_annnounce_interval
|
||||
), 'debug');
|
||||
|
||||
return $real_annnounce_interval;
|
||||
}
|
||||
|
||||
private function getDataTraffic(Torrent $torrent, $queries, User $user, Peer $peer): array
|
||||
{
|
||||
$log = sprintf(
|
||||
"torrent: %s, user: %s, peer: %s, queriesUploaded: %s, queriesDownloaded: %s",
|
||||
$torrent->id, $user->id, $peer->id, $queries['uploaded'], $queries['downloaded']
|
||||
);
|
||||
if ($peer->exists) {
|
||||
$realUploaded = max($queries['uploaded'] - $peer->uploaded, 0);
|
||||
$realDownloaded = max($queries['downloaded'] - $peer->downloaded, 0);
|
||||
$log .= ", [PEER_EXISTS], realUploaded: $realUploaded, realDownloaded: $realDownloaded";
|
||||
} else {
|
||||
$realUploaded = $queries['uploaded'];
|
||||
$realDownloaded = $queries['downloaded'];
|
||||
$log .= ", [PEER_NOT_EXISTS],, realUploaded: $realUploaded, realDownloaded: $realDownloaded";
|
||||
}
|
||||
$spStateReal = $torrent->spStateReal;
|
||||
$uploaderRatio = Setting::get('torrent.uploaderdouble');
|
||||
$log .= ", spStateReal: $spStateReal, uploaderRatio: $uploaderRatio";
|
||||
if ($torrent->owner == $user->id) {
|
||||
//uploader, use the bigger one
|
||||
$upRatio = max($uploaderRatio, Torrent::$promotionTypes[$spStateReal]['up_multiplier']);
|
||||
$log .= ", [IS_UPLOADER], upRatio: $upRatio";
|
||||
} else {
|
||||
$upRatio = Torrent::$promotionTypes[$spStateReal]['up_multiplier'];
|
||||
$log .= ", [IS_NOT_UPLOADER], upRatio: $upRatio";
|
||||
}
|
||||
$downRatio = Torrent::$promotionTypes[$spStateReal]['down_multiplier'];
|
||||
$log .= ", downRatio: $downRatio";
|
||||
$result = [
|
||||
'uploaded_increment' => $realUploaded,
|
||||
'uploaded_increment_for_user' => $realUploaded * $upRatio,
|
||||
'downloaded_increment' => $realDownloaded,
|
||||
'downloaded_increment_for_user' => $realDownloaded * $downRatio,
|
||||
];
|
||||
do_log("$log, result: " . json_encode($result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function givePeers($peers, $compact, $noPeerId, int $filterFlag = FILTER_FLAG_IPV4): string|array
|
||||
{
|
||||
if ($compact) {
|
||||
$pcomp = '';
|
||||
foreach ($peers as $p) {
|
||||
if (isset($p['ip'], $p['port']) && \filter_var($p['ip'], FILTER_VALIDATE_IP, $filterFlag)) {
|
||||
$pcomp .= \inet_pton($p['ip']);
|
||||
$pcomp .= \pack('n', (int) $p['port']);
|
||||
}
|
||||
}
|
||||
|
||||
return $pcomp;
|
||||
}
|
||||
|
||||
if ($noPeerId) {
|
||||
foreach ($peers as &$p) {
|
||||
unset($p['peer_id']);
|
||||
}
|
||||
|
||||
return $peers;
|
||||
}
|
||||
|
||||
return $peers;
|
||||
}
|
||||
|
||||
protected function generateFailedAnnounceResponse($reason): array
|
||||
{
|
||||
return [
|
||||
'failure reason' => $reason,
|
||||
'min interval' => self::MIN_ANNOUNCE_WAIT_SECOND,
|
||||
//'retry in' => self::MIN_ANNOUNCE_WAIT_SECOND
|
||||
];
|
||||
}
|
||||
|
||||
protected function sendFinalAnnounceResponse($repDict): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("[repDict] " . nexus_json_encode($repDict));
|
||||
return \response(Bencode::encode($repDict))
|
||||
->withHeaders(['Content-Type' => 'text/plain; charset=utf-8'])
|
||||
->withHeaders(['Connection' => 'close'])
|
||||
->withHeaders(['Pragma' => 'no-cache']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Torrent $torrent
|
||||
* @param $queries
|
||||
*/
|
||||
private function updateTorrent(Torrent $torrent, $queries)
|
||||
{
|
||||
if (empty($queries['event'])) {
|
||||
do_log("no event, return", 'debug');
|
||||
return;
|
||||
}
|
||||
$torrent->seeders = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('to_go', '=',0)
|
||||
->count();
|
||||
|
||||
$torrent->leechers = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('to_go', '>', 0)
|
||||
->count();
|
||||
|
||||
$torrent->visible = Torrent::VISIBLE_YES;
|
||||
$torrent->last_action = Carbon::now();
|
||||
|
||||
if ($queries['event'] == 'completed') {
|
||||
$torrent->times_completed = DB::raw("times_completed + 1");
|
||||
}
|
||||
|
||||
$torrent->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
private function updatePeer(Peer $peer, $queries)
|
||||
{
|
||||
if ($queries['event'] == 'stopped') {
|
||||
$peer->delete();
|
||||
do_log(last_query(), 'debug');
|
||||
return;
|
||||
}
|
||||
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
//torrent, userid, peer_id, ip, port, connectable, uploaded, downloaded, to_go, started, last_action, seeder, agent, downloadoffset, uploadoffset, passkey
|
||||
$peer->ip = $queries['ip'];
|
||||
$peer->port = $queries['port'];
|
||||
$peer->agent = $queries['user_agent'];
|
||||
$peer->updateConnectableStateIfNeeded();
|
||||
|
||||
$peer->to_go = $queries['left'];
|
||||
$peer->seeder = $queries['left'] == 0 ? 'yes' : 'no';
|
||||
$peer->last_action = $nowStr;
|
||||
$peer->uploaded = $queries['uploaded'];
|
||||
$peer->downloaded = $queries['downloaded'];
|
||||
|
||||
if ($peer->exists) {
|
||||
$peer->prev_action = $peer->last_action;
|
||||
}
|
||||
|
||||
if ($queries['event'] == 'started') {
|
||||
$peer->started = $nowStr;
|
||||
$peer->uploadoffset = $queries['uploaded'];
|
||||
$peer->downloadoffset = $queries['downloaded'];
|
||||
} elseif ($queries['event'] == 'completed') {
|
||||
$peer->finishedat = time();
|
||||
}
|
||||
|
||||
$peer->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update snatch, uploaded & downloaded, use the increment value to do increment
|
||||
*
|
||||
* @param Peer $peer
|
||||
* @param $queries
|
||||
* @param $dataTraffic
|
||||
*/
|
||||
private function updateSnatch(Peer $peer, $queries, $dataTraffic)
|
||||
{
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
|
||||
$snatch = Snatch::query()
|
||||
->where('torrentid', $peer->torrent)
|
||||
->where('userid', $peer->userid)
|
||||
->first();
|
||||
|
||||
//torrentid, userid, ip, port, uploaded, downloaded, to_go, ,seedtime, leechtime, last_action, startdat, completedat, finished
|
||||
if (!$snatch) {
|
||||
$snatch = new Snatch();
|
||||
//initial
|
||||
$snatch->torrentid = $peer->torrent;
|
||||
$snatch->userid = $peer->userid;
|
||||
$snatch->uploaded = $dataTraffic['uploaded_increment'];
|
||||
$snatch->downloaded = $dataTraffic['downloaded_increment'];
|
||||
$snatch->startdat = $nowStr;
|
||||
} else {
|
||||
//increase, use the increment value
|
||||
$snatch->uploaded = DB::raw("uploaded + " . $dataTraffic['uploaded_increment']);
|
||||
$snatch->downloaded = DB::raw("downloaded + " . $dataTraffic['downloaded_increment']);
|
||||
$timeIncrease = Carbon::now()->diffInSeconds($peer->last_action);
|
||||
if ($queries['left'] == 0) {
|
||||
//seeder
|
||||
$timeField = 'seedtime';
|
||||
} else {
|
||||
$timeField = 'leechtime';
|
||||
}
|
||||
$snatch->{$timeField} = DB::raw("$timeField + $timeIncrease");
|
||||
}
|
||||
|
||||
//always update
|
||||
$snatch->ip = $queries['ip'];
|
||||
$snatch->port = $queries['port'];
|
||||
$snatch->to_go = $queries['left'];
|
||||
$snatch->last_action = $nowStr;
|
||||
if ($queries['event'] == 'completed') {
|
||||
$snatch->completedat = $nowStr;
|
||||
$snatch->finished = 'yes';
|
||||
}
|
||||
|
||||
$snatch->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
public function scrape(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("queryString: " . $request->getQueryString());
|
||||
try {
|
||||
$infoHashArr = $this->checkScrapeFields($request);
|
||||
$user = $this->checkUser($request);
|
||||
$clientAllow = $this->checkClient($request);
|
||||
|
||||
if ($user->clientselect != $clientAllow->id) {
|
||||
$this->userUpdates['clientselect'] = $clientAllow->id;
|
||||
}
|
||||
if ($user->showclienterror == 'yes') {
|
||||
$this->userUpdates['showclienterror'] = 'no';
|
||||
}
|
||||
$repDict = $this->generateScrapeResponse($infoHashArr);
|
||||
} catch (ClientNotAllowedException $exception) {
|
||||
do_log("[ClientNotAllowedException] " . $exception->getMessage());
|
||||
if (isset($user) && $user->showclienterror == 'no') {
|
||||
$this->userUpdates['showclienterror'] = 'yes';
|
||||
}
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (TrackerException $exception) {
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
//other system exception
|
||||
do_log("[" . get_class($exception) . "] " . $exception->getMessage() . "\n" . $exception->getTraceAsString(), 'error');
|
||||
$repDict = $this->generateFailedAnnounceResponse("system error, report to sysop please, hint: " . nexus()->getRequestId());
|
||||
} finally {
|
||||
do_log("userUpdates: " . nexus_json_encode($this->userUpdates));
|
||||
if (isset($user) && count($this->userUpdates)) {
|
||||
$user->update($this->userUpdates);
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
return $this->sendFinalAnnounceResponse($repDict);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkScrapeFields(Request $request): array
|
||||
{
|
||||
preg_match_all('/info_hash=([^&]*)/i', urldecode($request->getQueryString()), $info_hash_match);
|
||||
|
||||
$info_hash_array = $info_hash_match[1];
|
||||
if (count($info_hash_array) < 1) {
|
||||
throw new TrackerException("key: info_hash is Missing !");
|
||||
} else {
|
||||
foreach ($info_hash_array as $item) {
|
||||
if (strlen($item) != 20) {
|
||||
throw new TrackerException("Invalid info_hash ! info_hash is not 20 bytes long");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $info_hash_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $info_hash_array
|
||||
* @return array[]
|
||||
* @see http://www.bittorrent.org/beps/bep_0048.html
|
||||
*/
|
||||
private function generateScrapeResponse($info_hash_array)
|
||||
{
|
||||
$torrent_details = [];
|
||||
foreach ($info_hash_array as $item) {
|
||||
$torrent = $this->getTorrentByInfoHash($item);
|
||||
if ($torrent) {
|
||||
$torrent_details[$item] = [
|
||||
'complete' => (int)$torrent->seeders,
|
||||
'downloaded' => (int)$torrent->times_completed,
|
||||
'incomplete' => (int)$torrent->leechers,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['files' => $torrent_details];
|
||||
}
|
||||
|
||||
private function getTorrentByInfoHash($infoHash)
|
||||
{
|
||||
$cacheKey = bin2hex($infoHash) . __METHOD__;
|
||||
return Cache::remember($cacheKey, 60, function () use ($infoHash) {
|
||||
$fieldRaw = 'id, owner, sp_state, seeders, leechers, added, banned, hr, visible, last_action, times_completed';
|
||||
$torrent = Torrent::query()->where('info_hash', $infoHash)->selectRaw($fieldRaw)->first();
|
||||
do_log("[getTorrentByInfoHash] cache miss, from database: " . last_query() . ", and get: " . $torrent->id);
|
||||
return $torrent;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user