zenobio93/seat-teamspeak

View on GitHub
src/Driver/TeamspeakClient.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * This file is part of SeAT Teamspeak Connector.
 *
 * Copyright (C) 2019  Warlof Tutsimo <loic.leuilliot@gmail.com>
 *
 * SeAT Teamspeak Connector  is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * SeAT Teamspeak Connector is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

namespace Warlof\Seat\Connector\Drivers\Teamspeak\Driver;

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Arr;
use Seat\Services\Exceptions\SettingException;
use Warlof\Seat\Connector\Drivers\IClient;
use Warlof\Seat\Connector\Drivers\ISet;
use Warlof\Seat\Connector\Drivers\IUser;
use Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException;
use Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ConnexionException;
use Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\LoginException;
use Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ServerException;
use Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\TeamspeakException;
use Warlof\Seat\Connector\Exceptions\DriverException;
use Warlof\Seat\Connector\Exceptions\DriverSettingsException;
use Warlof\Seat\Connector\Exceptions\InvalidDriverIdentityException;

/**
 * Class TeamspeakClient.
 *
 * @package Warlof\Seat\Connector\Drivers\Teamspeak\Driver
 */
class TeamspeakClient implements IClient
{
    private static ?\Warlof\Seat\Connector\Drivers\Teamspeak\Driver\TeamspeakClient $instance = null;

    /**
     * @var \Warlof\Seat\Connector\Drivers\IUser[]
     */
    private readonly \Illuminate\Support\Collection $speakers;

    /**
     * @var \Warlof\Seat\Connector\Drivers\ISet[]
     */
    private readonly \Illuminate\Support\Collection $server_groups;

    /**
     * @var \Warlof\Seat\Connector\Drivers\Teamspeak\Fetchers\IFetcher
     */
    private readonly object $client;

    /**
     * @var int
     */
    private $instance_id;

    /**
     * @var int
     */
    private $server_port;

    /**
     * @var string
     */
    private $api_base_uri;

    /**
     * @var string
     */
    private $api_key;

    /**
     * TeamspeakClient constructor.
     */
    public function __construct(array $parameters)
    {
        $this->server_port    = $parameters['server_port'];
        $this->instance_id    = $parameters['instance_id'] ?? 0;
        $this->api_base_uri   = $parameters['api_base_uri'];
        $this->api_key        = $parameters['api_key'];

        $this->speakers      = collect();
        $this->server_groups = collect();

        $fetcher = config('teamspeak.config.fetcher');
        $this->client = new $fetcher($this->api_base_uri, $this->api_key);
    }

    /**
     * @return \Warlof\Seat\Connector\Drivers\Teamspeak\Driver\TeamspeakClient
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     */
    public static function getInstance(): IClient
    {
        if (! isset(self::$instance)) {
            try {
                $settings = setting('seat-connector.drivers.teamspeak', true);
            } catch (SettingException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d : %s', $e->getCode(), $e->getMessage()));
                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }

            if (is_null($settings) || ! is_object($settings))
                throw new DriverSettingsException('The Driver has not been configured yet.');

            if (! property_exists($settings, 'server_host') || empty($settings->server_host))
                throw new DriverSettingsException('Parameter server_host is missing.');

            if (! property_exists($settings, 'server_port') || is_null($settings->server_port) || $settings->server_port == 0)
                throw new DriverSettingsException('Parameter server_port is missing.');

            if (! property_exists($settings, 'api_base_uri') || empty($settings->api_base_uri))
                throw new DriverSettingsException('Parameter api_base_uri is missing.');

            if (! property_exists($settings, 'api_key') || empty($settings->api_key))
                throw new DriverSettingsException('Parameter api_key is missing.');

            if (! property_exists($settings, 'instance_id') || is_null($settings->instance_id) || $settings->instance_id == 0)
                throw new DriverSettingsException('Parameter instance_id is missing.');

            self::$instance = new TeamspeakClient([
                'server_port'  => $settings->server_port,
                'instance_id'  => $settings->instance_id ?? 0,
                'api_base_uri' => $settings->api_base_uri,
                'api_key'      => $settings->api_key,
            ]);
        }

        return self::$instance;
    }

    /**
     * @return \Warlof\Seat\Connector\Drivers\IUser[]
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     */
    public function getUsers(): array
    {
        if ($this->speakers->isEmpty()) {
            try {
                $this->seedSpeakers();
            } catch (TeamspeakException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d: %s', $e->getCode(), $e->getMessage()));
                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }
        }

        return $this->speakers->toArray();
    }

    /**
     * @return \Warlof\Seat\Connector\Drivers\ISet[]
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     */
    public function getSets(): array
    {
        if ($this->server_groups->isEmpty()) {
            try {
                $this->seedServerGroups();
            } catch (TeamspeakException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d : %s', $e->getCode(), $e->getMessage()));
                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }
        }

        return $this->server_groups->toArray();
    }

