src/Sovereign.php
<?php
namespace Sovereign;
use Discord\Cache\Cache;
use Discord\Cache\Drivers\ArrayCacheDriver;
use Discord\Discord;
use Discord\Parts\Channel\Message;
use Discord\Parts\Guild\Guild;
use Discord\Parts\User\Game;
use Discord\Parts\WebSockets\PresenceUpdate;
use Discord\WebSockets\Event;
use Discord\WebSockets\WebSocket;
use Monolog\Logger;
use League\Container\Container;
use Sovereign\Lib\Config as globalConfig;
use Sovereign\Lib\cURL;
use Sovereign\Lib\Db;
use Sovereign\Lib\Permissions;
use Sovereign\Lib\Settings;
use Sovereign\Lib\Users;
use Sovereign\Plugins\onMessage\cleverBotMessage;
/**
* Class Sovereign
* @package Sovereign
*/
class Sovereign
{
/**
* @var WebSocket
*/
public $websocket;
/**
* @var Discord
*/
protected $discord;
/**
* @var Container
*/
protected $container;
/**
* @var Logger
*/
protected $log;
/**
* @var globalConfig
*/
protected $globalConfig;
/**
* @var Db
*/
protected $db;
/**
* @var cURL
*/
protected $curl;
/**
* @var Settings
*/
protected $settings;
/**
* @var Permissions
*/
protected $permissions;
/**
* @var Users
*/
protected $users;
/**
* @var array
*/
private $onMessage = [];
/**
* @var array
*/
private $onVoice = [];
/**
* @var array
*/
private $onTimer = [];
/**
* @var \Pool
*/
private $pool;
/**
* @var \Pool
*/
private $timers;
/**
* @var array
*/
private $audioStreams;
/**
* @var array
*/
private $extras = [];
/**
* Sovereign constructor.
* @param Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
$this->log = $container->get('log');
$this->globalConfig = $container->get('config');
$this->db = $container->get('db');
$this->curl = $container->get('curl');
$this->settings = $container->get('settings');
$this->permissions = $container->get('permissions');
$this->users = $container->get('users');
$this->extras['startTime'] = time();
$this->extras['memberCount'] = 0;
$this->extras['guildCount'] = 0;
$this->pool = new \Pool(count($this->onMessage), \Worker::class);
$this->timers = new \Pool(count($this->onTimer), \Worker::class);
// Init Discord and Websocket
$this->log->addInfo('Initializing Discord and Websocket connections..');
$this->discord = Discord::createWithBotToken($this->globalConfig->get('token', 'bot'));
Cache::setCache(new ArrayCacheDriver());
$this->websocket = new WebSocket($this->discord);
}
/**
* @param $type
* @param $command
* @param $class
* @param $perms
* @param $description
* @param $usage
* @param $timer
*/
public function addPlugin($type, $command, $class, $perms, $description, $usage, $timer)
{
$this->log->addInfo("Adding plugin: {$command}");
$this->$type[$command] = [
'permissions' => $perms,
'class' => $class,
'description' => $description,
'usage' => $usage,
'timer' => $timer
];
}
/**
*
*/
public function run()
{
// Reap the threads!
$this->websocket->loop->addPeriodicTimer(600, function () {
$this->log->addInfo('Restarting the threading pool, to clear out old threads..');
// Shutdown the pool
$this->pool->shutdown();
$this->timers->shutdown();
// Startup the pool again
$this->pool = new \Pool(count($this->onMessage), \Worker::class);
$this->timers = new \Pool(count($this->onTimer), \Worker::class);
});
// Handle the onReady event, and setup some timers and so forth
$this->websocket->on('ready', function (Discord $discord) {
$this->log->addInfo('Websocket connected..');
// Update our presence status
$game = new Game(array('name' => $this->globalConfig->get('presence', 'bot', "table flippin'"), 'url' => null, 'type' => null), true);
$this->websocket->updatePresence($game, false);
// Count the amount of people we are available to..
/** @var Guild $guild */
foreach ($this->discord->getClient()->getGuildsAttribute()->all() as $guild) {
$this->extras['memberCount'] += $guild->member_count;
$this->extras['guildCount']++;
$this->extras['guild']['memberCount']["id{$guild->id}"] = $guild->member_count;
$this->extras['onMessagePlugins'] = $this->onMessage;
$this->extras['onVoicePlugins'] = $this->onVoice;
}
$this->log->addInfo("Member count, currently available to: {$this->extras['memberCount']} people");
// Setup the timers for the timer plugins
foreach ($this->onTimer as $command => $data) {
$this->websocket->loop->addPeriodicTimer($data['timer'], function () use ($data, $discord) {
try {
$plugin = new $data['class']($discord, $this->log, $this->globalConfig, $this->db, $this->curl, $this->settings, $this->permissions, $this->container->get('serverConfig'), $this->users, $this->extras);
$this->timers->submit($plugin);
} catch (\Exception $e) {
$this->log->addError("Error running the periodic timer: {$e->getMessage()}");
}
});
}
// Issue periodically recounting and other things (Needed because of pthreads not putting the entire context into children - leading to some weirdness in some plugins)
$this->websocket->loop->addPeriodicTimer(600, function () {
$this->extras['memberCount'] = 0;
$this->extras['guildCount'] = 0;
/** @var Guild $guild */
foreach ($this->discord->getClient()->getGuildsAttribute()->all() as $guild) {
$this->extras['memberCount'] += $guild->member_count;
$this->extras['guildCount']++;
$this->extras['guild']['memberCount']["id{$guild->id}"] = $guild->member_count;
$this->extras['onMessagePlugins'] = $this->onMessage;
$this->extras['onVoicePlugins'] = $this->onVoice;
}
// Output periodic information while doing the recounting stuff
$this->log->addInfo('Currently running audio streams: ' . count($this->audioStreams));
$this->log->addInfo("Member recount, currently available to: {$this->extras['memberCount']} people");
});
// @todo run a timer to check if there are any active voice sessions - and if there are, if there are any people in those voice sessions
// If not, stop the session and leave the channel (To save some bandwidth)
});
$this->websocket->on('error', function ($error, $websocket) {
$this->log->addError('An error occurred on the websocket', [$error->getMessage()]);
die(1);
});
$this->websocket->on('close', function ($opCode, $reason) {
$this->log->addWarning('Websocket got closed', ['code' => $opCode, 'reason' => $reason]);
die(1);
});
$this->websocket->on('reconnecting', function () {
$this->log->addInfo('Websocket is reconnecting..');
});
$this->websocket->on('reconnected', function () {
$this->log->addInfo('Websocket was reconnected..');
});
// Handle incoming message logging
$this->websocket->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
$this->log->addInfo("Message from {$message->author->username}", [$message->content]);
// Don't update data for ourselves..
if ($message->author->id !== $discord->getClient()->id) {
$this->users->set($message->author->id, $message->author->username, 'online', null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), $message->content);
}
// @todo Create text logs
});
// Handle plugin running
$this->websocket->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
$guildID = $message->getChannelAttribute()->guild_id;
// Get server config
$config = $this->settings->get($guildID);
// Is the person admin?
$userDiscordID = $message->author->id;
foreach ($this->globalConfig->get('admins', 'permissions') as $admins) {
$message->isAdmin = $admins === $userDiscordID;
}
// Define the prefix if it isn't already set..
@$config->prefix = $config->prefix ?? $this->globalConfig->get('prefix', 'bot');
// Check if the user requested an onMessage plugin
if (substr($message->content, 0, strlen($config->prefix)) === $config->prefix) {
$content = explode(' ', $message->content);
foreach ($this->onMessage as $command => $data) {
$parts = [];
foreach ($content as $index => $c)
foreach (explode("\n", $c) as $p)
$parts[] = $p;
if ($parts[0] === $config->prefix . $command) {
// If they are listed under the admins array in the bot config, they're the super admins
if (in_array($message->author->id, $this->globalConfig->get('admins', 'permissions')))
$userPerms = 3;
// If they are guild owner, they're automatically getting permission level 2
elseif (null !== $message->getChannelAttribute()->getGuildAttribute()->owner_id && ($message->author->id === $message->getChannelAttribute()->getGuildAttribute()->owner_id))
$userPerms = 2;
// Everyone else are just users
else
$userPerms = 1;
if ($userPerms >= $data['permissions']) {
try {
$message->getChannelAttribute()->broadcastTyping();
if ($data['class'] === "\\Sovereign\\Plugins\\onMessage\\auth") {
/** @var \Threaded $plugin */
$plugin = new $data['class']($message, $discord, $config, $this->log, $this->globalConfig, $this->db, $this->curl, $this->settings, $this->permissions, $this->container->get('serverConfig'), $this->users, $this->extras);
$plugin->run();
} else {
/** @var \Threaded $plugin */
$plugin = new $data['class']($message, $discord, $config, $this->log, $this->globalConfig, $this->db, $this->curl, $this->settings, $this->permissions, $this->container->get('serverConfig'), $this->users, $this->extras);
$this->pool->submit($plugin);
}
$this->log->addInfo("{$message->author->username}#{$message->author->discriminator} ({$message->author}) ran command {$config->prefix}{$command}", $content);
} catch (\Exception $e) {
$this->log->addError("Error running command {$config->prefix}{$command}. Command run by {$message->author->username} in {$message->getChannelAttribute()->name}. Error: {$e->getMessage()}");
$message->reply("**Error:** There was a problem running the command: {$e->getMessage()}");
}
}
}
}
}
});
// Handle joining a voice channel, and playing.. stuff....
$this->websocket->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
// Get the guildID
$guildID = $message->getChannelAttribute()->guild_id;
// Get this guilds settings
$config = $this->settings->get($guildID);
// Get the prefix for this guild
@$config->prefix = $config->prefix ?? $this->globalConfig->get('prefix', 'bot');
if (substr($message->content, 0, strlen($config->prefix)) === $config->prefix) {
$content = explode(' ', $message->content);
foreach ($this->onVoice as $command => $data) {
$parts = [];
foreach ($content as $index => $c) {
foreach (explode("\n", $c) as $p)
$parts[] = $p;
}
if ($parts[0] === $config->prefix . $command) {
try {
$voiceChannels = $message->getFullChannelAttribute()->getGuildAttribute()->channels->getAll('type', 'voice');
foreach ($voiceChannels as $channel) {
if (!empty($channel->members[$message->author->id])) {
$voice = new $data['class']();
$voice->run($message, $discord, $this->websocket, $this->log, $this->audioStreams, $channel, $this->curl);
}
}
} catch (\Exception $e) {
$this->log->addError("Error running voice command {$config->prefix}{$command}. Command run by {$message->author->username} in {$message->getChannelAttribute()->name}. Error: {$e->getMessage()}");
$message->reply("**Error:** There was a problem running the command: {$e->getMessage()}");
}
}
}
}
});
// Handle if it's a message for the bot (CleverBot invocation)
$this->websocket->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
// If we got highlighted we should probably answer back
if (stristr($message->content, $discord->getClient()->id)) {
try {
$this->pool->submit(new cleverBotMessage($message, $discord, $this->log, $this->globalConfig, $this->db, $this->curl, $this->settings, $this->permissions, $this->container->get('serverConfig'), $this->users));
} catch (\Exception $e) {
$message->reply("**Error:** There was an error with CleverBot: {$e->getMessage()}");
}
}
});
// Handle presence updates
$this->websocket->on(Event::PRESENCE_UPDATE, function (PresenceUpdate $presenceUpdate) {
if ($presenceUpdate->user->id && $presenceUpdate->user->username) {
try {
$this->log->addInfo("Updating presence info for {$presenceUpdate->user->username}");
$game = $presenceUpdate->getGameAttribute()->name ?? null;
$this->users->set($presenceUpdate->user->id, $presenceUpdate->user->username, $presenceUpdate->status, $game, date('Y-m-d H:i:s'), null, null);
} catch (\Exception $e) {
$this->log->addError("Error: {$e->getMessage()}");
}
}
});
// Create a new cleverbot \nick\ for this new guild
$this->websocket->on(Event::GUILD_CREATE, function (Guild $guild) {
$cleverBotExists = $this->db->queryField("SELECT serverID FROM cleverbot WHERE serverID = :serverID", "serverID", array(":serverID" => $guild->id));
$guildExists = $this->db->queryField("SELECT guildID FROM guilds WHERE guildID = :serverID", "guildID", array(":serverID" => $guild->id));
// Only create a new server nick if the cleverbot instance doesn't exist.. (Hopefully cleverbot.io is done deleting them at random)
if(!isset($cleverBotExists)) {
$this->log->addInfo("Setting up Cleverbot for {$guild->name}");
$serverID = $guild->id;
$result = $this->curl->post('https://cleverbot.io/1.0/create', ['user' => $this->globalConfig->get('user', 'cleverbot'), 'key' => $this->globalConfig->get('key', 'cleverbot')]);
if ($result) {
$result = @json_decode($result);
$nick = $result->nick ?? false;
if ($nick) {
$this->db->execute('INSERT INTO cleverbot (serverID, nick) VALUES (:serverID, :nick) ON DUPLICATE KEY UPDATE nick = :nick', [':serverID' => $serverID, ':nick' => $nick]);
}
}
}
if(!isset($guildExists)) {
$this->db->execute("INSERT IGNORE INTO guilds (guildID) VALUES (:guildID)", array(":guildID" => $guild->id));
// Send a hello message to the channel (Only if it's new!)
//$message = "Hello, i was invited here by someone with admin permissions, i have quite a few features that you can discover by doing %help\n";
//$message .= "I am sorry if i am triggering other bots aswell, you can change my trigger with %config setTrigger newTrigger (Example: %config setTrigger *)\n";
//$message .= "If you for some reason don't want me here after all, just kick me ;)";
// Get the first channel in the list (usually the default channel)
//$channel = $guild->channels->first();
//$channel->sendMessage($message);
}
});
// Run the websocket, and in turn, the bot!
$this->websocket->run();
}
/**
* Get the configuration/settings container and return it upstream to the calling code.
*
* @return globalConfig
*/
public function getGlobalConfig() {
return $this->globalConfig;
}
}