====== WolfDispatcher ======
WolfDispatcher - це неймовірно простий, мінімалістичний та швидкий рівень абстракції над [[ubillingtelegram|API Telegram]] який дозволить з неймовірною швидкістю розробляти ваших **інтерактивних** Telegram-ботів з будь-яким функціоналом, максимально не задумуючись над низькорівневими речима які вони робитимуть. Також максимальну увагу приділено стандартизації форматів даних з якими ви зможете працювати в процесі імплементації вашого бота та простоті деплою його на продакшн. За допомогою WolfDispatcher ви можете використовувати [[development|ваш модуль Ubilling]], як платформу, що забезпечує функціонування бота, так і виконати його у вигляді переносимого on-premise рішення на базі фреймворку [[yalf|YALF]]. Також ви можете використовувати всього дві малесенькі бібліотеки, аби не тягнути за собою весь фреймворк та залишитись наодинці з вашим чудовим кодом та ідеями.
====== Як це працює? ======
Отак:
{{:wolfdispatcherscheme.png|}}
====== З чого почати? ======
У випадку, якщо у вас є досвід написання [[development|модулів для Ubilling]] у вас вже є експіренс роботи з фреймворком. Усі необхідні бібліотеки вже завантажено та ви можете пропустити підготовку середовища, перейшовши напряму до того аби фігачити код. Якщо ж ви не плануєте тягнути функціонал бота прямо в ваш біллінг, а хочете нормальне, окремо розташоване рішення, найпростішим та найзрозумілішим кейсом буде швиденько розвернути для цього [[yalf|YALF]] і вже на його базі зліпити цілісне та переносиме рішення.
Поїхали. Розгортаємо фреймворк, кудись в локацію доступну з вебу по https:
$ 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
Вмикаємо потрібні для функціонування нашого боту "шари" бібліотек в **config/yalf.ini**. Мінімально, що нам буде потрібне наразі це бібліотеки wolfdispatcher, telegram та ubconfig, отож:
LAYER_TELEBOT="telegram,wolfdispatcher,ubconfig"
Ось власне і все приготування оточення, котре буде необхідне нам для розробки. Переходимо власне до неї. Створюємо директорію, файлик модулю, та описовий файл модулю, де буде лежати власне обробник хуків нашого бота:
$ mkdir modules/general/ourbot
$ touch modules/general/ourbot/index.php
$ touch modules/general/ourbot/module.php
Та редагуємо їх відповідним чином
registerModule($module, 'main', __('OurBot'), 'Author name');
hookAutosetup(true);
// Слухаємо що нам кажуть
$bot->listen();
Все, цих чотирьох рядків коду достатньо, задля того, щоб наш бот почав функціонувати і обробляти сповіщення про події, що відбулись в його полі зору. Насправді достатньо трьох рядків але про це трішки згодом. Отож після цього всього, заходимо за посиланням
https://наш_хост/директорія_бота/?module=ourbot
та бачимо наступне:
{{:wolfdispatcherhookautoinstall.png|}}
що означає, що хук вашого бота автоматично встановлено і Телеграм в майбутньому, буде відсилати сповіщення власне в цей URL. При бажанні автоматичне встановлення хука можна просто вимкнути методом hookAutosetup(false) або ж просто не викликаючи його на цьому інстансі взагалі. Автоматичне встановлення хука може бути корисним та зручним як при перенесенні вашого бота на інший сервер/URL так і при зміні його токена. Власне відбувається воно у випадку зміни будь якого з елементів пари URL+Bot token.
Окей, а як переконатись, що наш бот все "дуже уважно слухає"? Та дуже просто. Одразу після створення інстансу, переключаємо його в режим відладки.
$bot->setDebug(true);
Всі необхідні вам сповіщення почнуть сипатись в **exports/botname_debug.log**
$ tail -F exports/ourbot_debug.log
і як реакцію на щось таке
{{:wolfdispatcherdebugtest.png|}}
ми отримаємо такий результат
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
==================
====== Хеллоуворлди ======
Окей, вже результат. Але ж ми тут інтерактивності хочемо? А з чого там, з хеллоуворлдів в нормальних книжках починають? Ну давайте будемо реагувати на текстові привіти ботом якось. Заодно і coding-style свій трішки виправимо. Оскільки звичайно тримати в прямо в контроллері імплементацію всього бота так собі затія, і здоровою ідеєю було б винести її в окрему однойменну бібліотеку, та й хардкодити токени і всі інші опції прямо в контроллері "ну такоє собі". Отож виносимо імплементацію нашого бота в автозавантажувану бібліотеку **api/libs/api.ourbot.php**
replyTo('Привіт');
}
}
запихуємо всі конфігурабельні штуки нашого бота в **config/alter.ini**
OURBOT_TOKEN="252624203:982384782349824232333"
OURBOT_DEBUG=1
OURBOT_AUTOSETUP=1
а контроллер приводимо до якогось такого вигляду
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();
отримуючи повністю закономірний результат
{{:wolfdispatcherhellospamle.png|}}
власне, якщо ми зазирнемо в дебаг-лог нашого бота ми побачимо там щось схоже на оце:
..........
[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
)
==================
Що власне свідчить, що на основі того, що в $this->receivedData (він же $this->message()) діспатчером було знайдено підрядок "привіт" задекларований в нашій структурі екшнів, було викликано відповідний йому метод $this->actionHello(). Власне екшн міг би бути і просто PHP-функцією а не методом. Тоді його було б викликано ось так: actionHello($this->receivedData). А в лозі ми б побачили сповіщення такого типу:
Called actions: Array
(
[0] => FUNC: actionHello
)
у випадку, якщо відповідно екшну не знайдено ні методу ні функції в рамках виконуваного коду, при ввімкненому дебаг режимі ми отримаємо прямо собі в чат отаке сповіщення
{{:wolfdispatchernometrhod.png|}}
що в лозі буде виглядати приблизно так
Called actions: Array
(
[0] => FAILED: actionHello2
)
Також є пачка "магічних методів", які за замовчуванням "роблять нічого" але все ж виконуються в різних ситуаціях. Їх ви теж можете задефайнити в вашій імплементації та навісити на них потрібний вам функціонал. Ось вони:
// виконується у випадку, якщо для повідомлення не знайдено відповідний екшн/обробник
$this->handleEmptyAction();
// виконується у випадку якщо надійшло повідомлення з порожнім текстом
$this->handleEmptyText();
// виконується у випадку якщо отримано фотокартку
$this->handlePhotoReceived();
// виконується як реакція на будь-яке повідомлення взагалі завжди
$this->handleAnyWay();
усі ці методи виконуються у випадку, якщо користувач, від якого надійшло повідомлення є "дозволеним для інтеракції", тобто або фігурує в структурі allowedChatiIds (типу ACL-ка) або ж не фігурує в структурі ignoredChatIds (читай бан-лист) які заповнюються відповідними їм сеттерами setAllowedChatIds() та setIgnoredChatIds(). Винятком є тільки обробник handleAnyWay() котрий відпрацьовує "взагалі завжди" і "взагалі для всіх".
Давайте розширимо функціонал нашого хеллоуворлдного боту чимось осмисленішим, по дорозі ненав'язливо продемонструвавши як редефайнити магічні хендлери та реагувати на різне. І будемо з цим всім закінчувати, переходячи до чогось осмисленого.
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);
}
}
}
$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();
====== Інтерфейс та всіляке таке інше ======
Реакція на текстові команди, це звичайно неймовірно круто і олдскульно, але нормальні люди звикли до **нормального інтерфейсу**. Наприклад до кнопочок. От натискаєш кнопочку і щось відбувається. Бачили колись таке? :)
Відкриваємо таємницю - в більшості випадків, кнопочка це теж текст. Який просто набирається кастомною клавіатурою. Давайте навчимось користуватись ними, на прикладі нашого наступного бота. Нехай це буде "бот-порадник" котрий при тицянні на кнопочки видає неймовірні поради на всі випадки життя. Добре? Заодно без палєва продемонструємо, що на одному й тому ж фреймворку може в сусідніх модулях жити нелімітована кількість ботів (насправді, і в одному й тому ж модулі, вони всі можуть бути, але це шиза) . Вогонь, так? ;)
$ mkdir modules/general/poradnyk
$ touch modules/general/poradnyk/index.php
$ touch modules/general/poradnyk/module.php
$ touch api/libs/api.poradnyk.php
малюємо його конфіг в **config/alter.ini**
PORADNYK_TOKEN="252624204:89929232322221"
PORADNYK_DEBUG=1
PORADNYK_AUTOSETUP=1
заповнюємо опис модуля в **modules/general/poradnyk/module.php**
registerModule($module, 'main', __('AdviceBot'), 'Author name');
збираємо імплементацію в **api/libs/api.poradnyk.php**
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('Нахуй иди, вот тебе совет.');
}
}
Та контроллер цього всього в **modules/general/poradnyk/index.php**
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();
Йдемо за URL нашого контроллера задля встановлення веб-хука. Можемо до речі браузером, можемо тупо curl-ом. Не важливо. Результат буде один.
{{:wolfdispatcherhookporadnyk.png|}}
Все, тестуємо:
{{:wolfdispatcherporadnykdemo.png|}}
Як бачимо, наш бот-порадник чудово реагує на натискання кнопок та видає поради на будь-які випадки життя як державною так і російською мовами за вибором користувача.
Не складно помітити, що actionKeyboard() викликається згідно команди "/start" котра завжди прилітає до бота на початку розмови з ним. В групових чатах (це ті, котрі $this->chatType!=private) клавіатуру не використовуємо, а орієнтуємось на текстові команди "!порада" та "!совет" пушачи цей набір команд в структуру setGroupActions котрий автоматично повністю замінює собою структуру setActions для цих самих групових чатів. Власне, якщо вам захочеться розділювати кнопочки кастомної клавіатури по рядах, то керується це наступним чином:
$buttons[] = array(self::ROUTE_ADVICE_UA, self::ROUTE_ADVICE_RU);
$buttons[] = array('🙏 Помолитись');
{{:wolfdispatcherkeybsample.png|}}
або якось так:
$buttons[] = array('🔥 Вогонь','🌊 Вода','☁️ Повітря');
$buttons[] = array(__('Use i18n Luke!'));
$buttons[] = array('Тут тупо текст','А ось ще одна','Типу так');
з отаким от результатом
{{:wolfdispatcherkeybsample2.png|}}
Чому на кнопочках стараємось використовувати емоджі? Власне з двох очевидних причин:
- Стараємось уникати випадкових реакцій бота, на рандомні повідомлення в чаті
- Красівоє
Також, насправді, ви не зобов`язані самі збирати клавіатуру повністю самі, викликаючи $this->telegram->makeKeyboard(). Для цього є зручний метод castKeyboard() що одразу відсилає клавіатуру до чатику
$buttons[] =array('Кнопка 1','Кнопка 2');
$buttons[] =array('Кнопка 3','Кнопка 4');
$this->castKeyboard($buttons, 'Що робимо далі?'));
====== Про складне ======
Так, а зараз видихніть, помоліться
{{wolfprey.webm?640|Будь хоробрим як вовк!}}
та зосередьтесь.
Будемо говорити про не самі очевидні речі. Про такі як "структура даних повідомлень", як параметризувати наші обробники подій, ловити фоточки, та всіляке таке інше.
Отож, розпочнемо з параметризації ваших обробників. Якщо у випадку, з зовнішніми функціями вони просто викликаються як "function_name(повідомлення)" ви можете задефайнити її якось так
function doSomething($argument) {
//тут далі робимо щось з цими даними, парсимо текст, наприклад.
}
То методи викликаються в лоб взагалі без параметрів. Зроблено це так, задля економії пам`яті. Звісно, навіщо плодити копії цих даних у середині об'єкта який і так вже їх отримав? Для роботи з "повідомленням" існує внутрішня структура **$this->receivedData** та захищений метод **$this->message()** котрий повертає її копію, що може бути зручним, якщо ви захочете якимось чином модифікувати дані прямо в цій структурі, але залишити їх незмінними для усіх інших подальших методів, що можуть бути виконані обробником. На практиці в коді ви це можете використовувати якось так:
protected function handleEmptyAction() {
if (!empty($this->receivedData['text'])) {
$this->reply($this->receivedData['text']);
}
}
Власне все що отримує бот в даних хука, ми рахуємо "повідомленням" формат якого ідентичний, для повідомлень отриманих в приватних, групових та групових чатах, а також і для постів в каналах, в незалежності від вмісту самих повідомлень. Цю ж структуру, ви можете бачити і в дебаг-лозі при увімкненій відладці. Також, з метою забезпечення більшої зручності та лаконічності коду, деякі важливі значення з нього автоматично вже замаплено на приватні проперті.
Ось трохи детальніше, про те, що міститься всередині
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() - у випадку, якщо поточне повідомлення, є відповіддю на якесь інше повідомлення, містить точно таку саму структуру "повідомлення"
як і ось ця, поточна, зі всіма даними повідомлення, на яке є відповіддю поточне.
)
У випадку, якщо крім просто тексту, до вас прилетіло ще щось, буде заповнено структури фоточок, документів, тощо. Виглядає це якось так:
[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
)
)
З самого вигляду цього всього, доволі очевидно, що це массив з превьюшками цієї фоточки, з різною якістю, розмірами та ідентифікаторами файликів, які можна завантажити собі. Також очевидно, що ви не дуже хочете працювати з цим всім мануально, тому двома сантиметрами нижче, ви довідаєтесь про зручні обгортки які дозволять вам дуже елегантно та лаконічно робити з отриманими зображеннями те, що вам скоріш за все захочеться з ними робити.
====== Робота з зображеннями ======
Перш за все, необхідно пам'ятати що зображення до нас можуть прийти двома шляхами. Як "швидко надіслати зображення" так і "надіслане без стиснення". Відповідно фігурувати вони можуть або як частина структури [photo] так і як частина [document] з відповідними зображенням mime-типами. Думати про це все, ми в принципі не хочемо. Саме тому, з цієї причини для роботи з зображеннями є дуже зручні обгортки. Давайте розглянемо на прикладі мінімалістичного боту, котрий на будь-яке повідомлення з зображенням, буде:
- Завантажувати це зображення собі
- Якось його модифікувати
- Відсилати в чатік назад уже спохабленим
От якось так
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();
З повністю очікуваним результатом
{{::wolfdispatcherimagesprocessingdemo.png|}}
Окей, а якщо ми не хочемо робити тільки "тлєн", а хочемо з зображеннями робити якесь різне і трохи інстаграмне, в залежності від тексту, який фігурує в описі зображення? Ну короче хочемо не один метод викликати для отриманого зображення, а декілька різних в залежності від того, що там від нас хочуть? Ну тоді setPhotoHandler будемо використовувати тільки для завантаження зображень, а весь процесинг параметрично винесемо в окремі методи. Модифікувати збережене зображення будемо в захищеній проперті $image.
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();
а ось, як це все працює на практиці:
{{instafiltersdemo.webm?614|Все обмежено тільки твоєю фантазією}}
====== Використання поза фреймворком ======
Також ви можете використовувати WolfDispatcher просто інклудячи тільки дві бібліотеки:
$ 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
і все працює:
hookAutosetup();
$bot->listen();
Власне тут [[https://github.com/nightflyza/WolfDispatcher|репозиторій на GitHub]] зі всім, що необхідно.