Executando verificação de segurança...
10

Criando bots no Telegram como um ninja, usando TelegramPhp

Vamos criar um bot simples para previsão do tempo usando a biblioteca https://github.com/httd1/TelegramPhp e Weather Forecast API https://open-meteo.com/en/docs, essa API é grátis e não precisa de token ou access_key, só tome cuidado com os limites!

Com essa biblioteca https://github.com/httd1/TelegramPhp o trabalho se torna mamão com açucar, podemos tem um bot executando em algumas poucas linhas de código, recomendo que dê uma lida na documentação do Telegram pois alguns conceitos que faremos uso aqui já foram explicados lá, dê uma olhada também no README.md do TelegramPhp para entender um pouco mais sobre a biblioteca.

Vamos utilizar aqui o conceito de webhook, onde o Telegram irá notificar nosso sistema sobre as interações dos usuários com o nosso bot.

Let’s bora.

Vamos começar criando nosso bot no @botfather do Telegram, não vou explicar como funciona essa parte pois considero você esperto de mais pra isso 😜.

@tempotestebot será nosso bot no Telegram.

Com o token do nosso bot em mãos, vamos para outra etapa que será instalar nossa biblioteca(favorita 😁) usando o composer.

composer require httd1/telegramphp

Já temos quase tudo que precisamos para fazer nosso bot, então, mão no código.

Vamos começar criando nosso arquivo principal bot.php que receberá nossa payload pelo webhook.

<?php

include __DIR__ . '/vendor/autoload.php';

use \TelegramPhp\TelegramPhp;
use \TelegramPhp\Config\Token;

// vamos registrar o token do nosso bot
Token::setToken ('6438698345:AAFXL4rrwem5SwfWTcp4V5Huh7yAvv1fo-Y');

// pega logs
\TelegramPhp\Config\Logs::catchLogs (Logs::class);

$tlg = new TelegramPhp ();

// localização
if (!empty ($tlg->getLocation ())){
    $tlg->runAction ('Controller@weather');
}

// comando /start e /help
$tlg->command ('/start', 'Controller@start');
$tlg->command ('/help', 'Controller@help');

// atualiza previsão
$tlg->command ('/weather {{latitude}} {{longitude}}', 'Controller@weather');

// mensagem padrão, sempre no final!
$tlg->commandDefault ('Controller@defaultResponse');

Como você viu, em poucas linhas já temos a estrutura principal do nosso bot, vamos passar por algumas partes do que fizemos acima.

use \TelegramPhp\TelegramPhp;
use \TelegramPhp\Config\Token;

// vamos registrar o token do nosso bot
Token::setToken ('6438698345:AAFXL4rrwem5SwfWTcp4V5Huh7yAvv1fo-Y');

// pega logs
\TelegramPhp\Config\Logs::catchLogs (Logs::class);

$tlg = new TelegramPhp ();

Nessa parte definimos as classes que iremos utilizar.

Classe Token para definir o token do nosso bot, ele será utilizado por outras classes da biblioteca, é importante defini-lo no início do projeto.

Classe TelegramPhp, por ela teremos acesso aos dados de interação do usuário com o bot e aos métodos command que será nossa rota até a classe Controller a diante.

Um adendo, temos aqui opção de capturar logs dos nossos comandos \TelegramPhp\Config\Logs::catchLogs(Logs::class), com isso podemos debugar nossos bots ou obter insights sobre o uso, usuários e até elaborar estátisticas, não faremos uso dela mas tá ai uma dica!

// localização
if (!empty ($tlg->getLocation ())){
    $tlg->runAction ('Controller@weather');
}

// comando /start e /help
$tlg->command ('/start', 'Controller@start');
$tlg->command ('/help', 'Controller@help');

// atualiza previsão
$tlg->command ('/weather {{latitude}} {{longitude}}', 'Controller@weather');

// mensagem padrão, sempre no final!
$tlg->commandDefault ('Controller@defaultResponse');

Aqui nossos comandos serão direcionados com base na interação do usuário, com getLocation conseguimos a localização que foi enviada pelo usuário, nos métodos command definimos o que esperamos como comando e onde eles serão processados, note que o método runAction foge do padrão, ele não é um método documentado na biblioteca pois é executado internamente nos métodos command, commandDefault e commandMatch, mas por ser um método público podemos fazer uso dele!

Precisamos também de uma classe para interagir com a API Open-Meteo, de onde vamos obter a previsão do tempo.

<?php

use \GuzzleHttp\Client;

class OpenMeteo {

