-
Notifications
You must be signed in to change notification settings - Fork 671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Todo] Add Commands Handler System #2
Comments
I have this working here. I'll try and post a gist or info here soon |
Sounds good 👍 I've actually built the whole system already, Just playing around with it to see if i can improve that better and or probably use a third-party package for command bus. Lets see your working version too :) |
OK, now I have code fear. Fear that my solution will be crap! Here's what I've done. I'm purely using Laravel and it's command bus as that's what suits me. This might not suit the project though. Also, the MOST important thing for me was that I could add new commands WITHOUT having to edit ANY original SDK file. In other words, it was a requirement that adding a new command could be done by adding a new class, NEVER editing an already existing file. Here's how I have that all setup. Lets setup a route for inbound calls from Telegram. I personally find this the easiest way, means I don't have to do 'looking' for updates and do all the polling myself.
Route::post('<unique string, api key is good>', ['as'=>'botcallback', 'uses' => 'TelegramBotController@callback']); Now we need a controller that will do 2 things:
<?php
namespace App\Http\Controllers;
use App\Bot\commandInvalid;
use App\Http\Requests;
use Irazasyed\Telegram\Objects\Message;
use Telegram;
class TelegramBotController extends Controller
{
public function callback()
{
//What keys are ALWAYS in the update object
$requiredKeys = ['message_id', 'from', 'chat', 'date'];
//Init the class name we will possibly be calling later.
$commandClass = null;
//Get the Update object
$update = Telegram::getWebhookUpdates();
$message = new Message($update->get('message'));
//If this is a message, lets see if its a command (starts with "/" )
if (isset($message['text']) && starts_with($message['text'], '/')) {
$arguments = explode(' ', substr($message['text'], 1));
$commandClass = "App\\Bot\\command" . ucfirst(strtolower(array_shift($arguments)));
} else {
//Not a message, lets check what type of update object it is by removing all keys that are normally provided
$typeOfInbound = array_except($message, $requiredKeys);
$commandClass = "App\\Bot\\processInbound" . ucfirst($typeOfInbound->keys()->first());
}
//If the class exists then we have a valid command or process to execute. Otherwise just return an
//acknowledgement that the message was received.
return class_exists($commandClass) ? $this->dispatch(new $commandClass($update)) : $this->dispatch(new commandInvalid($update));
}
} So this controller detects if the inbound is a command or normal message and if a class has been created to deal with that type of command/message, dispatches it via the Laravel command bus. Now I have a folder in my app,
etc. and classes that deal with inbound messages like this:
They all extend from an abstract
<?php namespace App\Bot;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\SelfHandling;
use Irazasyed\Telegram\Objects\Message;
use Irazasyed\Telegram\Objects\Update;
use Irazasyed\Telegram\Objects\User;
use Irazasyed\Telegram\Objects\GroupChat;
abstract class BotCommands implements SelfHandling
{
/**
* @var int
*/
protected $updateId;
/**
* @var Message
*/
protected $message;
/**
* @var array
*/
protected $arguments = [];
/**
* @var string
*/
protected $command;
/**
* @var mixed|static
*/
protected $messageId;
/**
* @var User
*/
protected $from;
/**
* @var Carbon
*/
protected $date;
/**
* @var User|GroupChat
*/
protected $chat;
public function __construct(Update $inbound)
{
$this->updateId = $inbound->get('update_id');
$this->message = new Message($inbound->get('message'));
$this->messageId = $this->message->get('message_id');
$this->from = new User($this->message->get('from'));
$this->date = Carbon::createFromTimestamp($this->message->get('date'));
$typeOfChat = $this->message->relations()['chat'];
$this->chat = new $typeOfChat($this->message->get('chat'));
if (isset($this->message['text'])) {
$this->arguments = explode(' ', substr($this->message['text'], 1));
$this->command = strtolower(array_shift($this->arguments));
}
}
public abstract function handle();
} And finally, here's how I would deal with the
<?php namespace App\Bot;
use Telegram;
class commandHelp extends BotCommands
{
public function handle()
{
Telegram::sendMessage($this->chat->get('id'), view('telegramBot.commandHelp')->render(), true);
}
} Perhaps it's complicated, but I think its brilliant for one reason...if I decide right now to add a new command to respond to All I have to do is create a class call Perhaps it might be of some use? |
@irazasyed any updates on when you're releasing your implementation of this? |
@defunctl I'll try this week. I'm testing a few things and I'm thinking to add support for multi bots in this release or maybe in another one. Will update soon. @jonnywilliamson Thanks for posting your solution. Looks good, Mine is different and quite flexible. You'll see once released. |
This is why I hate posting code. Someone always has a better way to do it than the way I come up with. Hahah. I'll have to get over it! :) |
Haha there's always room for improvements, no matter what :) I'm sure after i post mine, People would be able to suggest improvements i can make. Different minds, Different Ideas & Experiences but that's the best part of open source! We get to learn ;) P.S I liked how you're handling the dates and other inbound messages though. Good methods 👍 |
Update! So I've pushed my commands handling system to the master branch and here are few instructions on how to use it. I need help with testing this before i can tag and release as a stable version. Example {
"name": "project/name",
"require": {
"irazasyed/telegram-bot-sdk": "1.0.*"
},
"minimum-stability": "dev",
"prefer-stable": true
} Notice the SDK version is set to Once the new version is installed, You can follow the same instructions in README to setup this on Laravel or use it standalone. So the first step is to register our commands after we create one. In order to add a single command, we can use the For this example, I'm using the Help Command that comes with this library to get you started: $telegram->addCommand(Irazasyed\Telegram\Commands\HelpCommand::class);
// OR
$command = new Irazasyed\Telegram\Commands\HelpCommand();
$telegram->addCommand($command); With Laravel (Assuming Facade is used), You can either use the below method to dynamically register a command or simply use the config file which comes with Telegram::addCommand(Irazasyed\Telegram\Commands\HelpCommand::class);
// OR
$command = new Irazasyed\Telegram\Commands\HelpCommand();
Telegram::addCommand($command); To register multiple commands, You can pass an array with all the commands that has to be registered to the // Standalone
$telegram->addCommands([
Irazasyed\Telegram\Commands\HelpCommand::class,
Vendor\Project\TestCommand::class,
Vendor\Project\StartCommand::class,
]);
// Laravel
Telegram::addCommands([
Irazasyed\Telegram\Commands\HelpCommand::class,
Vendor\Project\TestCommand::class,
Vendor\Project\StartCommand::class,
]); Note: All commands are lazy loaded, So there shouldn't be any performance issues with the app. Now to handle inbound commands, You have to use the new method called // Laravel
Route::post('/<token>/webhook', function () {
Telegram::commandsHandler(true);
return 'ok';
});
// Standalone
$telegram->commandsHandler(true); Passing Now comes the actual command class: All the commands should extend the Command class which implements Irazasyed\Telegram\Commands\CommandInterface So for this example, Will build a Notice, The <?php
namespace Vendor\App\Commands;
use Irazasyed\Telegram\Actions;
use Irazasyed\Telegram\Commands\Command;
class StartCommand extends Command
{
/**
* @var string Command Name
*/
protected $name = "start";
/**
* @var string Command Description
*/
protected $description = "Start Command to get you started";
/**
* @inheritdoc
*/
public function handle($arguments)
{
// This will send a message using `sendMessage` method behind the scenes to
// the user/chat id who triggered this command.
// `replyWith<Message|Photo|Video|ChatAction>` all the available methods are dynamically
// handled when you replace `send<Method>` with `replyWith`.
$this->replyWithMessage('Hello! Welcome to our bot, Here are our available commands:');
// This will update the chat status to typing...
$this->replyWithChatAction(Actions::TYPING);
// This will prepare a list of available commands and send the user.
// First, Get an array of all registered commands
// They'll be in 'command-name' => 'Command Handler' format.
$commands = $this->telegram->getCommands();
// Build the list
$response = '';
foreach ($commands as $name => $command) {
$response .= sprintf('/%s - %s' . PHP_EOL, $name, $command->getDescription());
}
// Reply with the commands list
$this->replyWithMessage($response);
// Trigger another command dynamically from within this command
$this->triggerCommand('subscribe');
}
} So the above class should be self-explanatory but let me explain a lil bit though! All the commands you create should implement In your handle method, You get access to The commands system as you can see in above example comes with a few helper methods (They're optional just to help you and make things easier):
If a command is not registered but the user fires one (Lets say an invalid command), By default the system will look for a help command if its registered one and if yes, then it'll be triggered. So the default help command class if you were to use would respond the user with the available list of commands with description. Currently, It's still in development. So bugs are expected and if i could get some help with testing this whole thing, It would be very much helpful and speed things up to release the stable version. Any feedback / improvements / ideas / PRs are welcome and highly appreciated :) |
Only home from holiday, this is great to see. I have some thoughts but I'm going to play with your code for a little while and get back to you. Unfortunately I'm starting a very long week of work, so give me a little time before I can report back. I appreciate the work you do on this! |
@jonnywilliamson Great. That's okay! I can wait, I appreciate your help :) Looking forward for your feedback. |
OK. First query - Registering commands: Currently we have these options: $telegram->addCommand(Irazasyed\Telegram\Commands\HelpCommand::class);
// OR
$command = new Irazasyed\Telegram\Commands\HelpCommand();
$telegram->addCommand($command);
//OR
Telegram::addCommand(Irazasyed\Telegram\Commands\HelpCommand::class);
// OR
$command = new Irazasyed\Telegram\Commands\HelpCommand();
Telegram::addCommand($command);
// OR MULTIPLE COMMANDS AT ONCE
// Standalone
$telegram->addCommands([
Irazasyed\Telegram\Commands\HelpCommand::class,
Vendor\Project\TestCommand::class,
Vendor\Project\StartCommand::class,
]);
// OR
// Laravel
Telegram::addCommands([
Irazasyed\Telegram\Commands\HelpCommand::class,
Vendor\Project\TestCommand::class,
Vendor\Project\StartCommand::class,
]); Whilst this flexibility is great, Laravel Facades / Regular PHP, Arrays etc, I still have a long hankering to be able to do this WITHOUT having to register a command at all! It seems like an extra step. Another few lines of code added to my controller just to register a command. If I rename my custom command class I have to remember to update the registered line in the controller to match this change. I'd love to be able to skip this altogether. I'm trying to work out the best way to do this. Would there be benefit of using any php reflection facility to see if any "command classes" have been created by the user? Perhaps the reflection classes aren't the right tool, but finding a way to automatically find all user created commands and pre register them seems like a really slick thing to do. Just allowing you to drop in a new class into the folder and boom - ready to go! Thoughts? |
So to answer your query, I did think about autoloading commands from a directory before going with the current method, However i see a few issues with this and they are as follows:
Reasons why i went with registering commands method:
This is what i think but I'm still open for more options and nothing final though. Maybe i could also add support to simply pass a directory path and let it autoload them, will have to play with this. |
Hi. Ok you have some very valid points.
I suppose that is personal preference, but having a master location where commands live doesn't seem to be a overly bad thing. Allowing full flexibility inside that master location might make this more palatable. For example, given a required folder '/commands' (or whatever is sensible) is there a simple and non performance hitting way to check all files/class in sub folders for instances of That way a user could make folder
I have no better solution to this issue. Having disabled commands is not something I had considered, interested to see what others think. However, editing your controller to comment out / remove the registered command is almost the same amount of pain as either:
This seems to be the same as point 1 above.
Yes, but at the cost of having to manually add them and maintain the correct names if commands change etc. One more step in the process during coding. I'm not saying it's a difficult or onerous step, just one that I was hoping to eliminate! (PS it is a very flexible solution you have made - I do think it is clever!) My main issue is purely: If I want to add/remove a command I must add/remove the command's class file (that's cool), but ALSO edit another (config) file in the project to make it work.
To be fair, as I said your implementation is very flexible. It's hard to find fault with that! I suppose I may have to change track and open a new feature request issue! I would like to have a fall back option in the config file, to allow me to specify a folder to autoload all commands in that folder (either via a filename pattern or just if each file is found to be an instance of I could give that a bash via a PR, but I'll only try that IF its something you feel you would be happy to implement. It's ok if you say NO...it's your baby! :) |
On another unrelated issue:
I see there is already a help command added to the SDK, I think it would be also useful if we added the I think the start command is important because it's the first command that can/is sent when a user searches for your bot. They can't send the /help command until they have pressed the start button on the screen (at least that's the case with the ios version). |
Feel free to send me a PR. I'll take a look and merge after i test it well and see if its working like you say. Just make sure its flexible and we're not forcing anyone to do what we personally like, it's designed for everyone you know! So flexibility is important. As far as the P.S Also yeah, Lets keep the additional feature to another ticket. Please create a new one and we'll take this forward from there! |
Ugh. I wasn't thinking. By creating the /start command I forgot it wasn't editable by the user (ie it would be in the vendor folder). I was playing with it using just the project files and forgot about this. Ignore me. I wasn't thinking that through. It's a bad idea. |
Okay! No probs. I left that for a reason, now you know :) |
Hi,
|
Hello @irazasyed i use this code for webhook :
but the result is : The GET method is not supported for this route. Supported methods: POST.i'm using this code too but nothing happend to my bot
can you help me? |
A system to handle and process commands automatically. Maybe like Laravel's command bus.
So when a new message arrives through a webhook or when manually getting updates, Make it easy to process such messages if they're commands.
The text was updated successfully, but these errors were encountered: