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

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