Skip to content

feat(player-counter): add TeamSpeak 3 ServerQuery support#120

Open
IceCupe123 wants to merge 2 commits intopelican-dev:mainfrom
IceCupe123:feature/teamspeak-query
Open

feat(player-counter): add TeamSpeak 3 ServerQuery support#120
IceCupe123 wants to merge 2 commits intopelican-dev:mainfrom
IceCupe123:feature/teamspeak-query

Conversation

@IceCupe123
Copy link
Copy Markdown

@IceCupe123 IceCupe123 commented Apr 11, 2026

Summary

  • New schema: TeamSpeakQueryTypeSchema — connects via raw TCP to the TeamSpeak 3 ServerQuery interface, reads server name, max clients and connected players, filters out ServerQuery clients (client_type=1), and unescapes TS3 protocol encoding
  • Port resolution hook: GameQuery::runQuery() now checks for an optional resolvePort(Allocation $allocation): ?int method on schemas, allowing them to resolve the query port from server variables instead of relying solely on the query_port_offset. Schemas without this method continue to work exactly as before
  • Hostname alias fix: GameQuery::canRunQuery() and runQuery() now support hostname-based ip_alias values (not just raw IPs), which is required for servers bound to 0.0.0.0 behind a reverse proxy

Why the resolvePort() hook?

TeamSpeak exposes its ServerQuery port as a configurable server variable (QUERY_PORT), independent of the voice port. A static offset would break whenever an admin changes the query port. The hook lets the schema read the actual configured value directly from the server.

The hook is fully backwards-compatible — existing schemas and the query_port_offset field are unaffected.

Test plan

  • TeamSpeak 3 server with default ports (voice: 9987, query: 10011) shows player count widget
  • TeamSpeak 3 server with custom QUERY_PORT uses the correct port
  • Existing query types (Source, Minecraft, etc.) still work as before
  • Server bound to 0.0.0.0 with a hostname ip_alias resolves correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added TeamSpeak 3 ServerQuery support for player count queries.
    • Added support for query-type specific port resolution.
  • Bug Fixes / Reliability

    • Improved IP/alias handling for better network configuration flexibility.
    • More robust query execution with safer failure handling and reporting.

- Add TeamSpeakQueryTypeSchema with raw TCP ServerQuery protocol implementation
- Reads QUERY_PORT directly from server variables via new resolvePort() hook
- Filters out ServerQuery clients (client_type=1) from player list
- Unescapes TeamSpeak protocol encoding in server/player names

- Extend GameQuery model with resolvePort() hook: schemas can optionally
  implement resolvePort(Allocation) to override the default port-offset
  logic with custom resolution (e.g. from server variables)
- Fix canRunQuery() and runQuery() to support hostname aliases (ip_alias),
  not just raw IPs, enabling use behind reverse proxies with 0.0.0.0 bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

Adds TeamSpeak 3 ServerQuery support via a new QueryTypeSchema, updates GameQuery to resolve IPs and allow schema-specific port resolution, and registers the new schema in the plugin provider.

Changes

Cohort / File(s) Summary
TeamSpeak Query Schema Implementation
player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php
New class implementing QueryTypeSchemaInterface to query a TeamSpeak 3 ServerQuery endpoint: resolves query port from allocation variables, opens a TCP socket, validates greeting, runs use sid=1, serverinfo, and clientlist, parses/unescapes responses, filters query clients, returns normalized host/map/current/max/players, and reports exceptions with guaranteed socket cleanup.
Query Resolution Refactoring
player-counter/src/Models/GameQuery.php
Added resolveIp(Allocation) to prefer ip_alias per config; runQuery() now uses resolved IP, checks schema presence, calls schema resolvePort() when provided and falls back to allocation port+offset; canRunQuery() uses same IP resolution.
Service Provider Integration
player-counter/src/Providers/PlayerCounterPluginProvider.php
Imported and registered TeamSpeakQueryTypeSchema with QueryTypeService during default query type setup.

Sequence Diagram

sequenceDiagram
    participant GameQuery
    participant TeamSpeakSchema
    participant TS3Server as TeamSpeak Server

    GameQuery->>GameQuery: resolveIp(allocation)
    GameQuery->>TeamSpeakSchema: resolvePort(allocation)
    TeamSpeakSchema-->>GameQuery: port or null

    GameQuery->>TeamSpeakSchema: process(ip, port)
    TeamSpeakSchema->>TS3Server: TCP connect (ip:port)
    TS3Server-->>TeamSpeakSchema: ServerQuery greeting
    TeamSpeakSchema->>TS3Server: use sid=1
    TS3Server-->>TeamSpeakSchema: ack
    TeamSpeakSchema->>TS3Server: serverinfo
    TS3Server-->>TeamSpeakSchema: server metadata
    TeamSpeakSchema->>TS3Server: clientlist
    TS3Server-->>TeamSpeakSchema: clients + error marker
    TeamSpeakSchema->>TeamSpeakSchema: parse/unescape/filter clients
    TeamSpeakSchema->>TS3Server: quit
    TeamSpeakSchema->>TeamSpeakSchema: close socket
    TeamSpeakSchema-->>GameQuery: normalized result (hostname,map,current,max,players)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

🐰 A hopping rabbit sniffs the stream of bytes,

Sid set, serverinfo gleams in nightly lights,
IPs resolved, ports politely found,
Players counted, names unescaped and sound,
I dig the code — sockets closed — delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(player-counter): add TeamSpeak 3 ServerQuery support' is clear, specific, and directly summarizes the main change: adding TeamSpeak 3 ServerQuery functionality to the player-counter plugin.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`:
- Around line 77-84: The returned array in TeamSpeakQueryTypeSchema uses manual
alignment spacing around the => which causes Pint formatting failures; update
the array (the block that constructs 'hostname', 'map', 'current_players',
'max_players', 'players') to remove the extra spaces and use a single space on
each side of => (do not align keys), and leave the existing value expressions
and array_map callback unchanged so only spacing/formatting is modified.
- Around line 52-55: The greeting read uses fgets($socket) and then
trim($greeting) without validating the return value and the exception handler
only catches Exception, so change the greeting guard to check for falsy/false
return from fgets before calling trim and throw a descriptive Exception if
reading failed; also broaden the catch to catch \Throwable where the greeting is
validated (reference fgets and trim usage and the surrounding try/catch block)
so TypeError/\Error are handled. Additionally, add the missing parameter type
hint to the readUntilError($socket) function signature (use the same
socket/resource type used elsewhere) to satisfy PHPStan level 6 and ensure
consistent typing.
- Around line 97-114: The readUntilError function currently treats any "error
..." line as a termination but still returns accumulated data (and overwrites
rather than accumulates), hiding failures and lacks socket typing; update
readUntilError to declare the socket parameter as a resource in the PHPDoc
(e.g., `@param` resource $socket) and change the loop so it accumulates non-empty
trimmed lines into $data (append instead of overwrite) and when encountering a
line that starts with "error " parse the id (e.g., regex for id=(\d+)) and if
the id is not "0" throw/return a proper exception or error object containing the
id and message instead of returning $data, otherwise (id==0) break and return
the accumulated success data; reference the readUntilError function and the
local variables $socket, $line, $trimmed, and $data to locate where to apply
these changes.

In `@player-counter/src/Models/GameQuery.php`:
- Line 42: Calls to the private static method resolveIp in GameQuery are using
late static binding (static::resolveIp), which trips PHPStan level 6 because
private methods cannot be overridden; change those call sites to self::resolveIp
(both occurrences currently at the resolveIp invocations) so the private static
dispatch is safe and consistent with visibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52bc20cb-c03e-4bc9-ba5b-38cb15203fd2

📥 Commits

Reviewing files that changed from the base of the PR and between be96684 and 9d17556.