    public static function getWeather ($latitude, $longitude){

        $guzlle = new Client();
        $json = $guzlle->request ('GET', "https://api.open-meteo.com/v1/forecast?latitude={$latitude}&longitude={$longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,relative_humidity_2m,dew_point_2m,apparent_temperature,precipitation_probability,precipitation,rain,showers,snowfall,snow_depth,weather_code,pressure_msl,surface_pressure,cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,visibility,evapotranspiration,et0_fao_evapotranspiration,vapour_pressure_deficit,wind_speed_10m,wind_speed_80m,wind_speed_120m,wind_speed_180m,wind_direction_10m,wind_direction_80m,wind_direction_120m,wind_direction_180m,wind_gusts_10m,temperature_80m,temperature_120m,temperature_180m,soil_temperature_0cm,soil_temperature_6cm,soil_temperature_18cm,soil_temperature_54cm,soil_moisture_0_to_1cm,soil_moisture_1_to_3cm,soil_moisture_3_to_9cm,soil_moisture_9_to_27cm,soil_moisture_27_to_81cm&daily=weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,daylight_duration,sunshine_duration,uv_index_max,uv_index_clear_sky_max,precipitation_sum,rain_sum,showers_sum,snowfall_sum,precipitation_hours,precipitation_probability_max,wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant,shortwave_radiation_sum,et0_fao_evapotranspiration&timezone=America%2FSao_Paulo");

        return json_decode($json->getBody(), true);
    
    }

    public static function getWMOCode ($code, $is_day)
    {

        $codes =  [
            "0" => [
                "day" => [
                    "description" => "Ensolarado ☀️",
                    "image" => "http://openweathermap.org/img/wn/[email protected]"
                ],
                "night" => [
                    "description" => "Céu limpo 🌙",
                    "image" => "http://openweathermap.org/img/wn/[email protected]"
                ]
            ]
            //...
          ];

        return $codes [$code][($is_day) ? 'day' : 'night'] ?? null;
    }

}

Como você viu, já referenciamos a classe Controller mas ainda não há fizemos, ela é muito importante pois lá serão executados os comandos e enviadas as respostas para os usuários, falando em respostas temos também nosso arquivo langs.php onde terão os textos do bot em outros idiomas.

<?php

define ("LANG", [
    "pt-br" => [
        "start" => "🌞 Envie sua localização e saiba a previsão do tempo!",
        "help" => "Usando sua localização iremos enviar sua previsão do tempo.",
        "weather" => "<b>🌤️ Previsão do tempo para %s, %s</b>\n\n🎈 Agora %s, %s\n☔ %s chuva ao longo do dia\n\n ¦ • Temperatura: ↑%s, ↓%s\n ¦ • Cob. Nuvens: %s\n ¦ • Humidade: %s\n ¦ • Amanhecer: %s\n ¦ • Anoitecer: %s\n",
        "error_weather" => "<b>🙁 Não foi possível conseguir a previsão para a sua localização, tente novamente!</b>",
        "defaultResponse" => "Envie sua localização, o bot irá responder com a previsão do tempo.",
    ],
    "en" => [
        "start" => "🌞 Send me your location and know your weather!",
        "help" => "Using your location we'll send your weather.",
        "weather" => "<b>🌤️ Weather for %s, %s</b>\n\n🎈 Now %s, %s\n☔ %s problability of rain on day\n\n ¦ • Temperature: ↑%s, ↓%s\n ¦ • Clouds cover: %s\n ¦ • Humidity: %s\n ¦ • Sunrise: %s\n ¦ • Sunset: %s\n",
        "error_weather" => "<b>🙁 Didn't possible get your weather, try again!</b>",
        "defaultResponse" => "Send your location, the bot will response with your weather.",
    ]
]);

Agora vamos para o arquivo controller.php onde faremos a classe Controller e nossos métodos de resposta aos comandos.

<?php

use \TelegramPhp\Buttons;
use \TelegramPhp\Methods;

class Controller {

    public function start ($bot, $data){

        Methods::sendMessage ([
            'chat_id' => $bot->getChatId (),
            'text' => $this->lang ($bot, __FUNCTION__),
            'reply_markup' => Buttons::replyKeyBoardMarkup ([
                [Buttons::keyBoardButtonRequestLocation ('My location')]
            ], false, true, true)
        ]);

    }
    
    public function help ($bot, $data){

        Methods::sendMessage ([
            'chat_id' => $bot->getChatId (),
            'text' => $this->lang ($bot, __FUNCTION__)
        ]);

    }
    
