Skip to content
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

Closed
irazasyed opened this issue Jul 2, 2015 · 19 comments
Closed

[Todo] Add Commands Handler System #2

irazasyed opened this issue Jul 2, 2015 · 19 comments

Comments

@irazasyed
Copy link
Owner

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.

@jonnywilliamson
Copy link
Contributor

I have this working here. I'll try and post a gist or info here soon

@irazasyed
Copy link
Owner Author

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 :)

@jonnywilliamson
Copy link
Contributor

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.

routes.php file

Route::post('<unique string, api key is good>', ['as'=>'botcallback', 'uses' => 'TelegramBotController@callback']);

Now we need a controller that will do 2 things:

  • Detect if the inbound update is a command. If it is, check to see if a class exists to respond to that command.
  • If not a command, pass the inbound message (or photo or whatever) to a class that knows what to do with it.

TelegramBotController.php

<?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, app\Bot that has a number of classes. They are all of the format like follows:

app\Bot\commandDemo.php
app\Bot\commandStart.php
app\Bot\commandHelp.php
app\Bot\commandPing.php

etc.

and classes that deal with inbound messages like this:

app\Bot\processInboundAudio.php
app\Bot\processInboundDocument.php
app\Bot\processInboundLocation.php
app\Bot\processInboundVideo.php
app\Bot\processInboundPhoto.php

They all extend from an abstract app\Bot\BotCommands.php class. Here's an example:

app\Bot\BotCommands.php

<?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 command /help

app\Bot\commandHelp.php

<?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 /vote

All I have to do is create a class call app\Bot\commandVote.php and I am DONE. No editing of any other file, no changes to SDK or my code.

Perhaps it might be of some use?

@defunctl
Copy link

@irazasyed any updates on when you're releasing your implementation of this?

@irazasyed
Copy link
Owner Author

@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.

@jonnywilliamson
Copy link
Contributor

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! :)

@irazasyed
Copy link
Owner Author

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 👍

@irazasyed
Copy link
Owner Author

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 composer.json You need to add minimum-stability and prefer-stable options:

{
    "name": "project/name",
    "require": {
        "irazasyed/telegram-bot-sdk": "1.0.*"
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

Notice the SDK version is set to 1.0.* which would automatically load from dev-master.

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 addCommand() method which supports either the command object or full path to the command itself and it'll automatically initialise it behind the scenes.

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 commands option where you can register all your commands. Ref this.

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 addCommands() method. Example:

// 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 commandsHandler().
Here's an example used with Webhook registered and in Laravel:

// Laravel
Route::post('/<token>/webhook', function () {
    Telegram::commandsHandler(true);

    return 'ok';
});

// Standalone
$telegram->commandsHandler(true);

Passing true tells it has to process incoming updates from a webhook, defaults to false which makes a request to getUpdates endpoint to get the updates and then pass it through the commands handler system which will see if the messages content any commands and if so, then process them.

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 /start command which will be triggered when a user sends /start or when they start an interaction with your bot for the first time.

Notice, The $name of the command is start so when a user sends /start, this would be triggered. So always make sure the name is correct and in lowercase. The description is helpful when you get a list of all the available commands either with the /help command or for other purposes using the getCommands() method.

<?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 handle($arguments) method which would be called when a user sends the command and will be passed with the arguments (Currently we don't break the arguments into an array but you can use methods like explode() to break by space and use it for whatever purposes).

In your handle method, You get access to getTelegram() and getUpdate() methods which gives you access to the super class and the original update object sent from Telegram.

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):

  1. replyWith<Message|Photo|Audio|Video|Voice|Document|Sticker|Location|ChatAction>() - Basically, All the send<API Method> are supported and are pre-filled with the chat id, All other params can easily be passed to it like you would normally as per the docs.
  2. triggerCommand(<Command Name>) - This is useful to chain a bunch of commands within a command. Say for example, I want to fire the /subscribe command that is registered already on behalf of the user to subscribe him to get notifications from my bot, I would just use this method to trigger that command in my /start command, So as soon as the user sends /start or interacts with my bot for the first time, they would also be automatically get subscribed to my service. The function supports second param called $arguments which is optional and can be used to send some arguments from this command to the other command. By default, It would be the same arguments that were passed to your original command and the other command class would be same as above, So everything stays the same.

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 :)

@jonnywilliamson
Copy link
Contributor

@irazasyed

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!

@irazasyed
Copy link
Owner Author

@jonnywilliamson Great. That's okay!

I can wait, I appreciate your help :)

Looking forward for your feedback.

@jonnywilliamson
Copy link
Contributor

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?

@irazasyed
Copy link
Owner Author

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:

  1. I didn't want to bond the users to have their commands placed within a specific directory or force them to follow a structure. Sure, We could give an option to set a path but there's another problem.
  2. What if i want to disable a command for whatever reasons? Now since the system is autoloading the commands, I'm now forced to either just delete the command file or move to some other directory or maybe just end up renaming it to something else that would break the pattern (Like if we were to load files based on say <*>Command.php or .php).
  3. With a path based autoloading feature, I'm now forced to place all the commands in one directory when i could've placed and organised them in different directories or maybe have a nested structure (Say i have some bot which does authentication, notifications, etc. different features and now i wanna arrange them into Auth Directory, Notification, etc. multiple directories properly organized commands).
  4. Force users into using one naming convention (For example, The library checks if the command that was sent from the user matches the file name - /start = start.php or StartCommand.php). Or if we were to ignore the file name and rely on the $name var within the class, Then in that case there would be performance issues since everytime the bot receives a command, We have to first pull all the command classes matching a pattern or by .php, register them, then see for a match and then handle them, only then we could be flexible otherwise we're going to force the developer to follow one single pattern.