📒 Files selected for processing (3)
  • player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php
  • player-counter/src/Models/GameQuery.php
  • player-counter/src/Providers/PlayerCounterPluginProvider.php
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: Lint
player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

[error] 1-1: Laravel Pint reported 1 style issue (FAIL). The pint test run failed with exit code 1.

🪛 GitHub Check: PHPStan (8.2)
player-counter/src/Models/GameQuery.php

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.


[failure] 70-70:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.3)
player-counter/src/Models/GameQuery.php

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.


[failure] 70-70:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.4)
player-counter/src/Models/GameQuery.php

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.


[failure] 70-70:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.5)
player-counter/src/Models/GameQuery.php

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.


[failure] 70-70:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🔇 Additional comments (1)
player-counter/src/Providers/PlayerCounterPluginProvider.php (1)

15-15: TeamSpeak schema registration is correctly wired.

Import + registration align with QueryTypeService usage and keeps the default schema bootstrap path consistent.

Also applies to: 39-39

Comment on lines +52 to +55
$greeting = fgets($socket);
if (!str_starts_with(trim($greeting), 'TS3')) {
throw new Exception('Not a TeamSpeak 3 ServerQuery interface');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and read the file
find . -name "TeamSpeakQueryTypeSchema.php" -type f

Repository: pelican-dev/plugins

Length of output: 137


🏁 Script executed:

# Also check for composer.json to understand PHP version requirements
find . -name "composer.json" -type f

Repository: pelican-dev/plugins

Length of output: 45


🏁 Script executed:

# Read the file to verify the concerns
cat -n ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

Repository: pelican-dev/plugins

Length of output: 5772


🏁 Script executed:

# Check for composer.json in player-counter directory
cat -n ./player-counter/composer.json 2>/dev/null || echo "No composer.json in player-counter"

Repository: pelican-dev/plugins

Length of output: 97


🏁 Script executed:

# Check PHP version requirements at repository root
find . -maxdepth 2 -name "composer.json" -type f | head -5

Repository: pelican-dev/plugins

Length of output: 45


🏁 Script executed:

# Check if there are any other files that might indicate PHP version or linting rules
find . -maxdepth 2 \( -name ".php-version" -o -name "phpstan.neon" -o -name "pint.json" \) -type f

Repository: pelican-dev/plugins

Length of output: 89


🏁 Script executed:

# Check PHPStan and Pint configurations
cat -n ./phpstan.neon

Repository: pelican-dev/plugins

Length of output: 385


🏁 Script executed:

# Check Pint configuration
cat -n ./pint.json

Repository: pelican-dev/plugins

Length of output: 367


Guard greeting reads and catch \Throwable to prevent uncaught runtime failures.

Line 53: fgets() can return false on timeout or stream closure. Calling trim($greeting) with a false value throws TypeError in PHP 8+, which is not caught by catch (Exception) since TypeError extends \Error, not \Exception.

Additionally, line 97 has a missing parameter type hint on readUntilError($socket), which violates PHPStan level 6 checking.

Proposed fix
 use Exception;
+use Throwable;
@@
-            $greeting = fgets($socket);
-            if (!str_starts_with(trim($greeting), 'TS3')) {
+            $greeting = fgets($socket);
+            if (!is_string($greeting) || !str_starts_with(trim($greeting), 'TS3')) {
                 throw new Exception('Not a TeamSpeak 3 ServerQuery interface');
             }
@@
-        } catch (Exception $exception) {
+        } catch (Throwable $exception) {
             report($exception);
         } finally {
@@
-    private function readUntilError($socket): string
+    private function readUntilError(\Socket|false $socket): string
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`
around lines 52 - 55, The greeting read uses fgets($socket) and then
trim($greeting) without validating the return value and the exception handler
only catches Exception, so change the greeting guard to check for falsy/false
return from fgets before calling trim and throw a descriptive Exception if
reading failed; also broaden the catch to catch \Throwable where the greeting is
validated (reference fgets and trim usage and the surrounding try/catch block)
so TypeError/\Error are handled. Additionally, add the missing parameter type
hint to the readUntilError($socket) function signature (use the same
socket/resource type used elsewhere) to satisfy PHPStan level 6 and ensure
consistent typing.

Comment on lines +97 to +114
private function readUntilError($socket): string
{
$data = '';
while (!feof($socket)) {
$line = fgets($socket);
if ($line === false) {
break;
}
$trimmed = trim($line);
if (str_starts_with($trimmed, 'error ')) {
break;
}
if ($trimmed !== '') {
$data = $trimmed;
}
}
return $data;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "TeamSpeakQueryTypeSchema.php" | head -20

Repository: pelican-dev/plugins

Length of output: 137


🏁 Script executed:

cat -n ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php | sed -n '90,130p'

Repository: pelican-dev/plugins

Length of output: 1385


🏁 Script executed:

cat -n ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php | head -50

Repository: pelican-dev/plugins

Length of output: 1787


🏁 Script executed:

cat -n ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php | sed -n '50,100p'

Repository: pelican-dev/plugins

Length of output: 2182


🏁 Script executed:

rg "readUntilError" ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

Repository: pelican-dev/plugins

Length of output: 273


🏁 Script executed:

cat -n ./player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php | sed -n '116,127p'

Repository: pelican-dev/plugins

Length of output: 514


readUntilError() silently converts command failures into successful results and lacks socket parameter typing.

The function stops on any error ... line without validating whether id=0. When a TS3 ServerQuery command fails (e.g., error id=1024 msg=...), the accumulated data is still returned and subsequently parsed as a successful response, masking the actual failure. Additionally, the untyped $socket parameter causes PHPStan failures in strict mode.

Proposed fix
-    private function readUntilError($socket): string
+    /**
+     * `@param` resource $socket
+     */
+    private function readUntilError(mixed $socket): string
     {
         $data = '';
         while (!feof($socket)) {
             $line = fgets($socket);
             if ($line === false) {
                 break;
             }
             $trimmed = trim($line);
             if (str_starts_with($trimmed, 'error ')) {
+                $error = $this->parseLine($trimmed);
+                if (($error['id'] ?? '1') !== '0') {
+                    throw new Exception('TeamSpeak ServerQuery command failed: ' . ($error['msg'] ?? 'unknown'));
+                }
                 break;
             }
             if ($trimmed !== '') {
                 $data = $trimmed;
             }
         }
         return $data;
     }
🧰 Tools
🪛 GitHub Check: PHPStan (8.2)

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.3)

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.4)

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🪛 GitHub Check: PHPStan (8.5)

[failure] 97-97:
Method Boy132\PlayerCounter\Extensions\Query\Schemas\TeamSpeakQueryTypeSchema::readUntilError() has parameter $socket with no type specified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`
around lines 97 - 114, The readUntilError function currently treats any "error
..." line as a termination but still returns accumulated data (and overwrites
rather than accumulates), hiding failures and lacks socket typing; update
readUntilError to declare the socket parameter as a resource in the PHPDoc
(e.g., `@param` resource $socket) and change the loop so it accumulates non-empty
trimmed lines into $data (append instead of overwrite) and when encountering a
line that starts with "error " parse the id (e.g., regex for id=(\d+)) and if
the id is not "0" throw/return a proper exception or error object containing the
id and message instead of returning $data, otherwise (id==0) break and return
the accumulated success data; reference the readUntilError function and the
local variables $socket, $line, $trimmed, and $data to locate where to apply
these changes.

}

$ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip;
$ip = static::resolveIp($allocation);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the GameQuery.php file
find . -name "GameQuery.php" -type f

Repository: pelican-dev/plugins

Length of output: 104


🏁 Script executed:

# Read the GameQuery.php file to inspect the code
cat -n player-counter/src/Models/GameQuery.php

Repository: pelican-dev/plugins

Length of output: 2940


🏁 Script executed:

# Look for PHPStan configuration
find . -name "phpstan.neon*" -o -name "phpstan.json" -o -name ".phpstan*" 2>/dev/null | head -20

Repository: pelican-dev/plugins

Length of output: 77


🏁 Script executed:

# Search for any CI configuration that runs PHPStan
find . -name ".github" -o -name ".gitlab-ci.yml" -o -name ".travis.yml" -o -name "*.yml" 2>/dev/null | grep -E "(github|gitlab|github)" | head -10

Repository: pelican-dev/plugins

Length of output: 146


🏁 Script executed:

# Check if there are other usages of this private method besides lines 42 and 70
rg "resolveIp" player-counter/src/Models/GameQuery.php -n

Repository: pelican-dev/plugins

Length of output: 233


🏁 Script executed:

cat -n phpstan.neon

Repository: pelican-dev/plugins

Length of output: 385


🏁 Script executed:

