max_bot_api 0.1.0 → 0.2.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: fb9285c20da62a0c1fba896afde6af5fd10dff12079de7d44878f3cd4b3aea22
4
+ data.tar.gz: 11d42f73535db90bcbc32b89fa9886cc96d99222cca56e15ba3fb50eb4132ce8
5
5
  SHA512:
6
- metadata.gz: 0d9ee73288cf706f2ba868e82bab3456bc6f7857d8b1a46cc8cf6fa273d2d0d4e800c93f62bce64f6bb822231385c4166d160fb6c13300649a1dcd18cf80a217
7
- data.tar.gz: e4f90c9ccbc8ddaf116922c1dc3f6b7c83e4d261e20237e83b3d593703fd709898aa2ae201bfced427279c3b3378c318635e0fcbdf2c0b9721448f25a51962ed
6
+ metadata.gz: b10c0eb6c1bef5eedc551fc841cb65d43b65d4d43c28941cbbdf8bb85e0245b251b512b4a9b9f476ca859a189c1967e0ac81f9b09b225569bb66afffb70cfd02
7
+ data.tar.gz: 148837674accefde112f9e8195440bce6a7c6a9abd5b8243c87c076b6af72c9b1f148fe66a24104d0ecc3ac82e94011f8841ba3db900842dd8d58f183e895286
data/README.md CHANGED
@@ -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,28 @@ Notes:
54
54
  - The client uses `Authorization: <token>` (no `Bearer`).
55
55
  - Every request includes `v=<version>` query param.
56
56
 
57
+ ## 0.2.0 updates
58
+
59
+ - Default API host is now `https://platform-api.max.ru/` (override with `base_url:` if you need the legacy host).
60
+ - Message send/edit retries automatically when the API responds with `attachment.not.ready`.
61
+ - New helpers: `MessageBuilder#add_photo_by_token`, `KeyboardRowBuilder#add_message`.
62
+
63
+ Example:
64
+
65
+ ```ruby
66
+ message = MaxBotApi::Builders::MessageBuilder.new
67
+ .set_chat(12345)
68
+ .set_text("Hello")
69
+ .add_photo_by_token("photo-token")
70
+
71
+ keyboard = MaxBotApi::Builders::KeyboardBuilder.new
72
+ row = keyboard.add_row
73
+ row.add_message("Continue")
74
+
75
+ message.add_keyboard(keyboard)
76
+ client.messages.send(message)
77
+ ```
78
+
57
79
  ## Common tasks
58
80
 
59
81
  ### Send messages
@@ -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/"`.
@@ -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,7 @@ keyboard
18
18
  keyboard
19
19
  .add_row
20
20
  .add_callback("Picture", "positive", "picture")
21
+ .add_message("Continue")
21
22
 
22
23
  message = MaxBotApi::Builders::MessageBuilder.new
23
24
  .set_chat(12345)
@@ -34,3 +35,4 @@ client.messages.send(message)
34
35
  - `add_contact(text)`
35
36
  - `add_geolocation(text, quick)`
36
37
  - `add_open_app(text, app, payload, contact_id)`
38
+ - `add_message(text)`
@@ -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,7 @@ 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')
18
19
 
19
20
  message = MaxBotApi::Builders::MessageBuilder.new
20
21
  .set_chat(chat_id)
@@ -85,6 +85,15 @@ 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
+
88
97
  # Build row payload.
89
98
  def build
90
99
  @cols
@@ -4,6 +4,9 @@ module MaxBotApi
4
4
  module Builders
5
5
  # Builder for message payloads.
6
6
  class MessageBuilder
7
+ FORMAT_HTML = 'html'
8
+ FORMAT_MARKDOWN = 'markdown'
9
+
7
10
  attr_reader :user_id, :chat_id, :reset
8
11
 
9
12
  # Create a builder from an existing hash payload.
@@ -106,6 +109,11 @@ module MaxBotApi
106
109
  add_attachment(type: 'image', payload: { photos: payload[:photos] || payload['photos'] })
107
110
  end
108
111
 
112
+ # Attach a photo payload by token.
113
+ def add_photo_by_token(token)
114
+ add_attachment(type: 'image', payload: { token: token })
115
+ end
116
+
109
117
  # Attach audio payload.
110
118
  def add_audio(uploaded_info)
111
119
  add_attachment(type: 'audio', payload: uploaded_info)
@@ -6,7 +6,7 @@ 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
12
  # Default pause between update polling loops.
@@ -195,8 +195,8 @@ module MaxBotApi
195
195
  def handle_response(response)
196
196
  return parse_body(response) if response.status == 200
197
197
 
198
- api_message = parse_error_message(response)
199
- raise ApiError.new(code: response.status, message: api_message)
198
+ error_payload = parse_error_payload(response)
199
+ raise ApiError.new(code: response.status, message: error_payload[:message], details: error_payload[:details])
200
200
  end
201
201
 
202
202
  def parse_body(response)
@@ -209,17 +209,20 @@ module MaxBotApi
209
209
  JSON.parse(body, symbolize_names: true)
210
210
  end
211
211
 
212
- def parse_error_message(response)
212
+ def parse_error_payload(response)
213
213
  body = response.body.to_s
214
- return response.reason_phrase.to_s if body.empty?
214
+ return { message: response.reason_phrase.to_s, details: nil } if body.empty?
215
215
 
216
216
  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']
217
+ if json.is_a?(Hash)
218
+ return { message: json['code'], details: json['message'] } if json['code'] && json['message']
219
+ return { message: json['error'], details: json['details'] || json['message'] } if json['error']
220
+ return { message: json['message'], details: json['details'] } if json['message']
221
+ end
219
222
 
220
- response.reason_phrase.to_s
223
+ { message: response.reason_phrase.to_s, details: nil }
221
224
  rescue JSON::ParserError
222
- response.reason_phrase.to_s
225
+ { message: response.reason_phrase.to_s, details: nil }
223
226
  end
224
227
  end
225
228
  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
@@ -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.
@@ -133,6 +137,19 @@ module MaxBotApi
133
137
  end
134
138
  end
135
139
  end
140
+
141
+ def with_attachment_retry
142
+ attempts = 0
143
+ begin
144
+ yield
145
+ rescue ApiError => e
146
+ attempts += 1
147
+ raise e unless e.attachment_not_ready? && attempts < Client::MAX_RETRIES
148
+
149
+ sleep(2**(attempts - 1))
150
+ retry
151
+ end
152
+ end
136
153
  end
137
154
  end
138
155
  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.2.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.2.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-03-05 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: base64
@@ -120,6 +121,7 @@ licenses:
120
121
  metadata:
121
122
  homepage_uri: https://github.com/creogen/max_bot_api
122
123
  source_code_uri: https://github.com/creogen/max_bot_api/tree/main
124
+ post_install_message:
123
125
  rdoc_options: []
124
126
  require_paths:
125
127
  - lib
@@ -134,7 +136,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
136
  - !ruby/object:Gem::Version
135
137
  version: '0'
136
138
  requirements: []
137
- rubygems_version: 3.7.2
139
+ rubygems_version: 3.4.10
140
+ signing_key:
138
141
  specification_version: 4
139
142
  summary: Ruby client for MAX Bot API
140
143
  test_files: []