    /**
     * @param string $id
     * @return \Warlof\Seat\Connector\Drivers\IUser|null
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     * @throws \Warlof\Seat\Connector\Exceptions\InvalidDriverIdentityException
     */
    public function getUser(string $id): ?IUser
    {
        if ($this->speakers->isEmpty()) {
            try {
                $this->seedSpeakers();
            } catch (TeamspeakException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d : %s', $e->getCode(), $e->getMessage()));
                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }
        }

        $user = $this->speakers->get($id);

        if (is_null($user)) {
            try {
                // scope: manage_scope
                $response = $this->sendCall('GET', '/{instance}/clientdbinfo', [
                    'cldbid' => $id,
                    'instance' => $this->instance_id,
                ]);

                $client_info = Arr::first($response);

                $speaker = new TeamspeakSpeaker([
                    'client_database_id' => $client_info->client_database_id,
                    'client_unique_identifier' => $client_info->client_unique_identifier,
                    'client_nickname' => $client_info->client_nickname,
                ]);

                $this->speakers->put($speaker->getClientId(), $speaker);
            } catch (TeamspeakException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d : %s', $e->getCode(), $e->getMessage()));

                if ($e->getCode() == 512)
                    throw new InvalidDriverIdentityException(
                        sprintf('User ID %s is not found on Teamspeak Server.', $id),
                        $e->getCode(),
                        $e);

                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }
        }

        return $user;
    }

    /**
     * @return \Warlof\Seat\Connector\Drivers\Teamspeak\Driver\TeamspeakSpeaker
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\TeamspeakException
     * @throws \Warlof\Seat\Connector\Exceptions\InvalidDriverIdentityException
     */
    public function findUserByName(string $nickname): \Warlof\Seat\Connector\Drivers\Teamspeak\Driver\TeamspeakSpeaker
    {
        try {
            // scope: manage_scope
            $response = $this->sendCall('GET', '/{instance}/clientdbfind', [
                'pattern' => $nickname,
                'instance' => $this->instance_id,
            ]);

            $id = Arr::first($response)->cldbid;

            // scope: manage_scope
            $response = $this->sendCall('GET', '/{instance}/clientdbinfo', [
                'cldbid' => $id,
                'instance' => $this->instance_id,
            ]);
        } catch (TeamspeakException $e) {
            if ($e->getCode() == 1281)
                throw new InvalidDriverIdentityException(
                    sprintf('Unable to find user %s', $nickname),
                    $e->getCode(),
                    $e);

            if ($e->getCode() == 512)
                throw new InvalidDriverIdentityException(
                    sprintf('Unable to find user with Client ID %d', $id),
                    $e->getCode(),
                    $e);

            throw $e;
        }

        $identity = Arr::first($response);

        return new TeamspeakSpeaker([
            'client_database_id'       => $identity->client_database_id,
            'client_unique_identifier' => $identity->client_unique_identifier,
            'client_nickname'          => $identity->client_nickname,
        ]);
    }

    /**
     * @param string $id
     * @return \Warlof\Seat\Connector\Drivers\ISet|null
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     */
    public function getSet(string $id): ?ISet
    {
        if ($this->server_groups->isEmpty()) {
            try {
                $this->seedServerGroups();
            } catch (TeamspeakException $e) {
                logger()->error(sprintf('[seat-connector][teamspeak] %d : %s', $e->getCode(), $e->getMessage()));
                throw new DriverException($e->getMessage(), $e->getCode(), $e);
            }
        }

        return $this->server_groups->get($id);
    }

    /**
     * @return int
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\LoginException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ServerException
     */
    public function findInstanceIdByServerPort(int $server_port): int
    {
        // scope: manage_scope
        $response = $this->sendCall('GET', '/serverlist');

        $instances = collect($response);

        $instance = $instances->first(fn($instance): bool => (int) $instance->virtualserver_port === $server_port);

        if (! $instance)
            throw new ServerException(sprintf('Unable to find a server instance listening on port %d.', $server_port));

        return $instance->virtualserver_id;
    }

    /**
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\TeamspeakException
     */
    public function addSpeakerToServerGroup(IUser $speaker, ISet $server_group): void
    {
        // scope: manage_scope
        $this->sendCall('POST', '/{instance}/servergroupaddclient', [
            'sgid'     => $server_group->getId(),
            'cldbid'   => $speaker->getClientId(),
            'instance' => $this->instance_id,
        ]);
    }

    /**
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\TeamspeakException
     */
    public function removeSpeakerFromServerGroup(IUser $speaker, ISet $server_group): void
    {
        // scope: manage_scope
        $this->sendCall('POST', '/{instance}/servergroupdelclient', [
            'sgid'     => $server_group->getId(),
            'cldbid'   => $speaker->getClientId(),
            'instance' => $this->instance_id,
        ]);
    }

