Користувальницькькі налаштування

Налаштування сайту


wolfdispatcher

Розбіжності

Тут показані розбіжності між вибраною ревізією та поточною версією сторінки.

Посилання на цей список змін

Порівняння попередніх версій Попередня ревізія
Попередня ревізія
wolfdispatcher [2020/06/27 02:40]
wolfdispatcher [2023/11/16 13:08] (поточний)
nightfly
Рядок 1: Рядок 1:
 +====== WolfDispatcher ======
  
 +WolfDispatcher - це неймовірно простий, мінімалістичний та швидкий рівень абстракції над [[ubillingtelegram|API Telegram]] який дозволить з неймовірною швидкістю розробляти ваших **інтерактивних** Telegram-ботів з будь-яким функціоналом, максимально не задумуючись над низькорівневими речима які вони робитимуть. Також максимальну увагу приділено стандартизації форматів даних з якими ви зможете працювати в процесі імплементації вашого бота та простоті деплою його на продакшн. За допомогою WolfDispatcher ви можете використовувати [[development|ваш модуль Ubilling]], як платформу, що забезпечує функціонування бота, так і виконати його у вигляді переносимого on-premise рішення на базі фреймворку [[yalf|YALF]]. Також ви можете використовувати всього дві малесенькі бібліотеки, аби не тягнути за собою весь фреймворк та залишитись наодинці з вашим чудовим кодом та ідеями.
 +
 +====== Як це працює? ======
 +
 +Отак:
 +
 +{{:wolfdispatcherscheme.png|}}
 +
 +
 +====== З чого почати? ======
 +
 +У випадку, якщо у вас є досвід написання [[development|модулів для Ubilling]] у вас вже є експіренс роботи з фреймворком. Усі необхідні бібліотеки вже завантажено та ви можете пропустити підготовку середовища, перейшовши напряму до того аби фігачити код. Якщо ж ви не плануєте тягнути функціонал бота прямо в ваш біллінг, а хочете нормальне, окремо розташоване рішення, найпростішим та найзрозумілішим кейсом буде швиденько розвернути для цього [[yalf|YALF]] і вже на його базі зліпити цілісне та переносиме рішення. 
 +
 +Поїхали. Розгортаємо фреймворк, кудись в локацію доступну з вебу по https:
 +<code>
 +$ mkdir ourbot
 +$ cd ourbot
 +$ fetch http://yalf.nightfly.biz/yalf_current.tgz
 +$ tar zxvf yalf_current.tgz && rm -fr yalf_current.tgz
 +$ chmod -R 777 exports content config 
 +</code>
 +
 +Вмикаємо потрібні для функціонування нашого боту "шари" бібліотек в **config/yalf.ini**. Мінімально, що нам буде потрібне наразі це бібліотеки wolfdispatcher, telegram та ubconfig, отож:
 +
 +<file ini yalf.ini> 
 +LAYER_TELEBOT="telegram,wolfdispatcher,ubconfig"
 +</file>
 +
 +Ось власне і все приготування оточення, котре буде необхідне нам для розробки. Переходимо власне до неї. Створюємо директорію, файлик модулю, та описовий файл модулю, де буде лежати власне обробник хуків нашого бота:
 +<code>
 +$ mkdir modules/general/ourbot
 +$ touch modules/general/ourbot/index.php
 +$ touch modules/general/ourbot/module.php
 +</code>
 +
 +Та редагуємо їх відповідним чином
 +<file php module.php>
 +<?php
 +
 +$this->registerModule($module, 'main', __('OurBot'), 'Author name');
 +</file>
 +
 +
 +<file php index.php>
 +<?php 
 +
 +// Імплементація бота OurBot котра повинна наслідувати WolfDispatcher
 +class OurBot extends WolfDispatcher { }
 +
 +// Створюємо інстанс бота з токеном бота Telegram
 +$bot = new OurBot('252624203:982384782349824232333');
 +// Вмикаємо автоматичну інсталляцю web-хуку для взаємодії з Telegram
 +$bot->hookAutosetup(true);
 +// Слухаємо що нам кажуть
 +$bot->listen();
 +
 +</file>
 +
 +Все, цих чотирьох рядків коду достатньо, задля того, щоб наш бот почав функціонувати і обробляти сповіщення про події, що відбулись в його полі зору. Насправді достатньо трьох рядків але про це трішки згодом. Отож після цього всього, заходимо за посиланням 
 +
 +<code>
 +https://наш_хост/директорія_бота/?module=ourbot
 +</code>
 +
 +та бачимо наступне:
 +
 +{{:wolfdispatcherhookautoinstall.png|}}
 +
 +що означає, що хук вашого бота автоматично встановлено і Телеграм в майбутньому, буде відсилати сповіщення власне в цей URL. При бажанні автоматичне встановлення хука можна просто вимкнути методом hookAutosetup(false) або ж просто не викликаючи його на цьому інстансі взагалі. Автоматичне встановлення хука може бути корисним та зручним як при перенесенні вашого бота на інший сервер/URL так і при зміні його токена. Власне відбувається воно у випадку зміни будь якого з елементів пари URL+Bot token.
 +
 +
 +Окей, а як переконатись, що наш бот все "дуже уважно слухає"? Та дуже просто. Одразу після створення інстансу, переключаємо його в режим відладки.
 +
 +<code php>
 +$bot->setDebug(true);
 +</code>
 +
 +Всі необхідні вам сповіщення почнуть сипатись в **exports/botname_debug.log**
 +
 +<code>
 +$ tail -F exports/ourbot_debug.log
 +</code>
 +
 +і як реакцію на щось таке
 +
 +{{:wolfdispatcherdebugtest.png|}}
 +
 +ми отримаємо такий результат
 +
 +<file txt ourbot_debug.log>
 +OurBot: 2022-08-12 16:49:36
 +Array
 +(
 +    [message_id] => 1295
 +    [from] => Array
 +        (
 +            [id] => 777777777
 +            [first_name] => Nightfly
 +            [username] => MyxaBkyco4kax
 +            [language_code] => uk
 +        )
 +
 +    [chat] => Array
 +        (
 +            [id] => 777777777
 +            [type] => private
 +        )
 +
 +    [date] => 1660312176
 +    [text] => привіт бот!
 +    [photo] => 
 +    [document] => 
 +    [voice] => 
 +    [audio] => 
 +    [video_note] => 
 +    [location] => 
 +    [sticker] => 
 +    [new_chat_member] => 
 +    [left_chat_member] => 
 +    [reply_to_message] => 
 +)
 +
 +GT: 0.0038 QC: 0
 +
 +Called actions: NONE
 +==================
 +</file>
 +
 +
 +
 +====== Хеллоуворлди ======
 +
 +Окей, вже результат. Але ж ми тут інтерактивності хочемо? А з чого там, з хеллоуворлдів в нормальних книжках починають? Ну давайте будемо реагувати на текстові привіти ботом якось. Заодно і coding-style свій трішки виправимо. Оскільки звичайно тримати в прямо в контроллері імплементацію всього бота так собі затія, і здоровою ідеєю було б винести її в окрему однойменну бібліотеку, та й хардкодити токени і всі інші опції прямо в контроллері "ну такоє собі". Отож виносимо імплементацію нашого бота в автозавантажувану бібліотеку **api/libs/api.ourbot.php** 
 +
 +<file php api.ourbot.php>
 +<?php
 +
 +class OurBot extends WolfDispatcher {
 +
 +    protected function actionHello() {
 +        $this->replyTo('Привіт');
 +    }
 +
 +}
 +
 +</file>
 +
 +запихуємо всі конфігурабельні штуки нашого бота в **config/alter.ini**
 +
 +<file ini alter.ini>
 +OURBOT_TOKEN="252624203:982384782349824232333"
 +OURBOT_DEBUG=1
 +OURBOT_AUTOSETUP=1
 +</file>
 +
 +а контроллер приводимо до якогось такого вигляду
 +
 +<file php index.php>
 +<?php
 +
 +$bot = new OurBot($ubillingConfig->getAlterParam('OURBOT_TOKEN'));
 +$bot->setDebug($ubillingConfig->getAlterParam('OURBOT_DEBUG'));
 +$bot->hookAutosetup($ubillingConfig->getAlterParam('OURBOT_AUTOSETUP'));
 +
 +// Це масив екшонів. Формат рядок=>дія. 
 +// Дією може бути як метод класу імплементації боту (як приватний так і публічний) так і ім`я просто функції.
 +// У випадку, якщо це функція першим і єдиним параметром їй буде передано повний масив отриманого повідомлення $this->receivedData
 +$commands = array(
 +    'привіт' => 'actionHello'
 +);
 +
 +// Передаємо набір екшенів в діспатчер. Він сам розбереться, що з тим робити далі.
 +$bot->setActions($commands);
 +
 +// Слухаємо
 +$bot->listen();
 +</file>
 +
 +отримуючи повністю закономірний результат 
 +
 +{{:wolfdispatcherhellospamle.png|}}
 +
 +власне, якщо ми зазирнемо в дебаг-лог нашого бота ми побачимо там щось схоже на оце:
 +
 +<file txt ourbot_debug.log>
 +..........
 +    [date] => 1660313508
 +    [text] => привіт
 +    [photo] => 
 +    [document] => 
 +    [voice] => 
 +    [audio] => 
 +    [video_note] => 
 +    [location] => 
 +    [sticker] => 
 +    [new_chat_member] => 
 +    [left_chat_member] => 
 +    [reply_to_message] => 
 +)
 +
 +GT: 0.0164 QC: 0
 +
 +Called actions: Array
 +(
 +    [0] => METHOD: actionHello
 +)
 +
 +==================
 +
 +</file>
 +
 +Що власне свідчить, що на основі того, що в $this->receivedData (він же $this->message()) діспатчером було знайдено підрядок "привіт" задекларований в нашій структурі екшнів, було викликано відповідний йому метод $this->actionHello(). Власне екшн міг би бути і просто PHP-функцією а не методом. Тоді його було б викликано ось так: actionHello($this->receivedData). А в лозі ми б побачили сповіщення такого типу:
 +
 +<code>
 +Called actions: Array
 +(
 +    [0] => FUNC: actionHello
 +)
 +</code>
 +
 +у випадку, якщо відповідно екшну не знайдено ні методу ні функції в рамках виконуваного коду, при ввімкненому дебаг режимі ми отримаємо прямо собі в чат отаке сповіщення 
 +
 +{{:wolfdispatchernometrhod.png|}}
 +
 +що в лозі буде виглядати приблизно так
 +
 +<code>
 +Called actions: Array
 +(
 +    [0] => FAILED: actionHello2
 +)
 +</code>
 +
 +Також є пачка "магічних методів", які за замовчуванням "роблять нічого" але все ж виконуються в різних ситуаціях. Їх ви теж можете задефайнити в вашій імплементації та навісити на них потрібний вам функціонал. Ось вони:
 +
 +<code php>
 + // виконується у випадку, якщо для повідомлення не знайдено відповідний екшн/обробник
 + $this->handleEmptyAction();
 +
 + // виконується у випадку якщо надійшло повідомлення з порожнім текстом
 + $this->handleEmptyText();
 +
 + // виконується у випадку якщо отримано фотокартку
 + $this->handlePhotoReceived();
 +
 + // виконується як реакція на будь-яке повідомлення взагалі завжди
 + $this->handleAnyWay();
 +</code>
 +
 +усі ці методи виконуються у випадку, якщо користувач, від якого надійшло повідомлення є "дозволеним для інтеракції", тобто або фігурує в структурі allowedChatiIds (типу ACL-ка) або ж не фігурує в структурі ignoredChatIds (читай бан-лист) які заповнюються відповідними їм сеттерами setAllowedChatIds() та setIgnoredChatIds(). Винятком є тільки обробник handleAnyWay() котрий відпрацьовує "взагалі завжди" і "взагалі для всіх"
 +
 +Давайте розширимо функціонал нашого хеллоуворлдного боту чимось осмисленішим, по дорозі ненав'язливо продемонструвавши як редефайнити магічні хендлери та реагувати на різне. І будемо з цим всім закінчувати, переходячи до чогось осмисленого.
 +
 +
 +<file php api.ourbot.php>
 +class OurBot extends WolfDispatcher {
 +
 +    const CHATS_LOG_PATH = 'exports/allchats.log';
 +
 +    /**
 +     * Just reacts for user greeting personally
 +     
 +     * @return void
 +     */
 +    protected function actionHelloName() {
 +        $hisName = $this->receivedData['from']['first_name'];
 +        $this->reply(__('Hello') . ' ' . $hisName);
 +    }
 +
 +    /**
 +     * Replies on photo posted in chat
 +     
 +     * @return void
 +     */
 +    protected function actionPhotoWow() {
 +        if ($this->isPhotoReceived()) {
 +            $message = $this->message(); //returns $this->receivedData copy
 +            if ($message['text']) {
 +                $replyText = 'Ого яка фотка! Це точно ' . $message['text'] . '?';
 +            } else {
 +                $replyText = 'Нічогенька така картинка';
 +            }
 +            $this->replyTo($replyText);
 +        }
 +    }
 +
 +    /**
 +     * Just logs all chats non empty text messages to some log file
 +     
 +     * @return void
 +     */
 +    protected function handleAnyWay() {
 +        if (!empty($this->receivedData['text'])) {
 +            $logData = date("Y-m-d H:i:s", $this->receivedData['date']) . ' ' . $this->chatId . ' ';
 +            $logData .= $this->receivedData['from']['first_name'] . ' ' . $this->receivedData['text'] . PHP_EOL;
 +            file_put_contents(self::CHATS_LOG_PATH, $logData, FILE_APPEND);
 +        }
 +    }
 +
 +}
 +</file>
 +
 +
 +<file php index.php>
 +$bot = new OurBot($ubillingConfig->getAlterParam('OURBOT_TOKEN'));
 +$bot->setDebug($ubillingConfig->getAlterParam('OURBOT_DEBUG'));
 +$bot->hookAutosetup($ubillingConfig->getAlterParam('OURBOT_AUTOSETUP'));
 +
 +$commands = array(
 +    'привіт' => 'actionHelloName'
 +);
 +
 +$bot->setActions($commands);
 +$bot->setPhotoHandler('actionPhotoWow');
 +$bot->listen();
 +</file>
 +
 +
 +====== Інтерфейс та всіляке таке інше ======
 +
 +Реакція на текстові команди, це звичайно неймовірно круто і олдскульно, але нормальні люди звикли до **нормального інтерфейсу**. Наприклад до кнопочок. От натискаєш кнопочку і щось відбувається. Бачили колись таке? :)
 +
 +Відкриваємо таємницю - в більшості випадків, кнопочка це теж текст. Який просто набирається кастомною клавіатурою. Давайте навчимось користуватись ними, на прикладі нашого наступного бота. Нехай це буде "бот-порадник" котрий при тицянні на кнопочки видає неймовірні поради на всі випадки життя. Добре? Заодно без палєва продемонструємо, що на одному й тому ж фреймворку може в сусідніх модулях жити нелімітована кількість ботів (насправді, і в одному й тому ж модулі, вони всі можуть бути, але це шиза) . Вогонь, так? ;)
 +
 +<code>
 +$ mkdir modules/general/poradnyk
 +$ touch modules/general/poradnyk/index.php
 +$ touch modules/general/poradnyk/module.php
 +$ touch api/libs/api.poradnyk.php
 +</code>
 +
 +
 +малюємо його конфіг в **config/alter.ini**
 +
 +<file ini alter.ini>
 +PORADNYK_TOKEN="252624204:89929232322221"
 +PORADNYK_DEBUG=1
 +PORADNYK_AUTOSETUP=1
 +</file>
 +
 +заповнюємо опис модуля в **modules/general/poradnyk/module.php**
 +
 +<file php module.php>
 +<?php
 +
 +$this->registerModule($module, 'main', __('AdviceBot'), 'Author name');
 +</file>
 +
 +збираємо імплементацію в **api/libs/api.poradnyk.php**
 +<file php api.poradnyk.php>
 +<?php
 +
 +class Poradnyk extends WolfDispatcher {
 +
 +    /**
 +     * Some predefined text-routes aka actions
 +     */
 +    const ROUTE_INIT = '/start';
 +    const ROUTE_ADVICE_UA = '🇺🇦 Порада';
 +    const ROUTE_ADVICE_RU = '🇷🇺 Совет';
 +    const GROUTE_ADVICE_UA = '!порада';
 +    const GROUTE_ADVICE_RU = '!совет';
 +
 +    /**
 +     * Renders custom keyboard
 +     
 +     * @return void
 +     */
 +    protected function actionKeyboard() {
 +        $buttons[] = array(self::ROUTE_ADVICE_UA, self::ROUTE_ADVICE_RU);
 +        $keyboard = $this->telegram->makeKeyboard($buttons);
 +        $this->reply('Шо?', $keyboard);
 +    }
 +
 +    /**
 +     * Gets awesome advice and sends message with it to current chat
 +     
 +     * @return void
 +     */
 +    protected function actionAdvice() {
 +        $fga = new OmaeUrl('http://ubilling.net.ua/fga/api/random/?lang=ua');
 +        $advice = $fga->response();
 +        $advice = json_decode($advice, true);
 +        $advice = $advice['text'];
 +        $this->reply($advice);
 +    }
 +
 +    /**
 +     * Gets awasome advice in russian
 +     
 +     * @return void
 +     */
 +    protected function actionNahuy() {
 +        $this->replyTo('Нахуй иди, вот тебе совет.');
 +    }
 +
 +}
 +</file>
 +
 +Та контроллер цього всього в **modules/general/poradnyk/index.php**
 +
 +<file php index.php> 
 +
 +<?php
 +
 +// Bot instance creation
 +$bot = new Poradnyk($ubillingConfig->getAlterParam('PORADNYK_TOKEN'));
 +$bot->setDebug($ubillingConfig->getAlterParam('PORADNYK_DEBUG'));
 +$bot->hookAutosetup($ubillingConfig->getAlterParam('PORADNYK_AUTOSETUP'));
 +
 +// Commands/actions for private chats
 +$commands = array(
 +    $bot::ROUTE_INIT => 'actionKeyboard',
 +    $bot::ROUTE_ADVICE_UA => 'actionAdvice',
 +    $bot::ROUTE_ADVICE_RU => 'actionNahuy'
 +);
 +
 +// Group-chat commands is text-only and starts with "!" symbol.
 +// Using custom keyboards in group chats isn`t best practice.
 +$groupCommands = array(
 +    $bot::GROUTE_ADVICE_UA => 'actionAdvice',
 +    $bot::GROUTE_ADVICE_RU => 'actionNahuy',
 +);
 +
 +
 +$bot->setActions($commands);
 +$bot->setGroupActions($groupCommands);
 +$bot->listen();
 +
 +</file>
 +
 +Йдемо за URL нашого контроллера задля встановлення веб-хука. Можемо до речі браузером, можемо тупо curl-ом. Не важливо. Результат буде один.
 +
 +{{:wolfdispatcherhookporadnyk.png|}}
 +
 +
 +Все, тестуємо:
 +
 +{{:wolfdispatcherporadnykdemo.png|}}
 +
 +Як бачимо, наш бот-порадник чудово реагує на натискання кнопок та видає поради на будь-які випадки життя як державною так і російською мовами за вибором користувача. 
 +
 +Не складно помітити, що actionKeyboard() викликається згідно команди "/start" котра завжди прилітає до бота на початку розмови з ним. В групових чатах (це ті, котрі $this->chatType!=private) клавіатуру не використовуємо, а орієнтуємось на текстові команди "!порада" та "!совет" пушачи цей набір команд в структуру setGroupActions котрий автоматично повністю замінює собою структуру setActions для цих самих групових чатів. Власне, якщо вам захочеться розділювати кнопочки кастомної клавіатури по рядах, то керується це наступним чином:
 +
 +<code php> 
 + $buttons[] = array(self::ROUTE_ADVICE_UA, self::ROUTE_ADVICE_RU);
 + $buttons[] = array('🙏 Помолитись');
 +</code>
 +
 +{{:wolfdispatcherkeybsample.png|}}
 +
 +або якось так:
 +
 +<code php> 
 +        $buttons[] = array('🔥 Вогонь','🌊 Вода','☁️ Повітря');
 +        $buttons[] = array(__('Use i18n Luke!'));
 +        $buttons[] = array('Тут тупо текст','А ось ще одна','Типу так');
 +</code>
 +
 +з отаким от результатом
 +
 +{{:wolfdispatcherkeybsample2.png|}}
 +
 +Чому на кнопочках стараємось використовувати емоджі? Власне з двох очевидних причин:
 +  - Стараємось уникати випадкових реакцій бота, на рандомні повідомлення в чаті
 +  - Красівоє
 +
 +Також, насправді, ви не зобов`язані самі збирати клавіатуру повністю самі, викликаючи $this->telegram->makeKeyboard(). Для цього є зручний метод castKeyboard() що одразу відсилає клавіатуру до чатику
 +
 +<code php>
 +  $buttons[] =array('Кнопка 1','Кнопка 2');
 +  $buttons[] =array('Кнопка 3','Кнопка 4');
 +  
 +  $this->castKeyboard($buttons, 'Що робимо далі?'));
 +</code>
 +====== Про складне ======
 +
 +Так, а зараз видихніть, помоліться
 +
 +{{wolfprey.webm?640|Будь хоробрим як вовк!}}
 +
 +та зосередьтесь. 
 +
 +Будемо говорити про не самі очевидні речі. Про такі як "структура даних повідомлень", як параметризувати наші обробники подій, ловити фоточки, та всіляке таке інше.
 +
 +Отож, розпочнемо з параметризації ваших обробників. Якщо у випадку, з зовнішніми функціями вони просто викликаються як "function_name(повідомлення)" ви можете задефайнити її якось так
 +
 +<code php>
 +function doSomething($argument) {
 +  //тут далі робимо щось з цими даними, парсимо текст, наприклад.
 +}
 +</code>
 +
 +То методи викликаються в лоб взагалі без параметрів. Зроблено це так, задля економії пам`яті. Звісно, навіщо плодити копії цих даних у середині об'єкта який і так вже їх отримав? Для роботи з "повідомленням" існує внутрішня структура **$this->receivedData** та захищений метод **$this->message()** котрий повертає її копію, що може бути зручним, якщо ви захочете якимось чином модифікувати дані прямо в цій структурі, але залишити їх незмінними для усіх інших подальших методів, що можуть бути виконані обробником. На практиці в коді ви це можете використовувати якось так:
 +
 +<code php>
 +    protected function handleEmptyAction() {
 +        if (!empty($this->receivedData['text'])) {
 +            $this->reply($this->receivedData['text']);
 +        }
 +    }
 +</code>
 +
 +Власне все що отримує бот в даних хука, ми рахуємо "повідомленням" формат якого ідентичний, для повідомлень отриманих в приватних, групових та групових чатах, а також і для постів в каналах, в незалежності від вмісту самих повідомлень. Цю ж структуру, ви можете бачити і в дебаг-лозі при увімкненій відладці. Також, з метою забезпечення більшої зручності та лаконічності коду, деякі важливі значення з нього автоматично вже замаплено на приватні проперті. 
 +
 +
 +Ось трохи детальніше, про те, що міститься всередині
 +
 +<code php>
 +Array
 +(
 +    [message_id] => (int) 666 - ідентифікатор повідомлення. Воно ж зберігається в проперті this->messageId 
 +                                та на нього за замовчуванням відповідає this->replyTo().
 +    [from] => Array
 +        (
 +            [id] => (int) 7777777777 - chatId користувача від якого надійшло повідомлення.
 +            [first_name] => (string) Микола Петренко -  iм`я користувача 
 +            [username] => (string) l33tmykola - юзернейм користувача
 +            [language_code] => (string) uk - мова клієнту користувача, на кшталт uk/ru/en
 +        )
 +
 +    [chat] => Array
 +        (
 +            [id] => (int) 7777777777 - chatId чату з яким ведеться діалог. У випадку приватного чату він співпадає з [from][id].
 +                                       У випадку, якщо це не приватний чат, там знаходиться власне ID групи, супергрупи чи каналу (вони від`ємні).
 +                                       Для зручності це значення, знаходиться завжди у внутрішній проперті this->chatId. На нього ж за замовчуванням відповідають
 +                                       швидкі обгортки типу this->reply() та this->replyTo()
 +            [type] => (string) private  - тип чату, власне вони бувають: private (лічка), group (закрита група), supergroup (відкрита група), channel (канал)
 +        )
 +
 +    [date] => (int) 1660393421 - дата отримання повідомлення у вигляді unixtimestamp
 +    [text] => (string) test message - не повірите, це текст повідомлення. Сюди ж маппляться усілякі caption-и картинок, тощо. Загалом вони всі [text]
 +    [photo] => array() - структура, що описує зображення, у випадку якщо воно таки отримано з повідомленням. Буде розглянуто окремо, трохи згодом.
 +    [document] => array() - виглядає приблизно як і photo тільки містить файлики з якимось mime_type. стосується також voice, audio, video_note
 +    [voice] => array() - звукові повідомлення, можете одразу таких користувачів додавати в банліст ;)
 +    [audio] => array() - аудіо файлики.
 +    [video_note] => array() - коротки відео, типу сторізів.
 +    [location] => array() - дані про геолокацію, з координатами
 +    [sticker] => array() - стікери, біс зна що з ними робити.
 +    [new_chat_member] => array() - структура котра заповнюється при появі користувача в груповому чаті. 
 +                                   Також вона заповнюється і при додаванні самого бота в груповий чат. 
 +                                   Плоский массив, містить поля: id, is_bot, first_name, username, language_code, is_premium - користувачі
 +                                                                 id, is_bot, first_name, username - інші боти
 +    [left_chat_member] => array() - структура, котра містить дані про користувача, що вийшов з чату. Аналогічна попередній.
 +    [reply_to_message] => array() - у випадку, якщо поточне повідомлення, є відповіддю на якесь інше повідомлення, містить точно таку саму структуру "повідомлення"
 +                                    як і ось ця, поточна, зі всіма даними повідомлення, на яке є відповіддю поточне.
 +)
 +
 +</code>
 +
 +У випадку, якщо крім просто тексту, до вас прилетіло ще щось, буде заповнено структури фоточок, документів, тощо. Виглядає це якось так:
 +
 +<code php>
 +   [text] => це текстовий опис фоточки
 +    [photo] => Array
 +        (
 +            [0] => Array
 +                (
 +                    [file_id] => AgACAgIAAxkBAAIFkWL3nxfnsM4SlGHiXNi4qDPdX2bmAAKsvjEbdRC4S58-T_p7q3ZCAQADAgADcwADKQQ
 +                    [file_unique_id] => AQADrL4xG3UQuEt4
 +                    [file_size] => 1982
 +                    [width] => 90
 +                    [height] => 89
 +                )
 +
 +            [1] => Array
 +                (
 +                    [file_id] => AgACAgIAAxkBAAIFkWL3nxfnsM4SlGHiXNi4qDPdX2bmAAKsvjEbdRC4S58-T_p7q3ZCAQADAgADbQADKQQ
 +                    [file_unique_id] => AQADrL4xG3UQuEty
 +                    [file_size] => 19215
 +                    [width] => 320
 +                    [height] => 315
 +                )
 +
 +            [2] => Array
 +                (
 +                    [file_id] => AgACAgIAAxkBAAIFkWL3nxfnsM4SlGHiXNi4qDPdX2bmAAKsvjEbdRC4S58-T_p7q3ZCAQADAgADeAADKQQ
 +                    [file_unique_id] => AQADrL4xG3UQuEt9
 +                    [file_size] => 19267
 +                    [width] => 324
 +                    [height] => 319
 +                )
 +
 +        )
 +</code>
 +
 +З самого вигляду цього всього, доволі очевидно, що це массив з превьюшками цієї фоточки, з різною якістю, розмірами та ідентифікаторами файликів, які можна завантажити собі. Також очевидно, що ви не дуже хочете працювати з цим всім мануально, тому двома сантиметрами нижче, ви довідаєтесь про зручні обгортки які дозволять вам дуже елегантно та лаконічно робити з отриманими зображеннями те, що вам скоріш за все захочеться з ними робити.
 +
 +====== Робота з зображеннями ======
 +
 +Перш за все, необхідно пам'ятати що зображення до нас можуть прийти двома шляхами. Як "швидко надіслати зображення" так і "надіслане без стиснення". Відповідно фігурувати вони можуть або як частина структури [photo] так і як частина [document] з відповідними зображенням mime-типами. Думати про це все, ми в принципі не хочемо. Саме тому, з цієї причини для роботи з зображеннями є дуже зручні обгортки. Давайте розглянемо на прикладі мінімалістичного боту, котрий на будь-яке повідомлення з зображенням, буде:
 +
 +  - Завантажувати це зображення собі
 +  - Якось його модифікувати
 +  - Відсилати в чатік назад уже спохабленим
 +
 +От якось так
 +
 +<code php>
 +class DemoBot extends WolfDispatcher {
 +
 +    /**
 +     * Для відсилання зображень, локація, звідки ми їх шлемо повинна бути доступною з вебу.
 +     */
 +    const IMG_SAVE_PATH = 'tmp/';
 +    const IMG_URL = 'https://наш_хост/dev/demobot/';
 +
 +    /**
 +     * Catches and returns processed image to user
 +     
 +     * @return void
 +     */
 +    protected function actionProcessImage() {
 +        // Якусь фоточку зловлено? Насправді PhotoHandler метод відбудеться тільки за цієї умови
 +        // так, що ця перевірка надлишкова тут. Просто як ілюстрація.
 +        if ($this->isPhotoReceived()) {
 +            // Зберігаємо зображення до тимчасової директорії. Також ми могли б використати getPhoto()
 +            // задля того, щоб просто отримати в змінну вміст самого зображеня, а потім з ним щось робити, 
 +            // але навіщо, якщо є швидкий savePhoto()?
 +            $photoSaveResult = $this->savePhoto(self::IMG_SAVE_PATH . $this->messageId . '.jpg');
 +            // перевіряємо чи воно взагалі збереглось?
 +            if ($photoSaveResult) {
 +                //завантажуємо зображення
 +                $image = imagecreatefromjpeg($photoSaveResult);
 +                //робимо з зображенням різні штуки
 +                imagefilter($image, IMG_FILTER_BRIGHTNESS, 70);
 +                imagefilter($image, IMG_FILTER_GRAYSCALE);
 +                imagefilter($image, IMG_FILTER_MEAN_REMOVAL, 10);
 +                imagefilter($image, IMG_FILTER_SCATTER, 3, 5);
 +                imagefilter($image, IMG_FILTER_NEGATE);
 +
 +                //перезберігаємо вже оброблене зображення
 +                $newImageName = $photoSaveResult . '_processed.jpg';
 +                imagejpeg($image, $newImageName);
 +                $fullImageUrl = self::IMG_URL . $newImageName;
 +                //збираємо повідомлення
 +                $message = 'sendPhoto:[' . $fullImageUrl . ']{все тлєн}';
 +                //відповідаємо ним в чатік
 +                $this->reply($message);
 +            }
 +        }
 +    }
 +
 +}
 +
 +$bot = new DemoBot($ubillingConfig->getAlterParam('DEMOBOT_TOKEN'));
 +$bot->setPhotoHandler('actionProcessImage');
 +$bot->listen();
 +</code>
 +
 +З повністю очікуваним результатом
 +
 +{{::wolfdispatcherimagesprocessingdemo.png|}}
 +
 +Окей, а якщо ми не хочемо робити тільки "тлєн", а хочемо з зображеннями робити якесь різне і трохи інстаграмне, в залежності від тексту, який фігурує в описі зображення? Ну короче хочемо не один метод викликати для отриманого зображення, а декілька різних в залежності від того, що там від нас хочуть? Ну тоді setPhotoHandler будемо використовувати тільки для завантаження зображень, а весь процесинг параметрично винесемо в окремі методи. Модифікувати збережене зображення будемо в захищеній проперті $image.
 +
 +<code php>
 +class ImgFiltersBot extends WolfDispatcher {
 +
 +    /**
 +     * Contains per-user image file path
 +     *
 +     * @var string
 +     */
 +    protected $userImageFilePath = '';
 +
 +    /**
 +     * Contains GD image resource to apply some filters
 +     *
 +     * @var mixed
 +     */
 +    protected $image = '';
 +
 +    /**
 +     * Some predefined data here
 +     */
 +    const IMG_SAVE_PATH = 'tmp/';
 +    const IMG_URL = 'https://somehost.com/dev/imgfiltersbot/';
 +    const FILTER_MARK = '🔥 ';
 +
 +    /**
 +     * Sets user image path on any input action
 +     
 +     * @return void
 +     */
 +    protected function setImagePath() {
 +        $this->userImageFilePath = self::IMG_SAVE_PATH . $this->receivedData['from']['id'] . '.jpg';
 +    }
 +
 +    /**
 +     * Catches and saves one image per user
 +     
 +     * @return void
 +     */
 +    protected function actionCatchImage() {
 +        $this->setImagePath();
 +        $photoSaveResult = $this->savePhoto($this->userImageFilePath);
 +        if ($photoSaveResult) {
 +            $this->replyTo(__('Ok, we saved this image. Which filters will be applied'));
 +            $this->actionKeyboard();
 +        }
 +    }
 +
 +    /**
 +     * Renders available filters keyboard if image loaded
 +     
 +     * @return void
 +     */
 +    protected function actionKeyboard() {
 +        $this->setImagePath();
 +        //user loaded any image?
 +        if (file_exists($this->userImageFilePath)) {
 +
 +            $availableFilters = array(
 +                'tender', 'dream', 'frozen', 'forest', 'rain', 'orangepeel', 'darken', 'summer', 'retro', 'country', 'washed', 'freshblue',
 +                'everglow', 'hermajesty', 'concentrate', 'vintage', 'blur', 'blackwhite', 'antique', 'gray', 'boost', 'fuzzy', 'aqua', 'sepia'
 +            );
 +
 +            $buttonsInRow = 3;
 +            $i = 1;
 +            $buttons = array();
 +            $buttonsRow = array();
 +            foreach ($availableFilters as $io => $each) {
 +                
 +                if ($i <= $buttonsInRow) {
 +                    $buttonsRow[] = self::FILTER_MARK . $each;
 +                } else {
 +                    $buttons[] = $buttonsRow;
 +                    $buttonsRow = array();
 +                    $i = 0;
 +                }
 +                $i++;
 +            }
 +            $this->castKeyboard($buttons, __('Select filter'));
 +        } else {
 +            $this->reply(__('Upload any image please. I`m waiting...') . ' 🤔');
 +        }
 +    }
 +
 +    /**
 +     * Returns processed image to user
 +     
 +     * @return void
 +     */
 +    protected function returnProcessedImage() {
 +        $this->setImagePath();
 +        //is image loaded?
 +        if (!empty($this->image)) {
 +            $newImageName = $this->userImageFilePath . '_processed_' . time() . '.jpg';
 +            //re-save new image if not exists
 +            if (!file_exists($newImageName)) {
 +                imagejpeg($this->image, $newImageName);
 +            }
 +            $fullImageUrl = self::IMG_URL . $newImageName;
 +            $message = 'sendPhoto:[' . $fullImageUrl . ']';
 +            $this->reply($message);
 +        }
 +    }
 +
 +    /**
 +     * Runs selected filter and returns image to user
 +     */
 +    protected function applyFilter() {
 +        $this->setImagePath();
 +        if (file_exists($this->userImageFilePath)) {
 +            if (!empty($this->receivedData['text'])) {
 +                $filterName = str_replace(self::FILTER_MARK, '', $this->receivedData['text']);
 +                $filterName = trim($filterName);
 +                if (!empty($filterName)) {
 +                    if (method_exists($this, $filterName)) {
 +                        //load image from filesystem
 +                        $this->image = imagecreatefromjpeg($this->userImageFilePath);
 +                        $this->$filterName();
 +                        $this->returnProcessedImage();
 +                        $this->actionKeyboard();
 +                    } else {
 +                        $this->reply('🙈 ' . __('Not existing image filter!'));
 +                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Some image instagram-like filters here.
 +     */
 +    protected function tender() {
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, 5);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 80, 20, 40, 50);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 40, 40, 100);
 +        imagefilter($this->image, IMG_FILTER_SELECTIVE_BLUR);
 +    }
 +
 +    protected function dream() {
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 150, 0, 0, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 50, 0, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
 +    }
 +
 +    protected function frozen() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -15);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 0, 100, 50);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 0, 100, 50);
 +        imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
 +    }
 +
 +    protected function forest() {
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 0, 150, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 0, 150, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_SMOOTH, 10);
 +    }
 +
 +    protected function rain() {
 +        imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
 +        imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 80, 50, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_SMOOTH, 10);
 +    }
 +
 +    protected function orangepeel() {
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 20, -50, 20);
 +        imagefilter($this->image, IMG_FILTER_SMOOTH, 10);
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -10);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, 10);
 +        imagegammacorrect($this->image, 1, 1.2);
 +    }
 +
 +    protected function darken() {
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -50);
 +    }
 +
 +    protected function summer() {
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 150, 0, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 25, 50, 0, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +    }
 +
 +    protected function retro() {
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 25, 25, 50);
 +    }
 +
 +    protected function country() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -30);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 50, 50, 50, 50);
 +        imagegammacorrect($this->image, 1, 0.3);
 +    }
 +
 +    protected function washed() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, 30);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, -50, 0, 20, 50);
 +        imagefilter($this->image, IMG_FILTER_NEGATE);
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, 10);
 +        imagegammacorrect($this->image, 1, 1.2);
 +    }
 +
 +    protected function freshblue() {
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -5);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 20, 0, 80, 60);
 +    }
 +
 +    protected function everglow() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -30);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -5);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 30, 30, 0);
 +    }
 +
 +    protected function hermajesty() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -10);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -5);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 80, 0, 60);
 +    }
 +
 +    protected function concentrate() {
 +        imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
 +        imagefilter($this->image, IMG_FILTER_SMOOTH, -10);
 +    }
 +
 +    protected function vintage() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, 10);
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 40, 10, -15);
 +    }
 +
 +    protected function blur() {
 +        imagefilter($this->image, IMG_FILTER_SELECTIVE_BLUR);
 +        imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -15);
 +        imagefilter($this->image, IMG_FILTER_SMOOTH, -2);
 +    }
 +
 +    protected function blackwhite() {
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, 10);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -20);
 +    }
 +
 +    protected function antique() {
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, 0);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -30);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 75, 50, 25);
 +    }
 +
 +    protected function gray() {
 +
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -60);
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +    }
 +
 +    protected function boost() {
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -35);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 25, 25, 25);
 +    }
 +
 +    protected function fuzzy() {
 +        $gaussian = array(
 +            array(1.0, 1.0, 1.0),
 +            array(1.0, 1.0, 1.0),
 +            array(1.0, 1.0, 1.0)
 +        );
 +
 +        imageconvolution($this->image, $gaussian, 9, 20);
 +    }
 +
 +    protected function aqua() {
 +
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 0, 70, 0, 30);
 +    }
 +
 +    protected function sepia() {
 +        imagefilter($this->image, IMG_FILTER_GRAYSCALE);
 +        imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -10);
 +        imagefilter($this->image, IMG_FILTER_CONTRAST, -20);
 +        imagefilter($this->image, IMG_FILTER_COLORIZE, 60, 30, -15);
 +    }
 +
 +}
 +
 +$bot = new ImgFiltersBot($ubillingConfig->getAlterParam('BOT_TOKEN'));
 +
 +$commands = array(
 +    '/start' => 'actionKeyboard',
 +    $bot::FILTER_MARK => 'applyFilter'
 +);
 +
 +$bot->hookAutosetup(true);
 +$bot->setActions($commands);
 +$bot->setPhotoHandler('actionCatchImage');
 +
 +$bot->listen();
 +</code>
 +
 +а ось, як це все працює на практиці:
 +
 +{{instafiltersdemo.webm?614|Все обмежено тільки твоєю фантазією}}
 +
 +
 +====== Використання поза фреймворком ======
 +
 +Також ви можете використовувати WolfDispatcher просто інклудячи тільки дві бібліотеки:
 +
 +<code>
 +$ mkdir ourbot
 +$ cd ourbot/
 +$ mkdir exports
 +$ echo "deny from all" > exports/.htaccess
 +$ chmod 777 exports
 +$ wget https://raw.githubusercontent.com/nightflyza/WolfDispatcher/master/api.wolfgram.php
 +$ wget https://raw.githubusercontent.com/nightflyza/WolfDispatcher/master/api.wolfdispatcher.php
 +$ touch index.php
 +</code>
 +
 +і все працює:
 +
 +<file php index.php>
 +<?php
 +
 +// low-level Telegram API implementation
 +require_once('api.wolfgram.php');
 +// WolfDispatcher lib
 +require_once('api.wolfdispatcher.php');
 +
 +class OurBot extends WolfDispatcher { }
 +
 +$bot = new OurBot('252114203:AA111231235644444654');
 +$bot->hookAutosetup();
 +$bot->listen();
 +
 +</file>
 +
 +Власне тут [[https://github.com/nightflyza/WolfDispatcher|репозиторій на GitHub]] зі всім, що необхідно.