Merge branch '1.7' into php8

This commit is contained in:
xiaomlove
2022-03-31 16:43:51 +08:00
140 changed files with 21731 additions and 273 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}

View 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');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Exceptions;
class ClientNotAllowedException extends NexusException
{
}

View File

@@ -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)) {

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Exceptions;
class TrackerException extends NexusException
{
}

View File

@@ -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);
}
/**

View File

@@ -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]);
}
}

View File

@@ -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);
}
}

View 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());
}
}

View 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)
{
//
}
}

View File

@@ -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);

View File

@@ -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, '说谢谢成功!');
}
/**

View File

@@ -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);
}
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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,
];
/**

View 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);
}
}

View File

@@ -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!');
}

View File

@@ -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);

View File

@@ -15,5 +15,7 @@ class TrimStrings extends Middleware
'current_password',
'password',
'password_confirmation',
'peer_id',
'info_hash',
];
}

View File

@@ -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')),
];
}

View File

@@ -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'))
];
}

View 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;
}
}

View 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'))
];
}
}

View File

@@ -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),

View File

@@ -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')),
];
}

View File

@@ -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;

View 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();
}
}

View 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');
}
}
}

View File

@@ -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();
});

View File

@@ -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
View 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',
];
}

View File

@@ -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');

View File

@@ -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
View 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');
}
}

View File

@@ -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
View 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
View 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
View 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
View 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');
}
}

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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 = [

View File

@@ -5,6 +5,8 @@ namespace App\Models;
class Thank extends NexusModel
{
protected $fillable = ['torrentid', 'userid'];
public function user()
{
return $this->belongsTo(User::class, 'userid');

View File

@@ -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');
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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,
],
];
/**

View File

@@ -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'));
});
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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]]);
});

View 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');
}
}

View File

@@ -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] = [

View File

@@ -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;
}
}

View 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)
{
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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']);

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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;
});
}
}