    /**
     * @return IUser[]
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\TeamspeakException
     * @throws \Warlof\Seat\Connector\Exceptions\DriverException
     */
    public function getServerGroupMembers(ISet $server_group): array
    {
        // scope: manage_scope
        $response = $this->sendCall('GET', '/{instance}/servergroupclientlist', [
            'sgid'     => $server_group->getId(),
            'instance' => $this->instance_id,
        ]);

        $speakers = [];

        foreach ($response as $element) {
            $speakers[] = $this->getUser($element->cldbid);
        }

        return $speakers;
    }

    /**
     * @return ISet[]
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException
     */
    public function getSpeakerServerGroups(IUser $speaker): array
    {
        // scope: manage_scope
        $response = $this->sendCall('GET', '/{instance}/serverinfo', [
            'instance' => $this->instance_id,
        ]);

        $server_info = Arr::first($response);

        // scope: manage_scope
        $response = $this->sendCall('GET', '/{instance}/servergroupsbyclientid', [
            'cldbid' => $speaker->getClientId(),
            'instance' => $this->instance_id,
        ]);

        $server_group = [];

        foreach ($response as $element) {

            // ignore default server group - since it's automatically assigned
            if ($element->sgid == $server_info->virtualserver_default_server_group)
                continue;

            $server_group[] = new TeamspeakServerGroup([
                'sgid' => $element->sgid,
                'name' => $element->name,
            ]);
        }

        return $server_group;
    }

    /**
     * @return array
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\LoginException
     */
    private function sendCall(string $method, string $endpoint, array $arguments = []): array
    {
        $uri = ltrim($endpoint, '/');
        $method = strtoupper($method);

        foreach ($arguments as $uri_parameter => $value) {
            if (!str_contains($uri, sprintf('{%s}', $uri_parameter)))
                continue;

            $uri = str_replace(sprintf('{%s}', $uri_parameter), $value, $uri);

            Arr::pull($arguments, $uri_parameter);
        }

        try {
            if ($method == 'GET') {
                $response = $this->client->request($method, $uri, [
                    'query' => $arguments,
                ]);
            } else {
                $response = $this->client->request($method, $uri, [
                    'body' => json_encode($arguments, JSON_THROW_ON_ERROR),
                ]);
            }

            logger()->debug(
                sprintf('[seat-connector][teamspeak] [http %d, %s] %s -> /%s',
                    $response->getStatusCode(), $response->getReasonPhrase(), $method, $uri),
                $method == 'GET' ? [
                    'response' => [
                        'body' => $response->getBody()->getContents(),
                    ],
                ] : [
                    'request' => [
                        'body' => json_encode($arguments, JSON_THROW_ON_ERROR),
                    ],
                    'response' => [
                        'body' => $response->getBody()->getContents(),
                    ],
                ],
            );
        } catch (ConnectException $e) {
            throw new ConnexionException($e->getMessage(), $e->getCode(), $e);
        } catch (RequestException $e) {
            throw new ServerException($e->getMessage(), $e->getCode(), $e);
        }

        $result = json_decode($response->getBody(), null, 512, JSON_THROW_ON_ERROR);

        if ($result->status->code !== 0) {
            if (in_array($result->status->code, [5122, 5124]))
                throw new LoginException($result->status->message, $result->status->code);

            throw new CommandException($result->status->message, $result->status->code);
        }

        return $result->body ?? [];
    }

    /**
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ConnexionException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\LoginException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ServerException
     */
    private function seedSpeakers(): void
    {
        $from        = 0;

        while (true) {
            try {
                // scope: manage_scope
                $response = $this->sendCall('GET', '/{instance}/clientdblist', [
                    'start' => $from,
                    'instance' => $this->instance_id,
                ]);

                foreach ($response as $identity) {
                    $speaker = new TeamspeakSpeaker([
                        'cldbid' => $identity->cldbid,
                        'client_unique_identifier' => $identity->client_unique_identifier,
                        'client_nickname' => $identity->client_nickname,
                    ]);

                    $this->speakers->put($speaker->getClientId(), $speaker);
                    $from++;
                }
            } catch (TeamspeakException $e) {
                if ($e->getCode() == 1281)
                    break;

                throw $e;
            }
        }
    }

    /**
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ConnexionException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\LoginException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\ServerException
     * @throws \Warlof\Seat\Connector\Drivers\Teamspeak\Exceptions\CommandException
     */
    private function seedServerGroups(): void
    {
        // scope: manage_scope
        $response = $this->sendCall('GET', '/{instance}/serverinfo', [
            'instance' => $this->instance_id,
        ]);

        $server_info = Arr::first($response);

        // scope: manage_scope
        $response = $this->sendCall('GET', '/{instance}/servergrouplist', [
            'instance' => $this->instance_id,
        ]);

        foreach ($response as $group) {

            // ignore default server group - since it's automatically assigned
            if ($group->sgid == $server_info->virtualserver_default_server_group)
                continue;

            // groupDbType (0 = template, 1 = normal, 2 = query)
            if ($group->type != '1')
                continue;

            $server_group = new TeamspeakServerGroup([
                'sgid' => $group->sgid,
                'name' => $group->name,
            ]);

            $this->server_groups->put($server_group->getId(), $server_group);
        }
    }
}