    public function weather ($bot, $data){

        $location = $bot->getLocation ();
        $latitude = $location ['latitude'] ?? $data ['latitude'];
        $longitude = $location ['longitude'] ?? $data ['longitude'];

        $weather = OpenMeteo::getWeather ($latitude, $longitude);

        // erro
        if (!isset ($weather ['current'])){

            Methods::sendMessage([
                'chat_id' => $bot->getChatId(),
                'text' => $this->lang ($bot, 'error_weather'),
                'parse_mode' => 'html',
            ]);

            return;

        }

        $clima = OpenMeteo::getWMOCode ($weather ['current']['weather_code'], $weather ['current']['is_day']);

        // % de chuva
        $probabilidade_chuva = "{$weather ['hourly']['precipitation_probability'][0]}%";
        
        // temperatura
        $temperatura_atual = "{$weather ['current']['temperature_2m']}°C";
        $temperatura_max = "{$weather ['daily']['apparent_temperature_max'][0]}°C";
        $temperatura_min = "{$weather ['daily']['apparent_temperature_min'][0]}°C";

        // humidade
        $humidade = "{$weather ['current']['relative_humidity_2m']}%";

        // nuvens
        $nuvens = "{$weather ['current']['cloud_cover']}%";

        // fases do dia
        $amanhecer = date ('H:i', strtotime ($weather ['daily']['sunrise'][0]));
        $anoitecer = date ('H:i', strtotime ($weather ['daily']['sunset'][0]));

        $texto = sprintf (
            $this->lang ($bot, __FUNCTION__),
            $latitude,
            $longitude,
            $temperatura_atual,
            $clima ['description'],
            $probabilidade_chuva,
            $temperatura_max,
            $temperatura_min,
            $nuvens,
            $humidade,
            $amanhecer,
            $anoitecer
        );

        if ($bot->getCallbackQueryId () != null){

            // stop loading
            Methods::answerCallbackQuery ([
                'callback_query_id' => $bot->getCallbackQueryId ()
            ]);
            
            // response
            Methods::editMessageText ([
                'chat_id' => $bot->getChatId (),
                'message_id' => $bot->getMessageId (),
                'text' => $texto,
                'parse_mode' => 'html',
                'reply_markup' => Buttons::inlineKeyBoard ([
                    [Buttons::inlineKeyBoardCallbackData ('🔄️', "/weather {$latitude} {$longitude}")]
                ])
            ]);

        }else {
            
            Methods::sendMessage ([
                'chat_id' => $bot->getChatId (),
                'text' => $texto,
                'parse_mode' => 'html',
                'reply_markup' => Buttons::inlineKeyBoard ([
                    [Buttons::inlineKeyBoardCallbackData ('🔄️', "/weather {$latitude} {$longitude}")]
                ])
            ]);

        }

    }
    
    public function defaultResponse ($bot, $data){

        Methods::sendMessage ([
            'chat_id' => $bot->getChatId (),
            'text' => $this->lang ($bot, __FUNCTION__)
        ]);

    }

    public function lang ($bot, $key){

        return (LANG [$bot->getLanguageCode ()] ?? LANG ['en'])[$key];

    }

}

Essa classe é bem auto-explicativa, olhando para os métodos disponíveis já dá pra saber que eles processam os comandos enviados pelos usuários, sendo os mais importante o método lang onde iremos obter a resposta adequada ao idioma do usuário e o método weather que será o responsável por processar nossa previsão do tempo.

No final temos essa como nossa estrutura de código.

Não mencionei até agora, mas o autoload desses arquivos é feitos no composer.json do composer, assim todos esse arquivos que criamos estarão disponíveis com nosso include ‘vendor/autoload.php’

Online em localhost

Com o código pronto, agora podemos colocar nosso bot pra funcionar em localhost mesmo.

Pra isso não precisaremos instalar nada, vamos utilizar o localhost.run e colocar nosso servidor simples online.

Vamos começar rodando um servidor simples no php, esse servidor precisa ser iniciado na mesma pasta do nosso projeto com esse comando:

php -S localhost:8181

Agora já temos nosso servidor rodando em http://localhost:8181, só precisamos executar o próximo comando com o localhost.run para obtermos uma url que será registrada no webhook do nosso bot pela API do Telegram.

ssh -R 80:localhost:8181 [email protected]

Nossa url para usar no webhook do Telegram

Agora podemos registrar nossa url https://32efc6f14ba20d.lhr.life/bot.php no webhook do Telegram e assim receber os dados de interação dos usuários com nosso bot, para setar essa url é bem fácil, só precisamos do nosso token e usar o método setWebhook da API do Telegram, isso pode ser feito pela biblioteca que estamos usando ou diretamente por uma requisição na API informando nossa url de webhook https://api.telegram.org/bot6438698345:AAFXL4rrwem5SwfWTcp4V5Huh7yAvv1fo-Y/setWebhook?url=https://32efc6f14ba20d.lhr.life/bot.php

<?php

include __DIR__ . '/vendor/autoload.php';

use \TelegramPhp\Methods;
use \TelegramPhp\Config\Token;

// Token do nosso bot
Token::setToken('6438698345:AAFXL4rrwem5SwfWTcp4V5Huh7yAvv1fo-Y');

// registrando nossa url
$set_webhook = Methods::setWebhook([
    'url' => 'https://32efc6f14ba20d.lhr.life/bot.php'
]);

echo json_encode ($set_webhook);

Pronto, com isso nosso bot já deve funcionar.

Podemos ver que o Telegram notificou nosso bot com o /start enviado, assim o bot processou nossa mensagem e enviou sua resposta.

Agora vamos testar uma previsão do tempo.

Com isso nosso bot simples para previsão do tempo @tempotestebot está pronto.

🫡 Esse projeto está disponível no Github, você pode dar uma olhada melhor no código ou modificar como preferir https://github.com/httd1/tempotestebot

2