mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-14 20:40:49 +08:00
Merge branch '1.7' into php8
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Exceptions\NexusException;
|
||||
use App\Exceptions\ClientNotAllowedException;
|
||||
use App\Models\AgentAllow;
|
||||
use App\Models\AgentDeny;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class AgentAllowRepository extends BaseRepository
|
||||
{
|
||||
@@ -52,31 +53,40 @@ class AgentAllowRepository extends BaseRepository
|
||||
public function getPatternMatches($pattern, $start, $matchNum)
|
||||
{
|
||||
if (!preg_match($pattern, $start, $matches)) {
|
||||
throw new NexusException(sprintf('pattern: %s can not match start: %s', $pattern, $start));
|
||||
throw new ClientNotAllowedException(sprintf('pattern: %s can not match start: %s', $pattern, $start));
|
||||
}
|
||||
$matchCount = count($matches) - 1;
|
||||
//due to old data may be matchNum > matchCount
|
||||
if ($matchNum > $matchCount && !IN_NEXUS) {
|
||||
throw new NexusException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum.");
|
||||
}
|
||||
// if ($matchNum > $matchCount && !IN_NEXUS) {
|
||||
// throw new ClientNotAllowedException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum.");
|
||||
// }
|
||||
return array_slice($matches, 1, $matchNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $peerId
|
||||
* @param $agent
|
||||
* @param false $debug
|
||||
* @return \App\Models\NexusModel|mixed
|
||||
* @throws ClientNotAllowedException
|
||||
*/
|
||||
public function checkClient($peerId, $agent, $debug = false)
|
||||
{
|
||||
//check from high version to low version, if high version allow, stop!
|
||||
$allows = AgentAllow::query()
|
||||
->orderBy('peer_id_start', 'desc')
|
||||
->orderBy('agent_start', 'desc')
|
||||
->get();
|
||||
$allows = NexusDB::remember("all_agent_allows", 600, function () {
|
||||
return AgentAllow::query()
|
||||
->orderBy('peer_id_start', 'desc')
|
||||
->orderBy('agent_start', 'desc')
|
||||
->get();
|
||||
});
|
||||
$agentAllowPassed = null;
|
||||
$versionTooLowStr = '';
|
||||
foreach ($allows as $agentAllow) {
|
||||
$agentAllowId = $agentAllow->id;
|
||||
$logPrefix = "[ID: $agentAllowId]";
|
||||
$isPeerIdAllowed = $isAgentAllowed = $isPeerIdTooLow = $isAgentTooLow = false;
|
||||
//check peer_id
|
||||
if ($agentAllow->peer_id_pattern == '') {
|
||||
//check peer_id, when handle scrape request, no peer_id, so let it pass
|
||||
if ($agentAllow->peer_id_pattern == '' || $peerId === null) {
|
||||
$isPeerIdAllowed = true;
|
||||
} else {
|
||||
$pattern = $agentAllow->peer_id_pattern;
|
||||
@@ -93,7 +103,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
do_log("$logPrefix, check peer_id error: " . $exception->getMessage(), 'error');
|
||||
throw new NexusException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this");
|
||||
throw new ClientNotAllowedException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this");
|
||||
}
|
||||
if ($peerIdResult == 1) {
|
||||
$isPeerIdAllowed = true;
|
||||
@@ -121,7 +131,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
do_log("$logPrefix, check agent error: " . $exception->getMessage(), 'error');
|
||||
throw new NexusException("regular expression err for agent: " . $start . ", please ask sysop to fix this");
|
||||
throw new ClientNotAllowedException("regular expression err for agent: " . $start . ", please ask sysop to fix this");
|
||||
}
|
||||
if ($agentResult == 1) {
|
||||
$isAgentAllowed = true;
|
||||
@@ -142,11 +152,11 @@ class AgentAllowRepository extends BaseRepository
|
||||
}
|
||||
|
||||
if ($versionTooLowStr) {
|
||||
throw new NexusException($versionTooLowStr);
|
||||
throw new ClientNotAllowedException($versionTooLowStr);
|
||||
}
|
||||
|
||||
if (!$agentAllowPassed) {
|
||||
throw new NexusException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
|
||||
throw new ClientNotAllowedException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
@@ -160,14 +170,14 @@ class AgentAllowRepository extends BaseRepository
|
||||
if ($debug) {
|
||||
do_log("agentDeny: " . $agentDeny->toJson());
|
||||
}
|
||||
throw new NexusException(sprintf(
|
||||
throw new ClientNotAllowedException(sprintf(
|
||||
"[%s-%s]Client: %s is banned due to: %s",
|
||||
$agentAllowPassed->id, $agentDeny->id, $agentDeny->name, $agentDeny->comment
|
||||
));
|
||||
}
|
||||
}
|
||||
if (isHttps() && $agentAllowPassed->allowhttps != 'yes') {
|
||||
throw new NexusException(sprintf(
|
||||
throw new ClientNotAllowedException(sprintf(
|
||||
"[%s]This client does not support https well, Please goto %s/faq.php#id29 for a list of proper clients",
|
||||
$agentAllowPassed->id, getSchemeAndHttpHost()
|
||||
));
|
||||
@@ -202,7 +212,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
* @param bool $debug
|
||||
* @param string $logPrefix
|
||||
* @return int
|
||||
* @throws NexusException
|
||||
* @throws ClientNotAllowedException
|
||||
*/
|
||||
private function isAllowed($pattern, $start, $matchNum, $matchType, $value, $debug = false, $logPrefix = ''): int
|
||||
{
|
||||
@@ -234,7 +244,7 @@ class AgentAllowRepository extends BaseRepository
|
||||
$matchBench[$i] = hexdec($matchBench[$i]);
|
||||
$matchTarget[$i] = hexdec($matchTarget[$i]);
|
||||
} else {
|
||||
throw new NexusException(sprintf("Invalid match type: %s", $matchType));
|
||||
throw new ClientNotAllowedException(sprintf("Invalid match type: %s", $matchType));
|
||||
}
|
||||
if ($matchTarget[$i] > $matchBench[$i]) {
|
||||
//higher, pass directly
|
||||
|
||||
@@ -75,8 +75,8 @@ class AttendanceRepository extends BaseRepository
|
||||
->where('uid', $uid)
|
||||
->orderBy('id', 'desc');
|
||||
if (!empty($date)) {
|
||||
$query->where('added', '>=', Carbon::today())
|
||||
->where('added', '<', Carbon::tomorrow());
|
||||
$query->where('added', '>=', Carbon::parse($date)->startOfDay())
|
||||
->where('added', '<=', Carbon::parse($date)->endOfDay());
|
||||
}
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class AuthenticateRepository extends BaseRepository
|
||||
if (!$user || md5($user->secret . $password . $user->secret) != $user->passhash) {
|
||||
throw new \InvalidArgumentException('Username or password invalid.');
|
||||
}
|
||||
if (IS_PLATFORM_ADMIN && !$user->canAccessAdmin()) {
|
||||
if (nexus()->isPlatformAdmin() && !$user->canAccessAdmin()) {
|
||||
throw new UnauthorizedException('Unauthorized!');
|
||||
}
|
||||
$user->checkIsNormal();
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\HitAndRun;
|
||||
use App\Models\Medal;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\UserMedal;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Nexus\Database\NexusDB;
|
||||
@@ -66,7 +67,7 @@ class BonusRepository extends BaseRepository
|
||||
if ($medal->duration > 0) {
|
||||
$expireAt = Carbon::now()->addDays($medal->duration)->toDateTimeString();
|
||||
}
|
||||
$user->medals()->attach([$medal->id => ['expire_at' => $expireAt]]);
|
||||
$user->medals()->attach([$medal->id => ['expire_at' => $expireAt, 'status' => UserMedal::STATUS_NOT_WEARING]]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
111
app/Repositories/CommentRepository.php
Normal file
111
app/Repositories/CommentRepository.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Message;
|
||||
use App\Models\NexusModel;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Hamcrest\Core\Set;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class CommentRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Comment::query()->with(['create_user', 'update_user']);
|
||||
if (!empty($params['torrent_id'])) {
|
||||
$query->where('torrent', $params['torrent_id']);
|
||||
}
|
||||
if (!empty($params['offer_id'])) {
|
||||
$query->where('offer', $params['offer_id']);
|
||||
}
|
||||
if (!empty($params['request_id'])) {
|
||||
$query->where('request', $params['request_id']);
|
||||
}
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store(array $params, User $user)
|
||||
{
|
||||
$type = $params['type'];
|
||||
$modelName = Comment::TYPE_MAPS[$params['type']]['model'];
|
||||
/**
|
||||
* @var NexusModel $model
|
||||
*/
|
||||
$model = new $modelName;
|
||||
$target = $model->newQuery()->with('user')->find($params[$type]);
|
||||
return DB::transaction(function () use ($params, $user, $target) {
|
||||
$params['added'] = Carbon::now();
|
||||
$comment = $user->comments()->create($params);
|
||||
$commentCount = Comment::query()->type($params['type'], $params[$params['type']])->count();
|
||||
$target->comments = $commentCount;
|
||||
$target->save();
|
||||
|
||||
$userUpdate = [
|
||||
'seedbonus' => DB::raw('seedbonus + ' . Setting::get('bonus.addcomment')),
|
||||
'last_comment' => Carbon::now(),
|
||||
];
|
||||
$user->update($userUpdate);
|
||||
|
||||
//message
|
||||
if ($target->user->commentpm == 'yes' && $user->id != $target->user->id) {
|
||||
$messageInfo = $this->getNoticeMessage($target, $params['type']);
|
||||
$insert = [
|
||||
'sender' => 0,
|
||||
'receiver' => $target->user->id,
|
||||
'subject' => $messageInfo['subject'],
|
||||
'msg' => $messageInfo['body'],
|
||||
'added' => $params['added'],
|
||||
];
|
||||
Message::query()->insert($insert);
|
||||
NexusDB::cache_del('user_'.$target->user->id.'_unread_message_count');
|
||||
NexusDB::cache_del('user_'.$target->user->id.'_inbox_count');
|
||||
}
|
||||
|
||||
return $comment;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Comment::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getNoticeMessage($target, $type): array
|
||||
{
|
||||
$allTrans = require_once base_path('lang/_target/lang_comment.php');
|
||||
$lang = $target->user->language->site_lang_folder ?? 'en';
|
||||
$trans = $allTrans[$lang];
|
||||
$subject = $trans['msg_new_comment'];
|
||||
$targetScript = Comment::TYPE_MAPS[$type]['target_script'];
|
||||
$targetNameField = Comment::TYPE_MAPS[$type]['target_name_field'];
|
||||
$body = sprintf(
|
||||
'%s [url=%s]%s[/url]',
|
||||
$trans['msg_torrent_receive_comment'],
|
||||
sprintf($targetScript, $target->id),
|
||||
$target->{$targetNameField}
|
||||
);
|
||||
return compact('subject', 'body');
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class DashboardRepository extends BaseRepository
|
||||
$result[$name] = [
|
||||
'name' => $name,
|
||||
'text' => nexus_trans("dashboard.system_info.$name"),
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'],
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'] ?? '',
|
||||
];
|
||||
$name = 'load_average';
|
||||
$result[$name] = [
|
||||
|
||||
@@ -66,7 +66,23 @@ class MedalRepository extends BaseRepository
|
||||
if ($duration > 0) {
|
||||
$expireAt = Carbon::now()->addDays($duration)->toDateTimeString();
|
||||
}
|
||||
return $user->medals()->attach([$medal->id => ['expire_at' => $expireAt]]);
|
||||
return $user->medals()->attach([$medal->id => ['expire_at' => $expireAt, 'status' => UserMedal::STATUS_NOT_WEARING]]);
|
||||
}
|
||||
|
||||
function toggleUserMedalStatus($id, $userId)
|
||||
{
|
||||
$userMedal = UserMedal::query()->findOrFail($id);
|
||||
if ($userMedal->uid != $userId) {
|
||||
throw new \LogicException("no privilege");
|
||||
}
|
||||
$current = $userMedal->status;
|
||||
if ($current == UserMedal::STATUS_NOT_WEARING) {
|
||||
$userMedal->status = UserMedal::STATUS_WEARING;
|
||||
} elseif ($current == UserMedal::STATUS_WEARING) {
|
||||
$userMedal->status = UserMedal::STATUS_NOT_WEARING;
|
||||
}
|
||||
$userMedal->save();
|
||||
return $userMedal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
72
app/Repositories/PollRepository.php
Normal file
72
app/Repositories/PollRepository.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Poll;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PollRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Poll::query();
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store($torrentId, $value, User $user)
|
||||
{
|
||||
if ($user->seedbonus < $value) {
|
||||
throw new \LogicException("user bonus not enough.");
|
||||
}
|
||||
if ($user->reward_torrent_logs()->where('torrentid', $torrentId)->exists()) {
|
||||
throw new \LogicException("user already reward this torrent.");
|
||||
}
|
||||
$torrent = Torrent::query()->findOrFail($torrentId, ['owner']);
|
||||
$torrentOwner = User::query()->findOrFail($torrent->owner, ['id', 'seedbonus']);
|
||||
return DB::transaction(function () use ($torrentId, $value, $user, $torrentOwner) {
|
||||
$model = $user->reward_torrent_logs()->create([
|
||||
'torrentid' => $torrentId,
|
||||
'value' => $value,
|
||||
]);
|
||||
$affectedRows = $user->where('seedbonus', $user->seedbonus)->decrement('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("decrement user bonus fail.");
|
||||
}
|
||||
$affectedRows = $torrentOwner->where('seedbonus', $torrentOwner->seedbonus)->increment('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment owner bonus fail.");
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Poll::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function vote($selection, User $user)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
82
app/Repositories/RewardRepository.php
Normal file
82
app/Repositories/RewardRepository.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Reward;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Google\Service\ToolResults\StepLabelsEntry;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RewardRepository extends BaseRepository
|
||||
{
|
||||
public function getList(array $params)
|
||||
{
|
||||
$query = Reward::query()->with(['user']);
|
||||
if (!empty($params['torrent_id'])) {
|
||||
$query->where('torrentid', $params['torrent_id']);
|
||||
}
|
||||
list($sortField, $sortType) = $this->getSortFieldAndType($params);
|
||||
$query->orderBy($sortField, $sortType);
|
||||
return $query->paginate();
|
||||
}
|
||||
|
||||
public function store($torrentId, $value, User $user)
|
||||
{
|
||||
if ($user->seedbonus < $value) {
|
||||
throw new \LogicException("your bonus not enough.");
|
||||
}
|
||||
if ($user->reward_torrent_logs()->where('torrentid', $torrentId)->exists()) {
|
||||
throw new \LogicException("you already reward this torrent.");
|
||||
}
|
||||
$torrent = Torrent::query()->findOrFail($torrentId, Torrent::$commentFields);
|
||||
$torrent->checkIsNormal();
|
||||
$torrentOwner = User::query()->findOrFail($torrent->owner);
|
||||
if ($user->id == $torrentOwner->id) {
|
||||
throw new \LogicException("you can't reward to yourself.");
|
||||
}
|
||||
$torrentOwner->checkIsNormal();
|
||||
return DB::transaction(function () use ($torrentId, $value, $user, $torrentOwner) {
|
||||
$model = $user->reward_torrent_logs()->create([
|
||||
'torrentid' => $torrentId,
|
||||
'value' => $value,
|
||||
]);
|
||||
$affectedRows = User::query()
|
||||
->where('id', $user->id)
|
||||
->where('seedbonus', $user->seedbonus)
|
||||
->decrement('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("decrement user bonus fail.");
|
||||
}
|
||||
$affectedRows = User::query()
|
||||
->where('id', $torrentOwner->id)
|
||||
->where('seedbonus', $torrentOwner->seedbonus)
|
||||
->increment('seedbonus', $value);
|
||||
if ($affectedRows != 1) {
|
||||
do_log("affectedRows: $affectedRows, query: " . last_query(), 'error');
|
||||
throw new \RuntimeException("increment owner bonus fail.");
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
}
|
||||
|
||||
public function update(array $params, $id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
$model->update($params);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function getDetail($id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$model = Reward::query()->findOrFail($id);
|
||||
$result = $model->delete();
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
879
app/Repositories/SearchRepository.php
Normal file
879
app/Repositories/SearchRepository.php
Normal file
@@ -0,0 +1,879 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Bookmark;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\TorrentTag;
|
||||
use App\Models\User;
|
||||
use Elasticsearch\Client;
|
||||
use Elasticsearch\ClientBuilder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class SearchRepository extends BaseRepository
|
||||
{
|
||||
private Client $es;
|
||||
|
||||
private bool $enabled = false;
|
||||
|
||||
const INDEX_NAME = 'nexus_torrents';
|
||||
|
||||
const DOC_TYPE_TORRENT = 'torrent';
|
||||
const DOC_TYPE_TAG = 'tag';
|
||||
const DOC_TYPE_BOOKMARK = 'bookmark';
|
||||
const DOC_TYPE_USER = 'user';
|
||||
|
||||
const SEARCH_MODE_AND = '0';
|
||||
const SEARCH_MODE_OR = '1';
|
||||
const SEARCH_MODE_EXACT = '2';
|
||||
|
||||
const SEARCH_MODES = [
|
||||
self::SEARCH_MODE_AND => ['text' => 'and'],
|
||||
self::SEARCH_MODE_OR => ['text' => 'or'],
|
||||
self::SEARCH_MODE_EXACT => ['text' => 'exact'],
|
||||
];
|
||||
|
||||
const SEARCH_AREA_TITLE = '0';
|
||||
const SEARCH_AREA_DESC = '1';
|
||||
const SEARCH_AREA_OWNER = '3';
|
||||
const SEARCH_AREA_IMDB = '4';
|
||||
|
||||
const SEARCH_AREAS = [
|
||||
self::SEARCH_AREA_TITLE => ['text' => 'title'],
|
||||
self::SEARCH_AREA_DESC => ['text' => 'desc'],
|
||||
self::SEARCH_AREA_OWNER => ['text' => 'owner'],
|
||||
self::SEARCH_AREA_IMDB => ['text' => 'imdb'],
|
||||
];
|
||||
|
||||
|
||||
|
||||
private array $indexSetting = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => [
|
||||
'settings' => [
|
||||
'number_of_shards' => 1,
|
||||
'number_of_replicas' => 0,
|
||||
],
|
||||
'mappings' => [
|
||||
'properties' => [
|
||||
'_doc_type' => ['type' => 'keyword'],
|
||||
|
||||
//torrent
|
||||
'torrent_id' => ['type' => 'long', ],
|
||||
|
||||
//user
|
||||
'username' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
|
||||
//bookmark + user + tag
|
||||
'user_id' => ['type' => 'long', ],
|
||||
|
||||
//tag
|
||||
'tag_id' => ['type' => 'long', ],
|
||||
|
||||
//relations
|
||||
'torrent_relations' => [
|
||||
'type' => 'join',
|
||||
'eager_global_ordinals' => true,
|
||||
'relations' => [
|
||||
'user' => ['torrent'],
|
||||
'torrent' => ['bookmark', 'tag'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
//cat401=1&source1=1&medium1=1&codec1=1&audiocodec1=1&standard1=1&processing1=1&team1=1&incldead=1&spstate=2&inclbookmarked=1&search=tr&search_area=1&search_mode=1
|
||||
private static array $queryFieldToTorrentFieldMaps = [
|
||||
'cat' => 'category',
|
||||
'source' => 'source',
|
||||
'medium' => 'medium',
|
||||
'codec' => 'codec',
|
||||
'audiocodec' => 'audiocodec',
|
||||
'standard' => 'standard',
|
||||
'processing' => 'processing',
|
||||
'team' => 'team',
|
||||
];
|
||||
|
||||
private static array $sortFieldMaps = [
|
||||
'1' => 'name',
|
||||
'2' => 'numfiles',
|
||||
'3' => 'comments',
|
||||
'4' => 'added',
|
||||
'5' => 'size',
|
||||
'6' => 'times_completed',
|
||||
'7' => 'seeders',
|
||||
'8' => 'leechers',
|
||||
'9' => 'owner',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$elasticsearchEnabled = nexus_env('ELASTICSEARCH_ENABLED');
|
||||
if ($elasticsearchEnabled) {
|
||||
$this->enabled = true;
|
||||
$this->es = $this->getEs();
|
||||
} else {
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getEs(): Client
|
||||
{
|
||||
$config = nexus_config('nexus.elasticsearch');
|
||||
$es = ClientBuilder::create()->setHosts($config['hosts']);
|
||||
if (!empty($config['ssl_verification'])) {
|
||||
$es->setSSLVerification($config['ssl_verification']);
|
||||
}
|
||||
return $es->build();
|
||||
}
|
||||
|
||||
private function getTorrentRawMappingFields(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'descr' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'small_descr' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'category' => ['type' => 'long', ],
|
||||
'source' => ['type' => 'long', ],
|
||||
'medium' => ['type' => 'long', ],
|
||||
'codec' => ['type' => 'long', ],
|
||||
'standard' => ['type' => 'long', ],
|
||||
'processing' => ['type' => 'long', ],
|
||||
'team' => ['type' => 'long', ],
|
||||
'audiocodec' => ['type' => 'long', ],
|
||||
'size' => ['type' => 'long', ],
|
||||
'added' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
|
||||
'numfiles' => ['type' => 'long', ],
|
||||
'comments' => ['type' => 'long', ],
|
||||
'views' => ['type' => 'long', ],
|
||||
'hits' => ['type' => 'long', ],
|
||||
'times_completed' => ['type' => 'long', ],
|
||||
'leechers' => ['type' => 'long', ],
|
||||
'seeders' => ['type' => 'long', ],
|
||||
'last_action' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
|
||||
'visible' => ['type' => 'keyword', ],
|
||||
'banned' => ['type' => 'keyword', ],
|
||||
'owner' => ['type' => 'long', ],
|
||||
'sp_state' => ['type' => 'long', ],
|
||||
'url' => ['type' => 'text', 'analyzer' => 'ik_max_word', 'fields' => ['keyword' => ['type' => 'keyword', 'ignore_above' => 256]]],
|
||||
'pos_state' => ['type' => 'keyword', ],
|
||||
'picktype' => ['type' => 'keyword', ],
|
||||
'hr' => ['type' => 'long', ],
|
||||
];
|
||||
}
|
||||
|
||||
public function getEsInfo(): callable|array
|
||||
{
|
||||
return $this->es->info();
|
||||
}
|
||||
|
||||
public function createIndex()
|
||||
{
|
||||
$params = $this->indexSetting;
|
||||
$properties = $params['body']['mappings']['properties'];
|
||||
$properties = array_merge($properties, $this->getTorrentRawMappingFields());
|
||||
$params['body']['mappings']['properties'] = $properties;
|
||||
return $this->es->indices()->create($params);
|
||||
}
|
||||
|
||||
public function deleteIndex()
|
||||
{
|
||||
$params = ['index' => self::INDEX_NAME];
|
||||
return $this->es->indices()->delete($params);
|
||||
}
|
||||
|
||||
public function import($torrentId = null)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$page = 1;
|
||||
$size = 1000;
|
||||
$fields = $this->getTorrentBaseFields();
|
||||
array_unshift($fields, 'id');
|
||||
$query = Torrent::query()
|
||||
->with(['user', 'torrent_tags', 'bookmarks'])
|
||||
->select($fields);
|
||||
if (!is_null($torrentId)) {
|
||||
$idArr = preg_split('/[,\s]+/', $torrentId);
|
||||
$query->whereIn('id', $idArr);
|
||||
}
|
||||
while (true) {
|
||||
$log = "page: $page, size: $size";
|
||||
$torrentResults = (clone $query)->forPage($page, $size)->get();
|
||||
if ($torrentResults->isEmpty()) {
|
||||
do_log("$log, no more data...", 'info', true);
|
||||
break;
|
||||
}
|
||||
do_log("$log, get counts: " . $torrentResults->count(), 'info', true);
|
||||
|
||||
$torrentBodyBulk = $userBodyBulk = $tagBodyBulk = $bookmarkBodyBulk = ['body' => []];
|
||||
foreach ($torrentResults as $torrent) {
|
||||
$body = $this->buildUserBody($torrent->user, true);
|
||||
$userBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$userBodyBulk['body'][] = $body['body'];
|
||||
|
||||
$body = $this->buildTorrentBody($torrent, true);
|
||||
$torrentBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$torrentBodyBulk['body'][] = $body['body'];
|
||||
|
||||
foreach ($torrent->torrent_tags as $torrentTag) {
|
||||
$body = $this->buildTorrentTagBody($torrent, $torrentTag, true);
|
||||
$tagBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$tagBodyBulk['body'][] = $body['body'];
|
||||
}
|
||||
|
||||
foreach ($torrent->bookmarks as $bookmark) {
|
||||
$body = $this->buildBookmarkBody($torrent, $bookmark, true);
|
||||
$bookmarkBodyBulk['body'][] = ['index' => $body['index']];
|
||||
$bookmarkBodyBulk['body'][] = $body['body'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//index user
|
||||
$result = $this->es->bulk($userBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index user done!", $result);
|
||||
|
||||
//index torrent
|
||||
$result = $this->es->bulk($torrentBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index torrent done!", $result);
|
||||
|
||||
//index tag
|
||||
$result = $this->es->bulk($tagBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index tag done!", $result);
|
||||
|
||||
//index bookmark
|
||||
$result = $this->es->bulk($bookmarkBodyBulk);
|
||||
$this->logEsResponse("$log, bulk index bookmark done!", $result);
|
||||
|
||||
$page++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function buildUserBody(User $user, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_USER;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getUserId($user->id),
|
||||
'routing' => $user->id,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
private function buildTorrentBody($torrent, bool $underlinePrefix = false): array
|
||||
{
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
if (!$torrent instanceof Torrent) {
|
||||
$torrent = Torrent::query()->findOrFail((int)$torrent, array_merge(['id'], $baseFields));
|
||||
}
|
||||
$docType = self::DOC_TYPE_TORRENT;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getTorrentId($torrent->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$data = Arr::only($torrent->toArray(), $baseFields);
|
||||
$body = array_merge($data, [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $torrent->id,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'user_' . $torrent->owner,
|
||||
],
|
||||
]);
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function buildTorrentTagBody(Torrent $torrent, TorrentTag $torrentTag, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_TAG;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getTorrentTagId($torrentTag->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $torrentTag->torrent_id,
|
||||
'tag_id' => $torrentTag->tag_id,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'torrent_' . $torrent->id,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
private function buildBookmarkBody(Torrent $torrent, Bookmark $bookmark, bool $underlinePrefix = false)
|
||||
{
|
||||
$docType = self::DOC_TYPE_BOOKMARK;
|
||||
$indexName = 'index';
|
||||
$idName = 'id';
|
||||
if ($underlinePrefix) {
|
||||
$indexName = "_$indexName";
|
||||
$idName = "_$idName";
|
||||
}
|
||||
$index = [
|
||||
$indexName => self::INDEX_NAME,
|
||||
$idName => $this->getBookmarkId($bookmark->id),
|
||||
'routing' => $torrent->owner,
|
||||
];
|
||||
$body = [
|
||||
'_doc_type' => $docType,
|
||||
'torrent_id' => $bookmark->torrentid,
|
||||
'user_id' => $bookmark->userid,
|
||||
'torrent_relations' => [
|
||||
'name' => $docType,
|
||||
'parent' => 'torrent_' . $torrent->id,
|
||||
],
|
||||
];
|
||||
return compact('index', 'body');
|
||||
}
|
||||
|
||||
|
||||
private function logEsResponse($msg, $response)
|
||||
{
|
||||
if (isset($response['errors']) && $response['errors'] == true) {
|
||||
$msg .= var_export($response, true);
|
||||
}
|
||||
do_log($msg, 'info', app()->runningInConsole());
|
||||
}
|
||||
|
||||
private function getTorrentId($id): string
|
||||
{
|
||||
return "torrent_" . intval($id);
|
||||
}
|
||||
|
||||
private function getTorrentTagId($id): string
|
||||
{
|
||||
return "torrent_tag_" . intval($id);
|
||||
}
|
||||
|
||||
private function getUserId($id): string
|
||||
{
|
||||
return "user_" . intval($id);
|
||||
}
|
||||
|
||||
private function getBookmarkId($id): string
|
||||
{
|
||||
return "bookmark_" . intval($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* detect elastic response has error or not
|
||||
*
|
||||
* @param $esResponse
|
||||
* @return bool
|
||||
*/
|
||||
private function isEsResponseError($esResponse)
|
||||
{
|
||||
if (isset($esResponse['error'])) {
|
||||
return true;
|
||||
}
|
||||
//bulk insert
|
||||
if (isset($esResponse['errors']) && $esResponse['errors']) {
|
||||
return true;
|
||||
}
|
||||
//update by query
|
||||
if (!empty($esResponse['failures'])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* build es query
|
||||
*
|
||||
* @param array $params
|
||||
* @param $user
|
||||
* @param string $queryString cat401=1&cat404=1&source2=1&medium2=1&medium3=1&codec3=1&audiocodec3=1&standard2=1&standard3=1&processing2=1&team3=1&team4=1&incldead=1&spstate=0&inclbookmarked=0&search=&search_area=0&search_mode=0
|
||||
* @return array
|
||||
*/
|
||||
public function buildQuery(array $params, $user, string $queryString)
|
||||
{
|
||||
if (!($user instanceof User) || !$user->torrentsperpage || !$user->notifs) {
|
||||
$user = User::query()->findOrFail(intval($user));
|
||||
}
|
||||
//[cat401][cat404][sou1][med1][cod1][sta2][sta3][pro2][tea2][aud2][incldead=0][spstate=3][inclbookmarked=2]
|
||||
$userSetting = $user->notifs;
|
||||
$must = $must_not = [];
|
||||
$mustBoolShould = [];
|
||||
$must[] = ['match' => ['_doc_type' => self::DOC_TYPE_TORRENT]];
|
||||
|
||||
foreach (self::$queryFieldToTorrentFieldMaps as $queryField => $torrentField) {
|
||||
if (isset($params[$queryField]) && $params[$queryField] !== '') {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $params[$queryField]]];
|
||||
do_log("get mustBoolShould for $torrentField from params through $queryField: {$params[$queryField]}");
|
||||
} elseif (preg_match_all("/{$queryField}([\d]+)=/", $queryString, $matches)) {
|
||||
if (count($matches) == 2 && !empty($matches[1])) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $match]];
|
||||
do_log("get mustBoolShould for $torrentField from params through $queryField: $match");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//get user setting
|
||||
$pattern = sprintf("/\[%s([\d]+)\]/", substr($queryField, 0, 3));
|
||||
if (preg_match($pattern, $userSetting, $matches)) {
|
||||
if (count($matches) == 2 && !empty($matches[1])) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$mustBoolShould[$torrentField][] = ['match' => [$torrentField => $match]];
|
||||
do_log("get mustBoolShould for $torrentField from user setting through $queryField: $match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$includeDead = 1;
|
||||
if (isset($params['incldead'])) {
|
||||
$includeDead = (int)$params['incldead'];
|
||||
do_log("maybe get must for visible from params");
|
||||
} elseif (preg_match("/\[incldead=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$includeDead = $matches[1];
|
||||
do_log("maybe get must for visible from user setting");
|
||||
}
|
||||
if ($includeDead == 1) {
|
||||
//active torrent
|
||||
$must[] = ['match' => ['visible' => 'yes']];
|
||||
do_log("get must for visible = yes through incldead: $includeDead");
|
||||
} elseif ($includeDead == 2) {
|
||||
//dead torrent
|
||||
$must[] = ['match' => ['visible' => 'no']];
|
||||
do_log("get must for visible = no through incldead: $includeDead");
|
||||
}
|
||||
|
||||
|
||||
$includeBookmarked = 0;
|
||||
if (isset($params['inclbookmarked'])) {
|
||||
$includeBookmarked = (int)$params['inclbookmarked'];
|
||||
do_log("maybe get must or must_not for has_child.bookmark from params");
|
||||
} elseif (preg_match("/\[inclbookmarked=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$includeBookmarked = $matches[1];
|
||||
do_log("maybe get must or must_not for has_child.bookmark from user setting");
|
||||
}
|
||||
if ($includeBookmarked == 1) {
|
||||
//only bookmark
|
||||
$must[] = ['has_child' => ['type' => 'bookmark', 'query' => ['match' => ['user_id' => $user->id]]]];
|
||||
do_log("get must for has_child.bookmark through inclbookmarked: $includeBookmarked");
|
||||
} elseif ($includeBookmarked == 2) {
|
||||
//only not bookmark
|
||||
$must_not[] = ['has_child' => ['type' => 'bookmark', 'query' => ['match' => ['user_id' => $user->id]]]];
|
||||
do_log("get must_not for has_child.bookmark through inclbookmarked: $includeBookmarked");
|
||||
}
|
||||
|
||||
|
||||
$spState = 0;
|
||||
if (isset($params['spstate'])) {
|
||||
$spState = (int)$params['spstate'];
|
||||
do_log("maybe get must for spstate from params");
|
||||
} elseif (preg_match("/\[spstate=([\d]+)\]/", $userSetting, $matches)) {
|
||||
$spState = $matches[1];
|
||||
do_log("maybe get must for spstate from user setting");
|
||||
}
|
||||
if ($spState > 0) {
|
||||
$must[] = ['match' => ['sp_state' => $spState]];
|
||||
do_log("get must for sp_state = $spState through spstate: $spState");
|
||||
}
|
||||
|
||||
if (!empty($params['tag_id'])) {
|
||||
$must[] = ['has_child' => ['type' => 'tag', 'query' => ['match' => ['tag_id' => $params['tag_id']]]]];
|
||||
do_log("get must for has_child.tag through params.tag_id: {$params['tag_id']}");
|
||||
}
|
||||
|
||||
|
||||
if (!empty($params['search'])) {
|
||||
$searchMode = isset($params['search_mode']) && isset(self::SEARCH_MODES[$params['search_mode']]) ? $params['search_mode'] : self::SEARCH_MODE_AND;
|
||||
if (in_array($searchMode, [self::SEARCH_MODE_AND, self::SEARCH_MODE_OR])) {
|
||||
//and, or
|
||||
$keywordsArr = preg_split("/[\.\s]+/", trim($params['search']));
|
||||
} else {
|
||||
$keywordsArr = [trim($params['search'])];
|
||||
}
|
||||
$keywordsArr = array_slice($keywordsArr, 0, 10);
|
||||
$searchArea = isset($params['search_area']) && isset(self::SEARCH_AREAS[$params['search_area']]) ? $params['search_area'] : self::SEARCH_AREA_TITLE;
|
||||
if ($searchMode == self::SEARCH_MODE_AND || $searchMode == self::SEARCH_MODE_EXACT) {
|
||||
$keywordFlag = $searchMode == self::SEARCH_MODE_EXACT ? ".keyword" : "";
|
||||
if ($searchArea == self::SEARCH_AREA_TITLE) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould = [];
|
||||
$tmpMustBoolShould[] = ['match' => ["name{$keywordFlag}" => $keyword]];
|
||||
$tmpMustBoolShould[] = ['match' => ["small_descr{$keywordFlag}" => $keyword]];
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
do_log("get must bool should [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for name+small_descr match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_DESC) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['match' => ["descr{$keywordFlag}" => $keyword]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for descr match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_IMDB) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['match' => ["url{$keywordFlag}" => $keyword]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] for url match '$keyword' through search");
|
||||
}
|
||||
} elseif ($searchArea == self::SEARCH_AREA_OWNER) {
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$must[] = ['has_parent' => ['parent_type' => 'user', 'query' => ['match' => ["username{$keywordFlag}" => $keyword]]]];
|
||||
do_log("get must [SEARCH_MODE_AND + SEARCH_MODE_EXACT] has_parent.user match '$keyword' through search");
|
||||
}
|
||||
}
|
||||
} elseif ($searchMode == self::SEARCH_MODE_OR) {
|
||||
if ($searchArea == self::SEARCH_AREA_TITLE) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['name' => $keyword]];
|
||||
$tmpMustBoolShould[] = ['match' => ['small_descr' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for name+small_descr match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_DESC) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['descr' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for descr match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_IMDB) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['match' => ['url' => $keyword]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] for url match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
} elseif ($searchArea == self::SEARCH_AREA_OWNER) {
|
||||
$tmpMustBoolShould = [];
|
||||
foreach ($keywordsArr as $keyword) {
|
||||
$tmpMustBoolShould[] = ['has_parent' => ['parent_type' => 'user', 'query' => ['match' => ['username' => $keyword]]]];
|
||||
do_log("get must bool should [SEARCH_MODE_OR] has_parent.user match '$keyword' through search");
|
||||
}
|
||||
$must[]['bool']['should'] = $tmpMustBoolShould;
|
||||
}
|
||||
}
|
||||
}
|
||||
$query = [
|
||||
'bool' => [
|
||||
'must' => $must
|
||||
]
|
||||
];
|
||||
foreach ($mustBoolShould as $torrentField => $boolShoulds) {
|
||||
$query['bool']['must'][]['bool']['should'] = $boolShoulds;
|
||||
}
|
||||
if (!empty($must_not)) {
|
||||
$query['bool']['must_not'] = $must_not;
|
||||
}
|
||||
|
||||
|
||||
$sort = [];
|
||||
$sort[] = ['pos_state' => ['order' => 'desc']];
|
||||
$hasAddSetSortField = false;
|
||||
if (!empty($params['sort'])) {
|
||||
$direction = isset($params['type']) && in_array($params['type'], ['asc', 'desc']) ? $params['type'] : 'desc';
|
||||
foreach (self::$sortFieldMaps as $key => $value) {
|
||||
if ($key == $params['sort']) {
|
||||
$hasAddSetSortField = true;
|
||||
$sort[] = [$value => ['order' => $direction]];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$hasAddSetSortField) {
|
||||
$sort[] = ['torrent_id' => ['order' => 'desc']];
|
||||
}
|
||||
|
||||
$page = isset($params['page']) && is_numeric($params['page']) ? $params['page'] : 0;
|
||||
if ($user->torrentsperpage) {
|
||||
$size = $user->torrentsperpage;
|
||||
} elseif (($sizeFromConfig = Setting::get('main.torrentsperpage')) > 0) {
|
||||
$size = $sizeFromConfig;
|
||||
} else {
|
||||
$size = 50;
|
||||
}
|
||||
$size = min($size, 200);
|
||||
$offset = $page * $size;
|
||||
|
||||
$result = [
|
||||
'query' => $query,
|
||||
'sort' => $sort,
|
||||
'from' => $offset,
|
||||
'size' => $size,
|
||||
'_source' => ['torrent_id', 'name', 'small_descr', 'owner']
|
||||
];
|
||||
do_log(sprintf(
|
||||
"params: %s, user: %s, queryString: %s, result: %s",
|
||||
nexus_json_encode($params), $user->id, $queryString, nexus_json_encode($result)
|
||||
));
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
public function listTorrentFromEs(array $params, $user, string $queryString)
|
||||
{
|
||||
$query = $this->buildQuery($params, $user, $queryString);
|
||||
$esParams = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => $query,
|
||||
];
|
||||
$response = $this->es->search($esParams);
|
||||
$result = [
|
||||
'total' => 0,
|
||||
'data' => [],
|
||||
];
|
||||
if ($this->isEsResponseError($response)) {
|
||||
do_log("error response: " . nexus_json_encode($response), 'error');
|
||||
return $result;
|
||||
}
|
||||
if (empty($response['hits'])) {
|
||||
do_log("empty response hits: " . nexus_json_encode($response), 'error');
|
||||
return $result;
|
||||
}
|
||||
if ($response['hits']['total']['value'] == 0) {
|
||||
do_log("total = 0, " . nexus_json_encode($response));
|
||||
return $result;
|
||||
}
|
||||
$result['total'] = $response['hits']['total']['value'];
|
||||
$torrentIdArr = [];
|
||||
foreach ($response['hits']['hits'] as $value) {
|
||||
$torrentIdArr[] = $value['_source']['torrent_id'];
|
||||
}
|
||||
$fieldStr = 'id, sp_state, promotion_time_type, promotion_until, banned, picktype, pos_state, category, source, medium, codec, standard, processing, team, audiocodec, leechers, seeders, name, small_descr, times_completed, size, added, comments,anonymous,owner,url,cache_stamp, pt_gen, hr';
|
||||
$idStr = implode(',', $torrentIdArr);
|
||||
$result['data'] = Torrent::query()
|
||||
->selectRaw($fieldStr)
|
||||
->whereIn('id', $torrentIdArr)
|
||||
->orderByRaw("field(id,$idStr)")
|
||||
->get()
|
||||
->toArray()
|
||||
;
|
||||
|
||||
return $result;
|
||||
|
||||
|
||||
}
|
||||
|
||||
private function getTorrentBaseFields()
|
||||
{
|
||||
return array_keys($this->getTorrentRawMappingFields());
|
||||
}
|
||||
|
||||
public function updateTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[UPDATE_TORRENT]: $id";
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
$torrent = Torrent::query()->findOrFail($id, array_merge(['id'], $baseFields));
|
||||
$data = $this->buildTorrentBody($torrent);
|
||||
$params = $data['index'];
|
||||
$params['body']['doc'] = $data['body'];
|
||||
$result = $this->es->update($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($torrent);
|
||||
}
|
||||
|
||||
public function addTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[ADD_TORRENT]: $id";
|
||||
$baseFields = $this->getTorrentBaseFields();
|
||||
$torrent = Torrent::query()->findOrFail($id, array_merge(['id'], $baseFields));
|
||||
$data = $this->buildTorrentBody($torrent, true);
|
||||
$params = ['body' => []];
|
||||
$params['body'][] = ['index' => $data['index']];
|
||||
$params['body'][] = $data['body'];
|
||||
$result = $this->es->bulk($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($torrent);
|
||||
}
|
||||
|
||||
public function deleteTorrent(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[DELETE_TORRENT]: $id";
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'id' => $this->getTorrentId($id),
|
||||
];
|
||||
$result = $this->es->delete($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
|
||||
return $this->syncTorrentTags($id, true);
|
||||
}
|
||||
|
||||
public function syncTorrentTags($torrent, $onlyDelete = false): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$torrent instanceof Torrent) {
|
||||
$torrent = Torrent::query()->findOrFail((int)$torrent, ['id']);
|
||||
}
|
||||
$log = "sync torrent tags, torrent: " . $torrent->id;
|
||||
//remove first
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['match' => ['_doc_type' => self::DOC_TYPE_TAG]],
|
||||
['has_parent' => ['parent_type' => 'torrent', 'query' => ['match' => ['torrent_id' => $torrent->id]]]]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
$result = $this->es->deleteByQuery($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, delete torrent tag fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, delete torrent tag success: " . nexus_json_encode($result));
|
||||
if ($onlyDelete) {
|
||||
do_log("$log, only delete, return true");
|
||||
return true;
|
||||
}
|
||||
|
||||
//then insert new
|
||||
$bulk = ['body' => []];
|
||||
foreach ($torrent->torrent_tags as $torrentTag) {
|
||||
$body = $this->buildTorrentTagBody($torrent, $torrentTag, true);
|
||||
$bulk['body'][] = ['index' => $body['index']];
|
||||
$bulk['body'][] = $body['body'];
|
||||
}
|
||||
if (empty($bulk['body'])) {
|
||||
do_log("$log, no tags, return true");
|
||||
return true;
|
||||
}
|
||||
$result = $this->es->bulk($bulk);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, insert torrent tag fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, insert torrent tag success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateUser($user): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$user instanceof User) {
|
||||
$user = User::query()->findOrFail((int)$user, ['id', 'username']);
|
||||
}
|
||||
$log = "[UPDATE_USER]: " . $user->id;
|
||||
$data = $this->buildUserBody($user);
|
||||
$params = $data['index'];
|
||||
$params['body']['doc'] = $data['body'];
|
||||
$result = $this->es->update($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addBookmark($bookmark): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
if (!$bookmark instanceof Bookmark) {
|
||||
$bookmark = Bookmark::query()->with([
|
||||
'torrent' => function ($query) {$query->select(['id', 'owner']);}
|
||||
])->findOrFail((int)$bookmark);
|
||||
}
|
||||
$log = "[ADD_BOOKMARK]: " . $bookmark->toJson();
|
||||
$bulk = ['body' => []];
|
||||
$body = $this->buildBookmarkBody($bookmark->torrent, $bookmark, true);
|
||||
$bulk['body'][] = ['index' => $body['index']];
|
||||
$bulk['body'][] = $body['body'];
|
||||
$result = $this->es->bulk($bulk);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteBookmark(int $id): bool
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return true;
|
||||
}
|
||||
$log = "[DELETE_BOOKMARK]: $id";
|
||||
$params = [
|
||||
'index' => self::INDEX_NAME,
|
||||
'id' => $this->getBookmarkId($id),
|
||||
];
|
||||
$result = $this->es->delete($params);
|
||||
if ($this->isEsResponseError($result)) {
|
||||
do_log("$log, fail: " . nexus_json_encode($result), 'error');
|
||||
return false;
|
||||
}
|
||||
do_log("$log, success: " . nexus_json_encode($result));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -67,7 +67,10 @@ class TagRepository extends BaseRepository
|
||||
foreach ($renderIdArr as $tagId) {
|
||||
$value = $tagKeyById->get($tagId);
|
||||
if ($value) {
|
||||
$item = "<span style=\"background-color:{$value->color};color:white;padding: 1px 2px\">{$value->name}</span> ";
|
||||
$item = sprintf(
|
||||
"<span style=\"background-color:%s;color:%s;border-radius:%s;font-size:%s;margin:%s;padding:%s\">%s</span>",
|
||||
$value->color, $value->font_color, $value->border_radius, $value->font_size, $value->margin, $value->padding, $value->name
|
||||
);
|
||||
if ($withFilterLink) {
|
||||
$html .= sprintf('<a href="torrents.php?tag_id=%s">%s</a>', $tagId, $item);
|
||||
} else {
|
||||
@@ -129,7 +132,7 @@ class TagRepository extends BaseRepository
|
||||
return count($values);
|
||||
}
|
||||
|
||||
public static function getOrderByFieldIdString()
|
||||
public static function getOrderByFieldIdString(): string
|
||||
{
|
||||
if (is_null(self::$orderByFieldIdString)) {
|
||||
$results = self::createBasicQuery()->get(['id']);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<?php
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Message;
|
||||
use App\Models\News;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollAnswer;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -148,4 +153,72 @@ class ToolRepository extends BaseRepository
|
||||
{
|
||||
return new Encrypter($key, 'AES-256-CBC');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $to
|
||||
* @param $subject
|
||||
* @param $body
|
||||
* @return bool
|
||||
*/
|
||||
public function sendMail($to, $subject, $body): bool
|
||||
{
|
||||
do_log("to: $to, subject: $subject, body: $body");
|
||||
$smtp = Setting::get('smtp');
|
||||
// Create the Transport
|
||||
$encryption = null;
|
||||
if (isset($smtp['encryption']) && in_array($smtp['encryption'], ['ssl', 'tls'])) {
|
||||
$encryption = $smtp['encryption'];
|
||||
}
|
||||
$transport = (new \Swift_SmtpTransport($smtp['smtpaddress'], $smtp['smtpport'], $encryption))
|
||||
->setUsername($smtp['accountname'])
|
||||
->setPassword($smtp['accountpassword'])
|
||||
;
|
||||
|
||||
// Create the Mailer using your created Transport
|
||||
$mailer = new \Swift_Mailer($transport);
|
||||
|
||||
// Create a message
|
||||
$message = (new \Swift_Message($subject))
|
||||
->setFrom($smtp['accountname'], Setting::get('basic.SITENAME'))
|
||||
->setTo([$to])
|
||||
->setBody($body, 'text/html')
|
||||
;
|
||||
|
||||
// Send the message
|
||||
try {
|
||||
$result = $mailer->send($message);
|
||||
if ($result == 0) {
|
||||
do_log("send mail fail, unknown error", 'error');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
do_log("send email fail: " . $e->getMessage() . "\n" . $e->getTraceAsString(), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getNotificationCount(User $user): array
|
||||
{
|
||||
$result = [];
|
||||
//attend or not
|
||||
$attendRep = new AttendanceRepository();
|
||||
$attendance = $attendRep->getAttendance($user->id, date('Ymd'));
|
||||
$result['attendance'] = $attendance ? 0 : 1;
|
||||
|
||||
//unread news
|
||||
$count = News::query()->where('added', '>', $user->last_home)->count();
|
||||
$result['news'] = $count;
|
||||
|
||||
//unread messages
|
||||
$count = Message::query()->where('receiver', $user->id)->where('unread', 'yes')->count();
|
||||
$result['message'] = $count;
|
||||
|
||||
//un-vote poll
|
||||
$total = Poll::query()->count();
|
||||
$userVoteCount = PollAnswer::query()->where('userid', $user->id)->selectRaw('count(distinct(pollid)) as counts')->first()->counts;
|
||||
$result['poll'] = $total - $userVoteCount;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use Carbon\Carbon;
|
||||
use Hashids\Hashids;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
use Nexus\Database\NexusDB;
|
||||
|
||||
class TorrentRepository extends BaseRepository
|
||||
{
|
||||
@@ -93,8 +94,12 @@ class TorrentRepository extends BaseRepository
|
||||
|
||||
public function getDetail($id, User $user)
|
||||
{
|
||||
$with = ['user', 'basic_audio_codec', 'basic_category', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team'];
|
||||
$result = Torrent::query()->with($with)->withCount(['peers', 'thank_users'])->visible()->findOrFail($id);
|
||||
$with = [
|
||||
'user', 'basic_audio_codec', 'basic_category', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team',
|
||||
'thanks' => function ($query) use ($user) {$query->where('userid', $user->id);},
|
||||
'reward_logs' => function ($query) use ($user) {$query->where('userid', $user->id);},
|
||||
];
|
||||
$result = Torrent::query()->with($with)->withCount(['peers', 'thank_users', 'reward_logs'])->visible()->findOrFail($id);
|
||||
$result->download_url = $this->getDownloadUrl($id, $user->toArray());
|
||||
return $result;
|
||||
}
|
||||
@@ -368,12 +373,14 @@ class TorrentRepository extends BaseRepository
|
||||
|
||||
private function getTrackerReportAuthKeySecret($id, $uid, $initializeIfNotExists = false)
|
||||
{
|
||||
$secret = TorrentSecret::query()
|
||||
->where('uid', $uid)
|
||||
->whereIn('torrent_id', [0, $id])
|
||||
->orderBy('torrent_id', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
$secret = NexusDB::remember("tracker_report_authkey_secret:$id:$uid", 3600*24, function () use ($id, $uid) {
|
||||
return TorrentSecret::query()
|
||||
->where('uid', $uid)
|
||||
->whereIn('torrent_id', [0, $id])
|
||||
->orderBy('torrent_id', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
});
|
||||
if ($secret) {
|
||||
return $secret->secret;
|
||||
}
|
||||
|
||||
873
app/Repositories/TrackerRepository.php
Normal file
873
app/Repositories/TrackerRepository.php
Normal file
@@ -0,0 +1,873 @@
|
||||
<?php
|
||||
/**
|
||||
* Handle announce and scrape
|
||||
*
|
||||
* @link https://github.com/HDInnovations/UNIT3D-Community-Edition/blob/master/app/Http/Controllers/AnnounceController.php
|
||||
* @link https://github.com/Rhilip/RidPT/blob/master/application/Controllers/Tracker/AnnounceController.php
|
||||
*/
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Exceptions\ClientNotAllowedException;
|
||||
use App\Models\Cheater;
|
||||
use App\Models\Peer;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Snatch;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Exceptions\TrackerException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Rhilip\Bencode\Bencode;
|
||||
|
||||
class TrackerRepository extends BaseRepository
|
||||
{
|
||||
const MIN_ANNOUNCE_WAIT_SECOND = 300;
|
||||
|
||||
const MAX_PEER_NUM_WANT = 50;
|
||||
|
||||
const MUST_BE_CHEATER_SPEED = 1024 * 1024 * 1024; //1024 MB/s
|
||||
const MAY_BE_CHEATER_SPEED = 1024 * 1024 * 100; //100 MB/s
|
||||
|
||||
// Port Blacklist
|
||||
protected const BLACK_PORTS = [
|
||||
22, // SSH Port
|
||||
53, // DNS queries
|
||||
80, 81, 8080, 8081, // Hyper Text Transfer Protocol (HTTP) - port used for web traffic
|
||||
411, 412, 413, // Direct Connect Hub (unofficial)
|
||||
443, // HTTPS / SSL - encrypted web traffic, also used for VPN tunnels over HTTPS.
|
||||
1214, // Kazaa - peer-to-peer file sharing, some known vulnerabilities, and at least one worm (Benjamin) targeting it.
|
||||
3389, // IANA registered for Microsoft WBT Server, used for Windows Remote Desktop and Remote Assistance connections
|
||||
4662, // eDonkey 2000 P2P file sharing service. http://www.edonkey2000.com/
|
||||
6346, 6347, // Gnutella (FrostWire, Limewire, Shareaza, etc.), BearShare file sharing app
|
||||
6699, // Port used by p2p software, such as WinMX, Napster.
|
||||
];
|
||||
|
||||
private array $userUpdates = [];
|
||||
|
||||
public function announce(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("queryString: " . $request->getQueryString());
|
||||
try {
|
||||
$withPeers = false;
|
||||
$queries = $this->checkAnnounceFields($request);
|
||||
$user = $this->checkUser($request);
|
||||
$clientAllow = $this->checkClient($request);
|
||||
$torrent = $this->checkTorrent($queries, $user);
|
||||
if ($this->isReAnnounce($queries) === false) {
|
||||
$withPeers = true;
|
||||
$peerSelf = $this->checkMinInterval($torrent, $queries, $user);
|
||||
if (!$peerSelf) {
|
||||
$this->checkPeer($torrent, $queries, $user);
|
||||
$this->checkPermission($torrent, $queries, $user);
|
||||
$peerSelf = new Peer([
|
||||
'torrent' => $torrent->id,
|
||||
'peer_id' => $queries['peer_id'],
|
||||
'userid' => $user->id,
|
||||
'passkey' => $user->passkey,
|
||||
]);
|
||||
} else {
|
||||
$this->checkCheater($torrent, $queries, $user, $peerSelf);
|
||||
}
|
||||
/**
|
||||
* Note: Must get before update peer!
|
||||
*/
|
||||
$dataTraffic = $this->getDataTraffic($torrent, $queries, $user, $peerSelf);
|
||||
|
||||
$this->updatePeer($peerSelf, $queries);
|
||||
$this->updateSnatch($peerSelf, $queries, $dataTraffic);
|
||||
$this->updateTorrent($torrent, $queries);
|
||||
|
||||
if ($dataTraffic['uploaded_increment_for_user'] > 0) {
|
||||
$this->userUpdates['uploaded'] = DB::raw('uploaded + ' . $dataTraffic['uploaded_increment_for_user']);
|
||||
}
|
||||
if ($dataTraffic['downloaded_increment_for_user'] > 0) {
|
||||
$this->userUpdates['downloaded'] = DB::raw('downloaded + ' . $dataTraffic['downloaded_increment_for_user']);
|
||||
}
|
||||
if ($user->clientselect != $clientAllow->id) {
|
||||
$this->userUpdates['clientselect'] = $clientAllow->id;
|
||||
}
|
||||
if ($user->showclienterror == 'yes') {
|
||||
$this->userUpdates['showclienterror'] = 'no';
|
||||
}
|
||||
}
|
||||
$repDict = $this->generateSuccessAnnounceResponse($torrent, $queries, $user, $withPeers);
|
||||
} catch (ClientNotAllowedException $exception) {
|
||||
do_log("[ClientNotAllowedException] " . $exception->getMessage());
|
||||
if (isset($user) && $user->showclienterror == 'no') {
|
||||
$this->userUpdates['showclienterror'] = 'yes';
|
||||
}
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (TrackerException $exception) {
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
//other system exception
|
||||
do_log("[" . get_class($exception) . "] " . $exception->getMessage() . "\n" . $exception->getTraceAsString(), 'error');
|
||||
$repDict = $this->generateFailedAnnounceResponse("system error, report to sysop please, hint: " . nexus()->getRequestId());
|
||||
} finally {
|
||||
if (isset($user) && count($this->userUpdates)) {
|
||||
$user->update($this->userUpdates);
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
return $this->sendFinalAnnounceResponse($repDict);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @throws ClientNotAllowedException
|
||||
* @throws TrackerException
|
||||
* @refs
|
||||
*/
|
||||
protected function checkClient(Request $request)
|
||||
{
|
||||
// Miss Header User-Agent is not allowed.
|
||||
if (! $request->header('User-Agent')) {
|
||||
throw new TrackerException('Invalid user-agent !');
|
||||
}
|
||||
|
||||
// Block Other Browser, Crawler (May Cheater or Faker Client) by check Requests headers
|
||||
if ($request->header('accept-language') || $request->header('referer')
|
||||
|| $request->header('accept-charset')
|
||||
|
||||
/**
|
||||
* This header check may block Non-bittorrent client `Aria2` to access tracker,
|
||||
* Because they always add this header which other clients don't have.
|
||||
*
|
||||
* @see https://blog.rhilip.info/archives/1010/ ( in Chinese )
|
||||
*/
|
||||
|| $request->header('want-digest')
|
||||
) {
|
||||
throw new TrackerException('Abnormal access blocked !');
|
||||
}
|
||||
|
||||
$userAgent = $request->header('User-Agent');
|
||||
|
||||
// Should also block User-Agent strings that are to long. (For Database reasons)
|
||||
if (\strlen((string) $userAgent) > 64) {
|
||||
throw new TrackerException('The User-Agent of this client is too long!');
|
||||
}
|
||||
|
||||
// Block Browser by checking it's User-Agent
|
||||
if (\preg_match('/(Mozilla|Browser|Chrome|Safari|AppleWebKit|Opera|Links|Lynx|Bot|Unknown)/i', (string) $userAgent)) {
|
||||
throw new TrackerException('Browser, Crawler or Cheater is not Allowed.');
|
||||
}
|
||||
|
||||
$agentAllowRep = new AgentAllowRepository();
|
||||
|
||||
return $agentAllowRep->checkClient($request->peer_id, $userAgent, config('app.debug'));
|
||||
|
||||
}
|
||||
|
||||
protected function checkPasskey($passkey)
|
||||
{
|
||||
// If Passkey Lenght Is Wrong
|
||||
if (\strlen((string) $passkey) !== 32) {
|
||||
throw new TrackerException('Invalid passkey ! the length of passkey must be 32');
|
||||
}
|
||||
|
||||
// If Passkey Format Is Wrong
|
||||
if (\strspn(\strtolower($passkey), 'abcdef0123456789') !== 32) { // MD5 char limit
|
||||
throw new TrackerException("Invalid passkey ! The format of passkey is not correct");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function checkAuthkey($authkey)
|
||||
{
|
||||
$arr = explode('|', $authkey);
|
||||
if (count($arr) != 3) {
|
||||
throw new TrackerException('Invalid authkey');
|
||||
}
|
||||
$torrentId = $arr[0];
|
||||
$uid = $arr[1];
|
||||
$torrentRep = new TorrentRepository();
|
||||
try {
|
||||
$decrypted = $torrentRep->checkTrackerReportAuthKey($authkey);
|
||||
} catch (\Exception $exception) {
|
||||
throw new TrackerException($exception->getMessage());
|
||||
}
|
||||
if (empty($decrypted)) {
|
||||
throw new TrackerException('Invalid authkey');
|
||||
}
|
||||
return compact('torrentId', 'uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws TrackerException
|
||||
*/
|
||||
protected function checkAnnounceFields(Request $request): array
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
// Part.1 check Announce **Need** Fields
|
||||
foreach (['info_hash', 'peer_id', 'port', 'uploaded', 'downloaded', 'left'] as $item) {
|
||||
$itemData = $request->query->get($item);
|
||||
if (! \is_null($itemData)) {
|
||||
$queries[$item] = $itemData;
|
||||
} else {
|
||||
throw new TrackerException("key: $item is Missing !");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['info_hash', 'peer_id'] as $item) {
|
||||
if (($length = \strlen((string) $queries[$item])) !== 20) {
|
||||
throw new TrackerException("Invalid $item ! $item is not 20 bytes long($length)");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['uploaded', 'downloaded', 'left'] as $item) {
|
||||
$itemData = $queries[$item];
|
||||
if (! \is_numeric($itemData) || $itemData < 0) {
|
||||
throw new TrackerException("Invalid $item ! $item Must be a number greater than or equal to 0");
|
||||
}
|
||||
}
|
||||
|
||||
// Part.2 check Announce **Option** Fields
|
||||
foreach (['event' => '', 'no_peer_id' => 1, 'compact' => 0, 'numwant' => 50, 'corrupt' => 0, 'key' => ''] as $item => $value) {
|
||||
$queries[$item] = $request->query->get($item, $value);
|
||||
if ($queries[$item] && $item == 'event') {
|
||||
$queries[$item] = strtolower($queries[$item]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['numwant', 'corrupt', 'no_peer_id', 'compact'] as $item) {
|
||||
if (! \is_numeric($queries[$item]) || $queries[$item] < 0) {
|
||||
throw new TrackerException("Invalid $item ! $item Must be a number greater than or equal to 0");
|
||||
}
|
||||
}
|
||||
|
||||
if (! \in_array(\strtolower($queries['event']), ['started', 'completed', 'stopped', 'paused', ''])) {
|
||||
throw new TrackerException("Unsupported Event type {$queries['event']} .");
|
||||
}
|
||||
|
||||
// Part.3 check Port is Valid and Allowed
|
||||
/**
|
||||
* Normally , the port must in 1 - 65535 , that is ( $port > 0 && $port < 0xffff )
|
||||
* However, in some case , When `&event=stopped` the port may set to 0.
|
||||
*/
|
||||
if ($queries['port'] === 0 && \strtolower($queries['event']) !== 'stopped') {
|
||||
throw new TrackerException("Illegal port 0 under Event type {$queries['event']} .");
|
||||
}
|
||||
|
||||
if (! \is_numeric($queries['port']) || $queries['port'] < 0 || $queries['port'] > 0xFFFF || \in_array($queries['port'], self::BLACK_PORTS,
|
||||
true)) {
|
||||
throw new TrackerException("Illegal port {$queries['port']} . Port should between 6881-64999");
|
||||
}
|
||||
|
||||
// Part.4 Get User Ip Address
|
||||
$queries['ip'] = $request->getClientIp();
|
||||
|
||||
// Part.5 Get Users Agent
|
||||
$queries['user_agent'] = $request->headers->get('user-agent');
|
||||
|
||||
// Part.6 info_hash, binary
|
||||
$queries['info_hash'] = $queries['info_hash'];
|
||||
|
||||
// Part.7
|
||||
$queries['peer_id'] = $queries['peer_id'];
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
protected function checkUser(Request $request)
|
||||
{
|
||||
if ($authkey = $request->query->get('authkey')) {
|
||||
$checkResult = $this->checkAuthkey($authkey);
|
||||
$field = 'id';
|
||||
$value = $checkResult['uid'];
|
||||
} elseif ($passkey = $request->query->get('passkey')) {
|
||||
$this->checkPasskey($passkey);
|
||||
$field = 'passkey';
|
||||
$value = $passkey;
|
||||
} else {
|
||||
throw new TrackerException("Require authkey or passkey.");
|
||||
}
|
||||
/**
|
||||
* @var $user User
|
||||
*/
|
||||
$user = Cache::remember("user:$field:$value:" . __METHOD__, 60, function () use ($field, $value) {
|
||||
return User::query()->where($field, $value)->first();
|
||||
});
|
||||
if (!$user) {
|
||||
throw new TrackerException("Invalid $field: $value.");
|
||||
}
|
||||
$user->checkIsNormal();
|
||||
|
||||
if ($user->parked == 'yes') {
|
||||
throw new TrackerException("Your account is parked! (Read the FAQ)");
|
||||
}
|
||||
if ($user->downloadpos == 'no') {
|
||||
throw new TrackerException("Your downloading privilege have been disabled! (Read the rules)");
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function checkTorrent($queries, User $user)
|
||||
{
|
||||
// Check Info Hash Against Torrents Table
|
||||
$torrent = $this->getTorrentByInfoHash($queries['info_hash']);
|
||||
|
||||
// If Torrent Doesnt Exists Return Error to Client
|
||||
if ($torrent === null) {
|
||||
throw new TrackerException('Torrent not registered with this tracker.');
|
||||
}
|
||||
|
||||
if ($torrent->banned == 'yes' && $user->class < Setting::get('authority.seebanned')) {
|
||||
throw new TrackerException("torrent banned");
|
||||
}
|
||||
|
||||
return $torrent;
|
||||
}
|
||||
|
||||
protected function checkPeer(Torrent $torrent, array $queries, User $user): void
|
||||
{
|
||||
if ($queries['event'] === 'completed') {
|
||||
throw new TrackerException("Torrent being announced as complete but no record found.");
|
||||
}
|
||||
|
||||
$counts = Peer::query()
|
||||
->where('torrent', '=', $torrent->id)
|
||||
->where('userid', $user->id)
|
||||
->count();
|
||||
if ($queries['left'] == 0 && $counts >= 3) {
|
||||
throw new TrackerException("You cannot seed the same torrent from more than 3 locations.");
|
||||
}
|
||||
if ($queries['left'] > 0 && $counts >= 1) {
|
||||
throw new TrackerException("You already are downloading the same torrent. You may only leech from one location at a time.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkPermission(Torrent $torrent, $queries, User $user)
|
||||
{
|
||||
if ($user->class >= User::CLASS_VIP) {
|
||||
return;
|
||||
}
|
||||
$gigs = $user->downloaded / (1024*1024*1024);
|
||||
if ($gigs < 10) {
|
||||
return;
|
||||
}
|
||||
$ratio = ($user->downloaded > 0) ? ($user->uploaded / $user->downloaded) : 1;
|
||||
$settingsMain = Setting::get('main');
|
||||
if ($settingsMain['waitsystem'] == 'yes') {
|
||||
$elapsed = Carbon::now()->diffInHours($torrent->added);
|
||||
if ($ratio < 0.4) $wait = 24;
|
||||
elseif ($ratio < 0.5) $wait = 12;
|
||||
elseif ($ratio < 0.6) $wait = 6;
|
||||
elseif ($ratio < 0.8) $wait = 3;
|
||||
else $wait = 0;
|
||||
|
||||
if ($elapsed < $wait) {
|
||||
$msg = "Your ratio is too low! You need to wait " . mkprettytime($wait * 3600 - $elapsed) . " to start";
|
||||
throw new TrackerException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
if ($settingsMain['maxdlsystem'] == 'yes') {
|
||||
if ($ratio < 0.5) $max = 1;
|
||||
elseif ($ratio < 0.65) $max = 2;
|
||||
elseif ($ratio < 0.8) $max = 3;
|
||||
elseif ($ratio < 0.95) $max = 4;
|
||||
else $max = 0;
|
||||
|
||||
if ($max > 0) {
|
||||
$counts = Peer::query()->where('userid', $user->id)->where('seeder', 'no')->count();
|
||||
if ($counts > $max) {
|
||||
$msg = "Your slot limit is reached! You may at most download $max torrents at the same time";
|
||||
throw new TrackerException($msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Torrent $torrent
|
||||
* @param $queries
|
||||
* @param User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder|Model|object|null
|
||||
* @throws TrackerException
|
||||
*/
|
||||
protected function checkMinInterval(Torrent $torrent, $queries, User $user)
|
||||
{
|
||||
$peer = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('peer_id', $queries['peer_id'])
|
||||
->first();
|
||||
|
||||
if (
|
||||
$peer
|
||||
&& $queries['event'] == ''
|
||||
&& $peer->isValidDate('prev_action')
|
||||
&& Carbon::now()->diffInSeconds($peer->prev_action) < self::MIN_ANNOUNCE_WAIT_SECOND
|
||||
) {
|
||||
throw new TrackerException('There is a minimum announce time of ' . self::MIN_ANNOUNCE_WAIT_SECOND . ' seconds');
|
||||
}
|
||||
return $peer;
|
||||
}
|
||||
|
||||
protected function checkCheater(Torrent $torrent, $queries, User $user, Peer $peer)
|
||||
{
|
||||
$settingSecurity = Setting::get('security');
|
||||
$level = $settingSecurity['cheaterdet'];
|
||||
if ($level == 0) {
|
||||
//don't do check
|
||||
return;
|
||||
}
|
||||
if ($user->class >= $settingSecurity['nodetect']) {
|
||||
//forever trust
|
||||
return;
|
||||
}
|
||||
if (!$peer->isValidDate('last_action')) {
|
||||
//no last action
|
||||
return;
|
||||
}
|
||||
$duration = Carbon::now()->diffInSeconds($peer->last_action);
|
||||
$upSpeed = $queries['uploaded'] > 0 ? ($queries['uploaded'] / $duration) : 0;
|
||||
$oneGB = 1024 * 1024 * 1024;
|
||||
$tenMB = 1024 * 1024 * 10;
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
$cheaterBaseData = [
|
||||
'added' => $nowStr,
|
||||
'userid' => $user->id,
|
||||
'torrentid' => $torrent->id,
|
||||
'uploaded' => $queries['uploaded'],
|
||||
'downloaded' => $queries['downloaded'],
|
||||
'anctime' => $duration,
|
||||
'seeders' => $torrent->seeders,
|
||||
'leechers' => $torrent->leechers,
|
||||
];
|
||||
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > self::MUST_BE_CHEATER_SPEED / $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 1024 MByte/S (For Consertive level). This is no doubt cheating.
|
||||
$comment = "User account was automatically disabled by system";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
Cheater::query()->insert($data);
|
||||
$modComment = "We believe you're trying to cheat. And your account is disabled.";
|
||||
$user->updateWithModComment(['enabled' => User::ENABLED_NO], $modComment);
|
||||
throw new TrackerException($modComment);
|
||||
}
|
||||
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > self::MAY_BE_CHEATER_SPEED / $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 100 MByte/S (For Consertive level). This is likely cheating.
|
||||
$comment = "Abnormally high uploading rate";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
|
||||
if ($level > 1) {
|
||||
if ($queries['uploaded'] > $oneGB && ($upSpeed > 1024 * 1024) && ($queries['leechers'] < 2 * $level)) {
|
||||
//Uploaded more than 1 GB with uploading rate higher than 1 MByte/S when there is less than 8 leechers (For Consertive level). This is likely cheating.
|
||||
$comment = "User is uploading fast when there is few leechers";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
|
||||
if ($queries['uploaded'] > $tenMB && ($upSpeed > 1024 * 100) && ($queries['leechers'] == 0)) {
|
||||
///Uploaded more than 10 MB with uploading speed faster than 100 KByte/S when there is no leecher. This is likely cheating.
|
||||
$comment = "User is uploading when there is no leecher";
|
||||
$data = array_merge($cheaterBaseData, ['comment' => $comment]);
|
||||
$this->createOrUpdateCheater($torrent, $user, $data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function createOrUpdateCheater(Torrent $torrent, User $user, array $createData)
|
||||
{
|
||||
$existsCheater = Cheater::query()
|
||||
->where('torrentid', $torrent->id)
|
||||
->where('userid', $user->id)
|
||||
->where('added', '>', Carbon::now()->subHours(24))
|
||||
->first();
|
||||
if ($existsCheater) {
|
||||
$existsCheater->increment('hit');
|
||||
} else {
|
||||
$createData['hit'] = 1;
|
||||
Cheater::query()->insert($createData);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isReAnnounce(array $queries): bool
|
||||
{
|
||||
unset($queries['key']);
|
||||
$lockKey = md5(http_build_query($queries));
|
||||
$redis = Redis::connection()->client();
|
||||
if (!$redis->set($lockKey, nexus()->getStartTimestamp(), ['nx', 'ex' => 5])) {
|
||||
do_log('[RE_ANNOUNCE]');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function generateSuccessAnnounceResponse($torrent, $queries, $user, $withPeers = true): array
|
||||
{
|
||||
// Build Response For Bittorrent Client
|
||||
$minInterval = self::MIN_ANNOUNCE_WAIT_SECOND;
|
||||
$interval = max($this->getRealAnnounceInterval($torrent), $minInterval);
|
||||
$repDict = [
|
||||
'interval' => $interval + random_int(10, 100),
|
||||
'min interval' => $minInterval + random_int(1, 10),
|
||||
'complete' => (int) $torrent->seeders,
|
||||
'incomplete' => (int) $torrent->leechers,
|
||||
'peers' => [],
|
||||
'peers6' => [],
|
||||
];
|
||||
do_log("[REP_DICT_BASE] " . json_encode($repDict));
|
||||
|
||||
/**
|
||||
* For non `stopped` event only
|
||||
* We query peers from database and send peer list, otherwise just quick return.
|
||||
*/
|
||||
if (\strtolower($queries['event']) !== 'stopped' && $withPeers) {
|
||||
$limit = ($queries['numwant'] <= self::MAX_PEER_NUM_WANT ? $queries['numwant'] : self::MAX_PEER_NUM_WANT);
|
||||
$baseQuery = Peer::query()
|
||||
->select(['peer_id', 'ip', 'port'])
|
||||
->where('torrent', $torrent->id)
|
||||
->where('userid', '!=', $user->id)
|
||||
->limit($limit)
|
||||
->orderByRaw('rand()')
|
||||
;
|
||||
|
||||
// Get Torrents Peers
|
||||
if ($queries['left'] == 0) {
|
||||
// Only include leechers in a seeder's peerlist
|
||||
$peers = $baseQuery->where('seeder', 'no')->get()->toArray();
|
||||
} else {
|
||||
$peers = $baseQuery->get()->toArray();
|
||||
}
|
||||
do_log("[REP_DICT_PEER_QUERY] " . last_query());
|
||||
$repDict['peers'] = $this->givePeers($peers, $queries['compact'], $queries['no_peer_id']);
|
||||
$repDict['peers6'] = $this->givePeers($peers, $queries['compact'], $queries['no_peer_id'], FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
return $repDict;
|
||||
}
|
||||
|
||||
private function getRealAnnounceInterval(Torrent $torrent)
|
||||
{
|
||||
$settingMain = Setting::get('main');
|
||||
$announce_wait = self::MIN_ANNOUNCE_WAIT_SECOND;
|
||||
$real_annnounce_interval = $settingMain['announce_interval'];
|
||||
$torrentSurvivalDays = Carbon::now()->diffInDays($torrent->added);
|
||||
if (
|
||||
$settingMain['anninterthreeage']
|
||||
&& ($settingMain['anninterthree'] > $announce_wait)
|
||||
&& ($torrentSurvivalDays >= $settingMain['anninterthreeage'])
|
||||
) {
|
||||
$real_annnounce_interval = $settingMain['anninterthree'];
|
||||
} elseif (
|
||||
$settingMain['annintertwoage']
|
||||
&& ($settingMain['annintertwo'] > $announce_wait)
|
||||
&& ($torrentSurvivalDays >= $settingMain['annintertwoage'])
|
||||
) {
|
||||
$real_annnounce_interval = $settingMain['annintertwo'];
|
||||
}
|
||||
do_log(sprintf(
|
||||
'torrent: %s, survival days: %s, real_announce_interval: %s',
|
||||
$torrent->id, $torrentSurvivalDays, $real_annnounce_interval
|
||||
), 'debug');
|
||||
|
||||
return $real_annnounce_interval;
|
||||
}
|
||||
|
||||
private function getDataTraffic(Torrent $torrent, $queries, User $user, Peer $peer): array
|
||||
{
|
||||
$log = sprintf(
|
||||
"torrent: %s, user: %s, peer: %s, queriesUploaded: %s, queriesDownloaded: %s",
|
||||
$torrent->id, $user->id, $peer->id, $queries['uploaded'], $queries['downloaded']
|
||||
);
|
||||
if ($peer->exists) {
|
||||
$realUploaded = max($queries['uploaded'] - $peer->uploaded, 0);
|
||||
$realDownloaded = max($queries['downloaded'] - $peer->downloaded, 0);
|
||||
$log .= ", [PEER_EXISTS], realUploaded: $realUploaded, realDownloaded: $realDownloaded";
|
||||
} else {
|
||||
$realUploaded = $queries['uploaded'];
|
||||
$realDownloaded = $queries['downloaded'];
|
||||
$log .= ", [PEER_NOT_EXISTS],, realUploaded: $realUploaded, realDownloaded: $realDownloaded";
|
||||
}
|
||||
$spStateReal = $torrent->spStateReal;
|
||||
$uploaderRatio = Setting::get('torrent.uploaderdouble');
|
||||
$log .= ", spStateReal: $spStateReal, uploaderRatio: $uploaderRatio";
|
||||
if ($torrent->owner == $user->id) {
|
||||
//uploader, use the bigger one
|
||||
$upRatio = max($uploaderRatio, Torrent::$promotionTypes[$spStateReal]['up_multiplier']);
|
||||
$log .= ", [IS_UPLOADER], upRatio: $upRatio";
|
||||
} else {
|
||||
$upRatio = Torrent::$promotionTypes[$spStateReal]['up_multiplier'];
|
||||
$log .= ", [IS_NOT_UPLOADER], upRatio: $upRatio";
|
||||
}
|
||||
$downRatio = Torrent::$promotionTypes[$spStateReal]['down_multiplier'];
|
||||
$log .= ", downRatio: $downRatio";
|
||||
$result = [
|
||||
'uploaded_increment' => $realUploaded,
|
||||
'uploaded_increment_for_user' => $realUploaded * $upRatio,
|
||||
'downloaded_increment' => $realDownloaded,
|
||||
'downloaded_increment_for_user' => $realDownloaded * $downRatio,
|
||||
];
|
||||
do_log("$log, result: " . json_encode($result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function givePeers($peers, $compact, $noPeerId, int $filterFlag = FILTER_FLAG_IPV4): string|array
|
||||
{
|
||||
if ($compact) {
|
||||
$pcomp = '';
|
||||
foreach ($peers as $p) {
|
||||
if (isset($p['ip'], $p['port']) && \filter_var($p['ip'], FILTER_VALIDATE_IP, $filterFlag)) {
|
||||
$pcomp .= \inet_pton($p['ip']);
|
||||
$pcomp .= \pack('n', (int) $p['port']);
|
||||
}
|
||||
}
|
||||
|
||||
return $pcomp;
|
||||
}
|
||||
|
||||
if ($noPeerId) {
|
||||
foreach ($peers as &$p) {
|
||||
unset($p['peer_id']);
|
||||
}
|
||||
|
||||
return $peers;
|
||||
}
|
||||
|
||||
return $peers;
|
||||
}
|
||||
|
||||
protected function generateFailedAnnounceResponse($reason): array
|
||||
{
|
||||
return [
|
||||
'failure reason' => $reason,
|
||||
'min interval' => self::MIN_ANNOUNCE_WAIT_SECOND,
|
||||
//'retry in' => self::MIN_ANNOUNCE_WAIT_SECOND
|
||||
];
|
||||
}
|
||||
|
||||
protected function sendFinalAnnounceResponse($repDict): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("[repDict] " . nexus_json_encode($repDict));
|
||||
return \response(Bencode::encode($repDict))
|
||||
->withHeaders(['Content-Type' => 'text/plain; charset=utf-8'])
|
||||
->withHeaders(['Connection' => 'close'])
|
||||
->withHeaders(['Pragma' => 'no-cache']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Torrent $torrent
|
||||
* @param $queries
|
||||
*/
|
||||
private function updateTorrent(Torrent $torrent, $queries)
|
||||
{
|
||||
if (empty($queries['event'])) {
|
||||
do_log("no event, return", 'debug');
|
||||
return;
|
||||
}
|
||||
$torrent->seeders = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('to_go', '=',0)
|
||||
->count();
|
||||
|
||||
$torrent->leechers = Peer::query()
|
||||
->where('torrent', $torrent->id)
|
||||
->where('to_go', '>', 0)
|
||||
->count();
|
||||
|
||||
$torrent->visible = Torrent::VISIBLE_YES;
|
||||
$torrent->last_action = Carbon::now();
|
||||
|
||||
if ($queries['event'] == 'completed') {
|
||||
$torrent->times_completed = DB::raw("times_completed + 1");
|
||||
}
|
||||
|
||||
$torrent->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
private function updatePeer(Peer $peer, $queries)
|
||||
{
|
||||
if ($queries['event'] == 'stopped') {
|
||||
$peer->delete();
|
||||
do_log(last_query(), 'debug');
|
||||
return;
|
||||
}
|
||||
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
//torrent, userid, peer_id, ip, port, connectable, uploaded, downloaded, to_go, started, last_action, seeder, agent, downloadoffset, uploadoffset, passkey
|
||||
$peer->ip = $queries['ip'];
|
||||
$peer->port = $queries['port'];
|
||||
$peer->agent = $queries['user_agent'];
|
||||
$peer->updateConnectableStateIfNeeded();
|
||||
|
||||
$peer->to_go = $queries['left'];
|
||||
$peer->seeder = $queries['left'] == 0 ? 'yes' : 'no';
|
||||
$peer->last_action = $nowStr;
|
||||
$peer->uploaded = $queries['uploaded'];
|
||||
$peer->downloaded = $queries['downloaded'];
|
||||
|
||||
if ($peer->exists) {
|
||||
$peer->prev_action = $peer->last_action;
|
||||
}
|
||||
|
||||
if ($queries['event'] == 'started') {
|
||||
$peer->started = $nowStr;
|
||||
$peer->uploadoffset = $queries['uploaded'];
|
||||
$peer->downloadoffset = $queries['downloaded'];
|
||||
} elseif ($queries['event'] == 'completed') {
|
||||
$peer->finishedat = time();
|
||||
}
|
||||
|
||||
$peer->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update snatch, uploaded & downloaded, use the increment value to do increment
|
||||
*
|
||||
* @param Peer $peer
|
||||
* @param $queries
|
||||
* @param $dataTraffic
|
||||
*/
|
||||
private function updateSnatch(Peer $peer, $queries, $dataTraffic)
|
||||
{
|
||||
$nowStr = Carbon::now()->toDateTimeString();
|
||||
|
||||
$snatch = Snatch::query()
|
||||
->where('torrentid', $peer->torrent)
|
||||
->where('userid', $peer->userid)
|
||||
->first();
|
||||
|
||||
//torrentid, userid, ip, port, uploaded, downloaded, to_go, ,seedtime, leechtime, last_action, startdat, completedat, finished
|
||||
if (!$snatch) {
|
||||
$snatch = new Snatch();
|
||||
//initial
|
||||
$snatch->torrentid = $peer->torrent;
|
||||
$snatch->userid = $peer->userid;
|
||||
$snatch->uploaded = $dataTraffic['uploaded_increment'];
|
||||
$snatch->downloaded = $dataTraffic['downloaded_increment'];
|
||||
$snatch->startdat = $nowStr;
|
||||
} else {
|
||||
//increase, use the increment value
|
||||
$snatch->uploaded = DB::raw("uploaded + " . $dataTraffic['uploaded_increment']);
|
||||
$snatch->downloaded = DB::raw("downloaded + " . $dataTraffic['downloaded_increment']);
|
||||
$timeIncrease = Carbon::now()->diffInSeconds($peer->last_action);
|
||||
if ($queries['left'] == 0) {
|
||||
//seeder
|
||||
$timeField = 'seedtime';
|
||||
} else {
|
||||
$timeField = 'leechtime';
|
||||
}
|
||||
$snatch->{$timeField} = DB::raw("$timeField + $timeIncrease");
|
||||
}
|
||||
|
||||
//always update
|
||||
$snatch->ip = $queries['ip'];
|
||||
$snatch->port = $queries['port'];
|
||||
$snatch->to_go = $queries['left'];
|
||||
$snatch->last_action = $nowStr;
|
||||
if ($queries['event'] == 'completed') {
|
||||
$snatch->completedat = $nowStr;
|
||||
$snatch->finished = 'yes';
|
||||
}
|
||||
|
||||
$snatch->save();
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
|
||||
public function scrape(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
do_log("queryString: " . $request->getQueryString());
|
||||
try {
|
||||
$infoHashArr = $this->checkScrapeFields($request);
|
||||
$user = $this->checkUser($request);
|
||||
$clientAllow = $this->checkClient($request);
|
||||
|
||||
if ($user->clientselect != $clientAllow->id) {
|
||||
$this->userUpdates['clientselect'] = $clientAllow->id;
|
||||
}
|
||||
if ($user->showclienterror == 'yes') {
|
||||
$this->userUpdates['showclienterror'] = 'no';
|
||||
}
|
||||
$repDict = $this->generateScrapeResponse($infoHashArr);
|
||||
} catch (ClientNotAllowedException $exception) {
|
||||
do_log("[ClientNotAllowedException] " . $exception->getMessage());
|
||||
if (isset($user) && $user->showclienterror == 'no') {
|
||||
$this->userUpdates['showclienterror'] = 'yes';
|
||||
}
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (TrackerException $exception) {
|
||||
$repDict = $this->generateFailedAnnounceResponse($exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
//other system exception
|
||||
do_log("[" . get_class($exception) . "] " . $exception->getMessage() . "\n" . $exception->getTraceAsString(), 'error');
|
||||
$repDict = $this->generateFailedAnnounceResponse("system error, report to sysop please, hint: " . nexus()->getRequestId());
|
||||
} finally {
|
||||
do_log("userUpdates: " . nexus_json_encode($this->userUpdates));
|
||||
if (isset($user) && count($this->userUpdates)) {
|
||||
$user->update($this->userUpdates);
|
||||
do_log(last_query(), 'debug');
|
||||
}
|
||||
return $this->sendFinalAnnounceResponse($repDict);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkScrapeFields(Request $request): array
|
||||
{
|
||||
preg_match_all('/info_hash=([^&]*)/i', urldecode($request->getQueryString()), $info_hash_match);
|
||||
|
||||
$info_hash_array = $info_hash_match[1];
|
||||
if (count($info_hash_array) < 1) {
|
||||
throw new TrackerException("key: info_hash is Missing !");
|
||||
} else {
|
||||
foreach ($info_hash_array as $item) {
|
||||
if (strlen($item) != 20) {
|
||||
throw new TrackerException("Invalid info_hash ! info_hash is not 20 bytes long");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $info_hash_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $info_hash_array
|
||||
* @return array[]
|
||||
* @see http://www.bittorrent.org/beps/bep_0048.html
|
||||
*/
|
||||
private function generateScrapeResponse($info_hash_array)
|
||||
{
|
||||
$torrent_details = [];
|
||||
foreach ($info_hash_array as $item) {
|
||||
$torrent = $this->getTorrentByInfoHash($item);
|
||||
if ($torrent) {
|
||||
$torrent_details[$item] = [
|
||||
'complete' => (int)$torrent->seeders,
|
||||
'downloaded' => (int)$torrent->times_completed,
|
||||
'incomplete' => (int)$torrent->leechers,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['files' => $torrent_details];
|
||||
}
|
||||
|
||||
private function getTorrentByInfoHash($infoHash)
|
||||
{
|
||||
$cacheKey = bin2hex($infoHash) . __METHOD__;
|
||||
return Cache::remember($cacheKey, 60, function () use ($infoHash) {
|
||||
$fieldRaw = 'id, owner, sp_state, seeders, leechers, added, banned, hr, visible, last_action, times_completed';
|
||||
$torrent = Torrent::query()->where('info_hash', $infoHash)->selectRaw($fieldRaw)->first();
|
||||
do_log("[getTorrentByInfoHash] cache miss, from database: " . last_query() . ", and get: " . $torrent->id);
|
||||
return $torrent;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user