Reasons why i went with registering commands method:

  1. Registering commands will let you keep track of all the commands, So you can maintain and play with them easily from one place. Say you wanna disable one of these commands, Just comment it out or if you were as i said organizing them into different directories, Sure you can do that with this. Regardless of where the commands reside, All the commands are controllable from one place (If you were to register them in single location).
  2. You could either register from one place or split them into different packages and register the commands from those packages. Think of this as if a plugins system. I could have a auth system related commands that's common across all my commands built as a package and i would only have to require that in composer and use it easily.
  3. It's quite flexible as you can see.
  4. Name the commands however you want (Example the file name is Auth.php but the $name var is start the library will match /start command to this same file since it doesn't care what the filename is. Not that i recommend this but you get the idea as to how much flexible this is...), Put them under any namespace, directory, package or share them among all bots, anything is possible.

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.

@jonnywilliamson
Copy link
Contributor

Hi.

Ok you have some very valid points.

I didn't want to bond the users to have their commands placed within a specific directory or force them to follow a structure. Sure, We could give an option to set a path but there's another problem.

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 Command ?

That way a user could make folder /commands/my/crazy/folder and if there was a class inside there that was an instance of Command it gets added to the registered list?

What if i want to disable a command for whatever reasons? Now since the system is autoloading the commands, I'm now forced to either just delete the command file or move to some other directory or maybe just end up renaming it to something else that would break the pattern (Like if we were to load files based on say <*>Command.php or .php).

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:

  • moving the command class
  • renaming the command class
  • setting a disable = true; attribute in the class.

With a path based autoloading feature, I'm now forced to place all the commands in one directory when i could've placed and organised them in different directories or maybe have a nested structure (Say i have some bot which does authentication, notifications, etc. different features and now i wanna arrange them into Auth Directory, Notification, etc. multiple directories properly organized commands).

This seems to be the same as point 1 above.

Reasons why i went with registering method:

  1. Registering commands will let you keep track of all the commands, So you can maintain and play with them easily. Say you wanna disable one of these commands, Just comment it out or if you were as i said organizing them into different directories, Sure you can do that with this.

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.

  1. You could either register from one place or split them into different packages and register the commands from those packages. Think of this as if a plugins system. I could have a auth system related commands that's common across all my commands built as a package and i would only have to require that in composer and use it easily.

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 command).

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! :)

@jonnywilliamson
Copy link
Contributor

On another unrelated issue:

Telegram FAQ has this:

Global commands
In order to make it easier for users to navigate the bot multiverse, we ask all developers to support a few basic commands. Telegram apps will have interface shortcuts for these commands.

/start - begins interaction with the user, e.g., by sending a greeting message. This command can also be used to pass additional parameters to the bot (see Deep linking)
/help - returns a help message. It can be a short text about what your bot can do and a list of commands.
/settings - (if applicable) returns the bot's settings for this user and suggests commands to edit these settings.

I see there is already a help command added to the SDK, I think it would be also useful if we added the /start command too. It should probably just reply with a standard "Hello < user >. You've set the command system up correctly! Congratulations!"

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).

@irazasyed
Copy link
Owner Author

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 /start command is concerned, Sure i can add one but the thing is, That command supports deep linking which has some nice advantages and different developers would be having different methods and ways of implementing that and deep linking. I can just put up the command with a basic message and people can register that if required (Like help command) and keep it off by default. Sounds good?

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!

@jonnywilliamson
Copy link
Contributor

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.

@irazasyed
Copy link
Owner Author

Okay! No probs. I left that for a reason, now you know :)

@PeymanPanjehbashi
Copy link

Hi,
I used Laravel 5.0 and Irazasyed telegram bot, i want work by webhook and when a person send message to telegram bot, the telegram send a message automatically to that.
class HomeController extends Controller {

public function __construct()
{
    //$this->middleware('auth');
}

public function index()
{
    $telegram = new Api('117451573:*********************', 'true');
    $telegram->setWebhook(['url' => 'https://******.com/117451573:********************/webhook']);
    $update = $telegram->getWebhookUpdates();

    $telegram->sendMessage([
        'chat_id' => '********',
        'text' => 'thanks',
    ]);
    return response()->json(["status" => "success"]);
}

@QueenZe
Copy link

QueenZe commented Oct 20, 2021

Hello @irazasyed

i use this code for webhook :

Route::post('/<mytoken>/webhook', function () {
    Telegram::commandsHandler(true);

    return 'ok';
});

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

Route::get('/setwebhook', function () {
    $response = Telegram::setWebhook(['url' => 'https://c163-203-130-192-171.ngrok.io/L2J5oJlTYDPpv5LwVU2swx5zDqrAAbXiZgrLRBdu/webhook']);
    dd($response);
});

can you help me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants