Migrated to Gitea

This commit is contained in:
2020-12-19 15:51:19 +00:00
commit b0fac770a1
17 changed files with 4728 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
<?php
namespace nogueiracodes\RaspberryBot\Bot\Actions;
use nogueiracodes\RaspberryBot\Core\Interfaces\Action;
class MessageLogger implements Action
{
public function processAction($message)
{
echo "Sniffed message from " . $message->author->username . ": " . $message->content . PHP_EOL;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace nogueiracodes\RaspberryBot\Bot\Commands;
use Ely\Mojang\Api;
use Exception;
use nogueiracodes\RaspberryBot\Core\Interfaces\Command;
use nogueiracodes\RaspberryBot\Traits\HasSignature;
// Inspired by Laravel's artisan commands
class MinecraftInfo implements Command
{
use HasSignature;
/**
* Must be included at all times. This describes the command's parameters, which can be used in the run method.
*
* @var string
*/
private $signature = "minecraft {operation: The The kind of operation; for now, only convert is supported} {subOperationType: The sub operation type} {subOperationArgument: The argument you want to pass to to the sub operation}";
/**
* Parrots back whatever the user said. Proof of concept.
*
* @param string $parameters This is an array of named parameters with values, based on the signature. First value is always the command's name, like how PHP's $argv is organized.
* @return void
*/
public function run(array $parameters)
{
$mojangAPI = new Api();
switch($parameters['operation'])
{
// convert to username
case 'convert':
switch($parameters['subOperationType'])
{
case 'username':
$response = $parameters['subOperationType'] . "\'s UUID is " . $mojangAPI->usernameToUUID($parameters['subOperationArgument'])->getId();
break;
default:
throw new Exception("Sorry, but at the moment you can only convert usernames to UUIDs.");
}
break;
case 'blacklist':
switch($parameters['subOperationType'])
{
case 'is-banned':
$blockedServerCollection = $mojangAPI->blockedServers();
$response = ($blockedServerCollection->isBlocked($parameters['subOperationArgument']))
? 'Sorry! It appears that this server is indeed being blocked by Mojang. Users won\'t be able to join this Minecraft server using the official client.'
: 'A little bird told me that this server is NOT blocked by Mojang. Users can freely join this server.';
break;
default:
throw new Exception("Sorry, but at the moment you can only check if servers are banned.");
}
break;
default:
throw new Exception("Invalid usage! Please refer to !!rb help for more information.");
}
return $response;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace nogueiracodes\RaspberryBot\Bot\Commands;
use Exception;
use nogueiracodes\RaspberryBot\Core\Interfaces\Command;
use nogueiracodes\RaspberryBot\Traits\HasSignature;
use Zttp\Zttp;
class NumberFact implements Command
{
use HasSignature;
public $signature = "numberfact {number: The number for which you want facts for.}";
public function run(array $parameters)
{
$number = $parameters['number'];
$result = Zttp::get('http://numbersapi.com/' . $number . '?json');
if ($result->isOk())
{
return $result->json()['text'];
}
else
{
throw new Exception("Sorry, but I can\'t fetch number facts right now. Try again later.");
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace nogueiracodes\RaspberryBot\Bot\Commands;
use nogueiracodes\RaspberryBot\Core\Interfaces\Command;
use nogueiracodes\RaspberryBot\Traits\HasSignature;
// Inspired by Laravel's artisan commands
class SimplePlayback implements Command
{
use HasSignature;
/**
* Must be included at all times. This describes the command's parameters, which can be used in the run method.
*
* @var string
*/
private $signature = "capitalize {word: The word you want to convert} {state: lower or upper}";
/**
* Parrots back whatever the user said. Proof of concept.
*
* @param string $parameters This is an array of named parameters with values, based on the signature. First value is always the command's name, like how PHP's $argv is organized.
* @return void
*/
public function run(array $parameters)
{
// Play with the string first before sending it
switch($parameters['state'])
{
case 'lower':
$capitalized = strtolower($parameters['word']);
break;
case 'upper':
$capitalized = strtoupper($parameters['word']);
break;
default:
$capitalized = "Usage: !!rb capitalize [word] [upper|lower]. Use !!rb help for more.";
}
return $capitalized;
}
}

10
src/Bot/init.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
require realpath(__DIR__ . '/../../vendor/autoload.php');
$dotenv = Dotenv\Dotenv::createImmutable(realpath(__DIR__ . '/../../'));
$dotenv->load();
$dotenv->required('BOT_AUTH_TOKEN')->notEmpty();
$dotenv->required('COMMAND_PREFIX')->notEmpty();

View File

@@ -0,0 +1,15 @@
<?php
namespace nogueiracodes\RaspberryBot\Core\Interfaces;
interface Action
{
/**
* Process the said message. Can be used for cursing filters, etc.
*
* @param stdClass $message
* @return void
*/
public function processAction($message);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace nogueiracodes\RaspberryBot\Core\Interfaces;
use Discord\DiscordCommandClient;
use Discord\Parts\Channel\Message;
interface Command
{
/**
* Implement what the command should do here.
*
* @return void
*/
public function run(array $parameters);
/**
* Obtains the signature in an array from, from the command's internal signature.
*
* @param string $signature
* @return array
*/
public function getCommandSignature(string $signature): array;
/**
* Same as getCommandSignature, but for convenience.
*
* @see getCommandSignature()
* @return array
*/
public function getCommandArguments(): array;
}

174
src/Core/RaspberryBot.php Normal file
View File

@@ -0,0 +1,174 @@
<?php declare(strict_types = 1);
namespace nogueiracodes\RaspberryBot\Core;
use Discord\Discord;
use Discord\DiscordCommandClient;
use Exception;
use nogueiracodes\RaspberryBot\Core\Interfaces\Action;
use nogueiracodes\RaspberryBot\Core\Interfaces\Command;
use nogueiracodes\RaspberryBot\Traits\ParsesCommands;
class RaspberryBot
{
use ParsesCommands;
/**
* The Discord Command Client.
*
* @var \Discord\DiscordCommandClient;
*/
private $commandClient;
/**
* The loaded commands for this bot instance.
*
* @var array
*/
private $commands = [];
/**
* The loaded actions for this bot instance. Actions run on messages sent by users, and not when a prefix is detected, unlike commands.
*
* @var array
*/
private $actions = [];
/**
* The command prefix the bot should look for when parsing commands.
*
* @var string
*/
private $commandPrefix;
/**
* Stards the Discord Client and registers commands. Allows fluent chaining.
*
* @return void
*/
public function initialize(): RaspberryBot
{
$this->commandClient = new DiscordCommandClient([
'token' => $_ENV['BOT_AUTH_TOKEN'],
'description' => 'A helpful robot giving you access to the Staff Manager Web App.',
]);
return $this;
}
/**
* Sets the command prefix the bot responds to.
*
* @param string $prefix The prefix.
* @return RaspberryBot
*/
public function setCommandPrefix(string $prefix): RaspberryBot
{
$this->commandPrefix = $prefix;
return $this;
}
/**
* Adds a command the bot will recognise. Must implement the Command interface.
*
* @param Command $command
* @return RaspberryBot
*/
public function addCommand(array $command): RaspberryBot
{
foreach ($command as $singleCommand)
{
if ($singleCommand instanceof Command)
{
echo "INFO: Registering command " . $singleCommand->getCommandArguments()[0]['commandName'] . PHP_EOL;
array_push($this->commands, $singleCommand);
}
else
{
echo "WARNING: Checked command, but it is not valid. Discarding!" . PHP_EOL;
}
}
return $this;
}
/**
* Adds an action the bot will work on.
* Actions run on messages, and they can be used to detect swearing, or reply to a user saying hello, etc.
*
* @param Action $action The action.
* @return RaspberryBot
*/
public function addAction(Action $action): RaspberryBot
{
array_push($this->actions, $action);
return $this;
}
/**
* Runs the event loop, registers commands, and starts listening for mesesages and processing actions
*
* @return void
*/
public function run(): void
{
$this->commandClient->on('ready', function()
{
$this->commandClient->on('message', function($message)
{
if (!empty($this->commands) && $this->isCommand($message))
{
$reply = "";
try
{
$command = $this->determineCommand($message);
$arguments = $this->getNamedArguments($message, $command);
$reply = $command->run($arguments);
}
catch (Exception $ex)
{
$reply = $ex->getMessage();
}
return $message->reply($reply);
}
if (!empty($this->actions))
{
foreach($this->actions as $action)
{
$action->processAction($message);
}
}
else
{
echo "(Debug) WARNING: No actions have been defined. However, we received a message, but don\'t know what to do with it.";
}
});
});
$this->commandClient->run();
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace nogueiracodes\RaspberryBot\Traits;
trait HasSignature
{
// sample signature
// "commandName {argument: description} {argument2: description}";
public function getCommandSignature(string $signature): array
{
$argumentList = [];
$firstArgPos = strpos($signature, '{');
$commandName = substr($signature, 0, $firstArgPos - 1);
$hasRemainingArguments = true;
$argumentCounter = 0;
$executionTimer = 0;
while($hasRemainingArguments)
{
// Bug check: This is an hardcoded parameter limit.
// The bot will purposefully crash if it finds a command that defined more than twenty parameters,
// or if somehow an unknown bug caused an infinite loop.
// This can happen if something (or someone) messes with the internal counters
// This piece of code can be safely removed if there are unit tests ensuring this won't happen. (insecurity lol)
$executionTimer++;
if ($executionTimer > 20)
die('FATAL: Command signature parser - Infinite loop detected!');
// Here we conditionally set the current argument bound start.
// It can't be rewritten if it already has a value, which had been set to allow for the next iteration.
if (!isset($currentArgBoundStart))
{
$currentArgBoundStart = $firstArgPos;
}
/*
* An argument bound is defined by each curly bracket, e.g. Start and End {}.
* Here it's position and value is recorded for later use.
* An argument namespace is everything inside the curly brackets, including the brackets themsevles, e.g.
* {test: description} <-- arg namespace
*/
$currentArgBoundEnd = strpos($signature, '}', $currentArgBoundStart);
$currentArgNamespace = substr($signature, $currentArgBoundStart , $currentArgBoundEnd - $currentArgBoundStart + 1);
// Here we obtain the name and description values inside the namespace, basing ourselves off of the current namespace and the whole signature.
$currentArgName = substr($signature, $currentArgBoundStart, strpos($currentArgNamespace, ':'));
$currentArgDescription = substr($currentArgNamespace, strpos($currentArgNamespace, ':') + 2, strpos($currentArgNamespace, '}') - strlen($currentArgNamespace));
// The next arg bound position is the start of the next arg bound, e.g. an opening curly bracket,
// that will define the next namespace to scan.
$nextArgBoundPos = strpos($signature, '{', $currentArgBoundEnd + 1);
array_push($argumentList, [
'commandName' => $commandName,
'argumentName' => str_replace("{", "", $currentArgName), //FIXME: This is a cheap workaround hack, the first iteration always has that character and the bug is somewhere above
'argumentDescription' => $currentArgDescription,
'argumentPosition' => $argumentCounter
]);
$argumentCounter++;
// Here we check if we can find the next namespace.
// strpos() will always return false if it doesn't find what you told it to find.
// Following that logic, a non integer value means no other namespace is here and therefore we quit the loop.
if (!is_int($nextArgBoundPos))
{
$hasRemainingArguments = false;
}
else
{
// To prevent an infinite PC crashing loop, we reset the current loop argument namespace's bounds to the next namespace.
// Not doing this will cause an infinite loop where the function tries to parse the first namespace forever, because
// in this scenario there would always be a next namespace, causing the loop to not be exited.
$currentArgBoundStart = $nextArgBoundPos + 1;
$currentArgBoundEnd = strpos($signature, '}', $currentArgBoundStart) + 1;
}
}
return $argumentList;
}
public function getCommandArguments(): array
{
return $this->getCommandSignature($this->signature);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace nogueiracodes\RaspberryBot\Traits;
use Illuminate\Support\Str;
use Discord\Parts\Channel\Message;
use Exception;
use nogueiracodes\RaspberryBot\Core\Interfaces\Command;
trait ParsesCommands
{
/**
* Checks if said message is a command.
*
* @param Message $message
* @return boolean
*/
private function isCommand(Message $message): bool
{
if (strtok($message->content, " ") == $this->commandPrefix)
return true;
return false;
}
private function determineCommand(Message $message): Command
{
$commandName = strtok(str_replace($this->commandPrefix, "", $message->content), " ");
foreach ($this->commands as $command)
{
// Due to a bug, the command name is returned on all parameters. It doesn't matter which parameter we
// access to get the name, since they're all the same.
if ($command->getCommandArguments()[0]['commandName'] == $commandName)
{
return $command;
}
}
throw new \Exception("Unknown command (" . $commandName . "). Please try again or use {$this->commandPrefix} help.");
}
private function getNamedArguments(Message $message, Command $command): array
{
$commandArguments = [];
$parameters = [];
foreach($command->getCommandArguments() as $argument)
{
array_push($commandArguments, $argument['argumentName']);
}
$incomingString = $message->content;
$splicedMessage = preg_split('/\s+/', str_replace($this->commandPrefix, "", $incomingString), -1, PREG_SPLIT_NO_EMPTY);
unset($splicedMessage[0]); // Get rid of the cmd name (always same index)
sort($splicedMessage); // Reindex the array
if (count($splicedMessage) !== count($commandArguments))
throw new Exception("Invalid usage! Please supply the correct arguments. Use {$this->commandPrefix} help for more information.");
return array_combine($commandArguments, $splicedMessage);
}
}