api scrape + medal wearing status

This commit is contained in:
xiaomlove
2022-03-19 14:55:43 +08:00
parent 8c32b45e64
commit 4857b799b8
24 changed files with 16536 additions and 69 deletions

View File

@@ -64,4 +64,4 @@ GOOGLE_DRIVE_FOLDER_ID=
GEOIP2_DATABASE=
EXAM_PROGRESS_UPDATE_PROBABILITY=20
ANNOUNCE_API_LOCAL_HOST=
TRACKER_API_LOCAL_HOST=

View File

@@ -22,4 +22,13 @@ class TrackerController extends Controller
{
return $this->repository->announce($request);
}
/**
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function scrape(Request $request): \Illuminate\Http\Response
{
return $this->repository->scrape($request);
}
}

View File

@@ -320,8 +320,10 @@ class User extends Authenticatable
public function medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Medal::class, 'user_medals', 'uid', 'medal_id')
->withPivot(['id', 'expire_at'])
->withTimestamps();
->withPivot(['id', 'expire_at', 'status'])
->withTimestamps()
->orderByPivot('id', 'desc')
;
}
public function valid_medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
@@ -331,6 +333,11 @@ class User extends Authenticatable
});
}
public function wearing_medals(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->valid_medals()->where('user_medals.status', UserMedal::STATUS_WEARING);
}
public function getAvatarAttribute($value)
{
if ($value) {

View File

@@ -4,5 +4,10 @@ namespace App\Models;
class UserMedal extends NexusModel
{
protected $fillable = ['uid', 'medal_id', 'expire_at'];
protected $fillable = ['uid', 'medal_id', 'expire_at', 'status'];
const STATUS_NOT_WEARING = 0;
const STATUS_WEARING = 1;
}

View File

@@ -6,6 +6,7 @@ use 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

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

@@ -1,4 +1,10 @@
<?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;
@@ -13,13 +19,14 @@ 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 = 30;
const MIN_ANNOUNCE_WAIT_SECOND = 300;
const MAX_PEER_NUM_WANT = 50;
@@ -305,10 +312,7 @@ class TrackerRepository extends BaseRepository
protected function checkTorrent($queries, User $user)
{
// Check Info Hash Against Torrents Table
$torrent = Torrent::query()
->selectRaw('id, owner, sp_state, seeders, leechers, added, banned, hr, visible, last_action, times_completed')
->where('info_hash', '=', $queries['info_hash'])
->first();
$torrent = $this->getTorrentByInfoHash($queries['info_hash']);
// If Torrent Doesnt Exists Return Error to Client
if ($torrent === null) {
@@ -506,24 +510,23 @@ class TrackerRepository extends BaseRepository
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' => $this->getRealAnnounceInterval($torrent),
'min interval' => self::MIN_ANNOUNCE_WAIT_SECOND,
'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));
if (!$withPeers) {
return $repDict;
}
/**
* For non `stopped` event only
* We query peers from database and send peer list, otherwise just quick return.
*/
if (\strtolower($queries['event']) !== 'stopped') {
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'])
@@ -769,4 +772,86 @@ class TrackerRepository extends BaseRepository
do_log(last_query(), 'debug');
}
public function scrape(Request $request): \Illuminate\Http\Response
{
do_log(nexus_json_encode($_SERVER));
try {
$infoHashArr = $this->checkScrapeFields($request);
$user = $this->checkUser($request);
$clientAllow = $this->checkClient($request);
if ($user->clientselect != $clientAllow->id) {
$this->userUpdates['clientselect'] = $clientAllow->id;
}
$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 (\Exception $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: " . REQUEST_ID);
} finally {
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 ! :attribute 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)
{
return Cache::remember(bin2hex($infoHash), 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("cache miss, from database: " . last_query() . ", and get: " . $torrent->id);
return $torrent;
});
}
}

View File

@@ -14,9 +14,6 @@ define('LARAVEL_START', microtime(true));
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__ . '/include/globalfunctions.php';
require __DIR__ . '/include/functions.php';
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';

View File

