diff --git a/.env.example b/.env.example index e4a068b7..670e5402 100644 --- a/.env.example +++ b/.env.example @@ -98,3 +98,10 @@ FORCE_SCHEME= CROWDIN_ACCESS_TOKEN= CROWDIN_PROJECT_ID= + +CLICKHOUSE_HOST= +CLICKHOUSE_HTTP_PORT= +CLICKHOUSE_TCP_PORT= +CLICKHOUSE_USER= +CLICKHOUSE_PASSWORD= +CLICKHOUSE_DATABASE= diff --git a/app/Enums/AnnounceEventEnum.php b/app/Enums/AnnounceEventEnum.php new file mode 100644 index 00000000..4522ca25 --- /dev/null +++ b/app/Enums/AnnounceEventEnum.php @@ -0,0 +1,26 @@ + nexus_trans("announce_log.events.started"), + self::STOPPED => nexus_trans("announce_log.events.stopped"), + self::PAUSED => nexus_trans("announce_log.events.paused"), + self::COMPLETED => nexus_trans("announce_log.events.completed"), + self::NONE => nexus_trans("announce_log.events.none"), + default => '', + }; + } +} diff --git a/app/Filament/Pages/AnnounceMonitor.php b/app/Filament/Pages/AnnounceMonitor.php new file mode 100644 index 00000000..122b0fd5 --- /dev/null +++ b/app/Filament/Pages/AnnounceMonitor.php @@ -0,0 +1,35 @@ +schema([ + Infolists\Components\TextEntry::make('timestamp')->label(__('announce-log.timestamp')), + Infolists\Components\TextEntry::make('request_id')->label(__('announce-log.request_id'))->copyable(), + Infolists\Components\TextEntry::make('user_id')->label(__('announce-log.user_id'))->copyable(), + + + Infolists\Components\TextEntry::make('torrent_id')->label(__('announce-log.torrent_id'))->copyable(), + Infolists\Components\TextEntry::make('torrent_size')->label(__('announce-log.torrent_size'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('peer_id')->label(__('announce-log.peer_id'))->copyable(), + + Infolists\Components\TextEntry::make('announce_time')->label(__('announce-log.announce_time'))->copyable(), + Infolists\Components\TextEntry::make('seeder_count')->label(__('announce-log.seeder_count')), + Infolists\Components\TextEntry::make('leecher_count')->label(__('announce-log.leecher_count')), + + Infolists\Components\TextEntry::make('uploaded_offset')->label(__('announce-log.uploaded_offset'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('uploaded_total')->label(__('announce-log.uploaded_total'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('uploaded_increment')->label(__('announce-log.uploaded_increment'))->formatStateUsing(fn($state) => mksize($state)), + + Infolists\Components\TextEntry::make('downloaded_offset')->label(__('announce-log.downloaded_offset'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('downloaded_total')->label(__('announce-log.downloaded_total'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('downloaded_increment')->label(__('announce-log.downloaded_increment'))->formatStateUsing(fn($state) => mksize($state)), + + Infolists\Components\TextEntry::make('left')->label(__('announce-log.left'))->formatStateUsing(fn($state) => mksize($state)), + Infolists\Components\TextEntry::make('port')->label(__('announce-log.port')), + Infolists\Components\TextEntry::make('agent')->label(__('announce-log.agent')), + + Infolists\Components\TextEntry::make('started')->label(__('announce-log.started')), + Infolists\Components\TextEntry::make('last_action')->label(__('announce-log.last_action')), + Infolists\Components\TextEntry::make('prev_action')->label(__('announce-log.prev_action')), + + + Infolists\Components\TextEntry::make('scheme')->label(__('announce-log.scheme')), + Infolists\Components\TextEntry::make('host')->label(__('announce-log.host')), + Infolists\Components\TextEntry::make('path')->label(__('announce-log.path')), + Infolists\Components\TextEntry::make('ip')->label(__('announce-log.ip'))->copyable(), + Infolists\Components\TextEntry::make('ipv4')->label(__('announce-log.ipv4'))->copyable(), + Infolists\Components\TextEntry::make('ipv6')->label(__('announce-log.ipv6'))->copyable(), + Infolists\Components\TextEntry::make('continent')->label(__('announce-log.continent')), + Infolists\Components\TextEntry::make('country')->label(__('announce-log.country')), + Infolists\Components\TextEntry::make('city')->label(__('announce-log.city')), + + Infolists\Components\TextEntry::make('event')->label(__('announce-log.event')), + Infolists\Components\TextEntry::make('passkey')->label(__('announce-log.passkey'))->copyable(), + Infolists\Components\TextEntry::make('client_select')->label(__('announce-log.client_select')), + ])->columns(3); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('timestamp')->label(__('announce-log.timestamp'))->sortable(), + Tables\Columns\TextColumn::make('user_id')->label(__('announce-log.user_id')), + Tables\Columns\TextColumn::make('torrent_id')->label(__('announce-log.torrent_id')), + Tables\Columns\TextColumn::make('peer_id')->label(__('announce-log.peer_id')), + Tables\Columns\TextColumn::make('torrent_size') + ->label(__('announce-log.torrent_size')) + ->formatStateUsing(fn ($state): string => mksize($state)) + , + Tables\Columns\TextColumn::make('uploaded_total') + ->label(__('announce-log.uploaded_total')) + ->formatStateUsing(fn ($state): string => mksize($state)) + ->sortable() + , + Tables\Columns\TextColumn::make('uploaded_increment') + ->label(__('announce-log.uploaded_increment')) + ->formatStateUsing(fn ($state): string => mksize($state)) + ->sortable() + , + Tables\Columns\TextColumn::make('downloaded_total') + ->label(__('announce-log.downloaded_total')) + ->formatStateUsing(fn ($state): string => mksize($state)) + ->sortable() + , + Tables\Columns\TextColumn::make('downloaded_increment') + ->label(__('announce-log.downloaded_increment')) + ->formatStateUsing(fn ($state): string => mksize($state)) + ->sortable() + , + Tables\Columns\TextColumn::make('left') + ->label(__('announce-log.left')) + ->formatStateUsing(fn ($state): string => mksize($state)) + ->sortable() + , + Tables\Columns\TextColumn::make('announce_time') + ->label(__('announce-log.announce_time')) + ->sortable() + , + Tables\Columns\TextColumn::make('event')->label(__('announce-log.event')), + Tables\Columns\TextColumn::make('ip')->label('IP'), +// Tables\Columns\TextColumn::make('agent')->label(__('announce-log.agent')), + ]) + ->filters([ + Tables\Filters\Filter::make('user_id') + ->form([ + Forms\Components\TextInput::make('user_id') + ->label(__('announce-log.user_id')) + ->numeric() + , + ]) + , + Tables\Filters\Filter::make('torrent_id') + ->form([ + Forms\Components\TextInput::make('torrent_id') + ->label(__('announce-log.torrent_id')) + ->numeric() + , + ]) + , + Tables\Filters\Filter::make('peer_id') + ->form([ + Forms\Components\TextInput::make('peer_id') + ->label(__('announce-log.peer_id')) + , + ]) + , + Tables\Filters\Filter::make('ip') + ->form([ + Forms\Components\TextInput::make('ip') + ->label('IP') + , + ]) + , + Tables\Filters\Filter::make('event') + ->form([ + Forms\Components\Select::make('event') + ->label(__('announce-log.event')) + ->options(AnnounceLog::listEvents()) + , + ]) + , + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + ]) + ->bulkActions([ +// Tables\Actions\BulkActionGroup::make([ +// Tables\Actions\DeleteBulkAction::make(), +// ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListAnnounceLogs::route('/'), +// 'create' => Pages\CreateAnnounceLog::route('/create'), +// 'edit' => Pages\EditAnnounceLog::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Torrent/AnnounceLogResource/Pages/CreateAnnounceLog.php b/app/Filament/Resources/Torrent/AnnounceLogResource/Pages/CreateAnnounceLog.php new file mode 100644 index 00000000..fc9a1b76 --- /dev/null +++ b/app/Filament/Resources/Torrent/AnnounceLogResource/Pages/CreateAnnounceLog.php @@ -0,0 +1,12 @@ +all()); + $filters = []; + foreach ($request->get('tableFilters', []) as $field => $values) { + if (!in_array($field, $filterableColumns)) { + continue; + } + foreach ($values as $k => $v) { + if (in_array($k, $filterableColumns)) { + $filters[$field] = $v; + } + } + } + $page = $request->get('page', 1); + $perPage = $request->get('per_page', 10); + $sortColumn = null; + $sortDirection = null; + $sortColumnFromQuery = $request->get("tableSortColumn"); + $sortDirectionFromQuery = $request->get("tableSortDirection"); + if (in_array($sortColumnFromQuery, $sortableColumns)) { + $sortColumn = $sortColumnFromQuery; + } + if (in_array($sortDirectionFromQuery, $sortableDirections)) { + $sortDirection = $sortDirectionFromQuery; + } + + $sorts = []; + foreach ($request->input('components', []) as $component) { + $snapshot = json_decode($component['snapshot'], true); +// do_log("snapshot: " . $component['snapshot']); + if (isset($snapshot['data']['tableRecordsPerPage'])) { + $perPage = $snapshot['data']['tableRecordsPerPage']; + } + if (isset($snapshot['data']['tableSortColumn']) && in_array($snapshot['data']['tableSortColumn'], $sortableColumns)) { + $sortColumn = $snapshot['data']['tableSortColumn']; + } + if (isset($snapshot['data']['tableSortDirection']) && in_array($snapshot['data']['tableSortDirection'], $sortableDirections)) { + $sortDirection = $snapshot['data']['tableSortDirection']; + } + if ($sortColumn && $sortDirection) { + $sorts[$sortColumn] = $sortDirection; + } + if (isset($snapshot['data']['paginators'])) { + foreach ($snapshot['data']['paginators'] as $paginator) { + if (isset($paginator['page'])) { + $page = $paginator['page']; + } + } + } + if (isset($snapshot['data']['tableFilters'])) { +// dd($snapshot['data']['tableFilters']); + foreach ($snapshot['data']['tableFilters'] as $filterItems) { + foreach ($filterItems as $field => $items) { + if (!in_array($field, $filterableColumns) || !is_array($items)) { + continue; + } + foreach ($items as $values) { + if (!is_array($values)) { + continue; + } + foreach ($values as $subField => $value) { + if ($field == $subField && $value !== null) { + $filters[$field] = $value; + } + } + } + } + } + } +// do_log("updates: " . json_encode($component['updates'] ?? [])); + if (isset($component['updates']['tableRecordsPerPage'])) { + $perPage = $component['updates']['tableRecordsPerPage']; + } +// do_log("calls: " . json_encode($component['calls'] ?? [])); + if (isset($component['calls'])) { + foreach ($component['calls'] as $call) { + if ($call['method'] == "gotoPage") { + $page = $call['params'][0]; + } + if ($call['method'] == "sortTable") { + if (!in_array($call['params'][0], $sortableColumns)) { + continue; + } + $sortColumn = $call['params'][0]; + if (!isset($sorts[$sortColumn])) { + $sortDirection = "asc"; + } elseif ($sorts[$sortColumn] == "asc") { + $sortDirection = "desc"; + } elseif ($sorts[$sortColumn] == "desc") { + $sortDirection = null; + } + } + if ($call['method'] == "resetTableFiltersForm") { + $filters = []; + } + } + } + foreach ($filterableColumns as $field) { + if (isset($component['updates']["tableFilters.$field.$field"])) { + $filters[$field] = $component['updates']["tableFilters.$field.$field"]; + } + } + + } + $rep = new AnnounceLogRepository(); + $result = $rep->listAll($filters, $page, $perPage, $sortColumn, $sortDirection); + + // 转换数据格式以适配 Filament 表格 + $items = []; + foreach ($result['data'] as $announceLog) { + $model = new AnnounceLog($announceLog); + $items[] = $model; + } + return new LengthAwarePaginator($items, $result['total'], $perPage, $page); + } + + protected function getHeaderActions(): array + { + return [ +// Actions\CreateAction::make(), + ]; + } + + protected function resolveTableRecord(?string $key): ?Model + { + $rep = new AnnounceLogRepository(); + return $rep->getById($key); + } +} diff --git a/app/Filament/Widgets/AnnounceMonitor/MaxUploadedUser.php b/app/Filament/Widgets/AnnounceMonitor/MaxUploadedUser.php new file mode 100644 index 00000000..64ec4e8e --- /dev/null +++ b/app/Filament/Widgets/AnnounceMonitor/MaxUploadedUser.php @@ -0,0 +1,49 @@ +recordTitle("ssss") + ->heading(fn () => __('announce-monitor.max_uploaded_user', ['interval' => ' 1 ' . __('nexus.time_units.hour')])) + ->query(AnnounceLog::query()) + ->defaultPaginationPageOption(null) + ->columns([ + Tables\Columns\TextColumn::make('user_id') + ->label(__('announce-log.user_id')) + ->formatStateUsing(fn ($state) => username_for_admin($state)) + , + Tables\Columns\TextColumn::make('uploaded_total') + ->label(__('announce-log.uploaded_total')) + ->formatStateUsing(fn ($state) => mksize($state)) + , + ]); + } + + public function getTableRecords(): Collection|Paginator|CursorPaginator + { + $rep = new AnnounceLogRepository(); + $list = $rep->listMaxUploadedUser(1); + $items = []; + foreach ($list as $index => $item) { + $record = new AnnounceLog($item); + $record->request_id = $index; + $items[] = $record; + } + return new Collection($items); + } + +} diff --git a/app/Models/AnnounceLog.php b/app/Models/AnnounceLog.php new file mode 100644 index 00000000..ddfc044d --- /dev/null +++ b/app/Models/AnnounceLog.php @@ -0,0 +1,91 @@ +toLocaleTime($value, "Y-m-d H:i:s.u"); + }, + ); + } + + protected function started(): Attribute + { + return Attribute::make( + set: function (?string $value) { + return $this->toLocaleTime($value); + }, + ); + } + + protected function prevAction(): Attribute + { + return Attribute::make( + set: function (?string $value) { + return $this->toLocaleTime($value); + }, + ); + } + + protected function lastAction(): Attribute + { + return Attribute::make( + set: function (string $value) { + return $this->toLocaleTime($value); + }, + ); + } + + private function toLocaleTime(?string $time, string $format = "Y-m-d H:i:s"): ?string + { + static $fromTimezone; + static $toTimezone; + if ($fromTimezone == null) { + $fromTimezone = new DateTimeZone('UTC'); + } + if ($toTimezone == null) { + $toTimezone = new DateTimeZone(config('app.timezone')); + } + if (empty($time)) { + return $time; + } + // 创建 DateTime 对象 + $date = DateTime::createFromFormat($format, $time, $fromTimezone); + // 转换时区 + $date->setTimezone($toTimezone); + // 输出转换后的时间 + return $date->format($format); + } + + public static function listEvents(): array + { + $result = []; + foreach (AnnounceEventEnum::cases() as $event) { + $result[$event->value] = $event->value; + } + return $result; + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 8e5dcedf..3a2f2565 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -258,5 +258,10 @@ class Setting extends NexusModel return (int)self::get("backup.retention_count"); } + public static function getIsRecordAnnounceLog(): bool + { + return self::get('security.record_announce_logs') == 'yes'; + } + } diff --git a/app/Repositories/AnnounceLogRepository.php b/app/Repositories/AnnounceLogRepository.php new file mode 100644 index 00000000..702935e8 --- /dev/null +++ b/app/Repositories/AnnounceLogRepository.php @@ -0,0 +1,77 @@ +getClient(); + $bindFields = $bindValues = []; + foreach ($filters as $key => $value) { + $bindFields[] = "$key = :$key"; + if ($key == "event" && $value == "none") { + $value = ""; + } + $bindValues[$key] = $value; + } + $selectPrefix = sprintf("select * from %s", self::TABLE); + $countPrefix = sprintf("select count(*) as %s from %s", $totalAlias, self::TABLE); + $whereStr = ""; + if (count($bindFields) > 0) { + $whereStr = " where " . implode(" and ", $bindFields); + } + $selectSql = sprintf( + "%s %s order by %s %s limit %d offset %d", + $selectPrefix, $whereStr, $sortColumn ?: "timestamp", $sortDirection ?: "desc", $perPage, $offset + ); + $countSql = sprintf("%s %s", $countPrefix, $whereStr); + $data = $client->select($selectSql, $bindValues); + $total = $client->select($countSql, $bindValues)->rows()[0][$totalAlias] ?? 0; + do_log(sprintf( + "[REQUEST_CLICKHOUSE], filters: %s, page: %s, perPage: %s, sortColumn: %s, sortDirection: %s, selectSql: %s, binds: %s, costTime: %.3f sec.", + json_encode($filters), $page, $perPage, $sortColumn, $sortDirection, $selectSql, json_encode($bindValues), microtime(true) - $beginTimestamp + )); + return [ + 'data' => $data->rows(), + 'total' => (int)$total, + 'page' => $page, + 'perPage' => $perPage, + ]; + } + + private function getClient(): Client + { + return app(Client::class); + } + + public function getById(?string $id): ?AnnounceLog + { + if (empty($id)) { + return null; + } + $sql = sprintf("select * from %s where request_id = :id limit 1", self::TABLE); + $statement = $this->getClient()->select($sql, ['id' => $id]); + $arr = $statement->fetchOne(); + return $arr ? new AnnounceLog($arr) : null; + } + + public function listMaxUploadedUser(int $hours) + { + $sql = sprintf( + "select user_id, sum(uploaded_increment) as uploaded_total from %s where timestamp >= now() - INTERVAL %d HOUR group by user_id order by uploaded_total desc limit 5", + self::TABLE, $hours + ); + $data = $this->getClient()->select($sql); + return $data->rows(); + } + +} diff --git a/app/Repositories/SeedBoxRepository.php b/app/Repositories/SeedBoxRepository.php index 2cc88499..04ea9dfc 100644 --- a/app/Repositories/SeedBoxRepository.php +++ b/app/Repositories/SeedBoxRepository.php @@ -128,7 +128,7 @@ class SeedBoxRepository extends BaseRepository return SeedBoxRecord::query()->whereIn('id', Arr::wrap($id))->where('uid', $uid)->delete(); } - public function updateStatus(SeedBoxRecord $seedBoxRecord, $status, $reason = ''): bool + public function updateStatus(SeedBoxRecord $seedBoxRecord, $status, $reason = '') { if (Auth::user()->class < User::CLASS_ADMINISTRATOR) { throw new InsufficientPermissionException(); diff --git a/composer.json b/composer.json index 5a973a0f..d49f3d2d 100644 --- a/composer.json +++ b/composer.json @@ -31,11 +31,12 @@ "ext-pcntl": "*", "ext-posix": "*", "ext-redis": "*", + "ext-sqlite3": "*", "ext-xml": "*", "ext-zend-opcache": "*", "ext-zip": "*", - "ext-sqlite3": "*", "calebporzio/sushi": "^2.5", + "cybercog/laravel-clickhouse": "dev-master", "elasticsearch/elasticsearch": "^7.16", "filament/filament": "^3.3", "flowframe/laravel-trend": "^0.4", diff --git a/composer.lock b/composer.lock index 9f405f1a..9672ec40 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "784dd3fb3491bc979f55e3014d9010ae", + "content-hash": "785a13847b46eeebda6401298b89855f", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -481,6 +481,84 @@ ], "time": "2025-03-06T14:30:56+00:00" }, + { + "name": "cybercog/laravel-clickhouse", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/cybercog/laravel-clickhouse.git", + "reference": "ba90e0916ab0b7af594fa6c3874de9091afbd923" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cybercog/laravel-clickhouse/zipball/ba90e0916ab0b7af594fa6c3874de9091afbd923", + "reference": "ba90e0916ab0b7af594fa6c3874de9091afbd923", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.1.3|^11.0|^12.0", + "illuminate/contracts": "^8.0|^9.0|^10.1.3|^11.0|^12.0", + "illuminate/filesystem": "^8.0|^9.0|^10.1.3|^11.0|^12.0", + "illuminate/support": "^8.0|^9.0|^10.1.3|^11.0|^12.0", + "php": "^7.4|^8.0", + "smi2/phpclickhouse": "^1.5.3" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.6|^10.5|^11.5" + }, + "default-branch": true, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Cog\\Laravel\\Clickhouse\\ClickhouseServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Cog\\Laravel\\Clickhouse\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Komarev", + "email": "anton@komarev.com", + "homepage": "https://komarev.com", + "role": "Developer" + } + ], + "description": "ClickHouse migrations for Laravel", + "homepage": "https://komarev.com/sources/laravel-clickhouse", + "keywords": [ + "clickhouse", + "cog", + "cybercog", + "database", + "db", + "laravel", + "migration" + ], + "support": { + "docs": "https://github.com/cybercog/laravel-clickhouse", + "email": "open@cybercog.su", + "issues": "https://github.com/cybercog/laravel-clickhouse/issues", + "source": "https://github.com/cybercog/laravel-clickhouse", + "wiki": "https://github.com/cybercog/laravel-clickhouse/wiki" + }, + "funding": [ + { + "url": "https://paypal.me/antonkomarev", + "type": "custom" + } + ], + "time": "2025-06-16T07:24:59+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -6863,6 +6941,65 @@ ], "time": "2025-02-25T09:09:36+00:00" }, + { + "name": "smi2/phpclickhouse", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/smi2/phpClickHouse.git", + "reference": "f79dfb798df96185beff90891efda997b01eb51b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smi2/phpClickHouse/zipball/f79dfb798df96185beff90891efda997b01eb51b", + "reference": "f79dfb798df96185beff90891efda997b01eb51b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": "^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9.5", + "sebastian/comparator": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ClickHouseDB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Strykhar", + "email": "isublimity@gmail.com", + "homepage": "https://github.com/isublimity" + } + ], + "description": "PHP ClickHouse Client", + "homepage": "https://github.com/smi2/phpClickHouse", + "keywords": [ + "clickhouse", + "client", + "curl", + "driver", + "http", + "http client", + "php" + ], + "support": { + "issues": "https://github.com/smi2/phpClickHouse/issues", + "source": "https://github.com/smi2/phpClickHouse/tree/1.6.0" + }, + "time": "2025-01-15T07:04:59+00:00" + }, { "name": "spatie/color", "version": "1.8.0", @@ -12867,6 +13004,7 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { + "cybercog/laravel-clickhouse": 20, "phpgangsta/googleauthenticator": 20 }, "prefer-stable": true, @@ -12884,10 +13022,10 @@ "ext-pcntl": "*", "ext-posix": "*", "ext-redis": "*", + "ext-sqlite3": "*", "ext-xml": "*", "ext-zend-opcache": "*", - "ext-zip": "*", - "ext-sqlite3": "*" + "ext-zip": "*" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/config/clickhouse.php b/config/clickhouse.php new file mode 100644 index 00000000..0ec71cca --- /dev/null +++ b/config/clickhouse.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +return [ + + /* + |-------------------------------------------------------------------------- + | ClickHouse Client Configuration + |-------------------------------------------------------------------------- + | + | Here you can configure a connection to connect to the ClickHouse + | database and specify additional configuration options. + | + */ + + 'connection' => [ + 'host' => env('CLICKHOUSE_HOST', 'localhost'), + 'port' => env('CLICKHOUSE_HTTP_PORT', 8123), + 'username' => env('CLICKHOUSE_USER', 'default'), + 'password' => env('CLICKHOUSE_PASSWORD', ''), + 'options' => [ + 'database' => env('CLICKHOUSE_DATABASE', 'default'), + 'timeout' => 1, + 'connectTimeOut' => 2, + ], + ], + + /* + |-------------------------------------------------------------------------- + | ClickHouse Migration Settings + |-------------------------------------------------------------------------- + */ + + 'migrations' => [ + 'table' => env('CLICKHOUSE_MIGRATION_TABLE', 'migrations'), + 'path' => database_path('clickhouse-migrations'), + ], +]; diff --git a/database/clickhouse-migrations/2025_06_22_221711_create_announce_logs_table.php b/database/clickhouse-migrations/2025_06_22_221711_create_announce_logs_table.php new file mode 100644 index 00000000..7d1fe137 --- /dev/null +++ b/database/clickhouse-migrations/2025_06_22_221711_create_announce_logs_table.php @@ -0,0 +1,58 @@ +clickhouseClient->write( + <<getImageUrl($row['location']); + if ($driver == "local") { + if ($row['thumb'] == 1){ + $url = $httpdirectory_attachment."/".$row['location'].".thumb.jpg"; + } else { + $url = $httpdirectory_attachment."/".$row['location']; + } + } else { + $url = \Nexus\Attachment\Storage::getDriver($driver)->getImageUrl($row['location']); + } do_log(sprintf("driver: %s, location: %s, url: %s", $driver, $row['location'], $url)); if($imageresizer == true) $onclick = " onclick=\"Previewurl('".$url."')\""; @@ -1993,6 +1994,7 @@ function userlogin() { $oldip = $row['ip']; $row['ip'] = $ip; + $row['seedbonus'] = floatval($row['seedbonus']); $GLOBALS["CURUSER"] = $row; if (isset($_GET['clearcache']) && $_GET['clearcache'] && get_user_class() >= UC_MODERATOR) { $Cache->setClearCache(1); @@ -5816,27 +5818,14 @@ function can_access_torrent($torrent, $uid) function get_ip_location_from_geoip($ip): bool|array { - $database = nexus_env('GEOIP2_DATABASE'); - if (empty($database)) { - do_log("no geoip2 database."); - return false; - } - if (!is_readable($database)) { - do_log("geoip2 database: $database is not readable."); - return false; - } - static $reader; - if (is_null($reader)) { - $reader = new \GeoIp2\Database\Reader($database); - } - $lang = get_langfolder_cookie(); - $langMap = [ - 'chs' => 'zh-CN', - 'cht' => 'zh-CN', - 'en' => 'en', - ]; - $locale = $langMap[$lang] ?? $lang; - $locationInfo = \Nexus\Database\NexusDB::remember("locations_{$ip}", 3600, function () use ($locale, $ip, $reader) { + $locationInfo = \Nexus\Database\NexusDB::remember("locations_{$ip}", 3600, function () use ($ip) { + $lang = get_langfolder_cookie(); + $langMap = [ + 'chs' => 'zh-CN', + 'cht' => 'zh-CN', + 'en' => 'en', + ]; + $locale = $langMap[$lang] ?? $lang; $info = [ 'ip' => $ip, 'version' => '', @@ -5846,9 +5835,20 @@ function get_ip_location_from_geoip($ip): bool|array 'city_en' => '', ]; try { + $database = nexus_env('GEOIP2_DATABASE'); + if (empty($database)) { + do_log("no geoip2 database."); + return false; + } + if (!is_readable($database)) { + do_log("geoip2 database: $database is not readable."); + return false; + } + $reader = new \GeoIp2\Database\Reader($database); $record = $reader->city($ip); $countryName = $record->country->names[$locale] ?? $record->country->names['en'] ?? ''; $cityName = $record->city->names[$locale] ?? $record->city->names['en'] ?? ''; + $continentName = $record->continent->names[$locale] ?? $record->continent->names['en'] ?? ''; if (isIPV4($ip)) { $info['version'] = 4; } elseif (isIPV6($ip)) { @@ -5858,13 +5858,17 @@ function get_ip_location_from_geoip($ip): bool|array $info['country_en'] = $record->country->names['en'] ?? ''; $info['city'] = $cityName; $info['city_en'] = $record->city->names['en'] ?? ''; - + $info['continent'] = $continentName; + $info['continent_en'] = $record->continent->names['en'] ?? ''; } catch (\Exception $exception) { do_log($exception->getMessage() . $exception->getTraceAsString(), 'error'); } return $info; }); - do_log("ip: $ip, locale: $locale, result: " . nexus_json_encode($locationInfo)); + do_log("ip: $ip, result: " . nexus_json_encode($locationInfo)); + if ($locationInfo === false) { + return false; + } $name = sprintf('%s[v%s]', $locationInfo['city'] ? ($locationInfo['city'] . "·" . $locationInfo['country']) : $locationInfo['country'], $locationInfo['version']); return [ 'name' => $name, @@ -5876,6 +5880,7 @@ function get_ip_location_from_geoip($ip): bool|array 'ip_version' => $locationInfo['version'], 'country_en' => $locationInfo['country_en'], 'city_en' => $locationInfo['city_en'], + 'continent_en' => $locationInfo['continent_en'], ]; } diff --git a/lang/chs/lang_settings.php b/lang/chs/lang_settings.php index aaba2865..0d7e74f5 100644 --- a/lang/chs/lang_settings.php +++ b/lang/chs/lang_settings.php @@ -816,6 +816,8 @@ $lang_settings = array 'text_use_challenge_response_authentication_note' => '如果启用,登录时将不传输明文密码,建议启用。未来版本会删除此配置且启用此功能。', 'row_complain_enabled' => '启用申诉', 'row_complain_enabled_note' => '默认: "yes"', + 'row_record_announce_logs' => '记录汇报日志', + 'text_record_announce_logs_note' => '要启用,请先安装并启动 ClickHouse,并在 .env 文件中添加配置', ); ?> diff --git a/nexus/Install/Update.php b/nexus/Install/Update.php index 9b502922..4b996fc6 100644 --- a/nexus/Install/Update.php +++ b/nexus/Install/Update.php @@ -378,7 +378,12 @@ class Update extends Install ]); NexusDB::cache_del("nexus_plugin_store_all"); } - + /** + * @since 1.9.7 + */ + if (env("CLICKHOUSE_HOST")) { + Artisan::call("clickhouse:migrate"); + } } public function runExtraMigrate() diff --git a/nexus/Torrent/TechnicalInformation.php b/nexus/Torrent/TechnicalInformation.php index d2d3102e..2b454e1e 100644 --- a/nexus/Torrent/TechnicalInformation.php +++ b/nexus/Torrent/TechnicalInformation.php @@ -20,12 +20,12 @@ class TechnicalInformation $result = []; $parentKey = ""; foreach ($arr as $key => $value) { - $value = trim($value); + $value = $this->trim($value); if (empty($value)) { continue; } $rowKeyValue = explode(':', $value); - $rowKeyValue = array_filter(array_map('trim', $rowKeyValue)); + $rowKeyValue = array_filter(array_map([$this, 'trim'], $rowKeyValue)); if (count($rowKeyValue) == 1) { $parentKey = $rowKeyValue[0]; } elseif (count($rowKeyValue) == 2) { @@ -39,6 +39,11 @@ class TechnicalInformation } + private function trim(string $value): string + { + return trim($value, " \n\r\t\v\0\u{A0}"); + } + public function getRuntime() { return $this->mediaInfoArr['General']['Duration'] ?? ''; diff --git a/public/login.php b/public/login.php index d69f6375..893e748e 100644 --- a/public/login.php +++ b/public/login.php @@ -64,6 +64,19 @@ if (!$useChallengeResponseAuthentication) {
'); +//$script = <<--> +
+ \n"); } diff --git a/public/mybonus.php b/public/mybonus.php index 08f67e17..11732a97 100644 --- a/public/mybonus.php +++ b/public/mybonus.php @@ -369,6 +369,7 @@ for ($i=0; $i < count($allBonus); $i++) ) { continue; } + $bonusarrray['points'] = floatval($bonusarray['points']); print(""); print("
"); diff --git a/public/settings.php b/public/settings.php index b310383e..71a252f4 100644 --- a/public/settings.php +++ b/public/settings.php @@ -199,7 +199,7 @@ elseif ($action == 'savesettings_security') // save security $validConfig = array( 'securelogin', 'securetracker', 'https_announce_url','iv','maxip','maxloginattempts','changeemail','cheaterdet','nodetect', 'guest_visit_type', 'guest_visit_value_static_page', 'guest_visit_value_custom_content', 'guest_visit_value_redirect', - 'login_type', 'login_secret_lifetime', 'use_challenge_response_authentication' + 'login_type', 'login_secret_lifetime', 'use_challenge_response_authentication', 'record_announce_logs' ); GetVar($validConfig); $SECURITY = []; @@ -368,7 +368,8 @@ elseif ($action == 'securitysettings') //security settings print(""); print (""); tr($lang_settings['row_enable_ssl']," ".$lang_settings['text_yes']. " ".$lang_settings['text_no']. " ".$lang_settings['text_optional']."
".$lang_settings['text_ssl_note'], 1); - tr($lang_settings['row_enable_ssl_tracker']," ".$lang_settings['text_yes']. " ".$lang_settings['text_no']. " ".$lang_settings['text_optional']."
".$lang_settings['text_ssl_note'], 1); +// tr($lang_settings['row_enable_ssl_tracker']," ".$lang_settings['text_yes']. " ".$lang_settings['text_no']. " ".$lang_settings['text_optional']."
".$lang_settings['text_ssl_note'], 1); + tr($lang_settings['row_record_announce_logs']," ".$lang_settings['text_yes']. " ".$lang_settings['text_no']."
".$lang_settings['text_record_announce_logs_note'], 1); // tr($lang_settings['row_https_announce_url']," ".$lang_settings['text_https_announce_url_note'] . $_SERVER["HTTP_HOST"]."/announce.php", 1); yesorno($lang_settings['row_enable_image_verification'], 'iv', $SECURITY["iv"], $lang_settings['text_image_verification_note']); yesorno($lang_settings['row_allow_email_change'], 'changeemail', $SECURITY["changeemail"], $lang_settings['text_email_change_note']); diff --git a/resources/lang/zh_CN/admin.php b/resources/lang/zh_CN/admin.php index 2728e957..ba845a8e 100644 --- a/resources/lang/zh_CN/admin.php +++ b/resources/lang/zh_CN/admin.php @@ -46,6 +46,8 @@ return [ 'user_modify_logs' => '修改记录', 'message_templates' => '消息模板', 'tracker_url' => 'Tracker URL', + 'announce_logs' => '汇报记录', + 'announce_monitor' => '汇报监控', ], 'resources' => [ 'agent_allow' => [ diff --git a/resources/lang/zh_CN/announce-log.php b/resources/lang/zh_CN/announce-log.php new file mode 100644 index 00000000..3768edfa --- /dev/null +++ b/resources/lang/zh_CN/announce-log.php @@ -0,0 +1,47 @@ + '汇报时间', + 'request_id' => '请求 ID', + 'uploaded_total' => '累计上传量', + 'uploaded_increment' => '上传增量', + 'uploaded_offset' => '上传起始量', + 'downloaded_total' => '累计下载量', + 'downloaded_increment' => '下载增量', + 'downloaded_offset' => '下载起始量', + 'left' => '剩余量', + 'seeder' => '做种', + 'leecher' => '下载', + 'announce_time' => '时间间隔', + 'agent' => '客户端', + 'user_id' => '用户 ID', + 'passkey' => '用户 Passkey', + 'torrent_id' => '种子 ID', + 'peer_id' => 'Peer ID', + 'event' => '事件', + 'ip' => 'IP', + 'ipv4' => 'IPV4', + 'ipv6' => 'IPV6', + 'port' => '端口', + 'started' => '开始时间', + 'prev_action' => '上次汇报', + 'last_action' => '最近汇报', + 'seeder_count' => '做种数', + 'leecher_count' => '下载数', + 'scheme' => '汇报协议', + 'host' => '汇报主机', + 'path' => '汇报路径', + 'continent' => '洲', + 'country' => '国家', + 'city' => '城市', + 'show_client_error' => '是否客户端错误', + 'client_select' => '客户端 ID', + 'torrent_size' => '种子体积', + 'events' => [ + 'started' => '开始', + 'stopped' => '停止', + 'paused' => '暂停', + 'completed' => '完成', + 'none' => '无', + ], +]; diff --git a/resources/lang/zh_CN/announce-monitor.php b/resources/lang/zh_CN/announce-monitor.php new file mode 100644 index 00000000..e6279818 --- /dev/null +++ b/resources/lang/zh_CN/announce-monitor.php @@ -0,0 +1,5 @@ + '最近:interval最多上传', +]; diff --git a/resources/lang/zh_CN/nexus.php b/resources/lang/zh_CN/nexus.php index 217935d8..8090b22a 100644 --- a/resources/lang/zh_CN/nexus.php +++ b/resources/lang/zh_CN/nexus.php @@ -7,6 +7,7 @@ return [ 'user_not_exists' => '(无此帐户)', 'time_units' => [ 'week' => '周', + 'hour' => '小时', ], 'select_all' => '全选', 'unselect_all' => '全不选', diff --git a/resources/lang/zh_TW/admin.php b/resources/lang/zh_TW/admin.php index 69834a0a..acb51469 100644 --- a/resources/lang/zh_TW/admin.php +++ b/resources/lang/zh_TW/admin.php @@ -48,6 +48,8 @@ return [ 'user_modify_logs' => '修改記錄', 'message_templates' => '消息模板', 'tracker_url' => 'Tracker URL', + 'announce_logs' => '匯報記錄', + 'announce_monitor' => '匯報監控', ], 'resources' => [ 'agent_allow' => [ diff --git a/resources/lang/zh_TW/announce-log.php b/resources/lang/zh_TW/announce-log.php new file mode 100644 index 00000000..3cdd7743 --- /dev/null +++ b/resources/lang/zh_TW/announce-log.php @@ -0,0 +1,47 @@ + '匯報時間', + 'request_id' => '請求 ID', + 'uploaded_total' => '累計上傳量', + 'uploaded_increment' => '上傳增量', + 'uploaded_offset' => '上傳起始量', + 'downloaded_total' => '累計下載量', + 'downloaded_increment' => '下載增量', + 'downloaded_offset' => '下載起始量', + 'left' => '剩餘量', + 'seeder' => '做種', + 'leecher' => '下載', + 'announce_time' => '時間間隔', + 'agent' => '客戶端', + 'user_id' => '用戶 ID', + 'passkey' => '用戶 Passkey', + 'torrent_id' => '種子 ID', + 'peer_id' => 'Peer ID', + 'event' => '事件', + 'ip' => 'IP', + 'ipv4' => 'IPV4', + 'ipv6' => 'IPV6', + 'port' => '端口', + 'started' => '開始時間', + 'prev_action' => '上次匯報', + 'last_action' => '最近匯報', + 'seeder_count' => '做種數', + 'leecher_count' => '下載數', + 'scheme' => '匯報協議', + 'host' => '匯報主機', + 'path' => '匯報路徑', + 'continent' => '洲', + 'country' => '國家', + 'city' => '城市', + 'show_client_error' => '是否客戶端錯誤', + 'client_select' => '客戶端 ID', + 'torrent_size' => '種子體積', + 'events' => [ + 'started' => '開始', + 'stopped' => '停止', + 'paused' => '暫停', + 'completed' => '完成', + 'none' => '無', + ], +]; diff --git a/resources/lang/zh_TW/announce-monitor.php b/resources/lang/zh_TW/announce-monitor.php new file mode 100644 index 00000000..198a2eb3 --- /dev/null +++ b/resources/lang/zh_TW/announce-monitor.php @@ -0,0 +1,5 @@ + '最近:interval最多上傳', +];