Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Bot often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Bot, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
12 | */ |
||
13 | class DanySpin97\PhpBotFramework\Bot extends DanySpin97\PhpBotFramework\CoreBot { |
||
14 | |||
15 | /** |
||
16 | * \addtogroup Bot Bot |
||
17 | * \brief Properties and methods to handle the TelegramBot. |
||
18 | * \details Here are listed all the properties and methods that will help the developer create the basic bot functions. |
||
19 | * @{ |
||
20 | */ |
||
21 | |||
22 | /** \brief Text received in messages */ |
||
23 | protected $_text; |
||
24 | |||
25 | /** \brief Data received in callback query */ |
||
26 | protected $_data; |
||
27 | |||
28 | /** \brief Query sent by the user in the inline query */ |
||
29 | protected $_query; |
||
30 | |||
31 | /** \brief Store the inline keyboard */ |
||
32 | public $keyboard; |
||
33 | |||
34 | /** \brief Pdo reference */ |
||
35 | public $pdo; |
||
36 | |||
37 | /** \brief Redis connection */ |
||
38 | public $redis; |
||
39 | |||
40 | /** @} */ |
||
41 | |||
42 | /** |
||
43 | * \addtogroup Core Core(internal) |
||
44 | * @{ |
||
45 | */ |
||
46 | |||
47 | /** \brief Store the command triggered on message. */ |
||
48 | private $message_commands; |
||
49 | |||
50 | /** \brief Does the bot has message commands? Set by initBot. */ |
||
51 | private $message_commands_set; |
||
52 | |||
53 | /** \brief Store the command triggered on callback query. */ |
||
54 | private $callback_commands; |
||
55 | |||
56 | /** \brief Does the bot has message commands? Set by initBot. */ |
||
57 | private $callback_commands_set; |
||
58 | |||
59 | /** @} */ |
||
60 | |||
61 | /** |
||
62 | * \addtogroup Multilanguage Multilanguage |
||
63 | * \brief Methods to create a localized bot. |
||
64 | * @{ |
||
65 | */ |
||
66 | |||
67 | /** \brief Store the language for a multi-language bot */ |
||
68 | public $language; |
||
69 | |||
70 | /** \brief Store localization data */ |
||
71 | public $local; |
||
72 | |||
73 | /** \brief Table contaning bot users data in the sql database. */ |
||
74 | public $user_table = '"User"'; |
||
75 | |||
76 | /** \brief Name of the column that represents the user id in the sql database */ |
||
77 | public $id_column = 'chat_id'; |
||
78 | |||
79 | /** @} */ |
||
80 | |||
81 | /** \addtogroup State |
||
82 | * @{ |
||
83 | */ |
||
84 | |||
85 | /** \brief Status of the bot to handle data inserting and menu-like bot. */ |
||
86 | public $status; |
||
87 | |||
88 | /** @} */ |
||
89 | |||
90 | /** |
||
91 | * \addtogroup Bot |
||
92 | * @{ |
||
93 | */ |
||
94 | |||
95 | /** |
||
96 | * \brief Construct an empy bot. |
||
97 | * \details Construct a bot with commands, multilanguage and status. |
||
98 | */ |
||
99 | public function __construct(string $token) { |
||
100 | |||
101 | // Parent constructor |
||
102 | parent::__construct($token); |
||
103 | |||
104 | // Initialize to an empty array |
||
105 | $this->message_commands = []; |
||
106 | $this->callback_commands = []; |
||
107 | |||
108 | $this->keyboard = new DanySpin97\PhpBotFramework\InlineKeyboard($this); |
||
109 | |||
110 | } |
||
111 | |||
112 | /** \brief Descruct the class. */ |
||
113 | public function __destruct() { |
||
114 | |||
115 | parent::__destruct(); |
||
116 | |||
117 | // Close redis connection if it is open |
||
118 | if (isset($this->redis)) { |
||
119 | |||
120 | $this->redis->close(); |
||
121 | |||
122 | } |
||
123 | |||
124 | } |
||
125 | |||
126 | /** |
||
127 | * \brief Get the text of the message, if set (for updates of type "message"). |
||
128 | * @return Text of the message, empty string if not set. |
||
129 | */ |
||
130 | public function getMessageText() : string { |
||
131 | |||
132 | if (isset($this->_text)) { |
||
133 | |||
134 | return $this->_text; |
||
135 | |||
136 | } |
||
137 | |||
138 | return ''; |
||
139 | |||
140 | } |
||
141 | |||
142 | /** |
||
143 | * \brief Get the data of callback query, if set (for updates of type "callback_query"). |
||
144 | * @return Data of the callback query, empty string if not set. |
||
145 | */ |
||
146 | public function getCallbackData() : string { |
||
147 | |||
148 | if (isset($this->_data)) { |
||
149 | |||
150 | return $this->_data; |
||
151 | |||
152 | } |
||
153 | |||
154 | return ''; |
||
155 | |||
156 | } |
||
157 | |||
158 | /** |
||
159 | * \brief Get the query received from the inline query (for updates of type "inline_query"). |
||
160 | * @return The query sent by the user, throw exception if the current update is not an inline query. |
||
161 | */ |
||
162 | public function getInlineQuery() : string { |
||
163 | |||
164 | if (isset($this->_query)) { |
||
165 | |||
166 | return $this->_query; |
||
167 | |||
168 | } |
||
169 | |||
170 | throw new BotException("Query from inline query is not set: wrong update type"); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * \brief Get update and process it. |
||
175 | * \details Call this method if you are using webhook. |
||
176 | * It will get update from php::\input, check it and then process it using processUpdate. |
||
177 | */ |
||
178 | public function processWebhookUpdate() { |
||
179 | |||
180 | $this->initBot(); |
||
181 | |||
182 | $this->processUpdate(json_decode(file_get_contents('php://input'), true)); |
||
183 | |||
184 | } |
||
185 | |||
186 | /** @} */ |
||
187 | |||
188 | /** |
||
189 | * \addtogroup Core Core(Internal) |
||
190 | * @{ |
||
191 | */ |
||
192 | |||
193 | /** |
||
194 | * \brief Init variables to skip parsing commands if there aren't any. |
||
195 | * \details Called internnaly by |
||
196 | * - <code>getUpdatesLocal</code> |
||
197 | * - <code>getUpdatesRedis</code> |
||
198 | * - <code>getUpdatesDatabase</code> |
||
199 | * - <code>processWebhookUpdate</code> |
||
200 | */ |
||
201 | private function initBot() { |
||
202 | |||
203 | // Are there message commands? |
||
204 | $this->message_commands_set = !empty($this->message_commands); |
||
205 | |||
206 | // Are there callback commands? |
||
207 | $this->callback_commands_set = !empty($this->callback_commands); |
||
208 | |||
209 | } |
||
210 | |||
211 | /** |
||
212 | * \brief Dispatch each update to the right method (processMessage, processCallbackQuery, etc). |
||
213 | * \details Set $chat_id for each update, $text, $data and $query are set for each update that contains them. |
||
214 | * It also calls commands for each updates, before process methods. |
||
215 | * @param $update Reference to the update received. |
||
216 | * @return The id of the update processed. |
||
217 | */ |
||
218 | public function processUpdate(array $update) : int { |
||
219 | |||
220 | if (isset($update['message'])) { |
||
221 | |||
222 | // Set data from the message |
||
223 | $this->_chat_id = $update['message']['chat']['id']; |
||
224 | |||
225 | // If the message contains text |
||
226 | if (isset($update['message']['text'])) { |
||
227 | |||
228 | $this->_text = $update['message']['text']; |
||
229 | |||
230 | } |
||
231 | |||
232 | // If there are commands set by the user |
||
233 | // and there are bot commands in the message, checking message entities |
||
234 | if ($this->message_commands_set && isset($update['message']['entities']) && $update['message']['entities'][0]['type'] === 'bot_command') { |
||
235 | |||
236 | // The lenght of the command |
||
237 | $length = $update['message']['entities'][0]['length']; |
||
238 | |||
239 | // Offset of the command |
||
240 | $offset = $update['message']['entities'][0]['offset']; |
||
241 | |||
242 | // For each command added by the user |
||
243 | foreach ($this->message_commands as $trigger) { |
||
244 | |||
245 | // If the current command is a regex |
||
246 | if ($trigger['regex_active']) { |
||
247 | |||
248 | // Use preg_match to check if it is true |
||
249 | $matched = preg_match('/' . $trigger['regex_rule'] . '/', substr($update['message']['text'], $offset + 1, $length)); |
||
250 | |||
251 | // else check if the command sent by the user is the same as the one we are expecting |
||
252 | } else if ($trigger['length'] == $length && mb_strpos($trigger['command'], $update['message']['text'], $offset) !== false) { |
||
253 | |||
254 | // We found a valid command |
||
255 | $matched = true; |
||
256 | |||
257 | } else { |
||
258 | |||
259 | // We did not |
||
260 | $matched = false; |
||
261 | |||
262 | } |
||
263 | |||
264 | // Check the results for the current command |
||
265 | if ($matched) { |
||
266 | |||
267 | // Execute script, |
||
268 | $trigger['script']($this, $update['message']); |
||
269 | |||
270 | // clear text variable |
||
271 | unset($this->_text); |
||
272 | |||
273 | // and return the id of the current update to stop processing this update |
||
274 | return $update['update_id']; |
||
275 | |||
276 | } |
||
277 | |||
278 | } |
||
279 | |||
280 | } |
||
281 | |||
282 | // And process it |
||
283 | $this->processMessage($update['message']); |
||
284 | |||
285 | // clear text variable |
||
286 | unset($this->_text); |
||
287 | |||
288 | // If the update is a callback query |
||
289 | } elseif (isset($update['callback_query'])) { |
||
290 | |||
291 | // Set variables |
||
292 | $this->_chat_id = $update['callback_query']['from']['id']; |
||
293 | $this->_callback_query_id = $update['callback_query']['id']; |
||
294 | |||
295 | // If data is set for the current callback query |
||
296 | if (isset($update['callback_query']['data'])) { |
||
297 | |||
298 | $this->_data = $update['callback_query']['data']; |
||
299 | |||
300 | } |
||
301 | |||
302 | // Check for callback commands |
||
303 | if (isset($this->_data) && $this->callback_commands_set) { |
||
304 | |||
305 | // Parse all commands |
||
306 | foreach ($this->callback_commands as $trigger) { |
||
307 | |||
308 | // If command is found in callback data |
||
309 | if (strpos($trigger['data'], $this->_data) !== false) { |
||
310 | |||
311 | // Trigger the script |
||
312 | $trigger['script']($this, $update['callback_query']); |
||
313 | |||
314 | // Clear data |
||
315 | unset($this->_data); |
||
316 | unset($this->_callback_query_id); |
||
317 | |||
318 | // and return the id of the current update |
||
319 | return $update['update_id']; |
||
320 | |||
321 | } |
||
322 | |||
323 | } |
||
324 | |||
325 | } |
||
326 | |||
327 | // Process the callback query through processCallbackQuery |
||
328 | $this->processCallbackQuery($update['callback_query']); |
||
329 | |||
330 | // Unset callback query variables |
||
331 | unset($this->_callback_query_id); |
||
332 | unset($this->_data); |
||
333 | |||
334 | } elseif (isset($update['inline_query'])) { |
||
335 | |||
336 | $this->_chat_id = $update['inline_query']['from']['id']; |
||
337 | $this->_query = $update['inline_query']['query']; |
||
338 | $this->_inline_query_id = $update['inline_query']['id']; |
||
339 | |||
340 | $this->processInlineQuery($update['inline_query']); |
||
341 | |||
342 | unset($this->_query); |
||
343 | unset($this->_inline_query_id); |
||
344 | |||
345 | } elseif (isset($update['channel_post'])) { |
||
346 | |||
347 | // Set data from the post |
||
348 | $this->_chat_id = $update['channel_post']['chat']['id']; |
||
349 | |||
350 | $this->processChannelPost($update['channel_post']); |
||
351 | |||
352 | } elseif (isset($update['edited_message'])) { |
||
353 | |||
354 | $this->_chat_id = $update['edited_message']['chat']['id']; |
||
355 | |||
356 | $this->processEditedMessage($update['edited_message']); |
||
357 | |||
358 | } elseif (isset($update['edited_channel_post'])) { |
||
359 | |||
360 | $this->_chat_id = $update['edited_channel_post']['chat']['id']; |
||
361 | |||
362 | $this->processEditedChannelPost($update['edited_channel_post']); |
||
363 | |||
364 | } elseif (isset($update['chosen_inline_result'])) { |
||
365 | |||
366 | $this->_chat_id = $update['chosen_inline_result']['chat']['id']; |
||
367 | |||
368 | $this->processChosenInlineResult($update['chosen_inline_result']); |
||
369 | |||
370 | } |
||
371 | |||
372 | return $update['update_id']; |
||
373 | |||
374 | } |
||
375 | |||
376 | /** @} */ |
||
377 | |||
378 | /** |
||
379 | * \addtogroup Bot Bot |
||
380 | * @{ |
||
381 | */ |
||
382 | |||
383 | /** |
||
384 | * \brief Called every message received by the bot. |
||
385 | * \details Override it to script the bot answer for each message. |
||
386 | * <code>$chat_id</code> and <code>$text</code>, if the message contains text(use getMessageText() to access it), set inside of this function. |
||
387 | * @param $message Reference to the message received. |
||
388 | */ |
||
389 | protected function processMessage($message) {} |
||
390 | |||
391 | /** |
||
392 | * \brief Called every callback query received by the bot. |
||
393 | * \details Override it to script the bot answer for each callback. |
||
394 | * <code>$chat_id</code> and <code>$data</code>, if set in the callback query(use getCallbackData() to access it) set inside of this function. |
||
395 | * @param $callback_query Reference to the callback query received. |
||
396 | */ |
||
397 | protected function processCallbackQuery($callback_query) { |
||
398 | |||
399 | } |
||
400 | |||
401 | /** |
||
402 | * \brief Called every inline query received by the bot. |
||
403 | * \details Override it to script the bot answer for each inline query. |
||
404 | * $chat_id and $query(use getInlineQuery() to access it) set inside of this function. |
||
405 | * @param $inline_query Reference to the inline query received. |
||
406 | */ |
||
407 | protected function processInlineQuery($inline_query) { |
||
408 | |||
409 | } |
||
410 | |||
411 | /** |
||
412 | * \brief Called every chosen inline result received by the bot. |
||
413 | * \details Override it to script the bot answer for each chosen inline result. |
||
414 | * <code>$chat_id</code> set inside of this function. |
||
415 | * @param $chosen_inline_result Reference to the chosen inline result received. |
||
416 | */ |
||
417 | protected function processChosenInlineResult($chosen_inline_result) { |
||
418 | |||
419 | } |
||
420 | |||
421 | /** |
||
422 | * \brief Called every chosen edited message received by the bot. |
||
423 | * \details Override it to script the bot answer for each edited message. |
||
424 | * <code>$chat_id</code> set inside of this function. |
||
425 | * @param $edited_message The message edited by the user. |
||
426 | */ |
||
427 | protected function processEditedMessage($edited_message) { |
||
428 | |||
429 | } |
||
430 | |||
431 | /** |
||
432 | * \brief Called every new post in the channel where the bot is in. |
||
433 | * \details Override it to script the bot answer for each post sent in a channel. |
||
434 | * <code>$chat_id</code> set inside of this function. |
||
435 | * @param $post The message sent in the channel. |
||
436 | */ |
||
437 | protected function processChannelPost($post) { |
||
438 | |||
439 | } |
||
440 | |||
441 | /** |
||
442 | * \brief Called every time a post get edited in the channel where the bot is in. |
||
443 | * \details Override it to script the bot answer for each post edited in a channel. |
||
444 | * <code>$chat_id</code> set inside of this function. |
||
445 | * @param $post The message edited in the channel. |
||
446 | */ |
||
447 | protected function processEditedChannelPost($edited_post) { |
||
448 | |||
449 | } |
||
450 | |||
451 | /** |
||
452 | * \brief Get updates received by the bot, using redis to save and get the last offset. |
||
453 | * \details It check if an offset exists on redis, then get it, or call getUpdates to set it. |
||
454 | * Then it start an infinite loop where it process updates and update the offset on redis. |
||
455 | * Each update is surrounded by a try/catch. |
||
456 | * @see getUpdates |
||
457 | * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted. |
||
458 | * @param $timeout <i>Optional</i>. Timeout in seconds for long polling. |
||
459 | * @param $offset_key <i>Optional</i>. Name of the variable where the offset is saved on Redis |
||
460 | */ |
||
461 | public function getUpdatesRedis(int $limit = 100, int $timeout = 60, string $offset_key = 'offset') { |
||
462 | |||
463 | // Check redis connection |
||
464 | if (!isset($this->redis)) { |
||
465 | |||
466 | throw new BotException("Redis connection is not set"); |
||
467 | |||
468 | } |
||
469 | |||
470 | // If offset is already set in redis |
||
471 | if ($this->redis->exists($variable_name)) { |
||
472 | |||
473 | // just set $offset as the same value |
||
474 | $offset = $this->redis->get($variable_name); |
||
475 | |||
476 | } else { |
||
477 | // Else get the offset from the id from the first update received |
||
478 | |||
479 | do { |
||
480 | |||
481 | $update = $this->getUpdates(0, 1); |
||
482 | |||
483 | } while (empty($update)); |
||
484 | |||
485 | $offset = $update[0]['update_id']; |
||
486 | |||
487 | $this->redis->set($variable_name, $offset); |
||
488 | |||
489 | $update = null; |
||
490 | |||
491 | } |
||
492 | |||
493 | $this->initBot(); |
||
494 | |||
495 | // Process all updates received |
||
496 | while (true) { |
||
497 | |||
498 | $updates = $this->getUpdates($offset, $limit, $timeout); |
||
499 | |||
500 | // Parse all updates received |
||
501 | foreach ($updates as $key => $update) { |
||
502 | |||
503 | try { |
||
504 | |||
505 | $this->processUpdate($update); |
||
506 | |||
507 | } catch (BotException $e) { |
||
508 | |||
509 | echo $e->getMessage(); |
||
510 | |||
511 | } |
||
512 | |||
513 | } |
||
514 | |||
515 | // Update the offset in redis |
||
516 | $this->redis->set($variable_name, $offset + count($updates)); |
||
517 | } |
||
518 | |||
519 | } |
||
520 | |||
521 | /** |
||
522 | * \brief Get updates received by the bot, and hold the offset in $offset. |
||
523 | * \details Get the update_id of the first update to parse, set it in $offset and |
||
524 | * then it start an infinite loop where it processes updates and keep $offset on the update_id of the last update received. |
||
525 | * Each processUpdate() method call is surrounded by a try/catch. |
||
526 | * @see getUpdates |
||
527 | * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted. |
||
528 | * @param $timeout <i>Optional</i>. Timeout in seconds for long polling. |
||
529 | */ |
||
530 | public function getUpdatesLocal(int $limit = 100, int $timeout = 60) { |
||
531 | |||
532 | $update = []; |
||
533 | |||
534 | // While there aren't updates to process |
||
535 | do { |
||
536 | |||
537 | // Get updates from telegram |
||
538 | $update = $this->getUpdates(0, 1); |
||
539 | |||
540 | // While in the array received there aren't updates |
||
541 | } while (empty($update)); |
||
542 | |||
543 | // Set the offset to the first update recevied |
||
544 | $offset = $update[0]['update_id']; |
||
545 | |||
546 | $update = null; |
||
547 | |||
548 | $this->initBot(); |
||
549 | |||
550 | // Process all updates |
||
551 | while (true) { |
||
552 | |||
553 | // Set parameter for the url call |
||
554 | $parameters = [ |
||
555 | 'offset' => $offset, |
||
556 | 'limit' => $limit, |
||
557 | 'timeout' => $timeout |
||
558 | ]; |
||
559 | |||
560 | $updates = $this->exec_curl_request($this->_api_url . 'getUpdates?' . http_build_query($parameters)); |
||
561 | |||
562 | // Parse all update to receive |
||
563 | foreach ($updates as $key => $update) { |
||
564 | |||
565 | try { |
||
566 | |||
567 | // Process one at a time |
||
568 | $this->processUpdate($update); |
||
569 | |||
570 | } catch (BotException $e) { |
||
571 | |||
572 | echo $e->getMessage(); |
||
573 | |||
574 | } |
||
575 | |||
576 | } |
||
577 | |||
578 | // Update the offset |
||
579 | $offset += sizeof($updates); |
||
580 | |||
581 | } |
||
582 | |||
583 | } |
||
584 | |||
585 | /** |
||
586 | * \brief Get updates received by the bot, using the sql database to store and get the last offset. |
||
587 | * \details It check if an offset exists on redis, then get it, or call getUpdates to set it. |
||
588 | * Then it start an infinite loop where it process updates and update the offset on redis. |
||
589 | * Each update is surrounded by a try/catch. |
||
590 | * @see getUpdates |
||
591 | * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted. |
||
592 | * @param $timeout <i>Optional</i>. Timeout in seconds for long polling. |
||
593 | * @param $table_name <i>Optional</i>. Name of the table where offset is saved in the database |
||
594 | * @param $column_name <i>Optional</i>. Name of the column where the offset is saved in the database |
||
595 | */ |
||
596 | public function getUpdatesDatabase(int $limit = 100, int $timeout = 0, string $table_name = 'telegram', string $column_name = 'bot_offset') { |
||
597 | |||
598 | if (!isset($this->_database)) { |
||
599 | |||
600 | throw new BotException("Database connection is not set"); |
||
601 | |||
602 | } |
||
603 | |||
604 | // Get the offset from the database |
||
605 | $sth = $this->pdo->prepare('SELECT ' . $column_name . ' FROM ' . $table_name); |
||
606 | |||
607 | try { |
||
608 | |||
609 | $sth->execute(); |
||
610 | |||
611 | } catch (PDOException $e) { |
||
612 | |||
613 | echo $e->getMessage(); |
||
614 | |||
615 | } |
||
616 | |||
617 | $offset = $sth->fetchColumn(); |
||
618 | $sth = null; |
||
619 | |||
620 | // Get the offset from the first update to update |
||
621 | if ($offset === false) { |
||
622 | |||
623 | do { |
||
624 | |||
625 | $update = $this->getUpdates(0, 1); |
||
626 | |||
627 | } while (empty($update)); |
||
628 | |||
629 | $offset = $update[0]['update_id']; |
||
630 | |||
631 | $update = null; |
||
632 | |||
633 | } |
||
634 | |||
635 | // Prepare the query for updating the offset in the database |
||
636 | $sth = $this->pdo->prepare('UPDATE "' . $table_name . '" SET "' . $column_name . '" = :new_offset'); |
||
637 | |||
638 | $this->initBot(); |
||
639 | |||
640 | while (true) { |
||
641 | |||
642 | $updates = $this->getUpdates($offset, $limit, $timeout); |
||
643 | |||
644 | foreach ($updates as $key => $update) { |
||
645 | |||
646 | try { |
||
647 | |||
648 | $this->processUpdate($update); |
||
649 | |||
650 | } catch (BotException $e) { |
||
651 | |||
652 | echo $e->getMessage(); |
||
653 | |||
654 | } |
||
655 | |||
656 | } |
||
657 | |||
658 | // Update the offset on the database |
||
659 | $sth->bindParam(':new_offset', $offset + sizeof($updates)); |
||
660 | $sth->execute(); |
||
661 | } |
||
662 | } |
||
663 | |||
664 | /** |
||
665 | * \brief Add a function that will be executed everytime a message contain the selected command |
||
666 | * \details Use this syntax: |
||
667 | * |
||
668 | * addMessageCommand("start", function($bot, $message) { |
||
669 | * $bot->sendMessage("Hi"); }); |
||
670 | * @param $command The command that will trigger this function (without slash). Eg: "start", "help", "about" |
||
671 | * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received). |
||
672 | */ |
||
673 | public function addMessageCommand(string $command, callable $script) { |
||
674 | |||
675 | $this->message_commands[] = [ |
||
676 | 'script' => $script, |
||
677 | 'command' => '/' . $command, |
||
678 | 'length' => strlen($command) + 1, |
||
679 | 'regex_active' => false |
||
680 | ]; |
||
681 | |||
682 | } |
||
683 | |||
684 | /** |
||
685 | * \brief Add a function that will be executed everytime a message contain a command that match the regex |
||
686 | * \details Use this syntax: |
||
687 | * |
||
688 | * addMessageCommandRegex("number\d", function($bot, $message, $result) { |
||
689 | * $bot->sendMessage("You sent me a number"); }); |
||
690 | * @param $regex_rule Regex rule that will called for evalueting the command received. |
||
691 | * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received). |
||
692 | */ |
||
693 | public function addMessageCommandRegex(string $regex_rule, callable $script) { |
||
694 | |||
695 | $this->message_commands[] = [ |
||
696 | 'script' => $script, |
||
697 | 'regex_active' => true, |
||
698 | 'regex_rule' => $regex_rule |
||
699 | ]; |
||
700 | |||
701 | } |
||
702 | |||
703 | /** |
||
704 | * \brief Add a function that will be executed everytime a callback query contains a string as data |
||
705 | * \details Use this syntax: |
||
706 | * |
||
707 | * addMessageCommand("menu", function($bot, $callback_query) { |
||
708 | * $bot->editMessageText($callback_query['message']['message_id'], "This is the menu"); }); |
||
709 | * @param $data The string that will trigger this function. |
||
710 | * @param $script The function that will be triggered by the callback query if it contains the $data string. Must take an object(the bot) and an array(the callback query received). |
||
711 | */ |
||
712 | public function addCallbackCommand(string $data, callable $script) { |
||
713 | |||
714 | $this->callback_commands[] = [ |
||
715 | 'data' => $data, |
||
716 | 'script' => $script, |
||
717 | ]; |
||
718 | |||
719 | } |
||
720 | |||
721 | /** @} */ |
||
722 | |||
723 | /** |
||
724 | * \addtogroup Multilanguage Multilanguage |
||
725 | * @{ |
||
726 | */ |
||
727 | |||
728 | /** |
||
729 | * \brief Get current user language from the database, and set it in $language. |
||
730 | * @param $default_language <i>Optional</i>. Default language to return in case of errors. |
||
731 | * @return Language set for the current user, $default_language on errors. |
||
732 | */ |
||
733 | public function getLanguageDatabase($default_language = 'en') { |
||
734 | |||
735 | // If we have no database |
||
736 | if (!isset($this->_database)) { |
||
737 | |||
738 | // Set the language to english |
||
739 | $this->language = $default_language; |
||
740 | |||
741 | // Return english |
||
742 | return $default_language; |
||
743 | |||
744 | } |
||
745 | |||
746 | // Get the language from the bot |
||
747 | $sth = $this->pdo->prepare('SELECT language FROM ' . $this->user_table . ' WHERE ' . $this->id_column . ' = :chat_id'); |
||
748 | $sth->bindParam(':chat_id', $this->_chat_id); |
||
749 | |||
750 | try { |
||
751 | |||
752 | $sth->execute(); |
||
753 | |||
754 | } catch (PDOException $e) { |
||
755 | |||
756 | echo $e->getMessage(); |
||
757 | |||
758 | } |
||
759 | |||
760 | $row = $sth->fetch(); |
||
761 | |||
762 | $sth = null; |
||
763 | |||
764 | // If we got the language |
||
765 | if (isset($row['language'])) { |
||
766 | |||
767 | // Set the language in the bot |
||
768 | $this->language = $row['language']; |
||
769 | |||
770 | // And return it |
||
771 | return $row['language']; |
||
772 | |||
773 | } |
||
774 | |||
775 | // If we couldn't get it, set the language to english |
||
776 | $this->language = $default_language; |
||
777 | |||
778 | // and return english |
||
779 | return $this->language; |
||
780 | |||
781 | } |
||
782 | |||
783 | /** |
||
784 | * \brief Get current user language from redis, and set it in language. |
||
785 | * \details Using redis database we get language stored and the value does not expires. |
||
786 | * @param $default_language <i>Optional</i>. Default language to return in case of errors. |
||
787 | * @return Language for the current user, $default_language on errors. |
||
788 | */ |
||
789 | public function getLanguageRedis($default_language = 'en') : string { |
||
790 | |||
791 | // If redis or pdo connection are not set |
||
792 | if (!isset($this->redis)) { |
||
793 | |||
794 | // return default language |
||
795 | return $default_language; |
||
796 | |||
797 | } |
||
798 | |||
799 | // Does it exists on redis? |
||
800 | if ($this->redis->exists($this->_chat_id . ':language')) { |
||
801 | |||
802 | // Get the value |
||
803 | $this->language = $this->redis->get($this->_chat_id . ':language'); |
||
804 | return $this->language; |
||
805 | |||
806 | } |
||
807 | |||
808 | // If it doens't exist, set $language to $default_language |
||
809 | $this->language = $default_language; |
||
810 | |||
811 | // and return it |
||
812 | return $this->language; |
||
813 | |||
814 | } |
||
815 | |||
816 | /** |
||
817 | * \brief Get current user language from redis, as a cache, and set it in language. |
||
818 | * \details Using redis database as cache, seeks the language in it, if there isn't |
||
819 | * then get the language from the sql database and store it (with default expiring of one day) in redis. |
||
820 | * It also change $language parameter of the bot to the language returned. |
||
821 | * @param $default_language <i>Optional</i>. Default language to return in case of errors. |
||
822 | * @param $expiring_time <i>Optional</i>. Set the expiring time for the language on redis each time it is took from the sql database. |
||
823 | * @return Language for the current user, $default_language on errors. |
||
824 | */ |
||
825 | public function getLanguageRedisAsCache($default_language = 'en', $expiring_time = '86400') : string { |
||
826 | |||
827 | // If redis or pdo connection are not set |
||
828 | if (!isset($this->redis) || !isset($this->pdo)) { |
||
829 | |||
830 | // return default language |
||
831 | return $default_language; |
||
832 | |||
833 | } |
||
834 | |||
835 | // Does it exists on redis? |
||
836 | if ($this->redis->exists($this->_chat_id . ':language')) { |
||
837 | |||
838 | // Get the value |
||
839 | $this->language = $this->redis->get($this->_chat_id . ':language'); |
||
840 | return $this->language; |
||
841 | |||
842 | } |
||
843 | |||
844 | // Set the value from the db |
||
845 | $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $this->getLanguageDatabase($default_language)); |
||
846 | |||
847 | // and return it |
||
848 | return $this->language; |
||
849 | |||
850 | } |
||
851 | |||
852 | /** |
||
853 | * \brief Set the current user language in both redis, sql database and $language. |
||
854 | * \details Save it on database first, then create the expiring key on redis. |
||
855 | * @param $language The new language to set. |
||
856 | * @param $expiring_time <i>Optional</i>. Time for the language key in redis to expire. |
||
857 | * @return On sucess, return true, throw exception otherwise. |
||
858 | */ |
||
859 | public function setLanguageRedisAsCache($language, $expiring_time = '86400') { |
||
860 | |||
861 | // Check database connection |
||
862 | if (!isset($this->_database) && !isset($this->redis)) { |
||
863 | throw new BotException('Database connection not set'); |
||
864 | } |
||
865 | |||
866 | // Update the language in the database |
||
867 | $sth = $this->pdo->prepare('UPDATE ' . $this->user_table . ' SET language = :language WHERE ' . $this->id_column . ' = :id'); |
||
868 | $sth->bindParam(':language', $language); |
||
869 | $sth->bindParam(':id', $this->_chat_id); |
||
870 | |||
871 | try { |
||
872 | |||
873 | $sth->execute(); |
||
874 | |||
875 | } catch (PDOException $e) { |
||
876 | |||
877 | throw new BotException($e->getMessage()); |
||
878 | |||
879 | } |
||
880 | |||
881 | // Destroy statement |
||
882 | $sth = null; |
||
883 | |||
884 | // Set the language in redis with expiring |
||
885 | $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $language); |
||
886 | |||
887 | // Set language in the bot variable |
||
888 | $this->language = $language; |
||
889 | } |
||
890 | |||
891 | /** |
||
892 | * \brief Load localization files (JSON-serialized) from a folder and set them in $local variable. |
||
893 | * \details Save all localization files, saved as json format, from a directory and put the contents in $local variable. |
||
894 | * Each file will be saved into $local with the first two letters of the filename as the index. |
||
895 | * Access the english data as $this->local["en"]["Your key"]. |
||
896 | * File <code>./localization/en.json</code>: |
||
897 | * |
||
898 | * {"Hello_Msg": "Hello"} |
||
899 | * |
||
900 | * File <code>./localization/it.json</code>: |
||
901 | * |
||
902 | * {"Hello_Msg": "Ciao"} |
||
903 | * |
||
904 | * Usage in <code>processMessage()</code>: |
||
905 | * |
||
906 | * $sendMessage($this->local[$this->language]["Hello_Msg"]); |
||
907 | * |
||
908 | * @param $dir Directory where the localization files are saved. |
||
909 | */ |
||
910 | public function loadLocalization($dir = './localization') { |
||
911 | |||
912 | // Open directory |
||
913 | if ($handle = opendir($dir)) { |
||
914 | |||
915 | // Iterate over all files |
||
916 | while (false !== ($file = readdir($handle))) { |
||
917 | |||
918 | // If the file is a JSON data file |
||
919 | if (strlen($file) > 6 && substr($file, -5) === '.json') { |
||
920 | |||
921 | try { |
||
922 | |||
923 | // Add the contents of the file to the $local variable, after deserializng it from JSON format |
||
924 | // The contents will be added with the 2 letter of the file as the index |
||
925 | $this->local[substr($file, 0, 2)] = json_decode(file_get_contents("$dir/$file"), true); |
||
926 | |||
927 | } catch (BotException $e) { |
||
928 | |||
929 | echo $e->getMessage(); |
||
930 | |||
931 | } |
||
932 | |||
933 | } |
||
934 | |||
935 | } |
||
936 | |||
937 | } |
||
938 | |||
939 | } |
||
940 | |||
941 | /** @} */ |
||
942 | |||
943 | /** |
||
944 | * \addtogroup State |
||
945 | * \brief Create a state based bot using these methods. |
||
946 | * \details Bot will answer in different way based on the state. |
||
947 | * Here is an example where we use save user credential using bot states: |
||
948 | * |
||
949 | * <?php |
||
950 | * |
||
951 | * // Include the framework |
||
952 | * require './vendor/autoload.php'; |
||
953 | * |
||
954 | * // Define bot state |
||
955 | * define("SEND_USERNAME", 1); |
||
956 | * define("SEND_PASSWORD", 2); |
||
957 | * |
||
958 | * // Create the class for the bot that will handle login |
||
959 | * class LoginBot extends DanySpin97\PhpBotFramework\Bot { |
||
960 | * |
||
961 | * // Add the function for processing messages |
||
962 | * protected function processMessage($message) { |
||
963 | * |
||
964 | * switch($this->getStatus()) { |
||
965 | * |
||
966 | * // If we are expecting a username from the user |
||
967 | * case SEND_USERNAME: |
||
968 | * |
||
969 | * // Save the username |
||
970 | * |
||
971 | * // Say the user to insert the password |
||
972 | * $this->sendMessage("Please, send your password."); |
||
973 | * |
||
974 | * // Update the bot state |
||
975 | * $this->setStatus(SEND_PASSWORD); |
||
976 | * |
||
977 | * break; |
||
978 | * |
||
979 | * // Or if we are expecting a password from the user |
||
980 | * case SEND_PASSWORD: |
||
981 | * |
||
982 | * // Save the password |
||
983 | * |
||
984 | * // Say the user he completed the process |
||
985 | * $this->sendMessage("The registration is complete"); |
||
986 | * |
||
987 | * break; |
||
988 | * } |
||
989 | * |
||
990 | * } |
||
991 | * |
||
992 | * } |
||
993 | * |
||
994 | * // Create the bot |
||
995 | * $bot = new LoginBot("token"); |
||
996 | * |
||
997 | * // Create redis object |
||
998 | * $bot->redis = new Redis(); |
||
999 | * |
||
1000 | * // Connect to redis database |
||
1001 | * $bot->redis->connect('127.0.0.1'); |
||
1002 | * |
||
1003 | * // Create the awnser to the <code>/start</code> command |
||
1004 | * $start_closure = function($bot, $message) { |
||
1005 | * |
||
1006 | * // saying the user to enter a username |
||
1007 | * $bot->sendMessage("Please, send your username."); |
||
1008 | * |
||
1009 | * // and update the status |
||
1010 | * $bot->setStatus(SEND_USERNAME); |
||
1011 | * }; |
||
1012 | * |
||
1013 | * // Add the answer |
||
1014 | * $bot->addMessageCommand("start", $start_closure); |
||
1015 | * |
||
1016 | * $bot->getUpdatesLocal(); |
||
1017 | * @{ |
||
1018 | */ |
||
1019 | |||
1020 | /** |
||
1021 | * \brief Get current user status from redis and set it in status variable. |
||
1022 | * \details Throw exception if redis connection is missing. |
||
1023 | * @param $default_status <i>Optional</i>. The default status to return in case there is no status for the current user. |
||
1024 | * @return The status for the current user, $default_status if missing. |
||
1025 | */ |
||
1026 | public function getStatus(int $default_status = -1) : int { |
||
1027 | |||
1028 | if (!isset($this->redis)) { |
||
1029 | |||
1030 | throw new BotException('Redis connection not set'); |
||
1031 | |||
1032 | } |
||
1033 | |||
1034 | if ($this->redis->exists($this->_chat_id . ':status')) { |
||
1035 | |||
1036 | $this->status = $this->redis->get($this->_chat_id . ':status'); |
||
1037 | |||
1038 | return $this->status; |
||
1039 | |||
1040 | } |
||
1041 | |||
1042 | $this->redis->set($this->_chat_id . ':status', $default_status); |
||
1043 | $this->status = $default_status; |
||
1044 | return $default_status; |
||
1045 | |||
1046 | } |
||
1047 | |||
1048 | /** \brief Set the status of the bot in both redis and $status. |
||
1049 | * \details Throw exception if redis connection is missing. |
||
1050 | * @param $status The new status of the bot. |
||
1051 | */ |
||
1052 | public function setStatus(int $status) { |
||
1053 | |||
1054 | $this->redis->set($this->_chat_id . ':status', $status); |
||
1055 | |||
1056 | $this->status = $status; |
||
1057 | |||
1058 | } |
||
1059 | |||
1060 | /** @} */ |
||
1061 | |||
1062 | /** |
||
1063 | * \addtogroup Users-handle Users handling |
||
1064 | * \brief Handle bot users on the database. |
||
1065 | * @{ |
||
1066 | */ |
||
1067 | |||
1068 | /** \brief Add a user to the database. |
||
1069 | * \details Add a user to the database in Bot::$user_table table and Bot::$id_column column using Bot::$pdo connection. |
||
1070 | * @param $chat_id chat_id of the user to add. |
||
1071 | * @return True on success. |
||
1072 | */ |
||
1073 | public function addUser($chat_id) : bool { |
||
1074 | |||
1075 | // Is there database connection? |
||
1076 | if (!isset($this->pdo)) { |
||
1077 | |||
1078 | throw new BotException("Database connection not set"); |
||
1079 | |||
1080 | } |
||
1081 | |||
1082 | // Create insertion query and initialize variable |
||
1083 | $query = "INSERT INTO $this->user_table ($this->id_column) VALUES (:chat_id)"; |
||
1084 | |||
1085 | // Prepare the query |
||
1086 | $sth = $this->pdo->prepare($query); |
||
1087 | |||
1088 | // Add the chat_id to the query |
||
1089 | $sth->bindParam(':chat_id', $chat_id); |
||
1090 | |||
1091 | try { |
||
1092 | |||
1093 | $sth->execute(); |
||
1094 | $success = true; |
||
1095 | |||
1096 | } catch (PDOException $e) { |
||
1097 | |||
1098 | echo $e->getMessage(); |
||
1099 | |||
1100 | $success = false; |
||
1101 | |||
1102 | } |
||
1103 | |||
1104 | // Close statement |
||
1105 | $sth = null; |
||
1106 | |||
1107 | // Return result |
||
1108 | return $success; |
||
1109 | |||
1110 | } |
||
1111 | |||
1112 | /** |
||
1113 | * \brief Broadcast a message to all user registred on the database. |
||
1114 | * \details Send a message to all users subscribed, change Bot::$user_table and Bot::$id_column to match your database structure is. |
||
1115 | * This method requires Bot::$pdo connection set. |
||
1116 | * All parameters are the same as CoreBot::sendMessage. |
||
1117 | * Because a limitation of Telegram Bot API the bot will have a delay after 20 messages sent in different chats. |
||
1118 | * @see CoreBot::sendMessage |
||
1119 | */ |
||
1120 | public function broadcastMessage($text, string $reply_markup = null, string $parse_mode = 'HTML', bool $disable_web_preview = true, bool $disable_notification = false) { |
||
1121 | |||
1122 | // Is there database connection? |
||
1123 | if (!isset($this->pdo)) { |
||
1124 | |||
1125 | throw new BotException("Database connection not set"); |
||
1126 | |||
1127 | } |
||
1128 | |||
1129 | // Prepare the query to get all chat_id from the database |
||
1130 | $sth = $this->pdo->prepare("SELECT $this->id_column FROM $this->user_table"); |
||
1131 | |||
1132 | try { |
||
1133 | |||
1134 | $sth->execute(); |
||
1135 | |||
1136 | } catch (PDOException $e) { |
||
1137 | |||
1138 | echo $e->getMessage(); |
||
1139 | |||
1140 | } |
||
1141 | |||
1142 | // Iterate over all the row got |
||
1143 | while ($user = $sth->fetch()) { |
||
1144 | |||
1145 | // Call getChat to know that this users haven't blocked the bot |
||
1146 | $user_data = $this->getChat($user[$this->id_column]); |
||
1147 | |||
1148 | // Did they block it? |
||
1149 | if ($user_data !== false) { |
||
1150 | |||
1151 | // Change the chat_id for the next API method |
||
1152 | $this->setChatID($user[$this->id_column]); |
||
1153 | |||
1154 | // Send the message |
||
1155 | $this->sendMessage($text, $reply_markup, null, $parse_mode, $disable_web_preview, $disable_notification); |
||
1156 | |||
1169 |