@@ -3,7 +3,8 @@ defined('LARAVEL_START') || define('LARAVEL_START', microtime(true));
defined('NEXUS_START') || define('NEXUS_START', LARAVEL_START);
defined('IN_NEXUS') || define('IN_NEXUS', false);
require dirname(__DIR__) . '/include/constants.php';
require dirname(__DIR__) . '/include/globalfunctions.php';
require dirname(__DIR__) . '/include/functions.php';
/*
|--------------------------------------------------------------------------
| Create The Application

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddStatusToUserMedalsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_medals', function (Blueprint $table) {
$table->integer('status')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_medals', function (Blueprint $table) {
$table->dropColumn('status');
});
}
}

View File

@@ -3,7 +3,8 @@ defined('NEXUS_START') || define('NEXUS_START', microtime(true));
defined('IN_NEXUS') || define('IN_NEXUS', true);
defined('IN_TRACKER') || define('IN_TRACKER', true);
$rootpath= dirname(__DIR__) . '/';
require_once $rootpath . 'include/constants.php';
require_once $rootpath . 'include/globalfunctions.php';
require_once $rootpath . 'include/functions_announce.php';
require $rootpath . 'include/core.php';

View File

@@ -2929,7 +2929,7 @@ function commenttable($rows, $type, $parent_id, $review = false)
$uidArr = array_unique(array_column($rows, 'user'));
$neededColumns = array('id', 'noad', 'class', 'enabled', 'privacy', 'avatar', 'signature', 'uploaded', 'downloaded', 'last_access', 'username', 'donor', 'leechwarn', 'warned', 'title');
$userInfoArr = \App\Models\User::query()->with(['valid_medals'])->find($uidArr, $neededColumns)->keyBy('id');
$userInfoArr = \App\Models\User::query()->with(['wearing_medals'])->find($uidArr, $neededColumns)->keyBy('id');
foreach ($rows as $row)
{
@@ -2944,7 +2944,7 @@ function commenttable($rows, $type, $parent_id, $review = false)
}
}
print("<div style=\"margin-top: 8pt; margin-bottom: 8pt;\"><table id=\"cid".$row["id"]."\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\"><tr><td class=\"embedded\" width=\"99%\">#" . $row["id"] . "&nbsp;&nbsp;<font color=\"gray\">".$lang_functions['text_by']."</font>");
print(build_medal_image($userInfo->valid_medals, 20) . get_username($row["user"],false,true,true,false,false,true));
print(build_medal_image($userInfo->wearing_medals, 20) . get_username($row["user"],false,true,true,false,false,true));
print("&nbsp;&nbsp;<font color=\"gray\">".$lang_functions['text_at']."</font>".gettime($row["added"]).
($row["editedby"] && get_user_class() >= $commanage_class ? " - [<a href=\"comment.php?action=vieworiginal&amp;cid=".$row['id']."&amp;type=".$type."\">".$lang_functions['text_view_original']."</a>]" : "") . "</td><td class=\"embedded nowrap\" width=\"1%\"><a href=\"#top\"><img class=\"top\" src=\"pic/trans.gif\" alt=\"Top\" title=\"Top\" /></a>&nbsp;&nbsp;</td></tr></table></div>");
$avatar = ($CURUSER["avatars"] == "yes" ? htmlspecialchars(trim($userRow["avatar"])) : "");
@@ -5296,13 +5296,24 @@ function msgalert($url, $text, $bgcolor = "red")
print("</td></tr></table></p><br />");
}
function build_medal_image(\Illuminate\Support\Collection $medals, $maxHeight = 200): string
function build_medal_image(\Illuminate\Support\Collection $medals, $maxHeight = 200, $withActions = false): string
{
$medalImages = [];
$wrapBefore = '<div style="display: inline;">';
$wrapAfter = '</div>';
foreach ($medals as $medal) {
$medalImages[] = sprintf('<img src="%s" title="%s" style="max-height: %spx"/>', $medal->image_large, $medal->name, $maxHeight);
$html = sprintf('<div style="display: inline"><img src="%s" title="%s" style="max-height: %spx"/>', $medal->image_large, $medal->name, $maxHeight);
if ($withActions) {
$checked = '';
if ($medal->pivot->status == \App\Models\UserMedal::STATUS_WEARING) {
$checked = ' checked';
}
$html .= sprintf('<label>%s<input type="checkbox" name="medal_wearing_status" value="%s"%s></label>', '佩戴', $medal->id, $checked);
}
$html .= '</div>';
$medalImages[] = $html;
}
return implode('', $medalImages);
return $wrapBefore . implode('', $medalImages) . $wrapAfter;
}
function insert_torrent_tags($torrentId, $tagIdArr, $sync = false)

View File

@@ -337,4 +337,25 @@ function check_client($peer_id, $agent, &$agent_familyid)
}
}
function request_local_api($api)
{
$start = microtime(true);
$ch = curl_init();
$options = [
CURLOPT_URL => sprintf('%s?%s', trim($api, '/'), $_SERVER['QUERY_STRING']),
CURLOPT_USERAGENT => $_SERVER["HTTP_USER_AGENT"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 60,
];
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$log = sprintf(
"[LOCAL_ANNOUNCE_API] [%s] options: %s, response(%s): %s",
number_format(microtime(true) - $start, 3), nexus_json_encode($options), gettype($response), $response
);
do_log($log);
return $response;
}
?>

View File

@@ -669,24 +669,3 @@ function get_hr_ratio($uped, $downed)
return $ratio;
}
function request_local_announce_api($announceApiLocalHost)
{
$start = microtime(true);
$ch = curl_init();
$options = [
CURLOPT_URL => sprintf('%s/api/announce?%s', trim($announceApiLocalHost, '/'), $_SERVER['QUERY_STRING']),
CURLOPT_USERAGENT => $_SERVER["HTTP_USER_AGENT"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 60,
];
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$log = sprintf(
"[LOCAL_ANNOUNCE_API] [%s] options: %s, response(%s): %s",
number_format(microtime(true) - $start, 3), json_encode($options), gettype($response), $response
);
do_log($log);
return $response;
}

16251
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
},
"devDependencies": {
"axios": "^0.21",
"chokidar": "^3.5.3",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.19",
"postcss": "^8.1.14"

29
public/ajax.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
require "../include/bittorrent.php";
dbconn();
loggedinorreturn();
$action = $_POST['action'] ?? 'noAction';
$params = $_POST['params'] ?? [];
function noAction()
{
throw new \RuntimeException("no Action");
}
try {
$result = call_user_func($action, $params);
exit(json_encode(success($result)));
} catch (\Throwable $exception) {
exit(json_encode(fail($exception->getMessage(), $_POST)));
}
function toggleUserMedalStatus($params)
{
global $CURUSER;
$rep = new \App\Repositories\MedalRepository();
return $rep->toggleUserMedalStatus($params['id'], $CURUSER['id']);
}

View File

@@ -1,22 +1,17 @@
<?php
defined('NEXUS_START') || define('NEXUS_START', microtime(true));
defined('IN_NEXUS') || define('IN_NEXUS', true);
defined('IN_TRACKER') || define('IN_TRACKER', true);
require __DIR__ . '/../include/constants.php';
require __DIR__ . '/../include/globalfunctions.php';
require __DIR__ . '/../include/functions_announce.php';
$announceApiLocalHost = nexus_env('ANNOUNCE_API_LOCAL_HOST');
if ($announceApiLocalHost) {
do_log("[GOING_TO_REQUEST_LOCAL_ANNOUNCE_URL] $announceApiLocalHost");
$response = request_local_announce_api($announceApiLocalHost);
require '../include/bittorrent_announce.php';
$apiLocalHost = nexus_env('TRACKER_API_LOCAL_HOST');
if ($apiLocalHost) {
do_log("[TRACKER_API_LOCAL_HOST] $apiLocalHost");
$response = request_local_api(trim($apiLocalHost, '/') . '/api/announce');
if (empty($response)) {
err("error from ANNOUNCE_API_LOCAL_HOST");
err("error from TRACKER_API_LOCAL_HOST");
} else {
exit(benc_resp_raw($response));
}
}
//continue the normal process
require_once('../include/bittorrent_announce.php');
require ROOT_PATH . 'include/core.php';
dbconn_announce();
do_log(nexus_json_encode($_SERVER));
//1. BLOCK ACCESS WITH WEB BROWSERS AND CHEATS!

View File

@@ -38,7 +38,7 @@ if (!$row) {
) {
permissiondenied();
} else {
$owner = \App\Models\User::query()->with(['valid_medals'])->find($row['owner']);
$owner = \App\Models\User::query()->with(['wearing_medals'])->find($row['owner']);
if (!$owner) {
$owner = \App\Models\User::defaultUser();
}
@@ -87,10 +87,10 @@ if (!$row) {
if (get_user_class() < $viewanonymous_class)
$uprow = "<i>".$lang_details['text_anonymous']."</i>";
else
$uprow = "<i>".$lang_details['text_anonymous']."</i> (" . build_medal_image($owner->valid_medals, 20) . get_username($row['owner'], false, true, true, false, false, true) . ")";
$uprow = "<i>".$lang_details['text_anonymous']."</i> (" . build_medal_image($owner->wearing_medals, 20) . get_username($row['owner'], false, true, true, false, false, true) . ")";
}
else {
$uprow = (isset($row['owner']) ? build_medal_image($owner->valid_medals, 20) . get_username($row['owner'], false, true, true, false, false, true) : "<i>".$lang_details['text_unknown']."</i>");
$uprow = (isset($row['owner']) ? build_medal_image($owner->wearing_medals, 20) . get_username($row['owner'], false, true, true, false, false, true) : "<i>".$lang_details['text_unknown']."</i>");
}
if ($CURUSER["id"] == $row["owner"])

View File

@@ -632,7 +632,7 @@ if ($action == "viewtopic")
$uidArr = array_keys($uidArr);
unset($arr);
$neededColumns = array('id', 'noad', 'class', 'enabled', 'privacy', 'avatar', 'signature', 'uploaded', 'downloaded', 'last_access', 'username', 'donor', 'leechwarn', 'warned', 'title');
$userInfoArr = \App\Models\User::query()->with(['valid_medals'])->find($uidArr, $neededColumns)->keyBy('id');
$userInfoArr = \App\Models\User::query()->with(['wearing_medals'])->find($uidArr, $neededColumns)->keyBy('id');
$pn = 0;
$lpr = get_last_read_post_id($topicid);
@@ -674,7 +674,7 @@ if ($action == "viewtopic")
$avatar = ($CURUSER["avatars"] == "yes" ? htmlspecialchars($arr2["avatar"]) : "");
$uclass = get_user_class_image($arr2["class"]);
$by = build_medal_image($userInfo->valid_medals, 20) . get_username($posterid,false,true,true,false,false,true);
$by = build_medal_image($userInfo->wearing_medals, 20) . get_username($posterid,false,true,true,false,false,true);
if (!$avatar)
$avatar = "pic/default_avatar.png";

View File

@@ -31,9 +31,6 @@ if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
|
*/
require dirname(__DIR__) . '/include/globalfunctions.php';
require dirname(__DIR__) . '/include/functions.php';
require __DIR__.'/../vendor/autoload.php';
/*

View File

@@ -1,5 +1,17 @@
<?php
require_once('../include/bittorrent_announce.php');
$apiLocalHost = nexus_env('TRACKER_API_LOCAL_HOST');
if ($apiLocalHost) {
do_log("[TRACKER_API_LOCAL_HOST] $apiLocalHost");
$response = request_local_api(trim($apiLocalHost, '/') . '/api/scrape');
if (empty($response)) {
err("error from TRACKER_API_LOCAL_HOST");
} else {
exit(benc_resp_raw($response));
}
}
require ROOT_PATH . 'include/core.php';
//require_once('../include/benc.php');
dbconn_announce();

View File

@@ -275,7 +275,7 @@ if ($user["avatar"])
tr_small($lang_userdetails['row_avatar'], return_avatar_image(htmlspecialchars(trim($user["avatar"]))), 1);
if ($userInfo->valid_medals->isNotEmpty()) {
tr_small($lang_userdetails['row_medal'], build_medal_image($userInfo->valid_medals), 1);
tr_small($lang_userdetails['row_medal'], build_medal_image($userInfo->valid_medals, 200, $CURUSER['id'] == $user['id']), 1);
}
$uclass = get_user_class_image($user["class"]);
@@ -500,5 +500,21 @@ if (get_user_class() >= $prfmanage_class && $user["class"] < get_user_class())
}
}
end_main_frame();
echo <<<EOT
<script>
jQuery('input[type="checkbox"][name="medal_wearing_status"]').on("change", function (e) {
let input = jQuery(this);
let checked = input.prop("checked")
jQuery.post('ajax.php', {params: {id: this.value}, action: 'toggleUserMedalStatus'}, function (response) {
console.log(response)
if (response.ret != 0) {
input.prop("checked", !checked)
alert(response.msg)
}
}, 'json')
})
</script>
EOT;
stdfoot();
?>

View File

@@ -2,3 +2,4 @@
use Illuminate\Support\Facades\Route;
Route::get('announce', [\App\Http\Controllers\TrackerController::class, 'announce']);
Route::get('scrape', [\App\Http\Controllers\TrackerController::class, 'scrape']);