max_bot_api 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3513bab6b7e83202a50274c5eba30655b5825bc56a5a6e26de78eae549e0e38f
4
- data.tar.gz: 0f97c303649e2c8db1d54f64af70fffc99a030ab95866d089094cff533a4e98c
3
+ metadata.gz: 97114d1ff6c7d35facca9459c0543540867c2d48c6235dd6bb204e47c31a75b3
4
+ data.tar.gz: e7dec618f4d73dae79c33e692478c9dc5c8b06fcc0d3f14949c1178083d15aa3
5
5
  SHA512:
6
- metadata.gz: 0d9ee73288cf706f2ba868e82bab3456bc6f7857d8b1a46cc8cf6fa273d2d0d4e800c93f62bce64f6bb822231385c4166d160fb6c13300649a1dcd18cf80a217
7
- data.tar.gz: e4f90c9ccbc8ddaf116922c1dc3f6b7c83e4d261e20237e83b3d593703fd709898aa2ae201bfced427279c3b3378c318635e0fcbdf2c0b9721448f25a51962ed
6
+ metadata.gz: 8fec44603bf0f56f43e865097608bc786a3549ffa6a754d87803eeb0ada8f6851a074d44469c1e9f9e8629d342af908d252d30e0c576a8d701649af7ffdb2022
7
+ data.tar.gz: 7ac3fe1abc5125e7e63d610506ddf100c77b47c40c5bfe5c50ead48eea04b14cd79fc8f2a86e8073ee82eaa8f85212fcf30b2adea1a8fdbdfd56a9ff9694a99c
data/README.md CHANGED
@@ -33,10 +33,10 @@ client.messages.send(
33
33
 
34
34
  ## Features
35
35
 
36
- - Full API coverage: Bots, Chats, Messages, Subscriptions, Uploads, Debugs.
36
+ - Broad API coverage: Bots, Chats, Messages, Subscriptions, Uploads, Debugs.
37
37
  - Builder helpers for messages and keyboards.
38
38
  - Long polling updates with retry and backoff.
39
- - Webhook parsing helper.
39
+ - Webhook parsing and secret validation helpers.
40
40
  - Upload helpers for files, photos, audio, video.
41
41
 
42
42
  ## Configuration
@@ -44,7 +44,7 @@ client.messages.send(
44
44
  ```ruby
45
45
  client = MaxBotApi::Client.new(
46
46
  token: ENV.fetch("TOKEN"),
47
- base_url: "https://botapi.max.ru/",
47
+ base_url: "https://platform-api.max.ru/",
48
48
  version: "1.2.5"
49
49
  )
50
50
  ```
@@ -54,6 +54,27 @@ Notes:
54
54
  - The client uses `Authorization: <token>` (no `Bearer`).
55
55
  - Every request includes `v=<version>` query param.
56
56
 
57
+ ## Changelog
58
+
59
+ - See `CHANGELOG.md` for version-by-version release notes.
60
+ - See `BREAKING_CHANGES_0.1.0_to_0.2.0.md` for the detailed `0.2.0` migration notes.
61
+
62
+ Example:
63
+
64
+ ```ruby
65
+ message = MaxBotApi::Builders::MessageBuilder.new
66
+ .set_chat(12345)
67
+ .set_text("Hello")
68
+ .add_photo_by_token("photo-token")
69
+
70
+ keyboard = MaxBotApi::Builders::KeyboardBuilder.new
71
+ row = keyboard.add_row
72
+ row.add_message("Continue")
73
+
74
+ message.add_keyboard(keyboard)
75
+ client.messages.send(message)
76
+ ```
77
+
57
78
  ## Common tasks
58
79
 
59
80
  ### Send messages
@@ -103,9 +124,29 @@ end
103
124
 
104
125
  ```ruby
105
126
  body = request.body.read
127
+ halt 401 unless client.webhook_secret_valid?(headers: request.env, secret: ENV.fetch("WEBHOOK_SECRET"))
106
128
  update = client.parse_webhook(body)
107
129
  ```
108
130
 
131
+ ### Disable link previews
132
+
133
+ ```ruby
134
+ message = MaxBotApi::Builders::MessageBuilder.new
135
+ .set_chat(12345)
136
+ .set_text("https://max.ru")
137
+ .set_disable_link_preview(true)
138
+
139
+ client.messages.send(message)
140
+ ```
141
+
142
+ ### Pin and unpin chat messages
143
+
144
+ ```ruby
145
+ client.chats.pin_message(chat_id: 12345, message_id: "mid123")
146
+ client.chats.get_pinned_message(chat_id: 12345)
147
+ client.chats.unpin_message(chat_id: 12345)
148
+ ```
149
+
109
150
  ## Builders
110
151
 
111
152
  ### Keyboard
@@ -38,7 +38,9 @@ client.messages.send(message)
38
38
  ```ruby
39
39
  client = MaxBotApi::Client.new(
40
40
  token: ENV.fetch("TOKEN"),
41
- base_url: "https://botapi.max.ru/",
41
+ base_url: "https://platform-api.max.ru/",
42
42
  version: "1.2.5"
43
43
  )
44
44
  ```
45
+
46
+ Note: if you still use the legacy host, set `base_url: "https://botapi.max.ru/"`.
@@ -35,6 +35,7 @@ end
35
35
 
36
36
  ```ruby
37
37
  post "/webhook" do
38
+ halt 401 unless client.webhook_secret_valid?(headers: request.env, secret: ENV.fetch("WEBHOOK_SECRET"))
38
39
  update = client.parse_webhook(request.body.read)
39
40
 
40
41
  case update[:update_type]
@@ -47,3 +47,13 @@ message = MaxBotApi::Builders::MessageBuilder.new
47
47
 
48
48
  client.messages.send(message)
49
49
  ```
50
+
51
+ You can also attach a photo directly by token:
52
+
53
+ ```ruby
54
+ message = MaxBotApi::Builders::MessageBuilder.new
55
+ .set_chat(12345)
56
+ .add_photo_by_token("photo-token")
57
+
58
+ client.messages.send(message)
59
+ ```
data/docs/04-keyboard.md CHANGED
@@ -18,6 +18,11 @@ keyboard
18
18
  keyboard
19
19
  .add_row
20
20
  .add_callback("Picture", "positive", "picture")
21
+ .add_message("Continue")
22
+
23
+ keyboard
24
+ .add_row
25
+ .add_clipboard("Copy docs", "https://dev.max.ru/docs-api/methods/POST/messages")
21
26
 
22
27
  message = MaxBotApi::Builders::MessageBuilder.new
23
28
  .set_chat(12345)
@@ -34,3 +39,5 @@ client.messages.send(message)
34
39
  - `add_contact(text)`
35
40
  - `add_geolocation(text, quick)`
36
41
  - `add_open_app(text, app, payload, contact_id)`
42
+ - `add_message(text)`
43
+ - `add_clipboard(text, payload)`
@@ -14,6 +14,8 @@ client.subscriptions.subscribe(
14
14
  )
15
15
  ```
16
16
 
17
+ When handling webhook requests, validate the `X-Max-Bot-Api-Secret` header before parsing the body.
18
+
17
19
  ## Unsubscribe
18
20
 
19
21
  ```ruby
@@ -0,0 +1,59 @@
1
+ # Методы API
2
+
3
+ Источник: https://dev.max.ru/docs-api
4
+
5
+ Клиент: MaxBotApi Ruby (`lib/max_bot_api/`).
6
+
7
+ ## bots
8
+
9
+ | Метод | Путь | Описание | Клиент |
10
+ | --- | --- | --- | --- |
11
+ | `GET` | `/me` | Получение информации о боте | да |
12
+
13
+ ## chats
14
+
15
+ | Метод | Путь | Описание | Клиент |
16
+ | --- | --- | --- | --- |
17
+ | `GET` | `/chats` | Получение списка всех групповых чатов | да |
18
+ | `DELETE` | `/chats/{chatId}` | Удаление группового чата | нет |
19
+ | `GET` | `/chats/{chatId}` | Получение информации о групповом чате | да |
20
+ | `PATCH` | `/chats/{chatId}` | Изменение информации о групповом чате | да |
21
+ | `POST` | `/chats/{chatId}/actions` | Отправка действия бота в групповой чат | да |
22
+ | `DELETE` | `/chats/{chatId}/members` | Удаление участника из группового чата | частично (нет block) |
23
+ | `GET` | `/chats/{chatId}/members` | Получение участников группового чата | да |
24
+ | `POST` | `/chats/{chatId}/members` | Добавление участников в групповой чат | да |
25
+ | `GET` | `/chats/{chatId}/members/admins` | Получение списка администраторов группового чата | да |
26
+ | `POST` | `/chats/{chatId}/members/admins` | Назначить администратора группового чата | нет |
27
+ | `DELETE` | `/chats/{chatId}/members/admins/{userId}` | Отменить права администратора в групповом чате | нет |
28
+ | `DELETE` | `/chats/{chatId}/members/me` | Удаление бота из группового чата | да |
29
+ | `GET` | `/chats/{chatId}/members/me` | Получение информации о членстве бота в групповом чате | да |
30
+ | `DELETE` | `/chats/{chatId}/pin` | Удаление закреплённого сообщения в групповом чате | да |
31
+ | `GET` | `/chats/{chatId}/pin` | Получение закреплённого сообщения в групповом чате | да |
32
+ | `PUT` | `/chats/{chatId}/pin` | Закрепление сообщения в групповом чате | да |
33
+
34
+ ## subscriptions
35
+
36
+ | Метод | Путь | Описание | Клиент |
37
+ | --- | --- | --- | --- |
38
+ | `DELETE` | `/subscriptions` | Отписка от обновлений | да |
39
+ | `GET` | `/subscriptions` | Получение подписок | да |
40
+ | `POST` | `/subscriptions` | Подписка на обновления | да (есть version) |
41
+ | `GET` | `/updates` | Получение обновлений | да |
42
+
43
+ ## upload
44
+
45
+ | Метод | Путь | Описание | Клиент |
46
+ | --- | --- | --- | --- |
47
+ | `POST` | `/uploads` | Загрузка файлов | да |
48
+
49
+ ## messages
50
+
51
+ | Метод | Путь | Описание | Клиент |
52
+ | --- | --- | --- | --- |
53
+ | `POST` | `/answers` | Ответ на callback | да |
54
+ | `DELETE` | `/messages` | Удалить сообщение | да |
55
+ | `GET` | `/messages` | Получение сообщений | да |
56
+ | `POST` | `/messages` | Отправить сообщение | да |
57
+ | `PUT` | `/messages` | � едактировать сообщение | да |
58
+ | `GET` | `/messages/{messageId}` | Получить сообщение | да |
59
+ | `GET` | `/videos/{videoToken}` | Получить информацио о видео | нет |
@@ -0,0 +1,134 @@
1
+ # Объекты API
2
+
3
+ Источник: https://dev.max.ru/docs-api
4
+
5
+ Всего объектов: 126
6
+
7
+ | Объект | Краткое описание |
8
+ | --- | --- |
9
+ | `ActionRequestBody` | |
10
+ | `Attachment` | Общая схема, представляющая вложение сообщения |
11
+ | `AttachmentPayload` | |
12
+ | `AttachmentRequest` | Запрос на прикрепление данных к сообщению |
13
+ | `AudioAttachment` | |
14
+ | `AudioAttachmentRequest` | Запрос на прикрепление аудио к сообщению. ДОЛЖЕН быть единственным вложением в сообщении |
15
+ | `BotAddedToChatUpdate` | Вы получите этот update, как только бот будет добавлен в чат |
16
+ | `BotCommand` | до 32 элементов<br/>Команды, поддерживаемые ботом |
17
+ | `BotInfo` | Объект включает общую информацию о боте, URL аватара и описание. Дополнительно содержит список команд, поддерживаемых… |
18
+ | `BotPatch` | |
19
+ | `BotRemovedFromChatUpdate` | Вы получите этот update, как только бот будет удалён из чата |
20
+ | `BotStartedUpdate` | Бот получает этот тип обновления, как только пользователь нажал кнопку `Start` |
21
+ | `BotStoppedUpdate` | Бот получает этот тип обновления, как только пользователь останавливает бота |
22
+ | `Button` | |
23
+ | `Callback` | Объект, отправленный боту, когда пользователь нажимает кнопку |
24
+ | `CallbackAnswer` | Отправьте этот объект, когда ваш бот хочет отреагировать на нажатие кнопки |
25
+ | `CallbackButton` | После нажатия на такую кнопку клиент отправляет на сервер полезную нагрузку, которая содержит |
26
+ | `Chat` | |
27
+ | `ChatAdmin` | |
28
+ | `ChatAdminPermission` | Права администратора чата |
29
+ | `ChatAdminsList` | |
30
+ | `ChatButton` | Кнопка, которая создает новый чат, как только первый пользователь на нее нажмёт. Бот будет добавлен в участники чата… |
31
+ | `ChatList` | |
32
+ | `ChatMember` | Объект включает общую информацию о пользователе или боте, URL аватара и описание (при наличии). Дополнительно содержит… |
33
+ | `ChatMembersList` | |
34
+ | `ChatPatch` | |
35
+ | `ChatStatus` | Статус чата для текущего бота |
36
+ | `ChatTitleChangedUpdate` | Бот получит это обновление, когда будет изменено название чата |
37
+ | `ChatType` | Тип чата: диалог, чат |
38
+ | `ContactAttachment` | |
39
+ | `ContactAttachmentPayload` | |
40
+ | `ContactAttachmentRequest` | Запрос на прикрепление карточки контакта к сообщению. ДОЛЖЕН быть единственным вложением в сообщении |
41
+ | `ContactAttachmentRequestPayload` | |
42
+ | `DataAttachment` | Attachment contains payload sent through `SendMessageButton` |
43
+ | `DialogClearedUpdate` | Бот получает этот тип обновления сразу после очистки истории диалога. |
44
+ | `DialogMutedUpdate` | Вы получите этот update, когда пользователь заглушит диалог с ботом |
45
+ | `DialogRemovedUpdate` | Вы получите этот update, когда пользователь удаляет чат |
46
+ | `DialogUnmutedUpdate` | Вы получите этот update, когда пользователь включит уведомления в диалоге с ботом |
47
+ | `EmphasizedMarkup` | Представляет *курсив* |
48
+ | `Error` | Сервер возвращает это, если возникло исключение при вашем запросе |
49
+ | `FailedUserDetails` | Подробное описание, почему пользователь не был добавлен в чат |
50
+ | `FileAttachment` | |
51
+ | `FileAttachmentPayload` | |
52
+ | `FileAttachmentRequest` | Запрос на прикрепление файла к сообщению. ДОЛЖЕН быть единственным вложением в сообщении |
53
+ | `GetPinnedMessageResult` | |
54
+ | `GetSubscriptionsResult` | Список всех WebHook подписок |
55
+ | `HeadingMarkup` | Представляет заголовок текста |
56
+ | `HighlightedMarkup` | Представляет выделенную часть текста |
57
+ | `Image` | Общая схема, описывающая объект изображения |
58
+ | `InlineKeyboardAttachment` | Кнопки в сообщении |
59
+ | `InlineKeyboardAttachmentRequest` | Запрос на прикрепление клавиатуры к сообщению |
60
+ | `InlineKeyboardAttachmentRequestPayload` | |
61
+ | `Intent` | Намерение кнопки |
62
+ | `Keyboard` | Клавиатура - это двумерный массив кнопок |
63
+ | `LinkButton` | После нажатия на такую кнопку пользователь переходит по ссылке, которую она содержит |
64
+ | `LinkMarkup` | Представляет ссылку в тексте |
65
+ | `LinkedMessage` | |
66
+ | `LocationAttachment` | |
67
+ | `LocationAttachmentRequest` | Запрос на прикрепление клавиатуры к сообщению |
68
+ | `MarkupElement` | |
69
+ | `MediaAttachmentPayload` | |
70
+ | `Message` | Сообщение в чате |
71
+ | `MessageBody` | Схема, представляющая тело сообщения |
72
+ | `MessageButton` | Кнопка для запуска мини-приложения |
73
+ | `MessageCallbackUpdate` | Вы получите этот `update` как только пользователь нажмёт кнопку |
74
+ | `MessageChatCreatedUpdate` | Бот получит это обновление, когда чат будет создан, как только первый пользователь нажмёт кнопку чата |
75
+ | `MessageCreatedUpdate` | ы получите этот `update`, как только сообщение будет создано |
76
+ | `MessageEditedUpdate` | Вы получите этот `update`, как только сообщение будет отредактировано |
77
+ | `MessageLinkType` | Тип связанного сообщения |
78
+ | `MessageList` | Пагинированный список сообщений |
79
+ | `MessageRemovedUpdate` | Вы получите этот `update`, как только сообщение будет удалено |
80
+ | `MessageStat` | Статистика сообщения |
81
+ | `ModifyMembersResult` | � езультат запроса на изменение списка участников |
82
+ | `MonospacedMarkup` | Представляет `моноширинный` или блок ```код``` в тексте |
83
+ | `NewMessageBody` | |
84
+ | `NewMessageLink` | |
85
+ | `OpenAppButton` | Кнопка для запуска мини-приложения |
86
+ | `PhotoAttachment` | Вложение изображения |
87
+ | `PhotoAttachmentPayload` | |
88
+ | `PhotoAttachmentRequest` | |
89
+ | `PhotoAttachmentRequestPayload` | Запрос на прикрепление изображения (все поля являются взаимоисключающими) |
90
+ | `PhotoToken` | |
91
+ | `PhotoTokens` | Это информация, которую вы получите, как только изображение будет загружено |
92
+ | `PinMessageBody` | |
93
+ | `Recipient` | Новый получатель сообщения. Может быть пользователем или чатом |
94
+ | `ReplyButton` | After pressing this type of button client will send a message on behalf of user with given payload |
95
+ | `ReplyKeyboardAttachment` | Custom reply keyboard in message |
96
+ | `ReplyKeyboardAttachmentRequest` | Request to attach reply keyboard to message |
97
+ | `RequestContactButton` | AПосле нажатия на такую кнопку клиент отправляет новое сообщение с вложением текущего контакта пользователя |
98
+ | `RequestGeoLocationButton` | После нажатия на такую кнопку клиент отправляет новое сообщение с вложением текущего географического положения… |
99
+ | `SendContactButton` | AПосле нажатия на такую кнопку клиент отправляет новое сообщение с вложением текущего контакта пользователя |
100
+ | `SendGeoLocationButton` | После нажатия на такую кнопку клиент отправляет новое сообщение с вложением текущего географического положения… |
101
+ | `SendMessageButton` | After pressing this type of button client will send a message on behalf of user with given payload |
102
+ | `SendMessageResult` | |
103
+ | `SenderAction` | Действие, отправляемое участникам чата. Возможные значения: - `"typing_on"` — Бот набирает сообщение. -… |
104
+ | `ShareAttachment` | |
105
+ | `ShareAttachmentPayload` | Полезная нагрузка запроса ShareAttachmentRequest |
106
+ | `ShareAttachmentRequest` | Запрос на прикрепление предпросмотра медиафайла по внешнему URL |
107
+ | `SimpleQueryResult` | Простой ответ на запрос |
108
+ | `StickerAttachment` | |
109
+ | `StickerAttachmentPayload` | |
110
+ | `StickerAttachmentRequest` | Запрос на прикрепление стикера. ДОЛЖЕН быть единственным вложением в сообщении |
111
+ | `StickerAttachmentRequestPayload` | |
112
+ | `StrikethroughMarkup` | Представляет ~зачекрнутый~ текст |
113
+ | `StrongMarkup` | Представляет **жирный** текст |
114
+ | `Subscription` | Схема для описания подписки на WebHook |
115
+ | `SubscriptionRequestBody` | Запрос на настройку подписки WebHook |
116
+ | `TextFormat` | Формат текста сообщения |
117
+ | `UnderlineMarkup` | Представляет <ins>подчеркнутый</ins> текст |
118
+ | `Update` | Объект`Update` представляет различные типы событий, произошедших в чате. См. его наследников > Чтобы получать события… |
119
+ | `UpdateList` | Список всех обновлений в чатах, в которых ваш бот участвовал |
120
+ | `UploadEndpoint` | Точка доступа, куда следует загружать ваши бинарные файлы |
121
+ | `UploadType` | Тип загружаемого файла Поддерживаемые форматы: - `image`: JPG, JPEG, PNG, GIF, TIFF, BMP, HEIC - `video`: MP4, MOV,… |
122
+ | `UploadedInfo` | Это информация, которую вы получите, как только аудио/видео будет загружено |
123
+ | `User` | Объект, описывающий один из вариантов наследования: - [`User`](/docs-api/objects/User) — объект содержит общую… |
124
+ | `UserAddedToChatUpdate` | Вы получите это обновление, когда пользователь будет добавлен в чат, где бот является администратором |
125
+ | `UserIdsList` | |
126
+ | `UserMentionMarkup` | Представляет упоминание пользователя в тексте. Упоминание может быть как по имени пользователя, так и по ID, если у… |
127
+ | `UserRemovedFromChatUpdate` | Вы получите это обновление, когда пользователь будет удалён из чата, где бот является администратором |
128
+ | `UserWithPhoto` | Объект с общей информацией о пользователе или боте, дополнительно содержит URL аватара и описание |
129
+ | `VideoAttachment` | |
130
+ | `VideoAttachmentDetails` | |
131
+ | `VideoAttachmentRequest` | Запрос на прикрепление видео к сообщению |
132
+ | `VideoThumbnail` | |
133
+ | `VideoUrls` | |
134
+ | `bigint` | |
@@ -0,0 +1,73 @@
1
+ # Обзор API MAX
2
+
3
+ Источник: https://dev.max.ru/docs-api
4
+
5
+ ## Что такое API MAX
6
+
7
+ API MAX — это интерфейс для взаимодействия ботов с платформой через HTTPS-запросы к `platform-api.max.ru`. Он позволяет получать данные и выполнять действия (получение, создание, редактирование, удаление ресурсов).
8
+
9
+ ## HTTP методы
10
+
11
+ - `GET` — получить ресурсы
12
+ - `POST` — создать ресурсы (например, отправить сообщения)
13
+ - `PUT` — редактировать ресурсы
14
+ - `DELETE` — удалить ресурсы
15
+ - `PATCH` — исправить ресурсы
16
+
17
+ Параметры передаются в пути, query-строке или теле запроса.
18
+
19
+ ## Авторизация
20
+
21
+ Передавайте токен в заголовке:
22
+
23
+ ```
24
+ Authorization: <token>
25
+ ```
26
+
27
+ Передача токена через query-параметры не поддерживается.
28
+
29
+ ## Ответы и коды ошибок
30
+
31
+ Сервер возвращает JSON и HTTP-код:
32
+
33
+ - `200` — успешная операция
34
+ - `400` — недействительный запрос
35
+ - `401` — ошибка аутентификации
36
+ - `404` — ресурс не найден
37
+ - `405` — метод не допускается
38
+ - `429` — превышено количество запросов
39
+ - `503` — сервис недоступен
40
+
41
+ ## Рекомендации по получению обновлений
42
+
43
+ - Long Polling — для разработки и тестирования
44
+ - Webhook — для production-окружения
45
+
46
+ Ограничение по нагрузке: до **30 rps** на `platform-api.max.ru`.
47
+
48
+ ## Клавиатуры
49
+
50
+ Поддерживаются inline-клавиатуры с ограничениями:
51
+
52
+ - до `210` кнопок
53
+ - до `30` рядов
54
+ - до `7` кнопок в ряду (до `3` для кнопок `link`, `open_app`, `request_geo_location`, `request_contact`)
55
+ - для `link` максимальный размер ссылки — `2048` символов
56
+
57
+ Основные типы кнопок:
58
+
59
+ - `callback` — событие `message_callback`
60
+ - `link` — ссылка
61
+ - `request_contact` — запрос контакта
62
+ - `request_geo_location` — запрос геолокации
63
+ - `open_app` — открытие мини-приложения
64
+ - `message` — отправка текста
65
+
66
+ ## Форматирование текста
67
+
68
+ Поддерживаются `markdown` и `html` в поле `format` объекта `NewMessageBody`.
69
+
70
+ - Markdown: `*курсив*`, `**жирный**`, `~~зачерк~~`, `++подчерк++`, `` `code` ``
71
+ - HTML: `<i>`, `<b>`, `<del>`, `<ins>`, `<pre>/<code>`, `<a href="...">`
72
+
73
+ Для упоминаний используйте `max://user/user_id` и полное имя пользователя.
@@ -5,11 +5,17 @@ require 'max_bot_api'
5
5
  client = MaxBotApi::Client.new(token: ENV.fetch('TOKEN'))
6
6
  chat_id = ENV.fetch('CHAT_ID').to_i
7
7
 
8
- photo = client.uploads.upload_photo_from_file(path: './image.png')
8
+ photo_token = ENV['PHOTO_TOKEN']
9
9
 
10
10
  message = MaxBotApi::Builders::MessageBuilder.new
11
11
  .set_chat(chat_id)
12
- .add_photo(photo)
13
12
  .set_text('Photo attached')
14
13
 
14
+ if photo_token && !photo_token.empty?
15
+ message.add_photo_by_token(photo_token)
16
+ else
17
+ photo = client.uploads.upload_photo_from_file(path: './image.png')
18
+ message.add_photo(photo)
19
+ end
20
+
15
21
  client.messages.send(message)
data/examples/keyboard.rb CHANGED
@@ -15,6 +15,11 @@ keyboard
15
15
  .add_row
16
16
  .add_link('Open MAX', 'positive', 'https://max.ru')
17
17
  .add_callback('Audio', 'negative', 'audio')
18
+ .add_message('Continue')
19
+
20
+ keyboard
21
+ .add_row
22
+ .add_clipboard('Copy docs', 'https://dev.max.ru/docs-api/methods/POST/messages')
18
23
 
19
24
  message = MaxBotApi::Builders::MessageBuilder.new
20
25
  .set_chat(chat_id)
@@ -8,6 +8,10 @@ client = MaxBotApi::Client.new(token: ENV.fetch('TOKEN'))
8
8
  app = lambda do |env|
9
9
  req = Rack::Request.new(env)
10
10
  if req.post? && req.path == '/webhook'
11
+ unless client.webhook_secret_valid?(headers: env, secret: ENV.fetch('WEBHOOK_SECRET'))
12
+ return [401, { 'Content-Type' => 'text/plain' }, ['secret not allowed']]
13
+ end
14
+
11
15
  update = client.parse_webhook(req.body.read)
12
16
 
13
17
  if update[:update_type] == 'message_created'
@@ -85,6 +85,25 @@ module MaxBotApi
85
85
  add_button(button)
86
86
  end
87
87
 
88
+ # Add a message button.
89
+ def add_message(text)
90
+ button = {
91
+ type: 'message',
92
+ text: text
93
+ }
94
+ add_button(button)
95
+ end
96
+
97
+ # Add a clipboard button.
98
+ def add_clipboard(text, payload)
99
+ button = {
100
+ type: 'clipboard',
101
+ text: text,
102
+ payload: payload
103
+ }
104
+ add_button(button)
105
+ end
106
+
88
107
  # Build row payload.
89
108
  def build
90
109
  @cols
@@ -4,7 +4,10 @@ module MaxBotApi
4
4
  module Builders
5
5
  # Builder for message payloads.
6
6
  class MessageBuilder
7
- attr_reader :user_id, :chat_id, :reset
7
+ FORMAT_HTML = 'html'
8
+ FORMAT_MARKDOWN = 'markdown'
9
+
10
+ attr_reader :user_id, :chat_id, :reset, :disable_link_preview
8
11
 
9
12
  # Create a builder from an existing hash payload.
10
13
  # @param hash [Hash]
@@ -16,6 +19,9 @@ module MaxBotApi
16
19
  builder.set_user(hash[:user_id] || hash['user_id']) if hash.key?(:user_id) || hash.key?('user_id')
17
20
  builder.set_chat(hash[:chat_id] || hash['chat_id']) if hash.key?(:chat_id) || hash.key?('chat_id')
18
21
  builder.set_reset(hash[:reset] || hash['reset']) if hash.key?(:reset) || hash.key?('reset')
22
+ if hash.key?(:disable_link_preview) || hash.key?('disable_link_preview')
23
+ builder.set_disable_link_preview(hash[:disable_link_preview] || hash['disable_link_preview'])
24
+ end
19
25
 
20
26
  payload = hash[:message] || hash['message'] || hash
21
27
  builder.apply_payload(payload)
@@ -27,6 +33,7 @@ module MaxBotApi
27
33
  @user_id = nil
28
34
  @chat_id = nil
29
35
  @reset = false
36
+ @disable_link_preview = false
30
37
  @message = {
31
38
  attachments: []
32
39
  }
@@ -50,6 +57,12 @@ module MaxBotApi
50
57
  self
51
58
  end
52
59
 
60
+ # Toggle link previews in sent messages.
61
+ def set_disable_link_preview(disable_link_preview)
62
+ @disable_link_preview = !!disable_link_preview
63
+ self
64
+ end
65
+
53
66
  # Set message text.
54
67
  def set_text(text)
55
68
  @message[:text] = text
@@ -106,6 +119,11 @@ module MaxBotApi
106
119
  add_attachment(type: 'image', payload: { photos: payload[:photos] || payload['photos'] })
107
120
  end
108
121
 
122
+ # Attach a photo payload by token.
123
+ def add_photo_by_token(token)
124
+ add_attachment(type: 'image', payload: { token: token })
125
+ end
126
+
109
127
  # Attach audio payload.
110
128
  def add_audio(uploaded_info)
111
129
  add_attachment(type: 'audio', payload: uploaded_info)
@@ -169,6 +187,11 @@ module MaxBotApi
169
187
  @reset
170
188
  end
171
189
 
190
+ # Whether link previews are disabled for message delivery.
191
+ def disable_link_preview?
192
+ @disable_link_preview
193
+ end
194
+
172
195
  # Return the message payload hash.
173
196
  def to_h
174
197
  @message
@@ -6,9 +6,11 @@ module MaxBotApi
6
6
  # Main API client. Holds auth config and provides resource accessors.
7
7
  class Client
8
8
  # Default API base URL.
9
- DEFAULT_BASE_URL = 'https://botapi.max.ru/'
9
+ DEFAULT_BASE_URL = 'https://platform-api.max.ru/'
10
10
  # Default API version appended as query param.
11
11
  DEFAULT_VERSION = '1.2.5'
12
+ # Webhook secret header name.
13
+ SECRET_HEADER = 'X-Max-Bot-Api-Secret'
12
14
  # Default pause between update polling loops.
13
15
  DEFAULT_PAUSE = 1
14
16
  # Default limit for updates requests.
@@ -131,6 +133,13 @@ module MaxBotApi
131
133
  Updates::Parser.parse_update(body.to_s, debug: debug)
132
134
  end
133
135
 
136
+ # Validates the webhook secret header against the expected secret.
137
+ # @param headers [Hash]
138
+ # @param secret [String]
139
+ def webhook_secret_valid?(headers:, secret:)
140
+ header_value(headers, SECRET_HEADER) == secret.to_s
141
+ end
142
+
134
143
  # Perform an HTTP request.
135
144
  # @param method [Symbol]
136
145
  # @param path [String]
@@ -192,11 +201,23 @@ module MaxBotApi
192
201
  body.values.any? { |value| value.is_a?(Faraday::Multipart::FilePart) }
193
202
  end
194
203
 
204
+ def header_value(headers, name)
205
+ target = name.to_s.downcase
206
+
207
+ headers.each do |key, value|
208
+ normalized = key.to_s.downcase.tr('_', '-')
209
+ normalized = normalized.sub(/\Ahttp-/, '')
210
+ return value if normalized == target
211
+ end
212
+
213
+ nil
214
+ end
215
+
195
216
  def handle_response(response)
196
217
  return parse_body(response) if response.status == 200
197
218
 
198
- api_message = parse_error_message(response)
199
- raise ApiError.new(code: response.status, message: api_message)
219
+ error_payload = parse_error_payload(response)
220
+ raise ApiError.new(code: response.status, message: error_payload[:message], details: error_payload[:details])
200
221
  end
201
222
 
202
223
  def parse_body(response)
@@ -209,17 +230,20 @@ module MaxBotApi
209
230
  JSON.parse(body, symbolize_names: true)
210
231
  end
211
232
 
212
- def parse_error_message(response)
233
+ def parse_error_payload(response)
213
234
  body = response.body.to_s
214
- return response.reason_phrase.to_s if body.empty?
235
+ return { message: response.reason_phrase.to_s, details: nil } if body.empty?
215
236
 
216
237
  json = JSON.parse(body)
217
- return json['error'] if json.is_a?(Hash) && json['error']
218
- return json['message'] if json.is_a?(Hash) && json['message']
238
+ if json.is_a?(Hash)
239
+ return { message: json['code'], details: json['message'] } if json['code'] && json['message']
240
+ return { message: json['error'], details: json['details'] || json['message'] } if json['error']
241
+ return { message: json['message'], details: json['details'] } if json['message']
242
+ end
219
243
 
220
- response.reason_phrase.to_s
244
+ { message: response.reason_phrase.to_s, details: nil }
221
245
  rescue JSON::ParserError
222
- response.reason_phrase.to_s
246
+ { message: response.reason_phrase.to_s, details: nil }
223
247
  end
224
248
  end
225
249
  end
@@ -27,6 +27,10 @@ module MaxBotApi
27
27
  other.is_a?(ApiError) && other.code == code
28
28
  end
29
29
 
30
+ def attachment_not_ready?
31
+ message == 'attachment.not.ready'
32
+ end
33
+
30
34
  private
31
35
 
32
36
  def build_message
@@ -86,6 +86,25 @@ module MaxBotApi
86
86
  def send_action(chat_id:, action:)
87
87
  @client.request(:post, "chats/#{chat_id}/actions", body: { action: action })
88
88
  end
89
+
90
+ # Pin a message in chat.
91
+ # @param chat_id [Integer]
92
+ # @param message_id [String]
93
+ def pin_message(chat_id:, message_id:)
94
+ @client.request(:put, "chats/#{chat_id}/pin", body: { message_id: message_id })
95
+ end
96
+
97
+ # Fetch the currently pinned message.
98
+ # @param chat_id [Integer]
99
+ def get_pinned_message(chat_id:)
100
+ @client.request(:get, "chats/#{chat_id}/pin")
101
+ end
102
+
103
+ # Remove the currently pinned message.
104
+ # @param chat_id [Integer]
105
+ def unpin_message(chat_id:)
106
+ @client.request(:delete, "chats/#{chat_id}/pin")
107
+ end
89
108
  end
90
109
  end
91
110
  end
@@ -14,9 +14,11 @@ module MaxBotApi
14
14
  def get_messages(chat_id: nil, message_ids: nil, from: nil, to: nil, count: nil)
15
15
  query = {}
16
16
  query['chat_id'] = chat_id if chat_id && chat_id.to_i != 0
17
- Array(message_ids).each do |mid|
18
- query['message_ids'] ||= []
19
- query['message_ids'] << mid
17
+ query['message_ids'] = Array(message_ids).join(',') if message_ids && !Array(message_ids).empty?
18
+ if from && to
19
+ from_i = from.to_i
20
+ to_i = to.to_i
21
+ from, to = to, from if from_i > to_i
20
22
  end
21
23
  query['from'] = from if from && from.to_i != 0
22
24
  query['to'] = to if to && to.to_i != 0
@@ -36,11 +38,13 @@ module MaxBotApi
36
38
  # @param message_id [String]
37
39
  # @param message [MaxBotApi::Builders::MessageBuilder, Hash]
38
40
  def edit_message(message_id:, message:)
39
- body = message_payload(message)
40
- result = @client.request(:put, 'messages', query: { 'message_id' => message_id }, body: body)
41
- return true if result.is_a?(Hash) && result[:success]
41
+ with_attachment_retry do
42
+ body = message_payload(message)
43
+ result = @client.request(:put, 'messages', query: { 'message_id' => message_id }, body: body)
44
+ return true if result.is_a?(Hash) && result[:success]
42
45
 
43
- raise Error, (result[:message] || 'message update failed')
46
+ raise Error, (result[:message] || 'message update failed')
47
+ end
44
48
  end
45
49
 
46
50
  # Delete a message by ID.
@@ -64,13 +68,13 @@ module MaxBotApi
64
68
  # Send a message builder without returning the created message.
65
69
  # @param message [MaxBotApi::Builders::MessageBuilder, Hash]
66
70
  def send(message)
67
- send_message(message, with_result: false)
71
+ with_attachment_retry { send_message(message, with_result: false) }
68
72
  end
69
73
 
70
74
  # Send a message builder and return the created message hash.
71
75
  # @param message [MaxBotApi::Builders::MessageBuilder, Hash]
72
76
  def send_with_result(message)
73
- send_message(message, with_result: true)
77
+ with_attachment_retry { send_message(message, with_result: true) }
74
78
  end
75
79
 
76
80
  # Check if a message can be sent to the provided phone numbers.
@@ -106,6 +110,7 @@ module MaxBotApi
106
110
  query = {}
107
111
  query['chat_id'] = message.chat_id if message.chat_id && message.chat_id.to_i != 0
108
112
  query['user_id'] = message.user_id if message.user_id && message.user_id.to_i != 0
113
+ query['disable_link_preview'] = 'true' if message.disable_link_preview?
109
114
 
110
115
  response = @client.request(:post, 'messages', query: query, body: message_payload(message),
111
116
  reset: message.reset?)
@@ -133,6 +138,19 @@ module MaxBotApi
133
138
  end
134
139
  end
135
140
  end
141
+
142
+ def with_attachment_retry
143
+ attempts = 0
144
+ begin
145
+ yield
146
+ rescue ApiError => e
147
+ attempts += 1
148
+ raise e unless e.attachment_not_ready? && attempts < Client::MAX_RETRIES
149
+
150
+ sleep(2**(attempts - 1))
151
+ retry
152
+ end
153
+ end
136
154
  end
137
155
  end
138
156
  end
@@ -32,8 +32,12 @@ module MaxBotApi
32
32
  # @param url [String]
33
33
  def upload_media_from_url(type:, url:)
34
34
  response = Faraday.get(url.to_s)
35
+ raise Error, "fetch URL failed: HTTP #{response.status}" unless response.status.between?(200, 299)
36
+
35
37
  name = attachment_name(response.headers)
36
38
  upload_media_from_reader_with_name(type: type, io: StringIO.new(response.body), name: name)
39
+ rescue Faraday::Error => e
40
+ raise NetworkError.new(op: 'GET upload source', original_error: e)
37
41
  end
38
42
 
39
43
  # Upload media from an IO object.
@@ -89,20 +93,48 @@ module MaxBotApi
89
93
  upload_type = UPLOAD_TYPES.fetch(type.to_sym) { type.to_s }
90
94
  endpoint = @client.request(:post, 'uploads', query: { 'type' => upload_type })
91
95
 
92
- file_name = name.to_s.empty? ? 'file' : name.to_s
96
+ file_name = name.to_s.empty? ? 'file' : File.basename(name.to_s)
93
97
  file_part = Faraday::Multipart::FilePart.new(io, nil, file_name)
94
98
 
95
99
  response = Faraday.post(endpoint[:url]) do |req|
96
100
  req.body = { 'data' => file_part }
97
101
  end
98
102
 
99
- raise Error, "upload failed: #{response.status}" unless response.status == 200
103
+ raise upload_api_error(response) unless response.status.between?(200, 299)
104
+
105
+ return { token: endpoint[:token] } if %w[audio video].include?(upload_type) && endpoint[:token]
100
106
 
101
107
  JSON.parse(response.body, symbolize_names: true)
102
108
  rescue Faraday::Error => e
103
109
  raise NetworkError.new(op: 'POST uploads', original_error: e)
104
110
  end
105
111
 
112
+ def upload_api_error(response)
113
+ body = response.body.to_s
114
+ return Error.new("upload failed: HTTP #{response.status}") if body.empty?
115
+
116
+ json = begin
117
+ JSON.parse(body)
118
+ rescue StandardError
119
+ nil
120
+ end
121
+ if json.is_a?(Hash)
122
+ if json['code'] && json['message']
123
+ return ApiError.new(code: response.status, message: json['code'], details: json['message'])
124
+ end
125
+
126
+ if json['error']
127
+ return ApiError.new(code: response.status, message: json['error'],
128
+ details: json['details'] || json['message'])
129
+ end
130
+ if json['message']
131
+ return ApiError.new(code: response.status, message: json['message'], details: json['details'])
132
+ end
133
+ end
134
+
135
+ Error.new("upload failed: HTTP #{response.status}")
136
+ end
137
+
106
138
  def attachment_name(headers)
107
139
  disposition = headers['content-disposition'].to_s
108
140
  return '' if disposition.empty?
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MaxBotApi
4
4
  # Gem version.
5
- VERSION = '0.1.0'
5
+ VERSION = '0.3.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: max_bot_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ChatGPT Codex
8
8
  - Dmitry Merkushin
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 1980-01-02 00:00:00.000000000 Z
12
+ date: 2026-06-05 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: base64
@@ -94,6 +95,9 @@ files:
94
95
  - docs/04-keyboard.md
95
96
  - docs/05-uploads.md
96
97
  - docs/06-subscriptions.md
98
+ - docs/api/methods.md
99
+ - docs/api/objects.md
100
+ - docs/api/overview.md
97
101
  - examples/attachments.rb
98
102
  - examples/basic_send.rb
99
103
  - examples/keyboard.rb
@@ -120,6 +124,7 @@ licenses:
120
124
  metadata:
121
125
  homepage_uri: https://github.com/creogen/max_bot_api
122
126
  source_code_uri: https://github.com/creogen/max_bot_api/tree/main
127
+ post_install_message:
123
128
  rdoc_options: []
124
129
  require_paths:
125
130
  - lib
@@ -134,7 +139,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
139
  - !ruby/object:Gem::Version
135
140
  version: '0'
136
141
  requirements: []
137
- rubygems_version: 3.7.2
142
+ rubygems_version: 3.4.10
143
+ signing_key:
138
144
  specification_version: 4
139
145
  summary: Ruby client for MAX Bot API
140
146
  test_files: []