plugin management + user tables and torrents table text column migrate

This commit is contained in:
xiaomlove
2025-01-19 14:37:00 +08:00
parent 0f88ab8d82
commit 403a9447a9
66 changed files with 1432 additions and 786 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ ELASTICSEARCH_PORT=
ELASTICSEARCH_SCHEME=
ELASTICSEARCH_USER=
ELASTICSEARCH_PASS=
ELASTICSEARCH_SSL_VERIFICATION
ELASTICSEARCH_SSL_VERIFICATION=
ELASTICSEARCH_ENABLED=
SFTP_HOST=
+2 -5
View File
@@ -16,6 +16,7 @@ use App\Models\Invite;
use App\Models\LoginLog;
use App\Models\Medal;
use App\Models\Peer;
use App\Models\Post;
use App\Models\SearchBox;
use App\Models\Setting;
use App\Models\Snatch;
@@ -101,11 +102,7 @@ class Test extends Command
*/
public function handle()
{
$str = "1.abc.de";
$ext = "png";
$str = "202404/20240403215909f58f38ddd968a0e8a4bdd30690a9e92e.png";
$ext = pathinfo($str, PATHINFO_EXTENSION);
dd(basename($str), $ext);
$this->info("haha");
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands\Upgrade;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Nexus\Database\NexusDB;
class MigrateTorrentsTableTextColumn extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'upgrade:migrate_torrents_table_text_column';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate torrents table text column';
/**
* Execute the console command.
*/
public function handle()
{
$columns = ["ori_descr", "descr", "nfo", "technical_info", "pt_gen"];
$sql = "alter table torrents ";
$drops = [];
foreach ($columns as $column) {
if (Schema::hasColumn("torrents", $column)) {
$drops[] = "drop column $column";
}
}
if (!empty($drops)) {
$sql .= implode(",", $drops);
NexusDB::statement($sql);
}
if (Schema::hasTable("torrent_extras")) {
NexusDB::statement("insert torrent_extras (torrent_id, descr, media_info, nfo, created_at) select id, descr, technical_info, nfo, now() from torrents on duplicate key update torrent_id = values(torrent_id)");
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Console\Commands\Upgrade;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MigrateUsersTableCommentRelatedColumn extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'upgrade:migrate_users_table_comment_related_column';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate users table comment related column';
/**
* Execute the console command.
*/
public function handle()
{
Schema::table('users', function (Blueprint $table) {
if (Schema::hasColumn('users', 'modcomment')) {
$table->dropColumn('modcomment');
}
if (Schema::hasColumn('users', 'bonuscomment')) {
$table->dropColumn('bonuscomment');
}
});
}
}
+6 -3
View File
@@ -42,6 +42,9 @@ class Handler extends ExceptionHandler
*/
public function register()
{
if (app()->runningInConsole()) {
return;
}
$this->reportable(function (InsufficientPermissionException $e) {
if (request()->expectsJson()) {
return response()->json(fail($e->getMessage(), request()->all()), 403);
@@ -55,9 +58,9 @@ class Handler extends ExceptionHandler
});
//Other Only handle in json request
// if (!request()->expectsJson()) {
// return;
// }
if (!request()->expectsJson()) {
return;
}
$this->renderable(function (AuthenticationException $e) {
return response()->json(fail($e->getMessage(), ['guards' => $e->guards()]), 401);
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Filament\Clusters;
use Filament\Clusters\Cluster;
class Plugin extends Cluster
{
protected static ?string $navigationIcon = 'heroicon-o-squares-2x2';
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
class RunCommand extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.pages.run-command';
protected static bool $shouldRegisterNavigation = false;
}
@@ -35,7 +35,7 @@ class PluginResource extends Resource
return self::getNavigationLabel();
}
protected static bool $shouldRegisterNavigation = false;
protected static bool $shouldRegisterNavigation = true;
public static function form(Form $form): Form
{
@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Resources\System;
use App\Filament\Resources\System\PluginStoreResource\Pages;
use App\Filament\Resources\System\PluginStoreResource\RelationManagers;
use App\Livewire\InstallPluginModal;
use App\Models\Plugin;
use App\Models\PluginStore;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Infolists\Components;
use Filament\Infolists;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\HtmlString;
use Filament\Actions\Action;
use Livewire\Livewire;
class PluginStoreResource extends Resource
{
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = 'System';
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\Layout\Stack::make([
Tables\Columns\Layout\Stack::make([
Tables\Columns\TextColumn::make('title')
->weight(FontWeight::Bold)
,
Tables\Columns\TextColumn::make('description'),
]),
Tables\Columns\Layout\Stack::make([
Tables\Columns\TextColumn::make('version')
->formatStateUsing(fn (PluginStore $record) => sprintf("版本: %s | 更新时间: %s", $record->version, $record->release_date))
->color('gray')
,
])
])->space(3),
])
->contentGrid([
'md' => 2,
'xl' => 3,
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make()
->modalHeading("详细介绍")
->modalContent(fn (PluginStore $record) => $record->getFullDescription())
->extraModalFooterActions([
Action::make("viewOnBlog")
->url(fn (PluginStore $record) => $record->getBlogPostUrl())
->extraAttributes(['target' => '_blank'])
,
])
,
Tables\Actions\Action::make("install")
->label("安装")
->modalHeading(fn (PluginStore $record) => sprintf("安装插件: %s", $record->title))
->modalContent(function (PluginStore $record) {
$infolist = new Infolist();
$infolist->record = $record;
$infolist->schema([
Infolists\Components\TextEntry::make('plugin_id')
->label(fn () => sprintf("进入目录: %s, 以 root 用户的身份依次执行以下命令进行安装: ", base_path()))
->html(true)
->formatStateUsing(function (PluginStore $record) {
$user = executeCommand("whoami");
$commands = [
sprintf("sudo -u %s composer config repositories.%s %s", $user, $record->plugin_id, $record->remote_url),
sprintf("sudo -u %s composer require %s:%s", $user, $record->package_name, $record->version),
sprintf("sudo -u %s php artisan plugin install %s", $user, $record->package_name),
];
return implode("<br/>", $commands);
})
,
]);
return $infolist;
})
->modalFooterActions(fn () => [])
,
])
->recordAction(null)
->paginated(false)
;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPluginStores::route('/'),
// 'create' => Pages\CreatePluginStore::route('/create'),
// 'edit' => Pages\EditPluginStore::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\System\PluginStoreResource\Pages;
use App\Filament\Resources\System\PluginStoreResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreatePluginStore extends CreateRecord
{
protected static string $resource = PluginStoreResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\System\PluginStoreResource\Pages;
use App\Filament\Resources\System\PluginStoreResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPluginStore extends EditRecord
{
protected static string $resource = PluginStoreResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\System\PluginStoreResource\Pages;
use App\Filament\Resources\System\PluginStoreResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListPluginStores extends ListRecords
{
protected static string $resource = PluginStoreResource::class;
protected function getHeaderActions(): array
{
return [
// Actions\CreateAction::make(),
];
}
}
+11 -3
View File
@@ -130,8 +130,10 @@ class UserResource extends Resource
,
Infolists\Components\TextEntry::make('email')
->label(__("label.email"))
->copyable()
->placeholder("点击复制")
,
Infolists\Components\TextEntry::make('passkey'),
Infolists\Components\TextEntry::make('passkey')->limit(10)->copyable(),
Infolists\Components\TextEntry::make('added')->label(__("label.added")),
Infolists\Components\TextEntry::make('last_access')->label(__("label.last_access")),
Infolists\Components\TextEntry::make('inviter.username')->label(__("label.user.invite_by")),
@@ -141,7 +143,12 @@ class UserResource extends Resource
Infolists\Components\TextEntry::make('uploadedText')->label(__("label.uploaded")),
Infolists\Components\TextEntry::make('downloadedText')->label(__("label.downloaded")),
Infolists\Components\TextEntry::make('seedbonus')->label(__("label.user.seedbonus")),
])->columns(2),
Infolists\Components\TextEntry::make('seed_points')->label(__("label.user.seed_points")),
])
->columns(6)
->columnSpan(4)
,
Components\Group::make([
Infolists\Components\TextEntry::make('status')
->label(__('label.user.status'))
@@ -174,7 +181,8 @@ class UserResource extends Resource
->hintAction(self::buildActionCancelTwoStepAuthentication())
,
])
]),
->columnSpan(1)
])->columns(5),
]);
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use App\Repositories\PluginRepository;
use App\Repositories\ToolRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\Process\Process;
use Telegram\Bot\Api;
use Telegram\Bot\Commands\HelpCommand;
class PluginController extends Controller
{
private $repository;
public function __construct(PluginRepository $repository)
{
$this->repository = $repository;
}
public function runCommandSSE(Request $request)
{
$commands = [
['pwd'], // Example command 1
['ls', '-la'], // Example command 2
['composer', '--version'], // Example command 3
['composer', 'why', 'ext-zip']
];
foreach ($commands as $command) {
$process = new Process($command);
$process->setTimeout(0); // Set timeout for each process
try {
$process->mustRun(function ($type, $buffer) {
if (Process::OUT === $type) {
echo "Output: " . $buffer; // 实时输出
} else { // Process::ERR === $type
echo "Error: " . $buffer;
}
});
} catch (\Symfony\Component\Process\Exception\ProcessFailedException $e) {
echo "Command failed: " . implode(' ', $command) . "\n";
echo $e->getMessage();
break; // Stop executing further commands
}
}
}
}
@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers;
use App\Models\PluginStore;
use App\Repositories\ToolRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ServerSendEventController extends Controller
{
public function sse(Request $request)
{
$this->initSSE();
$response = response()->stream(function () use ($request) {
$action = $request->input('action');
$this->sendData("action: $action");
try {
if ($action == "install_plugin") {
$this->checkComposer();
$pluginId = $request->input('plugin_id');
$this->sendData("pluginId: $pluginId");
$this->doActionInstallPlugin($pluginId);
} else {
throw new \InvalidArgumentException("Invalid action: $action");
}
}catch (\Throwable $throwable) {
$this->sendData("error: " . $throwable->getMessage());
$this->sendData("close");
}
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('Connection', 'keep-alive');
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
private function initSSE(): void
{
@set_time_limit(0); // Disable time limit
// Prevent buffering
if(function_exists('apache_setenv')){
@apache_setenv('no-gzip', 1);
}
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
while (ob_get_level() != 0) {
ob_end_flush();
}
ob_implicit_flush(1);
}
private function sendData(string $data): void
{
echo sprintf("data: %s\n\n", $data);
@ob_flush();
@flush();
}
private function runCommand(string $command): void
{
$this->sendData("running $command, this may take a while...");
$process = Process::fromShellCommandline($command);
$process->setWorkingDirectory(base_path());
$process->setTimeout(null);
$process->mustRun(function ($type, $buffer) {
$this->sendData($buffer);
});
}
private function doActionInstallPlugin(string $pluginId): void
{
$pluginInfo = PluginStore::getInfo($pluginId);
if (!$pluginInfo) {
throw new \InvalidArgumentException("invalid plugin: $pluginId");
}
$this->sendData(sprintf("going to install plugin: %s, version: %s", $pluginInfo['title'], $pluginInfo['version']));
$commands = [];
$commands[] = sprintf("composer config repositories.%s git %s", $pluginInfo['plugin_id'], $pluginInfo['remote_url']);
$commands[] = sprintf("composer require %s:%s", $pluginInfo['package_name'], $pluginInfo['version']);
$commands[] = sprintf("php artisan plugin install %s", $pluginInfo['package_name']);
foreach ($commands as $command) {
$this->runCommand($command);
}
}
private function checkComposer(): void
{
$this->sendData("checking composer ...");
$composerCacheDir = executeCommand("composer config cache-vcs-dir -f " . base_path("composer.json"));
$this->sendData("composer cache-vcs-dir: $composerCacheDir");
if (!is_dir($composerCacheDir)) {
$this->sendData("going to mkdir: $composerCacheDir");
try {
mkdir($composerCacheDir, 0755, true);
$this->sendData("success create composer cache-vcs-dir: $composerCacheDir");
} catch (\Throwable $e) {
$this->sendData("fail to mkdir: " . $e->getMessage());
$this->sendData("Please execute the following command as root user:");
$this->sendData(sprintf("mkdir -p %s", $composerCacheDir));
throw new \RuntimeException("stop due to composer cache-vcs-dir: $composerCacheDir not exists");
}
}
if (!is_writable($composerCacheDir)) {
$this->sendData("cache directory: $composerCacheDir is not writable");
$this->sendData("Please execute the following command as root user:");
$user = executeCommand("whoami");
$this->sendData(sprintf("chown -R %s:%s %s", $user, $user, $composerCacheDir));
throw new \RuntimeException("stop due to composer cache-vcs-dir: $composerCacheDir not writable");
}
}
}
+3
View File
@@ -2,9 +2,12 @@
namespace App\Http\Controllers;
use App\Models\PluginStore;
use App\Repositories\ToolRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\Process\Process;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Telegram\Bot\Api;
use Telegram\Bot\Commands\HelpCommand;
+3 -1
View File
@@ -2,6 +2,7 @@
namespace App\Http;
use App\Http\Middleware\Filament;
use App\Http\Middleware\Locale;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -49,7 +50,8 @@ class Kernel extends HttpKernel
],
'filament' => [
\Illuminate\Session\Middleware\StartSession::class,
\Filament\Http\Middleware\Authenticate::class,
// \Filament\Http\Middleware\Authenticate::class,
Filament::class,
],
];
+1
View File
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Validation\UnauthorizedException;
class Admin
{
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Livewire;
use App\Models\PluginStore;
use Livewire\Component;
use Symfony\Component\Process\Process;
class InstallPluginModal extends Component
{
// public PluginStore $record;
public $output = ''; // 存储命令输出
// public function mount(PluginStore $record)
// {
// $this->recored = $record;
// $this->output = sprintf("点击按钮以开始安装 %s ...", $record->title);
// }
public function executeCommand($command)
{
$this->output = ''; // 清空之前的输出
// $command = "whereis composer";
// $process = new Process([$command]);
// $process->setTimeout(60); // 设置超时时间(秒)
//
// $process->start(); // 启动异步进程
//
// foreach ($process as $type => $data) {
// if ($type === Process::OUT) {
// $this->output .= $data; // 实时追加标准输出
// } elseif ($type === Process::ERR) {
// $this->output .= "[ERROR]: $data"; // 实时追加错误输出
// }
//
//// $this->dispatch('updateTextarea', $this->output); // 通知前端更新
// }
$process = new Process(['/usr/local/bin/composer', 'info']);
$process->setTimeout(3600); // 可选,设置超时时间
$basePath = base_path();
do_log("base path: $basePath");
$process->setWorkingDirectory($basePath);
try {
$process->mustRun(function ($type, $buffer) {
if (Process::OUT === $type) {
$this->output = $buffer;
$this->dispatch('updateTextarea', $this->output);
} else { // Process::ERR === $type
do_log("executeCommand, ERR: " . $buffer, 'error');
}
});
} catch (\Exception $e) {
do_log($e->getMessage(), 'error');
}
}
public function render()
{
return view('livewire.install-plugin-modal');
}
}
+22
View File
@@ -3,6 +3,8 @@
namespace App\Models;
use Carbon\Carbon;
class BonusLogs extends NexusModel
{
protected $table = 'bonus_logs';
@@ -41,6 +43,7 @@ class BonusLogs extends NexusModel
const BUSINESS_TYPE_ROLE_WORK_SALARY = 1000;
const BUSINESS_TYPE_TORRENT_BE_DOWNLOADED = 1001;
const BUSINESS_TYPE_RECEIVE_REWARD = 1002;
public static array $businessTypes = [
self::BUSINESS_TYPE_CANCEL_HIT_AND_RUN => ['text' => 'Cancel H&R'],
@@ -65,6 +68,7 @@ class BonusLogs extends NexusModel
self::BUSINESS_TYPE_ROLE_WORK_SALARY => ['text' => 'Role work salary'],
self::BUSINESS_TYPE_TORRENT_BE_DOWNLOADED => ['text' => 'Torrent be downloaded'],
self::BUSINESS_TYPE_RECEIVE_REWARD => ['text' => 'Receive reward'],
];
public function getBusinessTypeTextAttribute()
@@ -102,5 +106,23 @@ class BonusLogs extends NexusModel
return $result ?? self::DEFAULT_BONUS_BUY_CHANGE_USERNAME_CARD;
}
public static function add(int $userId, float $old, float $delta, float $new, string $comment, int $businessType)
{
if (!isset(self::$businessTypes[$businessType])) {
throw new \InvalidArgumentException("Invalid business type: $businessType");
}
$nowStr = Carbon::now()->toDateTimeString();
return self::query()->create([
'business_type' => $businessType,
'uid' => $userId,
'old_total_value' => $old,
'value' => $delta,
'new_total_value' => $new,
'comment' => sprintf("[%s] %s", self::$businessTypes[$businessType]['text'], $comment),
'created_at' => $nowStr,
'updated_at' => $nowStr,
]);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
use Sushi\Sushi;
class PluginStore extends Model
{
use Sushi;
const PLUGIN_LIST_API = "https://nppl.nexusphp.workers.dev";
const BLOG_POST_INFO_API = "https://nexusphp.org/wp-json/wp/v2/posts/%d";
const BLOG_POST_URL = "https://nexusphp.org/?p=%d";
public function getRows()
{
return Http::get(self::PLUGIN_LIST_API)->json();
}
public function getBlogPostUrl(): string
{
return sprintf(self::BLOG_POST_URL, $this->post_id);
}
public function getFullDescription(): Htmlable
{
$url = $this->getBlogPostInfoUrl($this->post_id);
$logPrefix = sprintf("post_id: %s, url: %s", $this->post_id, $url);
$defaultContent = "无法获取详细信息 ...";
try {
$result = Http::get($url)->json();
do_log("$logPrefix, result: " . json_encode($result));
$content = $result['content']['rendered'] ?? $result['message'] ?? $defaultContent;
} catch (\Exception $e) {
do_log(sprintf(
"%s, error: %s",
$logPrefix, $e->getMessage() . $e->getTraceAsString()
), 'error');
$content = $defaultContent;
}
return new HtmlString($content);
}
private function getBlogPostInfoUrl(int $postId): string
{
return sprintf(self::BLOG_POST_INFO_API, $postId);
}
public static function getInfo(string $id)
{
return Http::get(self::PLUGIN_LIST_API . "/plugin/$id")->json();
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ namespace App\Models;
class Post extends NexusModel
{
public function user()
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class, 'userid');
}
+8 -4
View File
@@ -9,11 +9,11 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
class Torrent extends NexusModel
{
protected $fillable = [
'name', 'filename', 'save_as', 'descr', 'small_descr', 'ori_descr',
'name', 'filename', 'save_as', 'small_descr',
'category', 'source', 'medium', 'codec', 'standard', 'processing', 'team', 'audiocodec',
'size', 'added', 'type', 'numfiles', 'owner', 'nfo', 'sp_state', 'promotion_time_type',
'promotion_until', 'anonymous', 'url', 'pos_state', 'cache_stamp', 'picktype', 'picktime',
'last_reseed', 'pt_gen', 'technical_info', 'leechers', 'seeders', 'cover', 'last_action',
'last_reseed', 'leechers', 'seeders', 'cover', 'last_action',
'times_completed', 'approval_status', 'banned', 'visible', 'pos_state_until', 'price',
];
@@ -25,7 +25,6 @@ class Torrent extends NexusModel
protected $casts = [
'added' => 'datetime',
'pt_gen' => 'array',
'promotion_until' => 'datetime',
'pos_state_until' => 'datetime',
];
@@ -241,7 +240,7 @@ class Torrent extends NexusModel
public static function getFieldsForList($appendTableName = false): array|bool
{
$fields = '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, approval_status, cover, price';
$fields = '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, hr, approval_status, cover, price';
$fields = preg_split('/[,\s]+/', $fields);
if ($appendTableName) {
foreach ($fields as &$value) {
@@ -513,4 +512,9 @@ class Torrent extends NexusModel
{
return $this->hasMany(TorrentOperationLog::class, 'torrent_id');
}
public function extra(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(TorrentExtra::class, 'torrent_id');
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Nexus\Database\NexusDB;
class TorrentExtra extends NexusModel
{
public $timestamps = true;
protected $fillable = ['torrent_id', 'descr', 'ori_descr', 'media_info'];
public function torrent()
{
return $this->belongsTo(Torrent::class, 'torrent_id');
}
}
+19 -7
View File
@@ -182,9 +182,9 @@ class User extends Authenticatable implements FilamentUser, HasName
* @var array
*/
protected $fillable = [
'username', 'email', 'passhash', 'secret', 'stylesheet', 'editsecret', 'added', 'modcomment', 'enabled', 'status',
'username', 'email', 'passhash', 'secret', 'stylesheet', 'editsecret', 'added', 'enabled', 'status',
'leechwarn', 'leechwarnuntil', 'page', 'class', 'uploaded', 'downloaded', 'clientselect', 'showclienterror', 'last_home',
'seedbonus', 'bonuscomment', 'downloadpos', 'vip_added', 'vip_until', 'title', 'invites', 'attendance_card',
'seedbonus', 'downloadpos', 'vip_added', 'vip_until', 'title', 'invites', 'attendance_card',
'seed_points_per_hour', 'passkey',
];
@@ -229,7 +229,7 @@ class User extends Authenticatable implements FilamentUser, HasName
'uploaded', 'downloaded', 'seedbonus', 'seedtime', 'leechtime',
'invited_by', 'enabled', 'seed_points', 'last_access', 'invites',
'lang', 'attendance_card', 'privacy', 'noad', 'downloadpos', 'donoruntil', 'donor',
'bonuscomment', 'downloadpos', 'vip_added', 'vip_until', 'title', 'invites', 'attendance_card',
'downloadpos', 'vip_added', 'vip_until', 'title', 'invites', 'attendance_card',
'seed_points_per_hour'
];
@@ -535,6 +535,11 @@ class User extends Authenticatable implements FilamentUser, HasName
return $this->examAndTasks()->wherePivot("status", ExamUser::STATUS_NORMAL);
}
public function modifyLogs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(UserModifyLog::class, "user_id");
}
public function getAvatarAttribute($value)
{
if ($value) {
@@ -560,10 +565,17 @@ class User extends Authenticatable implements FilamentUser, HasName
throw new \RuntimeException('This method only works when user exists !');
}
//@todo how to do prepare bindings here ?
$comment = addslashes($comment);
do_log("update: " . json_encode($update) . ", $commentField: $comment", 'notice');
$update[$commentField] = NexusDB::raw("if($commentField = '', '$comment', concat_ws('\n', '$comment', $commentField))");
return $this->update($update);
// $comment = addslashes($comment);
// do_log("update: " . json_encode($update) . ", $commentField: $comment", 'notice');
// $update[$commentField] = NexusDB::raw("if($commentField = '', '$comment', concat_ws('\n', '$comment', $commentField))");
if ($commentField != "modcomment") {
throw new \RuntimeException("unsupported commentField: $commentField !");
}
return NexusDB::transaction(function () use ($update, $comment) {
$this->modifyLogs()->create(['content' => $comment]);
return $this->update($update);
});
}
public function canAccessAdmin(): bool
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
class UserModifyLog extends NexusModel
{
protected $fillable = ['user_id', 'content', ];
public $timestamps = true;
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class, "user_id");
}
}
+8 -1
View File
@@ -22,6 +22,8 @@ use Illuminate\View\Middleware\ShareErrorsFromSession;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Table;
use Livewire\Livewire;
use NexusPlugin\TelegramBot\Filament\TelegramBotBindsResource;
use NexusPlugin\TelegramBot\Filament\TelegramBotResource;
class AppPanelProvider extends PanelProvider
{
@@ -29,7 +31,7 @@ class AppPanelProvider extends PanelProvider
{
return $panel
->default()
->id('app')
->id('admin')
->homeUrl("/")
->sidebarWidth("15rem")
->topbar(true)
@@ -40,6 +42,10 @@ class AppPanelProvider extends PanelProvider
->colors([
'primary' => Color::Amber,
])
->resources([
// TelegramBotResource::class,
// TelegramBotBindsResource::class
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
@@ -51,6 +57,7 @@ class AppPanelProvider extends PanelProvider
// Widgets\AccountWidget::class,
// Widgets\FilamentInfoWidget::class,
])
->discoverClusters(app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters')
->middleware([
// EncryptCookies::class,
\App\Http\Middleware\EncryptCookies::class,
+6 -4
View File
@@ -2,6 +2,7 @@
namespace App\Providers;
use App\Http\Middleware\Filament;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
@@ -47,14 +48,15 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace)
->group(base_path('routes/web.php'));
Route::prefix('api')
->namespace($this->namespace)
->group(base_path('routes/tracker.php'));
Route::prefix('api')
->namespace($this->namespace)
->middleware('throttle:third-party')
->group(base_path('routes/third-party.php'));
Route::prefix('admin')
->namespace($this->namespace)
->middleware([Filament::class])
->group(base_path('routes/admin.php'));
});
}
+10
View File
@@ -381,6 +381,14 @@ class TorrentRepository extends BaseRepository
return md5($passkey . date('Ymd') . $user['id']);
}
/**
* @deprecated
* @param $id
* @param $uid
* @param $initializeIfNotExists
* @return string
* @throws NexusException
*/
public function getTrackerReportAuthKey($id, $uid, $initializeIfNotExists = false): string
{
$key = $this->getTrackerReportAuthKeySecret($id, $uid, $initializeIfNotExists);
@@ -389,6 +397,8 @@ class TorrentRepository extends BaseRepository
}
/**
* @deprecated
*
* check tracker report authkey
* if valid, the result will be the date the key generate, else if will be empty string
*
+7
View File
@@ -227,6 +227,13 @@ class TrackerRepository extends BaseRepository
}
/**
* @deprecated
*
* @param $authkey
* @return array
* @throws TrackerException
*/
protected function checkAuthkey($authkey)
{
$arr = explode('|', $authkey);
+1
View File
@@ -9,6 +9,7 @@ if (!RUNNING_IN_OCTANE) {
}
$GLOBALS['hook'] = $hook = new \Nexus\Plugin\Hook();
$GLOBALS['plugin'] = $plugin = new \Nexus\Plugin\Plugin();
/*
|--------------------------------------------------------------------------
| Create The Application
+5 -3
View File
@@ -33,6 +33,7 @@
"ext-xml": "*",
"ext-zend-opcache": "*",
"ext-zip": "*",
"calebporzio/sushi": "^2.5",
"elasticsearch/elasticsearch": "^7.16",
"filament/filament": "^3.2",
"flowframe/laravel-trend": "^0.3",
@@ -45,7 +46,6 @@
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.5",
"league/flysystem-sftp-v3": "^3.0",
"masbug/flysystem-google-drive-ext": "^2.0",
"meilisearch/meilisearch-php": "^1.0",
"orangehill/iseed": "^3.0",
"phpgangsta/googleauthenticator": "dev-master",
@@ -74,7 +74,8 @@
"@php artisan filament:upgrade"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan passport:keys"
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
@@ -95,7 +96,8 @@
"secure-http": false,
"allow-plugins": {
"php-http/discovery": true
}
},
"process-timeout": 900
},
"minimum-stability": "dev",
"prefer-stable": true
Generated
+280 -597
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('torrent_extras', function (Blueprint $table) {
$table->id();
$table->integer('torrent_id')->unique();
$table->mediumText('descr');
$table->text('media_info')->nullable();
$table->binary('nfo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('torrent_extras');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_modify_logs', function (Blueprint $table) {
$table->id();
$table->integer('user_id')->index();
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_modify_logs');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
if (Schema::hasColumn('users', 'modcomment')) {
$table->dropColumn('modcomment');
}
if (Schema::hasColumn('users', 'bonuscomment')) {
$table->dropColumn("bonuscomment");
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('torrents', function (Blueprint $table) {
foreach (["ori_descr", "descr", "nfo", "technical_info", "pt_gen"] as $field) {
if (Schema::hasColumn('torrents', $field)) {
$table->dropColumn($field);
}
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
defined('VERSION_NUMBER') || define('VERSION_NUMBER', '1.9.0');
defined('RELEASE_DATE') || define('RELEASE_DATE', '2024-12-29');
defined('RELEASE_DATE') || define('RELEASE_DATE', '2025-01-19');
defined('IN_TRACKER') || define('IN_TRACKER', false);
defined('PROJECTNAME') || define("PROJECTNAME","NexusPHP");
defined('NEXUSPHPURL') || define("NEXUSPHPURL","https://nexusphp.org");
+11 -10
View File
@@ -3363,16 +3363,17 @@ function linkcolor($num) {
}
function writecomment($userid, $comment, $oldModcomment = null) {
if (is_null($oldModcomment)) {
$res = sql_query("SELECT modcomment FROM users WHERE id = '$userid'") or sqlerr(__FILE__, __LINE__);
$arr = mysql_fetch_assoc($res);
$modcomment = date("Y-m-d") . " - " . $comment . "" . ($arr['modcomment'] != "" ? "\n" : "") . $arr['modcomment'];
} else {
$modcomment = date("Y-m-d") . " - " . $comment . "" . ($oldModcomment != "" ? "\n" : "") .$oldModcomment;
}
$modcom = sqlesc($modcomment);
do_log("update user: $userid prepend modcomment: $comment, with oldModcomment: $oldModcomment");
return sql_query("UPDATE users SET modcomment = $modcom WHERE id = '$userid'") or sqlerr(__FILE__, __LINE__);
\App\Models\UserModifyLog::query()->create(['user_id' => $userid, 'content' => date("Y-m-d") . " - " . $comment]);
// if (is_null($oldModcomment)) {
// $res = sql_query("SELECT modcomment FROM users WHERE id = '$userid'") or sqlerr(__FILE__, __LINE__);
// $arr = mysql_fetch_assoc($res);
// $modcomment = date("Y-m-d") . " - " . $comment . "" . ($arr['modcomment'] != "" ? "\n" : "") . $arr['modcomment'];
// } else {
// $modcomment = date("Y-m-d") . " - " . $comment . "" . ($oldModcomment != "" ? "\n" : "") .$oldModcomment;
// }
// $modcom = sqlesc($modcomment);
// do_log("update user: $userid prepend modcomment: $comment, with oldModcomment: $oldModcomment");
// return sql_query("UPDATE users SET modcomment = $modcom WHERE id = '$userid'") or sqlerr(__FILE__, __LINE__);
}
function return_torrent_bookmark_array($userid)
+4
View File
@@ -264,6 +264,9 @@ function getLogFile($append = '')
if ($append) {
$name .= "-$append";
}
if (isRunningInConsole()) {
$name .= sprintf("-cli-%s-%s", get_current_user(), getmyuid());
}
$logFile = sprintf('%s-%s%s', $name, date('Y-m-d'), $suffix);
return $logFiles[$append] = $logFile;
@@ -1221,6 +1224,7 @@ function is_donor(array $userInfo): bool
}
/**
* @deprecated
* @param $authkey
* @return false|int|mixed|string|null
* @throws \App\Exceptions\NexusException
+14
View File
@@ -26,6 +26,8 @@ use App\Repositories\TorrentRepository;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Nexus\Database\NexusDB;
@@ -332,6 +334,18 @@ class Update extends Install
["value" => User::query()->where("class", User::CLASS_STAFF_LEADER)->first(["id"])->id]
);
}
/**
* @since 1.9.0
*/
if (!Schema::hasTable("torrent_extras")) {
$this->runMigrate("database/migrations/2025_01_08_133552_create_torrent_extra_table.php");
$this->runMigrate("database/migrations/2025_01_08_133847_create_user_modify_logs_table.php");
$this->runMigrate("database/migrations/2025_01_18_235747_drop_users_table_text_column.php");
$this->runMigrate("database/migrations/2025_01_18_235757_drop_torrents_table_text_column.php");
Artisan::call("upgrade:upgrade:migrate_torrents_table_text_column");
Artisan::call("upgrade:migrate_users_table_comment_related_column");
}
}
public function runExtraMigrate()
+23
View File
@@ -157,6 +157,28 @@ class AjaxInterface{
$rep = new \App\Repositories\ExamRepository();
return $rep->assignToUser($CURUSER['id'], $params['exam_id']);
}
public static function addToken($params)
{
global $CURUSER;
if (empty($params['name'])) {
throw new \InvalidArgumentException("Name is required");
}
$user = \App\Models\User::query()->findOrFail($CURUSER['id'], \App\Models\User::$commonFields);
$user->createToken($params['name']);
return true;
}
public static function removeToken($params)
{
global $CURUSER;
if (empty($params['id'])) {
throw new \InvalidArgumentException("id is required");
}
$user = \App\Models\User::query()->findOrFail($CURUSER['id'], \App\Models\User::$commonFields);
$user->tokens()->where('id', $params['id'])->delete();
return true;
}
}
$class = 'AjaxInterface';
@@ -171,5 +193,6 @@ try {
throw new \RuntimeException("Invalid action: $action");
}
}catch(\Throwable $exception){
do_log($exception->getMessage() . $exception->getTraceAsString(), "error");
exit(json_encode(fail($exception->getMessage(), $_POST)));
}
+2 -53
View File
@@ -26,45 +26,13 @@ if (isset($passkey) && strlen($passkey) != 32) warn("Invalid passkey (" . strlen
$redis = $Cache->getRedis();
$torrentNotExistsKey = "torrent_not_exists";
$authKeyInvalidKey = "authkey_invalid";
$passkeyInvalidKey = "passkey_invalid";
$isReAnnounce = false;
$reAnnounceInterval = 5;
$frequencyInterval = 30;
$isStoppedOrCompleted = !empty($GLOBALS['event']) && in_array($GLOBALS['event'], array('completed', 'stopped'));
$userAuthenticateKey = "";
if (!empty($_GET['authkey'])) {
$authkey = $_GET['authkey'];
$parts = explode("|", $authkey);
if (count($parts) != 3) {
warn("authkey format error");
}
$authKeyTid = $parts[0];
$authKeyUid = $userAuthenticateKey = $parts[1];
$subAuthkey = sprintf("%s|%s", $authKeyTid, $authKeyUid);
//check ReAnnounce
$lockParams = [
'user' => $authKeyUid,
'info_hash' => $info_hash
];
$lockString = http_build_query($lockParams);
$lockKey = "isReAnnounce:" . md5($lockString);
if (!$redis->set($lockKey, TIMENOW, ['nx', 'ex' => $reAnnounceInterval])) {
$isReAnnounce = true;
}
$reAnnounceCheckByAuthKey = "reAnnounceCheckByAuthKey:$subAuthkey";
if (!$isStoppedOrCompleted && !$isReAnnounce && !$redis->set($reAnnounceCheckByAuthKey, TIMENOW, ['nx', 'ex' => $frequencyInterval])) {
$msg = "Request too frequent(a)";
do_log(sprintf("[ANNOUNCE] %s key: %s already exists, value: %s", $msg, $reAnnounceCheckByAuthKey, TIMENOW));
warn($msg, 300);
}
if ($redis->get("$authKeyInvalidKey:$authkey")) {
$msg = "Invalid authkey";
do_log("[ANNOUNCE] $msg");
warn($msg);
}
} elseif (!empty($_GET['passkey'])) {
if (!empty($_GET['passkey'])) {
$passkey = $userAuthenticateKey = $_GET['passkey'];
if ($redis->get("$passkeyInvalidKey:$passkey")) {
$msg = "Passkey invalid";
@@ -81,7 +49,7 @@ if (!empty($_GET['authkey'])) {
$isReAnnounce = true;
}
} else {
warn("Require passkey or authkey");
warn("Require passkey");
}
if ($redis->get("$torrentNotExistsKey:$info_hash")) {
@@ -95,20 +63,7 @@ if (!$isStoppedOrCompleted && !$isReAnnounce && !$redis->set($torrentReAnnounceK
do_log(sprintf("[ANNOUNCE] %s key: %s already exists, value: %s", $msg, $torrentReAnnounceKey, TIMENOW));
warn($msg, 300);
}
dbconn_announce();
//check authkey
if (!empty($_REQUEST['authkey'])) {
try {
$GLOBALS['passkey'] = $_GET['passkey'] = get_passkey_by_authkey($_REQUEST['authkey']);
} catch (\Exception $exception) {
$redis->set("$authKeyInvalidKey:".$_REQUEST['authkey'], TIMENOW, ['ex' => 3600*24]);
warn($exception->getMessage());
}
}
//4. GET IP AND CHECK PORT
$ip = getip(); // avoid to get the spoof ip from some agent
@@ -229,12 +184,6 @@ if (!$torrent) {
}
$GLOBALS['torrent'] = $torrent;
$torrentid = $torrent["id"];
if (isset($authKeyTid) && $authKeyTid != $torrentid) {
$redis->set("$authKeyInvalidKey:$authkey", TIMENOW, ['ex' => 3600*24]);
$msg = "Invalid authkey: $authkey 2";
do_log("[ANNOUNCE] $msg");
warn($msg);
}
if ($torrent['banned'] == 'yes') {
if (!user_can('seebanned', false, $az['id'])) {
err("torrent banned");
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@ cropperjs/dist/cropper.min.css:
filepond/dist/filepond.min.css:
(*!
* FilePond 4.32.5
* FilePond 4.32.6
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*)
+4 -7
View File
@@ -11,8 +11,9 @@ if (!isset($id) || !$id)
die();
$taxonomyFields = "sources.name AS source_name, media.name AS medium_name, codecs.name AS codec_name, standards.name AS standard_name, processings.name AS processing_name, teams.name AS team_name, audiocodecs.name AS audiocodec_name";
$res = sql_query("SELECT torrents.cache_stamp, torrents.sp_state, torrents.url, torrents.small_descr, torrents.seeders, torrents.banned, torrents.leechers, torrents.info_hash, torrents.filename, nfo, LENGTH(torrents.nfo) AS nfosz, torrents.last_action, torrents.name, torrents.owner, torrents.save_as, torrents.descr, torrents.visible, torrents.size, torrents.added, torrents.views, torrents.hits, torrents.times_completed, torrents.id, torrents.type, torrents.numfiles, torrents.anonymous, torrents.pt_gen, torrents.technical_info, torrents.hr, torrents.promotion_until, torrents.promotion_time_type, torrents.approval_status, torrents.price,
categories.name AS cat_name, categories.mode as search_box_id, $taxonomyFields
$extraFields = "torrent_extras.descr, torrent_extras.nfo, LENGTH(torrent_extras.nfo) AS nfosz, torrent_extras.media_info as technical_info";
$res = sql_query("SELECT torrents.cache_stamp, torrents.sp_state, torrents.url, torrents.small_descr, torrents.seeders, torrents.banned, torrents.leechers, torrents.info_hash, torrents.filename, torrents.last_action, torrents.name, torrents.owner, torrents.save_as, torrents.visible, torrents.size, torrents.added, torrents.views, torrents.hits, torrents.times_completed, torrents.id, torrents.type, torrents.numfiles, torrents.anonymous, torrents.hr, torrents.promotion_until, torrents.promotion_time_type, torrents.approval_status, torrents.price,
categories.name AS cat_name, categories.mode as search_box_id, $taxonomyFields, $extraFields
FROM torrents LEFT JOIN categories ON torrents.category = categories.id
LEFT JOIN sources ON torrents.source = sources.id
LEFT JOIN media ON torrents.medium = media.id
@@ -21,6 +22,7 @@ FROM torrents LEFT JOIN categories ON torrents.category = categories.id
LEFT JOIN processings ON torrents.processing = processings.id
LEFT JOIN teams ON torrents.team = teams.id
LEFT JOIN audiocodecs ON torrents.audiocodec = audiocodecs.id
LEFT JOIN torrent_extras ON torrents.id = torrent_extras.torrent_id
WHERE torrents.id = $id LIMIT 1")
or sqlerr();
$row = mysql_fetch_array($res);
@@ -398,11 +400,6 @@ JS;
echo $Cache->next_row();
}
}
if (get_setting('main.enable_pt_gen_system') == 'yes' && !empty($row['pt_gen'])) {
$ptGen = new \Nexus\PTGen\PTGen();
$ptGen->updateTorrentPtGen($row);
}
if (!empty($otherCopiesIdArr))
{
// $where_area = " url = " . sqlesc((int)$imdb_id) ." AND torrents.id != ".sqlesc($id);
-2
View File
@@ -139,7 +139,6 @@ if (strlen($CURUSER['passkey']) != 32) {
$CURUSER['passkey'] = md5($CURUSER['username'].date("Y-m-d H:i:s").$CURUSER['passhash']);
sql_query("UPDATE users SET passkey=".sqlesc($CURUSER['passkey'])." WHERE id=".sqlesc($CURUSER['id']));
}
$trackerReportAuthKey = $torrentRep->getTrackerReportAuthKey($id, $CURUSER['id'], true);
$dict = \Rhilip\Bencode\Bencode::load($fn);
$dict['announce'] = $ssl_torrent . $base_announce_url . "?passkey=" . $CURUSER['passkey'];
do_log(sprintf("[ANNOUNCE_URL], user: %s, torrent: %s, url: %s", $CURUSER['id'] ?? '', $id, $dict['announce']));
@@ -221,6 +220,5 @@ else
//header ("Content-Disposition: attachment; filename=".$row["filename"]."");
//ob_implicit_flush(true);
//print(benc($dict));
\Nexus\Database\NexusDB::cache_put("authkey2passkey:$trackerReportAuthKey", $CURUSER['passkey'], 3600*24);
echo \Rhilip\Bencode\Bencode::encode($dict);
?>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+27 -16
View File
@@ -25,7 +25,7 @@ if (!$id)
die();
$res = sql_query("SELECT id, category, owner, filename, save_as, anonymous, picktype, picktime, added, pt_gen, banned FROM torrents WHERE id = ".mysql_real_escape_string($id));
$res = sql_query("SELECT id, category, owner, filename, save_as, anonymous, picktype, picktime, added, banned FROM torrents WHERE id = ".mysql_real_escape_string($id));
$row = mysql_fetch_array($res);
$torrentAddedTimeString = $row['added'];
if (!$row)
@@ -35,6 +35,7 @@ if ($CURUSER["id"] != $row["owner"] && !user_can('torrentmanage'))
bark($lang_takeedit['std_not_owner']);
$oldcatmode = get_single_value("categories","mode","WHERE id=".sqlesc($row['category']));
$updateset = array();
$extraUpdate = [];
//$fname = $row["filename"];
//preg_match('/^(.+)\.torrent$/si', $fname, $matches);
@@ -45,19 +46,23 @@ $url = parse_imdb_id($_POST['url'] ?? '');
/**
* add PT-Gen
* @since 1.6
*
* @deprecated
* @since 1.9
*/
if (!empty($_POST['pt_gen'])) {
$postPtGen = $_POST['pt_gen'];
$existsPtGenInfo = json_decode($row['pt_gen'], true) ?? [];
$ptGen = new \Nexus\PTGen\PTGen();
if ($postPtGen != $ptGen->getLink($existsPtGenInfo)) {
$updateset[] = "pt_gen = " . sqlesc($postPtGen);
}
} else {
$updateset[] = "pt_gen = ''";
}
//if (!empty($_POST['pt_gen'])) {
// $postPtGen = $_POST['pt_gen'];
// $existsPtGenInfo = json_decode($row['pt_gen'], true) ?? [];
// $ptGen = new \Nexus\PTGen\PTGen();
// if ($postPtGen != $ptGen->getLink($existsPtGenInfo)) {
// $updateset[] = "pt_gen = " . sqlesc($postPtGen);
// }
//} else {
// $updateset[] = "pt_gen = ''";
//}
$updateset[] = "technical_info = " . sqlesc($_POST['technical_info'] ?? '');
//$updateset[] = "technical_info = " . sqlesc($_POST['technical_info'] ?? '');
$extraUpdate["media_info"] = $_POST['technical_info'] ?? '';
$torrentOperationLog = [];
@@ -70,8 +75,11 @@ if ($nfoaction == "update")
if ($nfofile['size'] > 65535)
bark($lang_takeedit['std_nfo_too_big']);
$nfofilename = $nfofile['tmp_name'];
if (@is_uploaded_file($nfofilename) && @filesize($nfofilename) > 0)
$updateset[] = "nfo = " . sqlesc(str_replace("\x0d\x0d\x0a", "\x0d\x0a", file_get_contents($nfofilename)));
if (@is_uploaded_file($nfofilename) && @filesize($nfofilename) > 0) {
// $updateset[] = "nfo = " . sqlesc(str_replace("\x0d\x0d\x0a", "\x0d\x0a", file_get_contents($nfofilename)));
$extraUpdate["nfo"] = str_replace("\x0d\x0d\x0a", "\x0d\x0a", file_get_contents($nfofilename));
}
$Cache->delete_value('nfo_block_torrent_id_'.$id);
}
elseif ($nfoaction == "remove"){
@@ -93,7 +101,8 @@ if ($oldcatmode != $newcatmode && !$allowmove)
bark($lang_takeedit['std_cannot_move_torrent']);
$updateset[] = "anonymous = '" . (!empty($_POST["anonymous"]) ? "yes" : "no") . "'";
$updateset[] = "name = " . sqlesc($name);
$updateset[] = "descr = " . sqlesc($descr);
//$updateset[] = "descr = " . sqlesc($descr);
$extraUpdate["descr"] = $descr;
$updateset[] = "url = " . sqlesc($url);
$updateset[] = "small_descr = " . sqlesc($_POST["small_descr"]);
//$updateset[] = "ori_descr = " . sqlesc($descr);
@@ -232,7 +241,9 @@ if (user_can('torrent-set-price') && $paidTorrentEnabled) {
$sql = "UPDATE torrents SET " . join(",", $updateset) . " WHERE id = $id";
do_log("[UPDATE_TORRENT]: $sql");
$affectedRows = sql_query($sql) or sqlerr(__FILE__, __LINE__);
fire_event("torrent_updated", \App\Models\Torrent::query()->find($id), $torrentOld);
$torrentInfo = \App\Models\Torrent::query()->find($id);
$torrentInfo->extra()->update($extraUpdate);
fire_event("torrent_updated", $torrentInfo, $torrentOld);
$dateTimeStringNow = date("Y-m-d H:i:s");
/**
+14 -4
View File
@@ -330,8 +330,8 @@ $insert = [
'type' => $type,
'url' => $url,
'small_descr' => $small_descr,
'descr' => $descr,
'ori_descr' => $descr,
// 'descr' => $descr,
// 'ori_descr' => $descr,
'category' => $catid,
'source' => $sourceid,
'medium' => $mediumid,
@@ -346,12 +346,21 @@ $insert = [
'last_action' => $dateTimeStringNow,
'nfo' => $nfo,
'info_hash' => $infohash,
'pt_gen' => $_POST['pt_gen'] ?? '',
'technical_info' => $_POST['technical_info'] ?? '',
// 'pt_gen' => $_POST['pt_gen'] ?? '',
// 'technical_info' => $_POST['technical_info'] ?? '',
'cover' => $cover,
'pieces_hash' => sha1($info['pieces']),
'cache_stamp' => time(),
];
/**
* migrate to extra table and remove pt_gen field
* @since 1.9
*/
$extra = [
'descr' => $descr,
'media_info' => $_POST['technical_info'] ?? '',
'nfo' => $nfo,
];
if (isset($_POST['hr'][$catmod]) && isset(\App\Models\Torrent::$hrStatus[$_POST['hr'][$catmod]]) && user_can('torrent_hr')) {
$insert['hr'] = $_POST['hr'][$catmod];
}
@@ -432,6 +441,7 @@ if (!empty($tagIdArr)) {
foreach ($filelist as $file) {
@sql_query("INSERT INTO files (torrent, filename, size) VALUES ($id, ".sqlesc($file[0]).",".$file[1].")");
}
\App\Models\TorrentExtra::query()->create($extra);
//===add karma
KPS("+",$uploadtorrent_bonus,$CURUSER["id"]);
+90 -14
View File
@@ -844,17 +844,16 @@ EOD;
if ($CURUSER['privacy'] != $privacy) $privacyupdated = 1;
$user = $CURUSER["id"];
$query = sprintf("UPDATE users SET " . implode(",", $updateset) . " WHERE id ='%s'",
mysql_real_escape_string($user));
$result = sql_query($query);
if (!$result)
sqlerr(__FILE__,__LINE__);
if (!empty($_REQUEST['resetauthkey']) && $_REQUEST['resetauthkey'] == 1) {
//reset authkey
$torrentRep = new \App\Repositories\TorrentRepository();
$torrentRep->resetTrackerReportAuthKeySecret($user);
}
\Nexus\Database\NexusDB::transaction(function () use ($user, $updateset) {
$query = sprintf("UPDATE users SET " . implode(",", $updateset) . " WHERE id ='%s'", mysql_real_escape_string($user));
sql_query($query);
if (!empty($_REQUEST['resetauthkey']) && $_REQUEST['resetauthkey'] == 1) {
//reset authkey
$torrentRep = new \App\Repositories\TorrentRepository();
$torrentRep->resetTrackerReportAuthKeySecret($user);
}
do_action("usercp_security_update", $_POST);
});
$to = "usercp.php?action=security&type=saved";
if ($changedemail == 1)
$to .= "&mail=1";
@@ -891,7 +890,8 @@ EOD;
print("<input type=\"hidden\" name=\"two_step_secret\" value=\"$two_step_secret\">");
print("<input type=\"hidden\" name=\"two_step_code\" value=\"$two_step_code\">");
Print("<tr><td class=\"rowhead nowrap\" valign=\"top\" align=\"right\" width=1%>".$lang_usercp['row_security_check']."</td><td valign=\"top\" align=\"left\" width=\"99%\"><input type=password name=oldpassword style=\"width: 200px\"><br /><font class=small>".$lang_usercp['text_security_check_note']."</font></td></tr>\n");
submit();
do_action("usercp_security_update_confirm", $_POST);
submit();
print("</table>");
stdfoot();
die;
@@ -900,7 +900,7 @@ EOD;
print("<tr><td colspan=2 class=\"heading\" valign=\"top\" align=\"center\"><font color=red>".$lang_usercp['text_saved'].($_GET["mail"] == "1" ? $lang_usercp['std_confirmation_email_sent'] : "")." ".($_GET["passkey"] == "1" ? $lang_usercp['std_passkey_reset'] : "")." ".($_GET["password"] == "1" ? $lang_usercp['std_password_changed'] : "")." ".($_GET["privacy"] == "1" ? $lang_usercp['std_privacy_level_updated'] : "")."</font></td></tr>\n");
form ("security");
tr_small($lang_usercp['row_reset_passkey'],"<input type=checkbox name=resetpasskey value=1 />".$lang_usercp['checkbox_reset_my_passkey']."<br /><font class=small>".$lang_usercp['text_reset_passkey_note']."</font>", 1);
tr_small($lang_usercp['row_reset_authkey'],"<input type=checkbox name=resetauthkey value=1 />".$lang_usercp['checkbox_reset_my_authkey']."<br /><font class=small>".$lang_usercp['text_reset_authkey_note']."</font>", 1);
// tr_small($lang_usercp['row_reset_authkey'],"<input type=checkbox name=resetauthkey value=1 />".$lang_usercp['checkbox_reset_my_authkey']."<br /><font class=small>".$lang_usercp['text_reset_authkey_note']."</font>", 1);
//two step authentication
if (!empty($CURUSER['two_step_secret'])) {
@@ -926,7 +926,8 @@ EOD;
if ($disableemailchange != 'no' && $smtptype != 'none') //system-wide setting
tr_small($lang_usercp['row_email_address'], "<input type=\"text\" name=\"email\" style=\"width: 200px\" value=\"" . htmlspecialchars($CURUSER["email"]) . "\" /> <br /><font class=small>".$lang_usercp['text_email_address_note']."</font>", 1);
tr_small($lang_usercp['row_change_password'], "<input type=\"password\" name=\"chpassword\" style=\"width: 200px\" />", 1);
do_action("usercp_security_setting_form");
tr_small($lang_usercp['row_change_password'], "<input type=\"password\" name=\"chpassword\" style=\"width: 200px\" />", 1);
tr_small($lang_usercp['row_type_password_again'], "<input type=\"password\" name=\"passagain\" style=\"width: 200px\" />", 1);
tr_small($lang_usercp['row_privacy_level'], priv("normal", $lang_usercp['radio_normal']) . " " . priv("low", $lang_usercp['radio_low']) . " " . priv("strong", $lang_usercp['radio_strong']), 1);
submit();
@@ -1119,6 +1120,81 @@ JS;
}
//end seed box
//token start
$token = '';
$tokenLabel = nexus_trans("token.label");
$columnName = nexus_trans('label.name');
$columnCreatedAt = nexus_trans('label.created_at');
$actionCreate = nexus_trans('label.create');
//$res = \App\Models\SeedBoxRecord::query()->where('uid', $CURUSER['id'])->where('type', \App\Models\SeedBoxRecord::TYPE_USER)->get();
//if ($res->count() > 0)
//{
// $seedBox .= "<table border='1' cellspacing='0' cellpadding='5' id='seed-box-table'><tr><td class='colhead'>ID</td><td class='colhead'>{$columnOperator}</td><td class='colhead'>{$columnBandwidth}</td><td class='colhead'>{$columnIP}</td><td class='colhead'>{$columnComment}</td><td class='colhead'>{$columnStatus}</td><td class='colhead'></td></tr>";
// foreach ($res as $seedBoxRecord)
// {
// $seedBox .= "<tr>";
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->id);
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->operator);
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->bandwidth ?: '');
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->ip ?: sprintf('%s ~ %s', $seedBoxRecord->ip_begin, $seedBoxRecord->ip_end));
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->comment);
// $seedBox .= sprintf('<td>%s</td>', $seedBoxRecord->statusText);
// $seedBox .= sprintf('<td><img style="cursor: pointer" class="staff_delete remove-seed-box-btn" src="pic/trans.gif" alt="D" title="%s" data-id="%s"></td>', $lang_functions['text_delete'], $seedBoxRecord->id);
// $seedBox .= "</tr>";
// }
// $seedBox .= '</table>';
//}
$token .= sprintf('<div><input type="button" id="add-token-box-btn" value="%s"/></div>', $actionCreate);
tr_small($tokenLabel, $token, 1);
$tokenFoxForm = <<<FORM
<div class="form-box">
<form id="token-box-form">
<div class="form-control-row">
<div class="label">{$columnName}</div>
<div class="field"><input type="text" name="params[name]"></div>
</div>
</form>
</div>
FORM;
$tokenBoxJs = <<<JS
jQuery('#add-token-box-btn').on('click', function () {
layer.open({
type: 1,
title: "{$tokenLabel} {$actionCreate}",
content: `$tokenFoxForm`,
btn: ['OK'],
btnAlign: 'c',
yes: function () {
let params = jQuery('#token-box-form').serialize()
jQuery.post('ajax.php', params + "&action=addToken", function (response) {
console.log(response)
if (response.ret != 0) {
layer.alert(response.msg)
return
}
window.location.reload()
}, 'json')
}
})
});
jQuery('#token-box-table').on('click', '.remove-token-box-btn', function () {
let params = {action: "removeToken", params: {id: jQuery(this).attr("data-id")}}
layer.confirm("{$lang_functions['std_confirm_remove']}", {btnAlign: 'c'}, function (index) {
jQuery.post('ajax.php', params, function (response) {
console.log(response)
if (response.ret != 0) {
layer.alert(response.msg)
return
}
window.location.reload()
}, 'json')
})
});
JS;
\Nexus\Nexus::js($tokenBoxJs, 'footer', false);
//token end
if ($forumposts)
tr($lang_usercp['row_forum_posts'], $forumposts." [<a href=\"userhistory.php?action=viewposts&id=".$CURUSER['id']."\" title=\"".$lang_usercp['link_view_posts']."\">".$lang_usercp['text_view']."</a>] (".$dayposts.$lang_usercp['text_posts_per_day']."; ".$percentages.$lang_usercp['text_of_total_posts'].")", 1);
?>
+15 -2
View File
@@ -496,9 +496,22 @@ if (user_can('prfmanage') && $user["class"] < get_user_class())
if (user_can('cruprfmanage'))
{
$modcomment = htmlspecialchars($user["modcomment"]);
$modcomment = \App\Models\UserModifyLog::query()
->where("user_id", $user["id"])
->orderBy("id", "desc")
->limit(20)
->get()
->implode("content", "\n")
;
tr($lang_userdetails['row_comment'], "<textarea cols=\"60\" rows=\"6\" name=\"modcomment\">".$modcomment."</textarea>", 1);
$bonuscomment = htmlspecialchars($user["bonuscomment"]);
$bonuscomment = \App\Models\BonusLogs::query()
->where("uid", $user["id"])
->orderBy("id", "desc")
->limit(20)
->get()
->map(fn ($item) => sprintf("%s - %s", $item->created_at->format("Y-m-d"), $item->comment))
->implode("\n")
;
tr($lang_userdetails['row_seeding_karma'], "<textarea cols=\"60\" rows=\"6\" name=\"bonuscomment\" readonly=\"readonly\">".$bonuscomment."</textarea>", 1);
}
$warned = $user["warned"] == "yes";
+1 -1
View File
@@ -8,7 +8,7 @@ $id = intval($_GET["id"] ?? 0);
if (!user_can('viewnfo') || !is_valid_id($id) || $enablenfo_main != 'yes')
permissiondenied();
$r = sql_query("SELECT name,nfo FROM torrents WHERE id=$id") or sqlerr();
$r = sql_query("SELECT torrents.name, torrent_extras.nfo FROM torrents left join torrent_extras on torrents.id=torrent_extras.torrent_id WHERE torrents.id=$id") or sqlerr();
$a = mysql_fetch_assoc($r) or die($lang_viewnfo['std_puke']);
//error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
+1
View File
@@ -24,6 +24,7 @@ return [
\App\Models\BonusLogs::BUSINESS_TYPE_ROLE_WORK_SALARY => 'Role work salary',
\App\Models\BonusLogs::BUSINESS_TYPE_TORRENT_BE_DOWNLOADED => 'Torrent be downloaded',
\App\Models\BonusLogs::BUSINESS_TYPE_RECEIVE_REWARD => 'Receive reward',
],
'fields' => [
'business_type' => 'Business type',
+1
View File
@@ -26,6 +26,7 @@ return [
\App\Models\BonusLogs::BUSINESS_TYPE_ROLE_WORK_SALARY => '工作组工资',
\App\Models\BonusLogs::BUSINESS_TYPE_TORRENT_BE_DOWNLOADED => '种子被下载',
\App\Models\BonusLogs::BUSINESS_TYPE_RECEIVE_REWARD => '收到奖励',
],
'fields' => [
'business_type' => '业务类型',
+1
View File
@@ -41,6 +41,7 @@ return [
'client' => '客户端',
'reason' => '原因',
'change' => '修改',
'create' => '创建',
'setting' => [
'nav_text' => '设置',
'backup' => [
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
"label" => "访问令牌",
];
+1
View File
@@ -24,6 +24,7 @@ return [
\App\Models\BonusLogs::BUSINESS_TYPE_ROLE_WORK_SALARY => '工作組工資',
\App\Models\BonusLogs::BUSINESS_TYPE_TORRENT_BE_DOWNLOADED => '種子被下載',
\App\Models\BonusLogs::BUSINESS_TYPE_RECEIVE_REWARD => '收到獎勵',
],
'fields' => [
'business_type' => '業務類型',
@@ -0,0 +1,3 @@
<x-filament-panels::page>
@livewire('install-plugin-modal')
</x-filament-panels::page>
@@ -0,0 +1,6 @@
<div>
<h3 class="text-danger-700 pb-6">注意: 执行此操作会先将站点启用维护模式, 执行完毕后会关闭维护模式!</h3>
<div class="code">
<textarea readonly style="width: 100%;min-height: 200px"></textarea>
</div>
</div>
@@ -0,0 +1,39 @@
<div>
<h3 class="text-danger-700 pb-6">注意: 执行此操作会先将站点启用维护模式, 执行完毕后会关闭维护模式!</h3>
<textarea id="output" rows="10" class="w-full border rounded p-2" readonly></textarea>
<div class="mt-4">
<button id="btn-run" class="fi-btn">点击执行</button>
</div>
</div>
<script>
document.getElementById('btn-run').addEventListener('click', () => {
document.getElementById('output').textContent = ''; // 清空之前的输出
const es = new EventSource('/admin/sse?plugin_id=tgbot&action=install_plugin');
const textarea = document.getElementById("output")
es.onmessage = (event) => {
console.log("onmessage: ", event.data)
if (event.data === "close") {
console.log("close es ...")
es.close();
return
}
textarea.textContent += event.data + "\n"
textarea.scrollTop = textarea.scrollHeight
}
es.onerror = (event) => {
console.log("onerror: ", event)
switch (es.readyState) {
case EventSource.CONNECTING:
console.log("Reconnecting...");
break;
case EventSource.CLOSED:
console.log("Connection closed.");
break;
}
es.close()
}
});
</script>
+4
View File
@@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get("/sse", [\App\Http\Controllers\ServerSendEventController::class, "sse"]);
-5
View File
@@ -1,5 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('announce', [\App\Http\Controllers\TrackerController::class, 'announce']);
Route::get('scrape', [\App\Http\Controllers\TrackerController::class, 'scrape']);