JABirchall/NimdaTS3

View on GitHub
app/TeamSpeak3Bot.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/**
 * Created by PhpStorm.
 * User: Admin
 * Date: 25/07/2016
 * Time: 22:12
 */

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Capsule\Manager;
use TeamSpeak3\Helper\Convert;
use TeamSpeak3\Helper\Profiler\Timer;
use TeamSpeak3\TeamSpeak3;
use TeamSpeak3\Helper\Signal;
use TeamSpeak3\Ts3Exception;
use TeamSpeak3\Adapter\ServerQuery\Event;
use TeamSpeak3\Adapter\AbstractAdapter;
use Plugin\Models\Plugin;
use Illuminate\Database\Schema\Blueprint;

/**
 * Class TeamSpeak3Bot
 *
 * @package App\TeamSpeak3Bot
 */
class TeamSpeak3Bot
{
    /**
     * @var string
     */
    const NIMDA_VERSION = '0.14.2';
    const NIMDA_TYPE = '-alpha';

    /**
     * @var string
     */
    private $username;
    /**
     * @var string
     */
    private $password;
    /**
     * @var string
     */
    private $host;
    /**
     * @var string
     */
    private $port;
    /**
     * @var string
     */
    private $name;
    /**
     * @var string
     */
    private $serverPort;
    /**
     * @var string
     */
    private $timeout;
    /**
     * @var
     */
    public $node;
    /**
     * @var
     */
    public $channel;
    /**
     * @var
     */
    private $plugins;

    private $timers;

    public static $config;

    private $timer;
    /**
     * @var
     */
    private static $_username;
    /**
     * @var
     */
    private static $_password;
    /**
     * @var
     */
    private static $_host;
    /**
     * @var
     */
    private static $_port;
    /**
     * @var
     */
    private static $_name;
    /**
     * @var
     */
    private static $_serverPort;
    /**
     * @var
     */
    private static $_instance;
    /**
     * @var
     */
    private static $_timeout;

    /**
     * @var bool
     */
    public $online = false;

    private $database;

    private $lastEvent;

    public $carbon;

    /**
     * TeamSpeak3Bot constructor.
     *
     * @param string $username
     * @param string $password
     * @param string $host
     * @param int $port
     * @param string $name
     * @param int $serverPort
     * @param int $timeout
     */
    public function __construct($username, $password, $host = '127.0.0.1', $port = 10011, $name = 'Nimda', $serverPort = 9987, $timeout = 10)
    {
        $this->carbon = new Carbon;

       if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' && posix_getuid() === 0 && !Self::$config['ignoreWarnings']) {
           $this->printOutput("[WARNING] Running Nimda as root is dangerous!");
           $this->printOutput("Start anyway? Y/N:", false);
           $response = rtrim(fgets(STDIN));
           if (strcasecmp($response,'y')) {
               $this->printOutput("Aborted.");
               exit;
           }
       }

       if($username === 'serveradmin' && !Self::$config['ignoreWarnings']) {
           $this->printOutput("[WARNING] Running Nimda logged in as serveradmin is dangerous!");
           $this->printOutput("Start anyway? Y/N:", false);
           $response = rtrim(fgets(STDIN));
           if (strcasecmp($response,'y') ) {
               $this->printOutput('Aborted.');
               exit;
           }
       }

        $this->username = $username;
        $this->password = $password;
        $this->host = $host;
        $this->port = $port;
        $this->name = $name;
        $this->serverPort = $serverPort;
        $this->timeout = $timeout;

