====== 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]] зі всім, що необхідно.