diff --git a/app/Stats/User.php b/app/Stats/User.php index 4bb45ad4b..0bde26f4f 100644 --- a/app/Stats/User.php +++ b/app/Stats/User.php @@ -62,6 +62,17 @@ class User extends \Gazelle\BaseObject { return $this->commentTotal[$page] ?? 0; } + public function historyUseragentTracker(): array { + $result = $this->pg()->executeParams(' + select useragent, total + from history_useragent_tracker + where id_user = $1 + order by useragent + ', $this->id + ); + return $result->fetchAll(\PGSQL_ASSOC); + } + /** * @see \Gazelle\Stats\Users::refresh() */ diff --git a/app/Stats/Users.php b/app/Stats/Users.php index 853013210..e4fc7899c 100644 --- a/app/Stats/Users.php +++ b/app/Stats/Users.php @@ -678,6 +678,30 @@ class Users extends \Gazelle\Base { return $processed; } + public function refreshUseragentTracker(): int { + // NB: useragents may be null if the initial announce from ocelot + // was lost, e.g. because of a deadlock or queue overflow. + $result = $this->pg()->execute(" + merge into history_useragent_tracker hut using ( + select xfu.uid as id_user, + coalesce(xfu.useragent, '') as useragent, + count(*) as total + from relay.xbt_files_users xfu + group by xfu.uid, xfu.useragent + ) as i on hut.id_user = i.id_user + and hut.useragent = i.useragent + when not matched and i.total > 0 then + insert ( id_user, useragent, total) + values (i.id_user, i.useragent, i.total) + when matched and i.total > 0 then + update set + total = i.total + when matched then + delete + "); + return $result->getAffectedRows(); + } + public function registerActivity(string $tableName, int $days): int { if ($days > 0) { self::$db->prepared_query(" diff --git a/app/Task/CommunityStats.php b/app/Task/CommunityStats.php index 567b1999f..08ea18c98 100644 --- a/app/Task/CommunityStats.php +++ b/app/Task/CommunityStats.php @@ -2,9 +2,14 @@ namespace Gazelle\Task; +use Gazelle\Stats\Users as UsersStats; +use Gazelle\Stats\TGroups as TGroupsStats; + class CommunityStats extends \Gazelle\Task { public function run(): void { - $this->processed = new \Gazelle\Stats\Users()->refresh() - + new \Gazelle\Stats\TGroups()->refresh(); + $usersStats = new UsersStats(); + $this->processed = new TGroupsStats()->refresh() + + $usersStats->refresh() + + $usersStats->refreshUseragentTracker(); } } diff --git a/app/User.php b/app/User.php index 8b7adb9df..26ace20e3 100644 --- a/app/User.php +++ b/app/User.php @@ -1389,14 +1389,6 @@ class User extends BaseAttrObject { return $new; } - public function clients(): array { - self::$db->prepared_query(' - SELECT DISTINCT useragent FROM xbt_files_users WHERE uid = ? - ', $this->id - ); - return self::$db->collect(0) ?: ['None']; - } - protected function getSingleValue($cacheKey, $query): string { $cacheKey .= '_' . $this->id; if ($this->forceCacheFlush || ($value = self::$cache->get_value($cacheKey)) === false) { diff --git a/misc/pg-migrations/20250901000000_torrent_client_useragent.php b/misc/pg-migrations/20250901000000_torrent_client_useragent.php new file mode 100644 index 000000000..35ccb9c51 --- /dev/null +++ b/misc/pg-migrations/20250901000000_torrent_client_useragent.php @@ -0,0 +1,23 @@ +table('history_useragent_tracker', ['id' => false, 'primary_key' => 'id_history_useragent_tracker']) + ->addColumn('id_history_useragent_tracker', 'integer', ['identity' => true]) + ->addColumn('id_user', 'integer') + ->addColumn('total', 'integer') + ->addColumn('useragent', 'string', ['length' => 100]) + ->save(); + $this->execute(" + create index hut_u_idx on history_useragent_tracker (id_user) + "); + } + + public function down(): void { + $this->table('history_useragent_tracker')->drop()->save(); + } +} diff --git a/templates/user/header.twig b/templates/user/header.twig index e409507e5..ad97818de 100644 --- a/templates/user/header.twig +++ b/templates/user/header.twig @@ -123,7 +123,14 @@ Last seen: {{ user.lastAccessRealtime }} Up: {{ user.uploadedSize|octet_size(3) }} Down: {{ user.downloadedSize|octet_size(3) }} Ratio: {{ ratio(user.uploadedSize, user.downloadedSize) }} (required {{ user.requiredRatio|number_format(2) }}) -Torrent clients: {{ user.clients|join('; ') }} +Torrent clients: +{% for u in user.stats.historyUseragentTracker %} +{{ u.useragent }} ({{ u.total }}) +{% else %} +None +{% endfor %} + + {% endif %}
Statistics
diff --git a/templates/user/sidebar.twig b/templates/user/sidebar.twig index 14c23219d..e2fa4a4c4 100644 --- a/templates/user/sidebar.twig +++ b/templates/user/sidebar.twig @@ -76,7 +76,15 @@ {% endif %} {%- if own_profile or viewer.permitted('users_mod') or viewer.isFLS %} - Torrent clients: {{ user.clients|join('; ') }} + Torrent clients: +{% for u in user.stats.historyUseragentTracker %} +{{ u.useragent -}} +{% if viewer.permitted('users_mod') or (own_profile and user.hasAttr('seedbox-viewer')) %} ({{ u.total|number_format }}){% endif %} +{% if not loop.last %}; {% endif %} +{% else %} +None +{% endfor -%} + Password age: {{ user.history.passwordAge|time_interval }} {% endif %} diff --git a/tests/helper.php b/tests/helper.php index 1857aa95e..fa0fe8320 100644 --- a/tests/helper.php +++ b/tests/helper.php @@ -237,13 +237,17 @@ class Helper { \Gazelle\User $user, string $ipAddr = '127.0.0.1', int $interval = 5, + string|null $useragent = null, ): int { + if (is_null($useragent)) { + $useragent = 'ua-' . randomString(12); + } $db = \Gazelle\DB::DB(); $db->prepared_query(" INSERT INTO xbt_files_users - (fid, uid, useragent, peer_id, ip, active, remaining, timespent, mtime) - VALUES (?, ?, ?, ?, ?, 1, 0, 1, unix_timestamp(now() - interval ? second)) - ", $torrent->id, $user->id, 'ua-' . randomString(12), randomString(20), $ipAddr, $interval + (fid, uid, useragent, peer_id, ip, mtime, active, remaining, timespent) + VALUES (?, ?, ?, ?, ?, unix_timestamp(now() - interval ? second), 1, 0, 1) + ", $torrent->id, $user->id, $useragent, randomString(20), $ipAddr, $interval ); return $db->affected_rows(); } diff --git a/tests/phpunit/TorrentTest.php b/tests/phpunit/TorrentTest.php index 0a902c028..32e9ad3a5 100644 --- a/tests/phpunit/TorrentTest.php +++ b/tests/phpunit/TorrentTest.php @@ -162,7 +162,24 @@ class TorrentTest extends TestCase { // a seeder $this->userList['seeder'] = Helper::makeUser('torrent.' . randomString(10), 'rent', clearInbox: true); - Helper::generateTorrentSeed($torrent, $this->userList['seeder']); + $useragent = 'uahist-' . randomString(10); + Helper::generateTorrentSeed( + $torrent, + $this->userList['seeder'], + ipAddr: '127.1.1.1', + useragent: $useragent + ); + + // This is as good a place as any to test this + new Stats\Users()->refreshUseragentTracker(); + $this->assertEquals( + [[ + "useragent" => $useragent, + "total" => 1, + ]], + $this->userList['seeder']->stats()->historyUseragentTracker(), + 'user-status-history-useragent-tracker', + ); $name = $torrent->fullName(); $path = $torrent->path();