        $this->timer = new Timer('start_up');
    }

    /**
     * Run the TeamSpeak3Bot instance
     */
    public function run()
    {
        $this->timer->start();

        $this->subscribe();
        try {
            $this->node = TeamSpeak3::factory("serverquery://{$this->username}:{$this->password}@{$this->host}:{$this->port}/?server_port={$this->serverPort}&blocking=0&nickname={$this->name}&timeout={$this->timeout}&no_query_clients=0");
        } catch (Ts3Exception $e) {
            $this->onException($e);

            return;
        }

        $this->database = new Database;
        $this->setup();
        $this->initializePlugins();
        $this->initializeTimers();
        $this->register();
        $this->timer->stop();

        $this->printOutput("Nimda version " . $this::NIMDA_VERSION . $this::NIMDA_TYPE . " Started in " . round($this->timer->getRuntime(), 3) . " seconds, Using " . Convert::bytes($this->timer->getMemUsage()) . " memory.");
        $this->timer = new Timer('runTime');
        $this->timer->start();
        $this->wait();
    }

    protected function subscribe()
    {
        Signal::getInstance()->subscribe('serverqueryWaitTimeout', [$this, 'onTimeout']);
        Signal::getInstance()->subscribe('serverqueryConnected', [$this, 'onConnect']);
        Signal::getInstance()->subscribe('notifyTextmessage', [$this, 'onMessage']);
        Signal::getInstance()->subscribe('notifyEvent', [$this, 'onEvent']);
        //Signal::getInstance()->subscribe('errorException', [$this, 'onException']);
        Signal::getInstance()->subscribe('serverqueryDisconnected', [$this, 'onDisconnect']);
    }

    protected function register()
    {
        $this->node->notifyRegister('textserver');
        $this->node->notifyRegister('textchannel');
        $this->node->notifyRegister('textprivate');
        $this->node->notifyRegister('server');
        $this->node->notifyRegister('channel');
    }

    /**
     * @param $output
     * @param $eol
     * @param $send
     */
    public function printOutput($output, $eol = true, $send = true)
    {
        if(!ob_get_contents()) {
            ob_start(null, 1024);
            printf("[%s]: %s ", $this->carbon->now()->toTimeString(), $output);
        } else {
            echo $output;
        }
        if ($eol) {
            echo PHP_EOL;
        }

        if($send) {
            ob_end_flush();
        }
    }

    /**
     * Wait for messages
     */
    protected function wait()
    {
        while ($this->online === true) {
            $this->node->getAdapter()->wait();
        }
    }

    /**
     * @param array $options
     */
    public static function setOptions(Array $options = [])
    {
        Self::$_username = $options['username'];
        Self::$_password = urlencode($options['password']);
        Self::$_host = $options['host'];
        Self::$_port = $options['port'];
        Self::$_name = urlencode($options['name']);
        Self::$_serverPort = $options['serverPort'];
        Self::$_timeout = $options['timeout'];
        Self::$config = $options['misc'];
    }

    /**
     * @return TeamSpeak3Bot
     */
    public static function getInstance()
    {
        if (Self::$_instance === null) {
            Self::$_instance = new Self(Self::$_username, Self::$_password, Self::$_host, Self::$_port, Self::$_name, Self::$_serverPort, Self::$_timeout);
        }

        return Self::$_instance;
    }

    /**
     * @return TeamSpeak3Bot
     */
    public static function getNewInstance()
    {
        Self::$_instance = new Self(Self::$_username, Self::$_password, Self::$_host, Self::$_port, Self::$_name, Self::$_serverPort, Self::$_timeout);

        return Self::$_instance;
    }


    private function setup()
    {
        if (!Manager::schema()->hasTable('plugins')) {
            Manager::schema()->create('plugins', function(Blueprint $table) {
                $table->increments('id');
                $table->text('name');
                $table->double('version');
                $table->timestamps();
            });

            Plugin::create([
                'name' => 'Nimda',
                'version' => Self::NIMDA_VERSION,
            ]);
        } else {
            $nimda = Plugin::where('name', 'Nimda')->first();
            if (version_compare($nimda->version, Self::NIMDA_VERSION, '<')) {
                $this->update($nimda->version);

                $nimda->update(['version' => Self::NIMDA_VERSION]);
            }
        }
    }

    private function update($version)
    {
        // TODO: Add bot update logic
    }

    private function initializePlugins()
    {
        foreach (glob('./config/plugins/*.json') as $file) {
            $this->loadPlugin($file);
        }
    }

    private function initializeTimers()
    {
        foreach (glob('./config/timers/*.json') as $file) {
            $this->loadTimer($file);
        }
    }

    /**
     * @param $configFile
     *
     * @return bool
     */
    private function loadPlugin($configFile)
    {
        $config = $this->parseConfigFile($configFile);
        $config['configFile'] = $configFile;

        if (!$config) {
            $this->printOutput("Plugin with config file {$configFile} has not been loaded because it doesn't exist.");

            return false;
        } elseif (!isset($config['name'])) {
            $this->printOutput("Plugin with config file {$configFile} has not been loaded because it has no name.");

            return false;
        } elseif (@!$config['event'] && @!in_array($config['permissions'], array('groups', 'everyone')) || (@$config['permissions'] == 'groups' && @empty($config['groups']))) {
            $this->printOutput("Plugin with config file {$configFile} has not been loaded because its permissions aren't set correctly.");

            return false;
        }

        if(@!$config['event']) {
            $this->printOutput(sprintf("%- 80s %s", "Loading plugin [{$config['name']}] by {$config['author']} | Permissions: {$config['permissions']}", "::"), false, false);
        } else {
            $this->printOutput(sprintf("%- 80s %s", "Loading plugin [{$config['name']}] by {$config['author']}", "::"), false, false);
        }

        $config['class'] = \Plugin::class . '\\' . $config['name'];

        if (!class_exists($config['class'])) {
            $this->printOutput("Loading failed because class {$config['class']} doesn't exist.");

            return false;
        } elseif (!is_a($config['class'], \Plugin\PluginContract::class, true)) {
            $this->printOutput("Loading failed because class {$config['class']} does not implement [PluginContract].");

            return false;
        }

        $this->plugins[$config['name']] = new $config['class']($config, $this);

        if ($this->plugins[$config['name']] instanceof \Plugin\AdvancedPluginContract) {
            if (!Manager::schema()->hasTable($config['table'])) {
                $this->printOutput("Install, ", false, false);
                $this->plugins[$config['name']]->install();

                Plugin::create([
                    'name' => $config['name'],
                    'version' => $config['version'],
                ]);
            }

            $plugin = Plugin::where('name', $config['name'])->first();

            if (version_compare($plugin->version, $config['version'], '<')) {
                $this->plugins[$config['name']]->update($plugin->version);
                $plugin->update(['version' => $config['version']]);
            }
        }

        $this->printOutput("Success.");

        return true;
    }

    private function loadTimer($configFile)
    {
        $config = $this->parseConfigFile($configFile);
        $config['configFile'] = $configFile;

        if (!$config) {
            $this->printOutput("Timer with config file {$configFile} has not been loaded because it doesn't exist.");
            return false;
        } elseif (!isset($config['name'])) {
            $this->printOutput("Timer with config file {$configFile} has not been loaded because it has no name.");

            return false;
        }

        $this->printOutput(sprintf("%- 80s %s", "Loading Timer [{$config['name']}] by {$config['author']} ", "::"), false, false);

        $config['class'] = \Timer::class . '\\' . $config['name'];

        if (!class_exists($config['class'])) {
            $this->printOutput("Loading failed because class {$config['class']} doesn't exist.");

            return false;
        } elseif (!is_a($config['class'], \Timer\TimerContract::class, true)) {
            $this->printOutput("Loading failed because class {$config['class']} does not implement [TimerContract].");

            return false;
        }

        $this->timers[$config['name']] = new $config['class']($config, $this);

        $this->printOutput('Success.');

        return true;
    }

    /**
     * @param $file
     *
     * @return array|bool
     */
    public function parseConfigFile($file)
    {
        if (!file_exists($file)) {
            return false;
        }

        $array = json_decode(file_get_contents($file), true);

        return $array;
    }

    /**
     * @param AbstractAdapter $adapter
     */
    public function onConnect(AbstractAdapter $adapter)
    {
        $this->online = true;
    }

    /**
     * @param Event $event
     */
    public function onMessage(Event $event)
    {
        $data = $event->getData();
        $mode = $this->parseMessageMode(@$data['targetmode']);
        if (@$data['invokername'] == $this->name || @$data['invokeruid'] == 'serveradmin') {
            return;
        }

        foreach ($this->plugins as $name => $config) {
            if (@$config->CONFIG['event']) {
                continue;
            }
            foreach ($config->triggers as $trigger) {
                if ($trigger == 'event') {
                    break;
                }

                if ($event['msg']->startsWith($trigger)) {
                    if(@$config->CONFIG['permissions'] == 'groups') {
                        $client = $this->node->clientGetByUid($data['invokeruid']);
                        if(empty(@array_intersect_assoc($config->CONFIG['groups'], array_keys($this->node->clientGetServerGroupsByDbid($client['client_database_id']))))) {
                            continue;
                        }
                    } elseif (@$config->CONFIG['permissions'] != 'everyone') {
                        continue;
                    }

                    if (@is_array($config->CONFIG['limitation']) && $mode != null) {
                        if(@!in_array($mode, $config->CONFIG['limitation'])) {
                            continue;
                        }
                    }
                    $info = $data;

                    $info['triggerUsed'] = $trigger;
                    $text = $event['msg']->substr(strlen($trigger) + 1);
                    $info['fullText'] = $event['msg'];

                    if (!empty($text->toString())) {
                        $info['text'] = $text;
                    }

                    $this->plugins[$name]->info = $info;
                    $this->plugins[$name]->trigger();
                    break;
                }
            }
        }
    }

    /**
     * @param Event $event
     */
    public function onEvent(Event $event)
    {
        $data = $event->getData();
        if (@$data['client_type'] == 1) {
            return;
        } elseif ($this->lastEvent && $event->getMessage()->contains($this->lastEvent)) {
            return;
        }

        $this->lastEvent = $event->getMessage()->toString();
        $this->updateList();

        foreach ($this->plugins as $name => $config) {
            if (!@$config->CONFIG['event']) {
                continue;
            }

            foreach ($config->triggers as $trigger) {
                if ($event->getType() != $config->CONFIG['event']) {
                    break;
                }

                $this->plugins[$name]->info = $data;

                $this->plugins[$name]->trigger();
                break;
            }
        }
    }

    protected function updateList()
    {
        $this->node->clientListReset();
        $this->node->channelListReset();
    }

    public function onTimeout($time, AbstractAdapter $adapter)
    {
        if ($adapter->getQueryLastTimestamp() < $this->carbon->now()->subSeconds(120)->timestamp) {
            $adapter->request('clientupdate');
            $this->updateList();
        }
        foreach($this->timers as $timer){
            $timer->handle();
        }

        if(Self::$config['debug'] === true) {
            $this->printOutput("Nimda runtime: " . Convert::seconds($this->timer->getRuntime()) . ", Using " . Convert::bytes($this->timer->getMemUsage()) . " memory.");
        }
    }

    public function onDisconnect()
    {
        $this->online = false;
        $this->timer->stop();
        $this->printOutput("Nimda finished total runtime: " . Convert::seconds($this->timer->getRuntime()) . " seconds, Using " . Convert::bytes($this->timer->getMemUsage()) . " memory.");
    }

    /**
     * @param Ts3Exception $e
     */
    public function onException(Ts3Exception $e)
    {
        $this->printOutput("Error {$e->getCode()}: {$e->getMessage()}");
    }

    public function parseMessageMode($input)
    {
        switch($input) {
            case 1:
                return "private";
                break;
            case 2:
                return "channel";
                break;
            case 3:
                return "server";
                break;
            default:
                return null;
                break;
        }
    }

}