cat -n .github/workflows/lint.yml

Repository: pelican-dev/plugins

Length of output: 2537


Use self::resolveIp() to fix unsafe private static dispatch.

Lines 42 and 70 call a private static method via static::, which violates PHPStan analysis at level 6. Late static binding is semantically inconsistent with private visibility—private methods cannot be overridden, so static:: serves no purpose. Change to self::.

Proposed fix
-        $ip = static::resolveIp($allocation);
+        $ip = self::resolveIp($allocation);

Apply to lines 42 and 70.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$ip = static::resolveIp($allocation);
$ip = self::resolveIp($allocation);
🧰 Tools
🪛 GitHub Check: PHPStan (8.2)

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

🪛 GitHub Check: PHPStan (8.3)

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

🪛 GitHub Check: PHPStan (8.4)

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

🪛 GitHub Check: PHPStan (8.5)

[failure] 42-42:
Unsafe call to private method Boy132\PlayerCounter\Models\GameQuery::resolveIp() through static::.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@player-counter/src/Models/GameQuery.php` at line 42, Calls to the private
static method resolveIp in GameQuery are using late static binding
(static::resolveIp), which trips PHPStan level 6 because private methods cannot
be overridden; change those call sites to self::resolveIp (both occurrences
currently at the resolveIp invocations) so the private static dispatch is safe
and consistent with visibility.

- Add @param resource type hint to readUntilError() socket parameter
- Change resolveIp() from private to protected static to allow safe
  static:: calls from canRunQuery() and runQuery()
- Fix code style issues flagged by Laravel Pint
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php (2)

107-113: ⚠️ Potential issue | 🟠 Major

Handle non-zero TeamSpeak error id as failure instead of success.

readUntilError() currently breaks on any error ... line but does not validate id=0, so failed commands can be parsed as valid empty/partial results.

Proposed fix
     private function readUntilError($socket): string
     {
-        $data = '';
+        $data = [];
         while (!feof($socket)) {
             $line = fgets($socket);
             if ($line === false) {
                 break;
             }
             $trimmed = trim($line);
             if (str_starts_with($trimmed, 'error ')) {
+                $error = $this->parseLine($trimmed);
+                if (($error['id'] ?? '1') !== '0') {
+                    throw new Exception(
+                        'TeamSpeak ServerQuery command failed: ' . ($error['msg'] ?? 'unknown')
+                    );
+                }
                 break;
             }
             if ($trimmed !== '') {
-                $data = $trimmed;
+                $data[] = $trimmed;
             }
         }
 
-        return $data;
+        return implode("\n", $data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`
around lines 107 - 113, The readUntilError() loop currently breaks on any line
starting with 'error ' but doesn't check the error id; update the logic in
readUntilError() to parse the 'error id=...' token from the $trimmed line and
treat non-zero ids as a failure (e.g., return/throw or signal an error) instead
of accepting the response as successful; ensure you still accept the command as
success only when the error id equals '0' and continue parsing/assigning $data
for other non-error lines.

52-55: ⚠️ Potential issue | 🔴 Critical

Guard greeting reads and catch \Throwable in process().

fgets() may return false (Line 52); trim($greeting) can throw TypeError on PHP 8+, and catch (Exception) (Line 86) won’t catch it.

Proposed fix
 use Boy132\PlayerCounter\Extensions\Query\QueryTypeSchemaInterface;
 use Exception;
+use Throwable;
@@
             // Read greeting: "TS3", welcome text, empty line
             $greeting = fgets($socket);
-            if (!str_starts_with(trim($greeting), 'TS3')) {
+            if (!is_string($greeting) || !str_starts_with(trim($greeting), 'TS3')) {
                 throw new Exception('Not a TeamSpeak 3 ServerQuery interface');
             }
@@
-        } catch (Exception $exception) {
+        } catch (Throwable $exception) {
             report($exception);
         } finally {
#!/bin/bash
# Verifies the risky pattern is still present.
rg -n "greeting = fgets|trim\(\$greeting\)|catch \(Exception" player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php

Also applies to: 86-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`
around lines 52 - 55, The greeting read from fgets in process() can be false and
trim($greeting) can raise a TypeError on PHP 8+, and the current catch
(Exception) won't catch it; update the code around the $greeting =
fgets($socket) check in TeamSpeakQueryTypeSchema::process() to first verify
$greeting !== false before calling trim(), handle the false case by throwing or
returning a clear error, and replace or supplement the existing catch
(Exception) with catch (\Throwable) so TypeError and other throwables are
caught; ensure the thrown/returned error message remains descriptive (e.g., "Not
a TeamSpeak 3 ServerQuery interface" or a distinct message for read failures).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php`:
- Around line 107-113: The readUntilError() loop currently breaks on any line
starting with 'error ' but doesn't check the error id; update the logic in
readUntilError() to parse the 'error id=...' token from the $trimmed line and
treat non-zero ids as a failure (e.g., return/throw or signal an error) instead
of accepting the response as successful; ensure you still accept the command as
success only when the error id equals '0' and continue parsing/assigning $data
for other non-error lines.
- Around line 52-55: The greeting read from fgets in process() can be false and
trim($greeting) can raise a TypeError on PHP 8+, and the current catch
(Exception) won't catch it; update the code around the $greeting =
fgets($socket) check in TeamSpeakQueryTypeSchema::process() to first verify
$greeting !== false before calling trim(), handle the false case by throwing or
returning a clear error, and replace or supplement the existing catch
(Exception) with catch (\Throwable) so TypeError and other throwables are
caught; ensure the thrown/returned error message remains descriptive (e.g., "Not
a TeamSpeak 3 ServerQuery interface" or a distinct message for read failures).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a671b75-e6c3-4757-92c4-4678b59b47f4

📥 Commits

Reviewing files that changed from the base of the PR and between 9d17556 and 6e6eccc.

📒 Files selected for processing (2)
  • player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php
  • player-counter/src/Models/GameQuery.php
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PHPStan (8.2)
🔇 Additional comments (3)
player-counter/src/Extensions/Query/Schemas/TeamSpeakQueryTypeSchema.php (1)

21-36: resolvePort() implementation looks clean and backward-compatible.

The null-safe fallback behavior is good for schemas/environments where QUERY_PORT is absent.

player-counter/src/Models/GameQuery.php (2)

48-61: Schema lookup + optional resolvePort() fallback flow is solid.

Good defensive handling when schema is missing, while preserving legacy offset-based behavior.


70-82: resolveIp() reuse in canRunQuery() is a good fix for hostname alias support.

This keeps eligibility checks consistent with the actual query target resolution.

@IceCupe123 IceCupe123 marked this pull request as draft April 11, 2026 19:39
@IceCupe123 IceCupe123 marked this pull request as ready for review April 11, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants