├── Api.php ├── ApiFactory.php ├── Client ├── Client.php ├── GuzzleClient.php └── Response.php ├── Daemon ├── Daemon.php └── NaiveDaemon.php ├── Exception └── NotOkException.php ├── LICENSE ├── Type ├── Audio.php ├── CallbackQuery.php ├── Chat.php ├── ChatMember.php ├── Contact.php ├── Document.php ├── File.php ├── ForceReply.php ├── GroupChat.php ├── Inline │ ├── ChosenInlineResult.php │ └── InlineQuery.php ├── InlineKeyboardButton.php ├── InlineKeyboardMarkup.php ├── Keyboard.php ├── KeyboardButton.php ├── Location.php ├── Message.php ├── MessageEntity.php ├── PhotoSize.php ├── ReplyKeyboardHide.php ├── ReplyKeyboardMarkup.php ├── ReplyMarkup.php ├── Sticker.php ├── Type.php ├── Update.php ├── User.php ├── UserProfilePhotos.php ├── Venue.php ├── Video.php └── Voice.php ├── composer.json └── readme.md /Api.php: -------------------------------------------------------------------------------- 1 | client = $client; 31 | } 32 | 33 | /** 34 | * @param string $method 35 | * @param array $params 36 | * 37 | * @return Response 38 | */ 39 | public function request($method, array $params = []) 40 | { 41 | $response = $this->client->request($method, $params); 42 | if (!$response->getOk()) { 43 | throw new NotOkException(sprintf('Code: %s. Description: "%s".', $response->getErrorCode(), $response->getDescription())); 44 | } 45 | 46 | return $response; 47 | } 48 | 49 | /** 50 | * @return User 51 | */ 52 | public function getMe() 53 | { 54 | return User::createFromResponse($this->request('getMe')); 55 | } 56 | 57 | /** 58 | * @param array $params 59 | * 60 | * @return Message 61 | */ 62 | public function sendMessage(array $params) 63 | { 64 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 65 | $params['reply_markup'] = json_encode($params['reply_markup']); 66 | } 67 | 68 | return Message::createFromResponse($this->request('sendMessage', $params)); 69 | } 70 | 71 | /** 72 | * @param array $params 73 | * 74 | * @return Message 75 | */ 76 | public function forwardMessage(array $params) 77 | { 78 | return Message::createFromResponse($this->request('forwardMessage', $params)); 79 | } 80 | 81 | /** 82 | * @param array $params 83 | * 84 | * @return Message 85 | */ 86 | public function sendPhoto(array $params) 87 | { 88 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 89 | $params['reply_markup'] = json_encode($params['reply_markup']); 90 | } 91 | 92 | return Message::createFromResponse($this->request('sendPhoto', $params)); 93 | } 94 | 95 | /** 96 | * @param array $params 97 | * 98 | * @return Message 99 | */ 100 | public function sendAudio(array $params) 101 | { 102 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 103 | $params['reply_markup'] = json_encode($params['reply_markup']); 104 | } 105 | 106 | return Message::createFromResponse($this->request('sendAudio', $params)); 107 | } 108 | 109 | /** 110 | * @param array $params 111 | * 112 | * @return Message 113 | */ 114 | public function sendDocument(array $params) 115 | { 116 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 117 | $params['reply_markup'] = json_encode($params['reply_markup']); 118 | } 119 | 120 | return Message::createFromResponse($this->request('sendDocument', $params)); 121 | } 122 | 123 | /** 124 | * @param array $params 125 | * 126 | * @return Message 127 | */ 128 | public function sendSticker(array $params) 129 | { 130 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 131 | $params['reply_markup'] = json_encode($params['reply_markup']); 132 | } 133 | 134 | return Message::createFromResponse($this->request('sendSticker', $params)); 135 | } 136 | 137 | /** 138 | * @param array $params 139 | * 140 | * @return Message 141 | */ 142 | public function sendVideo(array $params) 143 | { 144 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 145 | $params['reply_markup'] = json_encode($params['reply_markup']); 146 | } 147 | 148 | return Message::createFromResponse($this->request('sendVideo', $params)); 149 | } 150 | 151 | /** 152 | * @param array $params 153 | * 154 | * @return Message 155 | */ 156 | public function sendVoice(array $params) 157 | { 158 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 159 | $params['reply_markup'] = json_encode($params['reply_markup']); 160 | } 161 | 162 | return Message::createFromResponse($this->request('sendVideo', $params)); 163 | } 164 | 165 | /** 166 | * @param array $params 167 | * 168 | * @return Message 169 | */ 170 | public function sendLocation(array $params) 171 | { 172 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 173 | $params['reply_markup'] = json_encode($params['reply_markup']); 174 | } 175 | 176 | return Message::createFromResponse($this->request('sendLocation', $params)); 177 | } 178 | 179 | /** 180 | * @param array $params 181 | * 182 | * @return Message 183 | */ 184 | public function sendVenue(array $params) 185 | { 186 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 187 | $params['reply_markup'] = json_encode($params['reply_markup']); 188 | } 189 | 190 | return Message::createFromResponse($this->request('sendVenue', $params)); 191 | } 192 | 193 | /** 194 | * @param array $params 195 | * 196 | * @return Message 197 | */ 198 | public function sendContact(array $params) 199 | { 200 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 201 | $params['reply_markup'] = json_encode($params['reply_markup']); 202 | } 203 | 204 | return Message::createFromResponse($this->request('sendContact', $params)); 205 | } 206 | 207 | /** 208 | * @param array $params 209 | * 210 | * @return Response 211 | */ 212 | public function sendChatAction(array $params) 213 | { 214 | return $this->request('sendChatAction', $params); 215 | } 216 | 217 | /** 218 | * @param array $params 219 | * 220 | * @return UserProfilePhotos 221 | */ 222 | public function getUserProfilePhotos(array $params) 223 | { 224 | return UserProfilePhotos::createFromResponse($this->request('getUserProfilePhotos', $params)); 225 | } 226 | 227 | /** 228 | * @param array $params 229 | * 230 | * @return File 231 | */ 232 | public function getFile(array $params) 233 | { 234 | return File::createFromResponse($this->request('getFile', $params)); 235 | } 236 | 237 | /** 238 | * @param array $params 239 | * 240 | * @return Response 241 | */ 242 | public function kickChatMember(array $params) 243 | { 244 | return $this->request('kickChatMember', $params); 245 | } 246 | 247 | /** 248 | * @param array $params 249 | * 250 | * @return Response 251 | */ 252 | public function leaveChat(array $params) 253 | { 254 | return $this->request('leaveChat', $params); 255 | } 256 | 257 | /** 258 | * @param array $params 259 | * 260 | * @return Response 261 | */ 262 | public function unbanChatMember(array $params) 263 | { 264 | return $this->request('unbanChatMember', $params); 265 | } 266 | 267 | /** 268 | * @param array $params 269 | * 270 | * @return Chat 271 | */ 272 | public function getChat(array $params) 273 | { 274 | return Chat::createFromResponse($this->request('getChat', $params)); 275 | } 276 | 277 | /** 278 | * @param array $params 279 | * 280 | * @return ChatMember[] 281 | */ 282 | public function getChatAdministrators(array $params) 283 | { 284 | return array_map(function ($user) { 285 | return ChatMember::create($user); 286 | }, $this->request('getChatAdministrators', $params)->getResult()); 287 | } 288 | 289 | /** 290 | * @param array $params 291 | * 292 | * @return Response 293 | */ 294 | public function getChatMembersCount(array $params) 295 | { 296 | return $this->request('getChatMembersCount', $params); 297 | } 298 | 299 | /** 300 | * @param array $params 301 | * 302 | * @return ChatMember 303 | */ 304 | public function getChatMember(array $params) 305 | { 306 | return ChatMember::createFromResponse($this->request('getChatMember', $params)->getResult()); 307 | } 308 | 309 | /** 310 | * @param array $params 311 | * 312 | * @return Response 313 | */ 314 | public function answerCallbackQuery(array $params) 315 | { 316 | return $this->request('answerCallbackQuery', $params); 317 | } 318 | 319 | /** 320 | * @param array $params 321 | * 322 | * @return Message 323 | */ 324 | public function editMessageText(array $params) 325 | { 326 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 327 | $params['reply_markup'] = json_encode($params['reply_markup']); 328 | } 329 | 330 | return Message::createFromResponse($this->request('editMessageText', $params)); 331 | } 332 | 333 | /** 334 | * @param array $params 335 | * 336 | * @return Message 337 | */ 338 | public function editMessageCaption(array $params = []) 339 | { 340 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 341 | $params['reply_markup'] = json_encode($params['reply_markup']); 342 | } 343 | 344 | return Message::createFromResponse($this->request('editMessageCaption', $params)); 345 | } 346 | 347 | /** 348 | * @param array $params 349 | * 350 | * @return Message 351 | */ 352 | public function editMessageReplyMarkup(array $params = []) 353 | { 354 | if (isset($params['reply_markup']) && $params['reply_markup'] instanceof Keyboard) { 355 | $params['reply_markup'] = json_encode($params['reply_markup']); 356 | } 357 | 358 | return Message::createFromResponse($this->request('editMessageReplyMarkup', $params)); 359 | } 360 | 361 | /** 362 | * @param array $params 363 | * 364 | * @return Update[] 365 | */ 366 | public function getUpdates(array $params = []) 367 | { 368 | return array_map(function (stdClass $update) { 369 | return Update::create($update); 370 | }, $this->request('getUpdates', $params)->getResult()); 371 | } 372 | 373 | /** 374 | * @param array $params 375 | * 376 | * @return Response 377 | */ 378 | public function setWebhook(array $params = []) 379 | { 380 | return $this->request('setWebhook', $params); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /ApiFactory.php: -------------------------------------------------------------------------------- 1 | token = $token; 31 | } 32 | 33 | /** 34 | * @param string $method 35 | * @param array $params 36 | * 37 | * @return Response 38 | */ 39 | public function request($method, array $params = []) 40 | { 41 | $client = new \GuzzleHttp\Client(); 42 | 43 | $multipartParams = []; 44 | foreach ($params as $key => $value) { 45 | $multipartParams[] = [ 46 | 'name' => $key, 47 | 'contents' => is_scalar($value) ? (string)$value : $value 48 | ]; 49 | } 50 | 51 | try { 52 | $response = $client->post($this->getUrl($method), [ 53 | 'verify' => false, 54 | 'multipart' => $multipartParams ?: null 55 | ]); 56 | $response = json_decode($response->getBody()); 57 | 58 | if ($response === null) { 59 | throw new HttpRuntimeException('Empty response.'); 60 | } 61 | return new Response($response->ok, $response->result); 62 | } catch (ClientException $e) { 63 | $response = json_decode($e->getResponse()->getBody()->getContents()); 64 | return new Response($response->ok, null, $response->error_code, $response->description); 65 | } catch (ServerException $e) { 66 | return new Response(false, null, $e->getResponse()->getStatusCode(), $e->getResponse()->getReasonPhrase()); 67 | } 68 | } 69 | 70 | /** 71 | * @param string $method 72 | * 73 | * @return string 74 | */ 75 | private function getUrl($method) 76 | { 77 | return strtr($this->baseUrl, [ 78 | '{token}' => $this->token, 79 | '{method}' => $method 80 | ]); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Client/Response.php: -------------------------------------------------------------------------------- 1 | ok = (bool)$ok; 38 | $this->result = $result; 39 | $this->error_code = $error_code; 40 | $this->description = $description; 41 | } 42 | 43 | /** 44 | * @return boolean 45 | */ 46 | public function getOk() 47 | { 48 | return $this->ok; 49 | } 50 | 51 | /** 52 | * @return mixed|stdClass 53 | */ 54 | 55 | public function getResult() 56 | { 57 | return $this->result; 58 | } 59 | 60 | /** 61 | * @return integer|null 62 | */ 63 | public function getErrorCode() 64 | { 65 | return $this->error_code; 66 | } 67 | 68 | /** 69 | * @return null|string 70 | */ 71 | public function getDescription() 72 | { 73 | return $this->description; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Daemon/Daemon.php: -------------------------------------------------------------------------------- 1 | api = $api; 40 | $this->offset = (int)$offset; 41 | $this->timeout = (int)$timeout; 42 | } 43 | 44 | public function run() 45 | { 46 | if (!is_callable($this->updateCallback)) { 47 | throw new InvalidArgumentException(sprintf('"%s" is not a callable function. Set it via "%s".', 'updateCallback', 'onUpdate')); 48 | } 49 | 50 | while (true) { 51 | foreach ($this->getUpdates() as $update) { 52 | $this->runCallback($update); 53 | $this->incrementOffset($update); 54 | } 55 | 56 | $this->handleSignals(); 57 | 58 | sleep($this->timeout); 59 | } 60 | } 61 | 62 | /** 63 | * @return Generator 64 | */ 65 | private function getUpdates() 66 | { 67 | $updates = $this->api->getUpdates(['offset' => $this->offset]); 68 | foreach ($updates as $update) { 69 | yield $update; 70 | } 71 | } 72 | 73 | /** 74 | * @param callable $callback 75 | * 76 | * @return $this 77 | */ 78 | public function onUpdate(callable $callback) 79 | { 80 | $this->updateCallback = $callback; 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param Update $update 86 | */ 87 | private function runCallback(Update $update) 88 | { 89 | call_user_func($this->updateCallback, $update); 90 | } 91 | 92 | /** 93 | * @param Update $update 94 | */ 95 | private function incrementOffset(Update $update) 96 | { 97 | if ($update->update_id >= $this->offset) { 98 | $this->offset = $update->update_id + 1; 99 | } 100 | } 101 | 102 | private function handleSignals() 103 | { 104 | declare(ticks = 1); 105 | 106 | pcntl_signal(SIGINT, function ($signal) { 107 | switch ($signal) { 108 | case SIGINT: 109 | exit(0); 110 | break; 111 | } 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Exception/NotOkException.php: -------------------------------------------------------------------------------- 1 | from = User::create($attributes['from']); 51 | } 52 | 53 | if (isset($attributes['message'])) { 54 | $this->message = Message::create($attributes['message']); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Type/Chat.php: -------------------------------------------------------------------------------- 1 | user = User::create($attributes['user']); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Type/Contact.php: -------------------------------------------------------------------------------- 1 | thumb = PhotoSize::create($attributes['thumb']); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Type/File.php: -------------------------------------------------------------------------------- 1 | / to get the file. 23 | * 24 | * @var string 25 | */ 26 | public $file_path; 27 | } 28 | -------------------------------------------------------------------------------- /Type/ForceReply.php: -------------------------------------------------------------------------------- 1 | from = User::create($attributes['from']); 55 | } 56 | 57 | if (isset($attributes['location'])) { 58 | $this->location = Location::create($attributes['location']); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Type/Inline/InlineQuery.php: -------------------------------------------------------------------------------- 1 | from = User::create($attributes['from']); 55 | } 56 | 57 | if (isset($attributes['location'])) { 58 | $this->location = Location::create($attributes['location']); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Type/InlineKeyboardButton.php: -------------------------------------------------------------------------------- 1 | from = User::create($attributes['from']); 240 | } 241 | 242 | if (isset($attributes['chat'])) { 243 | $this->chat = isset($attributes['chat']->title) ? GroupChat::create($attributes['chat']) : User::create($attributes['chat']); 244 | } 245 | 246 | if (isset($attributes['forward_from'])) { 247 | $this->forward_from = User::create($attributes['forward_from']); 248 | } 249 | 250 | if (isset($attributes['forward_from_chat'])) { 251 | $this->forward_from_chat = Chat::create($attributes['forward_from_chat']); 252 | } 253 | 254 | if (isset($attributes['reply_to_message'])) { 255 | $this->reply_to_message = Message::create($attributes['reply_to_message']); 256 | } 257 | 258 | if (isset($attributes['entities'])) { 259 | $this->entities = array_map(function ($entity) { 260 | return MessageEntity::create($entity); 261 | }, $attributes['entities']); 262 | } 263 | 264 | if (isset($attributes['audio'])) { 265 | $this->audio = Audio::create($attributes['audio']); 266 | } 267 | 268 | if (isset($attributes['document'])) { 269 | $this->document = Document::create($attributes['document']); 270 | } 271 | 272 | if (isset($attributes['photo'])) { 273 | $this->photo = array_map(function ($photo) { 274 | return PhotoSize::create($photo); 275 | }, $attributes['photo']); 276 | } 277 | 278 | if (isset($attributes['sticker'])) { 279 | $this->sticker = Sticker::create($attributes['sticker']); 280 | } 281 | 282 | if (isset($attributes['video'])) { 283 | $this->video = Video::create($attributes['video']); 284 | } 285 | 286 | if (isset($attributes['voice'])) { 287 | $this->voice = Voice::create($attributes['voice']); 288 | } 289 | 290 | if (isset($attributes['contact'])) { 291 | $this->contact = Contact::create($attributes['contact']); 292 | } 293 | 294 | if (isset($attributes['location'])) { 295 | $this->location = Location::create($attributes['location']); 296 | } 297 | 298 | if (isset($attributes['venue'])) { 299 | $this->venue = Venue::create($attributes['venue']); 300 | } 301 | 302 | if (isset($attributes['new_chat_member'])) { 303 | $this->new_chat_member = User::create($attributes['new_chat_member']); 304 | } 305 | 306 | if (isset($attributes['left_chat_member'])) { 307 | $this->left_chat_member = new User($attributes['left_chat_member']); 308 | } 309 | 310 | if (isset($attributes['new_chat_photo'])) { 311 | $this->new_chat_photo = array_map(function ($photo) { 312 | return PhotoSize::create($photo); 313 | }, $attributes['new_chat_photo']); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /Type/MessageEntity.php: -------------------------------------------------------------------------------- 1 | user = User::create($attributes['user']); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Type/PhotoSize.php: -------------------------------------------------------------------------------- 1 | thumb = PhotoSize::create($attributes['thumb']); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Type/Type.php: -------------------------------------------------------------------------------- 1 | load($attributes); 16 | $this->loadRelated($attributes); 17 | } 18 | 19 | /** 20 | * @param array $attributes 21 | */ 22 | private function load(array $attributes) 23 | { 24 | foreach ($attributes as $key => $value) { 25 | $this->$key = $value; 26 | } 27 | } 28 | 29 | /** 30 | * @param array $attributes 31 | */ 32 | protected function loadRelated(array $attributes) 33 | { 34 | } 35 | 36 | /** 37 | * @param Response $response 38 | * 39 | * @return static 40 | */ 41 | public static function createFromResponse(Response $response) 42 | { 43 | return static::create($response->getResult()); 44 | } 45 | 46 | /** 47 | * @param stdClass $object 48 | * 49 | * @return static 50 | */ 51 | public static function create(stdClass $object) 52 | { 53 | return new static((array)$object); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Type/Update.php: -------------------------------------------------------------------------------- 1 | message = Message::create($attributes['message']); 61 | } 62 | 63 | if (isset($attributes['edited_message'])) { 64 | $this->edited_message = Message::create($attributes['edited_message']); 65 | } 66 | 67 | if (isset($attributes['inline_query'])) { 68 | $this->inline_query = InlineQuery::create($attributes['inline_query']); 69 | } 70 | 71 | if (isset($attributes['chosen_inline_result'])) { 72 | $this->chosen_inline_result = ChosenInlineResult::create($attributes['chosen_inline_result']); 73 | } 74 | 75 | if (isset($attributes['callback_query'])) { 76 | $this->callback_query = CallbackQuery::create($attributes['callback_query']); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Type/User.php: -------------------------------------------------------------------------------- 1 | photos = []; 29 | 30 | if ($attributes['total_count'] > 0) { 31 | $this->photos = array_map(function ($photo) { 32 | return new PhotoSize($photo); 33 | }, $attributes['photos'][0]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Type/Venue.php: -------------------------------------------------------------------------------- 1 | location = Location::create($attributes['location']); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Type/Video.php: -------------------------------------------------------------------------------- 1 | thumb = PhotoSize::create($attributes['thumb']); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Type/Voice.php: -------------------------------------------------------------------------------- 1 | =5.5.0", 24 | "guzzlehttp/guzzle": "~6.0" 25 | }, 26 | "suggest": { 27 | "ext/pcntl": "Usage of signal handler" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Zelenin\\Telegram\\Bot\\": "" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot API Client 2 | 3 | [Telegram](https://telegram.org) [Bot](https://core.telegram.org/bots) [API](https://core.telegram.org/bots/api) Client. 4 | 5 | ## Installation 6 | 7 | ### Composer 8 | 9 | The preferred way to install this extension is through [Composer](http://getcomposer.org/). 10 | 11 | Either run 12 | 13 | ``` 14 | php composer.phar require "zelenin/telegram-bot-api" "~1.0" 15 | ``` 16 | 17 | or add 18 | 19 | ``` 20 | "zelenin/telegram-bot-api": "~1.0" 21 | ``` 22 | 23 | to the require section of your ```composer.json``` 24 | 25 | ## Usage 26 | 27 | ```php 28 | $api = ApiFactory::create($token); 29 | 30 | try { 31 | $response = $api->sendMessage([ 32 | 'chat_id' => $chatId, 33 | 'text' => 'Test message' 34 | ]); 35 | print_r($response); 36 | 37 | $response = $api->sendPhoto([ 38 | 'chat_id' => $myId, 39 | 'photo' => fopen('/home/www/photo.jpg', 'r') 40 | ]); 41 | print_r($response); 42 | } catch (\Zelenin\Telegram\Bot\Exception\NotOkException $e) { 43 | echo $e->getMessage(); 44 | } 45 | ``` 46 | 47 | See [Bot API documentation](https://core.telegram.org/bots/api) for other methods. 48 | 49 | ### Daemon 50 | 51 | ```php 52 | $api = ApiFactory::create($token); 53 | 54 | $daemon = new \Zelenin\Telegram\Bot\Daemon\NaiveDaemon($api); 55 | 56 | $daemon 57 | ->onUpdate(function (\Zelenin\Telegram\Bot\Type\Update $update) { 58 | print_r($update); 59 | }); 60 | 61 | $daemon->run(); 62 | ``` 63 | 64 | ## Author 65 | 66 | [Aleksandr Zelenin](https://github.com/zelenin/), e-mail: [aleksandr@zelenin.me](mailto:aleksandr@zelenin.me) 67 | --------------------------------------------------------------------------------