-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathPHPBot.php
485 lines (443 loc) · 16 KB
/
PHPBot.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
<?php
namespace kyle2142;
use function count;
use function in_array;
use InvalidArgumentException, LogicException, Exception, RuntimeException,
CURLFile, stdClass;
use function is_resource;
use function is_string;
use function property_exists;
/**
* Class PHPBot
*
* @package kyle2142
*/
class PHPBot
{
private $webhook_reply_used = false, $token;
public $api, $parse_mode = 'markdown';
/**
* PHPBot constructor.
*
* @param string $token The botAPI token provided by t.me/Botfather
*/
public function __construct(string $token) {
$this->token = $token;
$this->api = new api($token);
}
/**
* Executes a botAPI method as response to the webhook
* which is faster than using action() but can only be used once per webhook call, and cannot retrieve result
*
* @param string $method The Telegram botAPI method to call
* @param array $params Array of parameters needed for the method
* @throws LogicException Thrown when called more than once per instantiation
*/
public function quickAction($method, array $params = []) {
if ($this->webhook_reply_used) {
throw new LogicException('This function may only be called once per webhook call');
}
header_remove();
header('Status: 200 OK', true, 200);
header('Content-Type: application/json');
$params['method'] = $method;
echo json_encode($params);
$this->webhook_reply_used = true;
}
/** Asks TG for updates (infinite loop) and uses the provided callable
*
* @param callable $handler the handler. Pass a string to call the function with that name. Must accept a parameter for the update array
* @param int $offset
* @param array $allowed_updates optional, types of updates to fetch
* @param int $timeout optional, long polling timeout
*/
public function handle_updates(callable $handler, int $offset = 0, array $allowed_updates = [], int $timeout = 20) {
$options = ['offset' => $offset, 'timeout' => $timeout];
if ($allowed_updates !== []) {
$options['allowed_updates'] = $allowed_updates;
}
while (true) {
$updates = $this->api->getUpdates($options);
foreach ($updates as $update) {
try {
$handler($update);
$options['offset'] = $update->update_id + 1;
} catch (Exception $exception) {
}
}
}
}
/**
* Downloads file from TG, returning the bytes or saving to $destination
*
* @param string $file_id The file_id given by TG
* @param string|resource $destination Can be a path (string) or a stream resource. When unspecified, the function will return the data
* @return bool|string If $destination was set, returns whether the downloaded size matched the file_size given by TG, else returns the data
*/
public function downloadFile(string $file_id, $destination = null) {
$File = $this->api->getFile(['file_id' => $file_id]);
$file_path = "https://api.telegram.org/file/bot{$this->token}/{$File->file_path}";
if (is_string($destination)) {
return file_put_contents($destination, $file_path) === $File->file_size;
}
$data = file_get_contents($file_path);
if (is_resource($destination)) {
return fwrite($destination, $data) === $File->file_size;
}
return $data;
}
/**
* Send $text to $chat_id with optional extras such as reply_markup, see https://core.telegram.org/bots/api#sendmessage
*
* @param $chat_id
* @param string $text
* @param array $extras Markdown is enabled by default
* @return stdClass
*/
public function sendMessage($chat_id, string $text, array $extras = []): stdClass {
return $this->api->sendMessage(array_merge(['chat_id' => $chat_id, 'text' => $text], array_merge(['parse_mode' => $this->parse_mode], $extras)));
}
/**
* Edits $msg_id from $chat_id to become $text, with optional $extras
*
* @param $chat_id
* @param int $msg_id
* @param string $text
* @param array $extras see https://core.telegram.org/bots/api#editmessagetext
* @return stdClass|bool
*/
public function editMessageText($chat_id, int $msg_id, string $text, array $extras = []) {
return $this->api->editMessageText(array_merge(['chat_id' => $chat_id, 'message_id' => $msg_id, 'text' => $text], array_merge(['parse_mode' => $this->parse_mode], $extras)));
}
/**
* Edits only reply_markup of $msg_id from $chat_id
*
* @param $chat_id
* @param int $msg_id
* @param array $reply_markup The new reply_markup
* @return mixed
*/
public function editMarkup($chat_id, int $msg_id, array $reply_markup = []) {
return $this->api->editMessageReplyMarkup(['chat_id' => $chat_id, 'message_id' => $msg_id, 'reply_markup' => $reply_markup]);
}
/**
* Deletes $msg_id from $chat_id
*
* @param $chat_id
* @param int $msg_id
* @return bool
*/
public function deleteMessage($chat_id, int $msg_id): bool {
return $this->api->deleteMessage(['chat_id' => $chat_id, 'message_id' => $msg_id]);
}
/**
* Template function to edit/give $perms to $user_id in $chat_id
*
* @param int $user_id
* @param $chat_id
* @param array $perms
* @return bool
*/
public function editAdmin(int $user_id, $chat_id, array $perms = []): bool {
return $this->api->promotechatmember(
array_merge(
[
'user_id' => $user_id,
'chat_id' => $chat_id
],
$perms
)
);
}
/**
* Promotes user to full admin by default
*
* @param int $user_id
* @param $chat_id
* @param array $perms
* @return bool
*/
public function promoteUser(int $user_id, $chat_id, array $perms = []): bool {
if ($perms === []) {
$perms = [
'can_change_info' => 1,
'can_delete_messages' => 1,
'can_invite_users' => 1,
'can_restrict_members' => 1,
'can_pin_messages' => 1,
'can_promote_members' => 1
];
}
return $this->editAdmin($user_id, $chat_id, $perms);
}
/**
* Restricts user (forever by default) to be only able to read messages
*
* @param int $user_id
* @param $chat_id
* @param int $until
* @return bool
*/
public function muteUser(int $user_id, $chat_id, int $until = 0): bool {
return $this->api->restrictChatMember([
'chat_id' => $chat_id,
'user_id' => $user_id,
'can_send_messages' => false,
'until_date' => $until
]);
}
/**
* Gives {delete/pin messages, invite users} permissions to $user_id in $chat_id
*
* @param int $user_id
* @param $chat_id
* @return bool
*/
public function makeModerator(int $user_id, $chat_id): bool {
return $this->editAdmin($user_id, $chat_id,
[
'can_delete_messages' => 1,
'can_invite_users' => 1,
'can_pin_messages' => 1
]
);
}
/**
* Removes all admin permissions of $user_id in $chat_id
*
* @param int $user_id
* @param $chat_id
* @return bool
*/
public function demote(int $user_id, $chat_id): bool {
return $this->editAdmin($user_id, $chat_id); //no args means no perms
}
//begin totally custom methods
/**
* Checks what privileges the bot has inside $chat_id
*
* @param $chat_id
* @return stdClass
*/
public function getPermissions($chat_id): stdClass {
try {
$api_reply = $this->api->getChatMember(['chat_id' => $chat_id, 'user_id' => $this->getBotID()]);
} catch (TelegramException $e) {
$api_reply = new stdClass();
$api_reply->error_code = $e->getCode();
}
if (property_exists($api_reply, 'error_code')) {
switch ($api_reply->error_code) {
case 403: //forbidden
$api_reply->status = 'banned';
break;
case 400: //chat not found
$api_reply->status = 'invalid';
break;
}
}
if ($api_reply->status !== 'administrator') {
$admin_perms = [
'can_change_info',
'can_delete_messages',
'can_invite_users',
'can_restrict_members',
'can_pin_messages',
'can_promote_members'
];
foreach ($admin_perms as $perm) {
$api_reply->$perm = false;
}
}
if ($api_reply->status !== 'restricted') {
$restricted_perms = [
'can_send_messages',
'can_send_media_messages',
'can_send_other_messages',
'can_add_web_page_previews'
];
$in_group = !in_array($api_reply->status, ['left', 'banned', 'invalid']); //false if the bot isn't in the chat
foreach ($restricted_perms as $perm) {
$api_reply->$perm = $in_group;
}
}
return $api_reply;
}
/**
* Edits info of group, using any info given
*
* @param $chat_id
* @param array $info At least one of {title, description, photo path} must be in this array
* @return array
*/
public function editInfo($chat_id, array $info): array {
if (count($info) < 1) {
return [null];
}
$results = [];
if (isset($info['title'])) {
$results[] = $this->api->setChatTitle(['chat_id' => $chat_id, 'title' => $info['title']]);
}
if (isset($info['description'])) {
$results[] = $this->api->setChatDescription(['chat_id' => $chat_id, 'description' => $info['description']]);
}
if (isset($info['photo']) && file_exists($info['photo'])) {
$data = [
'chat_id' => $chat_id,
'photo' => new CURLFile(realpath($info['photo']))
];
$results[] = $this->api->setChatPhoto($data);
}
return $results;
}
/**
* Call $method with $params, 100ms timeout and not care about response/errors
* @param string $method
* @param array $params
*/
public function fireAndForget(string $method, array $params = []) {
$this->api->fireAndForget($method, $params);
}
/**
* Takes a list of entities and restores markdown in $msg using botAPI format.
*
* Example usage: get message, pass to this function and get back original message before sending,
* so it can be resent.
*
* @param string $msg
* @param array $entities
* @return string
*/
public static function createMarkdownFromEntities(string $msg, array $entities): string {
$dict = ['bold' => '*', 'italic' => '_', 'code' => '`', 'pre' => '```'];
foreach (array_reverse($entities) as $e) { //edit in reverse order to preserve offsets
switch ($e->type) {
case 'bold':
case 'italic':
case 'code':
case 'pre':
$msg = substr_replace($msg, $dict[$e->type], $e->offset + $e->length, 0);
$msg = substr_replace($msg, $dict[$e->type], $e->offset, 0); //0 means "insert"
break;
case 'text_mention':
$e->url = 'tg://user?id=' . $e->user->id;
case 'text_link':
$original = substr($msg, $e->offset, $e->length);
$msg = substr_replace($msg, "[$original]({$e->url})", $e->offset, $e->length);
break;
}
}
return $msg;
}
/**
* @return int
*/
public function getBotID(): int {
return $this->api->getBotID();
}
}
/**
* Handles contacting Telegram on PHPBot's behalf
* Class api
*
* @package kyle2142
*/
class api
{
const TIMEOUT = 62;
private $BOTID, $curl, $token;
public $api_host;
public function __construct(string $token, string $api_host = "https://api.telegram.org") {
if (preg_match('/^(\d+):[\w-]{30,}$/', $token, $matches) === 0) {
throw new InvalidArgumentException('The supplied token does not look correct...');
}
$this->BOTID = (int)$matches[0];
$this->token = $token;
$this->api_host = $api_host;
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_HTTPHEADER, ['Content-Type:multipart/form-data']);
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_TIMEOUT, $this::TIMEOUT); // botAPI might take 60s before returning error
}
public function __destruct() {
curl_close($this->curl);
}
/**
* Template function to make API calls using method name and array of parameters
*
* @param string $method The method name from https://core.telegram.org/bots/api
* @param array $params The arguments of the method, as an array
* @return stdClass|bool
* @throws TelegramException, RuntimeException
*/
public function __call(string $method, array $params) {
curl_setopt($this->curl, CURLOPT_URL, "{$this->api_host}/bot{$this->token}/$method");
curl_setopt($this->curl, CURLOPT_POSTFIELDS, $params[0] ?? []);
$result = curl_exec($this->curl);
if (curl_errno($this->curl)) {
throw new RuntimeException(curl_error($this->curl), curl_errno($this->curl));
}
$object = json_decode($result);
if (!$object->ok) {
if (property_exists($object, 'parameters')) {
if (property_exists($object->parameters, 'retry_after')) {
throw new TelegramFloodWait($object);
}
if (property_exists($object->parameters, 'migrate_to_chat_id')) {
throw new TelegramChatMigrated($object);
}
}
throw new TelegramException($object);
}
return $object->result;
}
/**
* @return int
*/
public function getBotID(): int {
return $this->BOTID;
}
public function fireAndForget(string $method, array $params = []) {
curl_setopt($this->curl, CURLOPT_TIMEOUT_MS, 100);
curl_setopt($this->curl, CURLOPT_URL, "{$this->api_host}/bot{$this->token}/$method");
curl_setopt($this->curl, CURLOPT_POSTFIELDS, $params);
curl_exec($this->curl);
curl_setopt($this->curl, CURLOPT_TIMEOUT, $this::TIMEOUT);
}
}
class TelegramException extends Exception
{
protected $result;
public function __construct(stdClass $result) {
$this->result = $result;
parent::__construct($result->description, $result->error_code);
}
public function __toString(): string {
return get_class($this) . ": {$this->code} ({$this->message})\nTrace:\n{$this->getTraceAsString()}";
}
public function getResult(): stdClass {
return $this->result;
}
}
class TelegramFloodWait extends TelegramException
{
protected $retry_after;
public function __construct(stdClass $result) {
$this->retry_after = $result->parameters->retry_after;
parent::__construct($result);
}
public function getRetryAfter(): int {
return $this->retry_after;
}
}
class TelegramChatMigrated extends TelegramException
{
protected $migrate_to_chat_id;
public function __construct(stdClass $result) {
$this->migrate_to_chat_id = $result->parameters->migrate_to_chat_id;
parent::__construct($result);
}
public function getMigrateToChatId(): int {
return $this->migrate_to_chat_id;
}
}