-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
TelegramBotHandler.php
297 lines (253 loc) · 9.22 KB
/
TelegramBotHandler.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
<?php declare(strict_types=1);
/*
* This file is part of the Monolog package.
*
* (c) Jordi Boggiano <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Monolog\Handler;
use RuntimeException;
use Monolog\Level;
use Monolog\Utils;
use Monolog\LogRecord;
/**
* Handler sends logs to Telegram using Telegram Bot API.
*
* How to use:
* 1) Create a Telegram bot with https://telegram.me/BotFather;
* 2) Create a Telegram channel or a group where logs will be recorded;
* 3) Add the created bot from step 1 to the created channel/group from step 2.
*
* In order to create an instance of TelegramBotHandler use
* 1. The Telegram bot API key from step 1
* 2. The channel name with the `@` prefix if you created a public channel (e.g. `@my_public_channel`),
* or the channel ID with the `-100` prefix if you created a private channel (e.g. `-1001234567890`),
* or the group ID from step 2 (e.g. `-1234567890`).
*
* @link https://core.telegram.org/bots/api
*
* @author Mazur Alexandr <[email protected]>
*/
class TelegramBotHandler extends AbstractProcessingHandler
{
private const BOT_API = 'https://api.telegram.org/bot';
/**
* The available values of parseMode according to the Telegram api documentation
*/
private const AVAILABLE_PARSE_MODES = [
'HTML',
'MarkdownV2',
'Markdown', // legacy mode without underline and strikethrough, use MarkdownV2 instead
];
/**
* The maximum number of characters allowed in a message according to the Telegram api documentation
*/
private const MAX_MESSAGE_LENGTH = 4096;
/**
* Telegram bot access token provided by BotFather.
* Create telegram bot with https://telegram.me/BotFather and use access token from it.
*/
private string $apiKey;
/**
* Telegram channel name.
* Since to start with '@' symbol as prefix.
*/
private string $channel;
/**
* The kind of formatting that is used for the message.
* See available options at https://core.telegram.org/bots/api#formatting-options
* or in AVAILABLE_PARSE_MODES
*/
private string|null $parseMode;
/**
* Disables link previews for links in the message.
*/
private bool|null $disableWebPagePreview;
/**
* Sends the message silently. Users will receive a notification with no sound.
*/
private bool|null $disableNotification;
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
*/
private bool $splitLongMessages;
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
*/
private bool $delayBetweenMessages;
/**
* Telegram message thread id, unique identifier for the target message thread (topic) of the forum; for forum supergroups only
* See how to get the `message_thread_id` https://stackoverflow.com/a/75178418
*/
private int|null $topic;
/**
* @param string $apiKey Telegram bot access token provided by BotFather
* @param string $channel Telegram channel name
* @param bool $splitLongMessages Split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages
* @param bool $delayBetweenMessages Adds delay between sending a split message according to Telegram API
* @param int $topic Telegram message thread id, unique identifier for the target message thread (topic) of the forum
* @throws MissingExtensionException If the curl extension is missing
*/
public function __construct(
string $apiKey,
string $channel,
$level = Level::Debug,
bool $bubble = true,
?string $parseMode = null,
?bool $disableWebPagePreview = null,
?bool $disableNotification = null,
bool $splitLongMessages = false,
bool $delayBetweenMessages = false,
?int $topic = null
) {
if (!\extension_loaded('curl')) {
throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler');
}
parent::__construct($level, $bubble);
$this->apiKey = $apiKey;
$this->channel = $channel;
$this->setParseMode($parseMode);
$this->disableWebPagePreview($disableWebPagePreview);
$this->disableNotification($disableNotification);
$this->splitLongMessages($splitLongMessages);
$this->delayBetweenMessages($delayBetweenMessages);
$this->setTopic($topic);
}
/**
* @return $this
*/
public function setParseMode(string|null $parseMode = null): self
{
if ($parseMode !== null && !\in_array($parseMode, self::AVAILABLE_PARSE_MODES, true)) {
throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.');
}
$this->parseMode = $parseMode;
return $this;
}
/**
* @return $this
*/
public function disableWebPagePreview(bool|null $disableWebPagePreview = null): self
{
$this->disableWebPagePreview = $disableWebPagePreview;
return $this;
}
/**
* @return $this
*/
public function disableNotification(bool|null $disableNotification = null): self
{
$this->disableNotification = $disableNotification;
return $this;
}
/**
* True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages.
* False - truncates a message that is too long.
*
* @return $this
*/
public function splitLongMessages(bool $splitLongMessages = false): self
{
$this->splitLongMessages = $splitLongMessages;
return $this;
}
/**
* Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests).
*
* @return $this
*/
public function delayBetweenMessages(bool $delayBetweenMessages = false): self
{
$this->delayBetweenMessages = $delayBetweenMessages;
return $this;
}
/**
* @return $this
*/
public function setTopic(?int $topic = null): self
{
$this->topic = $topic;
return $this;
}
/**
* @inheritDoc
*/
public function handleBatch(array $records): void
{
$messages = [];
foreach ($records as $record) {
if (!$this->isHandling($record)) {
continue;
}
if (\count($this->processors) > 0) {
$record = $this->processRecord($record);
}
$messages[] = $record;
}
if (\count($messages) > 0) {
$this->send((string) $this->getFormatter()->formatBatch($messages));
}
}
/**
* @inheritDoc
*/
protected function write(LogRecord $record): void
{
$this->send($record->formatted);
}
/**
* Send request to @link https://api.telegram.org/bot on SendMessage action.
*/
protected function send(string $message): void
{
$messages = $this->handleMessageLength($message);
foreach ($messages as $key => $msg) {
if ($this->delayBetweenMessages && $key > 0) {
sleep(1);
}
$this->sendCurl($msg);
}
}
protected function sendCurl(string $message): void
{
$ch = curl_init();
$url = self::BOT_API . $this->apiKey . '/SendMessage';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$params = [
'text' => $message,
'chat_id' => $this->channel,
'parse_mode' => $this->parseMode,
'disable_web_page_preview' => $this->disableWebPagePreview,
'disable_notification' => $this->disableNotification,
];
if ($this->topic !== null) {
$params['message_thread_id'] = $this->topic;
}
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
$result = Curl\Util::execute($ch);
if (!\is_string($result)) {
throw new RuntimeException('Telegram API error. Description: No response');
}
$result = json_decode($result, true);
if ($result['ok'] === false) {
throw new RuntimeException('Telegram API error. Description: ' . $result['description']);
}
}
/**
* Handle a message that is too long: truncates or splits into several
* @return string[]
*/
private function handleMessageLength(string $message): array
{
$truncatedMarker = ' (...truncated)';
if (!$this->splitLongMessages && \strlen($message) > self::MAX_MESSAGE_LENGTH) {
return [Utils::substr($message, 0, self::MAX_MESSAGE_LENGTH - \strlen($truncatedMarker)) . $truncatedMarker];
}
return str_split($message, self::MAX_MESSAGE_LENGTH);
}
}