telegram-support-bot 0.1.06 → 0.1.07

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: 443be16da398a9055e09a98aa7a828d887baa3828916f0afa2a509e9e228d1a3
4
- data.tar.gz: 959d8dbe4eaeb3660bd688dafa26712575af0b0269d94acbea1e294ce9901af9
3
+ metadata.gz: e0dba7130ca446bc645510735687aa8501f31a6671cfb63fc29e7eb5e535c957
4
+ data.tar.gz: 3fa484d6c00969af671bdce6e964fd1484fce7ff96496aac9e92c3cd7e694dba
5
5
  SHA512:
6
- metadata.gz: adfa333fb11bd3f68551a68a7b25f17d09a3840c5326ae65a0872741c326c7956e9d7ccdfe74f33d2e7f017f8e307f9fccd804a744df81004a05a6b460da0f7a
7
- data.tar.gz: 75a42d5e219572496a5d3011a1c66373878a1cba685fbf7ff8574e9a00b451002df6b082f998e6d6be77c7b3dd8972d9d9e7faaaa3c486c83feece51724c8294
6
+ metadata.gz: c23fe383ec0166b10ab54edf9b536a83bc8973afbd9d373f04e9d281def477682406fd7b665f31c52b622b6e52ab33e47a7478c2193bc28e0a1a9139b06da840
7
+ data.tar.gz: ede12ddc03a8cb4cf213507d5fa8e35faeed2bb78d5c9dc76dfa1978901164130842f055b8b66fa604b2e9d3cf3a43881752ece10a94dd2063f09a92ebce19cd
data/.idea/workspace.xml CHANGED
@@ -6,9 +6,7 @@
6
6
  <component name="ChangeListManager">
7
7
  <list default="true" id="edf498b0-8552-42f1-846d-0c79d29ff991" name="Changes" comment="">
8
8
  <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
9
- <change beforePath="$PROJECT_DIR$/Gemfile" beforeDir="false" afterPath="$PROJECT_DIR$/Gemfile" afterDir="false" />
10
- <change beforePath="$PROJECT_DIR$/lib/telegram_support_bot/adapters/telegram_bot_adapter.rb" beforeDir="false" afterPath="$PROJECT_DIR$/lib/telegram_support_bot/adapters/telegram_bot_adapter.rb" afterDir="false" />
11
- <change beforePath="$PROJECT_DIR$/spec/telegram_support_bot/adapters/telegram_bot_adapter_spec.rb" beforeDir="false" afterPath="$PROJECT_DIR$/spec/telegram_support_bot/adapters/telegram_bot_adapter_spec.rb" afterDir="false" />
9
+ <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
12
10
  </list>
13
11
  <option name="SHOW_DIALOG" value="false" />
14
12
  <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -39,30 +37,30 @@
39
37
  <option name="hideEmptyMiddlePackages" value="true" />
40
38
  <option name="showLibraryContents" value="true" />
41
39
  </component>
42
- <component name="PropertiesComponent"><![CDATA[{
43
- "keyToString": {
44
- "DefaultRubyCreateTestTemplate": "Minitest Spec",
45
- "RSpec.Unnamed.executor": "Run",
46
- "Ruby.scratch_61.executor": "Run",
47
- "RunOnceActivity.OpenProjectViewOnStart": "true",
48
- "RunOnceActivity.ShowReadmeOnStart": "true",
49
- "git-widget-placeholder": "main",
50
- "last_opened_file_path": "/home/max/code/gems/telegram_support_bot",
51
- "node.js.detected.package.eslint": "true",
52
- "node.js.detected.package.tslint": "true",
53
- "node.js.selected.package.eslint": "(autodetect)",
54
- "node.js.selected.package.tslint": "(autodetect)",
55
- "nodejs_package_manager_path": "npm",
56
- "ruby.structure.view.model.defaults.configured": "true",
57
- "settings.editor.selected.configurable": "org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable",
58
- "vue.rearranger.settings.migration": "true"
40
+ <component name="PropertiesComponent">{
41
+ &quot;keyToString&quot;: {
42
+ &quot;DefaultRubyCreateTestTemplate&quot;: &quot;Minitest Spec&quot;,
43
+ &quot;RSpec.Unnamed.executor&quot;: &quot;Run&quot;,
44
+ &quot;Ruby.scratch_61.executor&quot;: &quot;Run&quot;,
45
+ &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
46
+ &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
47
+ &quot;git-widget-placeholder&quot;: &quot;main&quot;,
48
+ &quot;last_opened_file_path&quot;: &quot;/home/max/code/tg-sample&quot;,
49
+ &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
50
+ &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
51
+ &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
52
+ &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
53
+ &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
54
+ &quot;ruby.structure.view.model.defaults.configured&quot;: &quot;true&quot;,
55
+ &quot;settings.editor.selected.configurable&quot;: &quot;org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable&quot;,
56
+ &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
59
57
  },
60
- "keyToStringList": {
61
- "com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
62
- "ruby"
58
+ &quot;keyToStringList&quot;: {
59
+ &quot;com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File&quot;: [
60
+ &quot;ruby&quot;
63
61
  ]
64
62
  }
65
- }]]></component>
63
+ }</component>
66
64
  <component name="RecentsManager">
67
65
  <key name="CopyFile.RECENT_KEYS">
68
66
  <recent name="$PROJECT_DIR$/spec/telegram_support_bot/adapters" />
@@ -139,6 +137,10 @@
139
137
  <updated>1708600824931</updated>
140
138
  <workItem from="1708600827139" duration="10157000" />
141
139
  <workItem from="1708784534887" duration="682000" />
140
+ <workItem from="1708944130416" duration="99000" />
141
+ <workItem from="1708944249622" duration="627000" />
142
+ <workItem from="1708949675518" duration="80000" />
143
+ <workItem from="1708951183744" duration="310000" />
142
144
  </task>
143
145
  <servers />
144
146
  </component>
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.07] - 2026-02-13
6
+
7
+ ### Added
8
+ - Mirroring of `message_reaction` updates between support chat and user chats.
9
+ - Message mapping storage (`message_map` and `reverse_message_map`) to correlate forwarded/replied messages for reaction sync.
10
+ - Adapter API method `set_message_reaction` with implementations for both supported adapters.
11
+ - Test coverage for reaction handling and adapter reaction calls.
12
+ - Local polling helper script for development testing without Rails (`script/dev_poll.rb`).
13
+ - README documentation for local development testing workflow.
14
+
15
+ ### Fixed
16
+ - Local polling script now uses the proper polling call for `telegram-bot` gem (`get_updates`) and supports multiple client styles.
17
+ - Reaction mapping now correctly handles wrapped Telegram API responses (`{ "ok": true, "result": ... }`).
18
+ - Reaction mirroring now gracefully handles `REACTIONS_TOO_MANY` by retrying with a single reaction instead of crashing.
19
+ - Reaction mapping lookup now tolerates chat/message ID type mismatches (string vs integer).
20
+ - Added optional reaction-flow debug logs via `TSB_DEBUG=1`.
21
+ - Support-chat reaction mirroring now also handles `message_reaction_count` updates (anonymous reaction counts).
data/README.md CHANGED
@@ -115,13 +115,84 @@ end
115
115
  Implement custom adapters by inheriting from `TelegramSupportBot::Adapter::Base` and defining
116
116
  message sending and forwarding methods.
117
117
 
118
+ ## Handling User Privacy Settings for Message Forwarding
119
+
120
+ Due to Telegram's privacy settings, users may have restricted the ability for bots to forward their
121
+ messages with identifiable information. This restriction impacts the `forward_from` key, necessary
122
+ for the bot to recognize and reply to users directly. To ensure seamless communication and support,
123
+ we recommend including instructions in your bot's welcome message, asking users to allow message
124
+ forwarding from your bot. Here's an example of how you can phrase this request:
125
+
126
+ ```
127
+ Please mind, that your privacy settings might prevent the bot from sending you the reply from the support team. Please consider adding this bot to your allow-list for forwarding. Here’s how you can do it:
128
+
129
+ 1. Go to Settings in your Telegram app.
130
+ 2. Tap on 'Privacy and Security'.
131
+ 3. Scroll to 'Forwarded Messages'.
132
+ 4. Add this bot to the list of exceptions by selecting 'Always Allow' for it.
133
+
134
+ This will allow the bot to send you back replies from the support team.
135
+
136
+ ```
137
+
138
+ Including such instructions can help in reducing the friction in user support interactions and
139
+ ensure that your support team can effectively communicate with users through the bot.
118
140
 
119
141
  ## Development
120
142
 
121
143
  - Run `bin/setup` to install dependencies.
122
144
  - Use `rake spec` for tests and `bin/console` for an interactive prompt.
123
145
  - To install locally, use `bundle exec rake install`.
124
- - For releases, update `version.rb`, and run `bundle exec rake release`.
146
+ - For releases, update `lib/telegram_support_bot/version.rb` and `CHANGELOG.md`, then run `bundle exec rake release`.
147
+
148
+ ### Local Testing Without Rails (Polling)
149
+
150
+ You can run the bot locally without a Rails app by using the included script:
151
+
152
+ Prerequisites:
153
+ - Add the bot as an **administrator** in the support chat if you want support-side reactions to be delivered as updates.
154
+ - Bots can set only one reaction per message via Bot API, so only one mirrored reaction is applied when multiple are present.
155
+
156
+ 1. Export required environment variables:
157
+
158
+ ```bash
159
+ export TELEGRAM_BOT_TOKEN=your_bot_token
160
+ export SUPPORT_CHAT_ID=your_support_chat_id
161
+ # optional; defaults to telegram_bot
162
+ export TSB_ADAPTER=telegram_bot
163
+ # optional; used by telegram_bot adapter
164
+ export TELEGRAM_BOT_USERNAME=your_bot_username
165
+ ```
166
+
167
+ 2. Disable webhook mode for that bot token (polling and webhooks cannot be used together):
168
+
169
+ ```bash
170
+ curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/deleteWebhook" > /dev/null
171
+ ```
172
+
173
+ 3. Start the local poller:
174
+
175
+ ```bash
176
+ bundle exec ruby script/dev_poll.rb
177
+ ```
178
+
179
+ 4. In Telegram, verify:
180
+ - user message is forwarded to support chat
181
+ - support reply is sent back to user
182
+ - reactions are mirrored in both directions
183
+
184
+ If you want to test with `telegram_bot_ruby` adapter, set `TSB_ADAPTER=telegram_bot_ruby` and add
185
+ the `telegram-bot-ruby` gem in your environment.
186
+
187
+ ### Switch Back To Webhook Mode
188
+
189
+ After polling tests, set your webhook again:
190
+
191
+ ```bash
192
+ curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
193
+ -H "Content-Type: application/json" \
194
+ -d '{"url":"https://YOUR_PUBLIC_HOST/telegram/webhook","allowed_updates":["message","message_reaction","message_reaction_count","my_chat_member"]}'
195
+ ```
125
196
 
126
197
  ## Contributing
127
198
 
@@ -8,15 +8,24 @@ module TelegramSupportBot
8
8
  }.freeze
9
9
 
10
10
  def self.build(adapter_specification, adapter_options = {})
11
+ adapter_options ||= {}
11
12
  case adapter_specification
12
13
  when Symbol
13
- adapter_class = ADAPTERS[adapter_specification].constantize
14
- adapter_class.new(adapter_options)
14
+ class_name = ADAPTERS[adapter_specification]
15
+ raise ArgumentError, "Unsupported adapter specification: #{adapter_specification}" unless class_name
16
+
17
+ adapter_class = constantize(class_name)
18
+ adapter_class.new(**adapter_options)
15
19
  when Class
16
- adapter_specification.new(adapter_options)
20
+ adapter_specification.new(**adapter_options)
17
21
  else
18
22
  raise ArgumentError, "Unsupported adapter specification: #{adapter_specification}"
19
23
  end
20
24
  end
25
+
26
+ def self.constantize(class_name)
27
+ class_name.split('::').reject(&:empty?).inject(Object) { |namespace, constant| namespace.const_get(constant) }
28
+ end
29
+ private_class_method :constantize
21
30
  end
22
31
  end
@@ -35,6 +35,10 @@ module TelegramSupportBot
35
35
  # forward messages to the support chat
36
36
  end
37
37
 
38
+ def set_message_reaction(chat_id:, message_id:, reaction:, **options)
39
+ # set reaction to a message
40
+ end
41
+
38
42
  def on_message(&block)
39
43
  # Implementation to register a block to be called on new messages
40
44
  end
@@ -50,6 +50,15 @@ module TelegramSupportBot
50
50
  message_id: message_id
51
51
  )
52
52
  end
53
+
54
+ def set_message_reaction(chat_id:, message_id:, reaction:, **options)
55
+ @bot.set_message_reaction(
56
+ chat_id: chat_id,
57
+ message_id: message_id,
58
+ reaction: reaction,
59
+ **options
60
+ )
61
+ end
53
62
  end
54
63
  end
55
64
  end
@@ -43,6 +43,10 @@ module TelegramSupportBot
43
43
  def forward_message(from_chat_id:, chat_id:, message_id:)
44
44
  bot.api.forward_message(chat_id: chat_id, from_chat_id: from_chat_id, message_id: message_id)
45
45
  end
46
+
47
+ def set_message_reaction(chat_id:, message_id:, reaction:, **options)
48
+ bot.api.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: reaction, **options)
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TelegramSupportBot
4
- VERSION = "0.1.06"
4
+ VERSION = "0.1.07"
5
5
  end
@@ -25,6 +25,18 @@ module TelegramSupportBot
25
25
  @adapter ||= AdapterFactory.build(configuration.adapter, configuration.adapter_options)
26
26
  end
27
27
 
28
+ def message_map
29
+ @message_map ||= {}
30
+ end
31
+
32
+ def reverse_message_map
33
+ @reverse_message_map ||= {}
34
+ end
35
+
36
+ def reaction_count_state
37
+ @reaction_count_state ||= {}
38
+ end
39
+
28
40
  def scheduler
29
41
  @scheduler ||= AutoAwayScheduler.new(adapter, configuration)
30
42
  end
@@ -34,6 +46,10 @@ module TelegramSupportBot
34
46
  if update['message']
35
47
  # Process standard messages
36
48
  process_message(update['message'])
49
+ elsif update['message_reaction']
50
+ process_message_reaction(update['message_reaction'])
51
+ elsif update['message_reaction_count']
52
+ process_message_reaction_count(update['message_reaction_count'])
37
53
  elsif update['my_chat_member']
38
54
  # Handle the bot being added to or removed from a chat
39
55
  handle_my_chat_member_update(update['my_chat_member'])
@@ -121,7 +137,18 @@ module TelegramSupportBot
121
137
  reply_to_message_id: message_id
122
138
  )
123
139
  else
124
- adapter.send_media(chat_id: original_user_id, type: type, media: media, **options)
140
+ result = adapter.send_media(chat_id: original_user_id, type: type, media: media, **options)
141
+ if result
142
+ user_message_id = extract_message_id(result)
143
+ if user_message_id
144
+ support_message_id = message['message_id']
145
+ store_message_mapping(
146
+ support_message_id: support_message_id,
147
+ user_chat_id: original_user_id,
148
+ user_message_id: user_message_id
149
+ )
150
+ end
151
+ end
125
152
  # scheduler.cancel_scheduled_task(message_id)
126
153
  end
127
154
  end
@@ -156,10 +183,21 @@ module TelegramSupportBot
156
183
 
157
184
  def forward_message_to_support_chat(message)
158
185
  message_id = message['message_id']
159
- adapter.forward_message(
186
+ result = adapter.forward_message(
160
187
  from_chat_id: message_chat_id,
161
188
  message_id: message_id,
162
189
  chat_id: configuration.support_chat_id)
190
+
191
+ if result
192
+ support_message_id = extract_message_id(result)
193
+ if support_message_id
194
+ store_message_mapping(
195
+ support_message_id: support_message_id,
196
+ user_chat_id: message_chat_id,
197
+ user_message_id: message_id
198
+ )
199
+ end
200
+ end
163
201
  # scheduler.schedule_auto_away_message(message_id, message_chat_id)
164
202
  end
165
203
 
@@ -171,6 +209,200 @@ module TelegramSupportBot
171
209
  end
172
210
  end
173
211
 
212
+ def process_message_reaction(message_reaction)
213
+ chat_id = message_reaction['chat']['id']
214
+ message_id = message_reaction['message_id']
215
+ new_reaction = message_reaction['new_reaction']
216
+
217
+ if same_chat_id?(chat_id, configuration.support_chat_id)
218
+ # Reaction in support chat, forward to user
219
+ if (mapping = find_message_mapping(message_id))
220
+ debug_log("Support->user mapping hit: support_message_id=#{message_id.inspect} => user_chat_id=#{mapping[:chat_id].inspect}, user_message_id=#{mapping[:message_id].inspect}")
221
+ mirror_reaction(
222
+ chat_id: mapping[:chat_id],
223
+ message_id: mapping[:message_id],
224
+ reaction: new_reaction
225
+ )
226
+ else
227
+ debug_log("Missing support->user mapping for support_message_id=#{message_id.inspect}; known_mappings=#{message_map.size}")
228
+ end
229
+ else
230
+ # Reaction in user chat, forward to support chat
231
+ if (support_message_id = reverse_message_map[reverse_mapping_key(chat_id, message_id)])
232
+ debug_log("User->support mapping hit: key=#{reverse_mapping_key(chat_id, message_id)} => support_message_id=#{support_message_id.inspect}")
233
+ mirror_reaction(
234
+ chat_id: configuration.support_chat_id,
235
+ message_id: support_message_id,
236
+ reaction: new_reaction
237
+ )
238
+ else
239
+ debug_log("Missing user->support mapping for key=#{reverse_mapping_key(chat_id, message_id)}; known_reverse_mappings=#{reverse_message_map.size}")
240
+ end
241
+ end
242
+ end
243
+
244
+ def process_message_reaction_count(message_reaction_count)
245
+ chat_id = message_reaction_count.dig('chat', 'id')
246
+ message_id = message_reaction_count['message_id']
247
+
248
+ unless same_chat_id?(chat_id, configuration.support_chat_id)
249
+ debug_log("Ignoring message_reaction_count outside support chat: chat_id=#{chat_id.inspect}")
250
+ return
251
+ end
252
+
253
+ mapping = find_message_mapping(message_id)
254
+ unless mapping
255
+ debug_log("Missing support->user mapping for message_reaction_count support_message_id=#{message_id.inspect}; known_mappings=#{message_map.size}")
256
+ return
257
+ end
258
+
259
+ state_key = reverse_mapping_key(chat_id, message_id)
260
+ previous_counts = reaction_count_state[state_key] || {}
261
+ current_counts, reaction_types = extract_reaction_counts(message_reaction_count['reactions'])
262
+ reaction_to_mirror = infer_reaction_from_count_diff(
263
+ previous_counts: previous_counts,
264
+ current_counts: current_counts,
265
+ reaction_types: reaction_types
266
+ )
267
+ reaction_count_state[state_key] = current_counts
268
+
269
+ if reaction_to_mirror.nil?
270
+ debug_log("No actionable reaction diff in message_reaction_count for support_message_id=#{message_id.inspect}")
271
+ return
272
+ end
273
+
274
+ debug_log("Support->user inferred reaction from count update: support_message_id=#{message_id.inspect} payload=#{reaction_to_mirror.inspect}")
275
+ mirror_reaction(
276
+ chat_id: mapping[:chat_id],
277
+ message_id: mapping[:message_id],
278
+ reaction: reaction_to_mirror
279
+ )
280
+ end
281
+
282
+ def mirror_reaction(chat_id:, message_id:, reaction:)
283
+ normalized_reaction = normalize_reaction_payload(reaction)
284
+ debug_log("Mirroring reaction to chat_id=#{chat_id.inspect} message_id=#{message_id.inspect} payload=#{normalized_reaction.inspect}")
285
+ adapter.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: normalized_reaction)
286
+ debug_log("Reaction mirrored to chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}")
287
+ rescue StandardError => error
288
+ if reaction_too_many_error?(error) && normalized_reaction.size > 1
289
+ begin
290
+ adapter.set_message_reaction(chat_id: chat_id, message_id: message_id, reaction: [normalized_reaction.first])
291
+ debug_log("Reaction mirrored after fallback to single reaction for chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}")
292
+ rescue StandardError => retry_error
293
+ warn_reaction_forwarding_failure(chat_id: chat_id, message_id: message_id, error: retry_error)
294
+ end
295
+ else
296
+ warn_reaction_forwarding_failure(chat_id: chat_id, message_id: message_id, error: error)
297
+ end
298
+ end
299
+
300
+ def normalize_reaction_payload(reaction)
301
+ return [] if reaction.nil?
302
+ return reaction if reaction.is_a?(Array)
303
+
304
+ [reaction]
305
+ end
306
+
307
+ def reaction_too_many_error?(error)
308
+ error.message.to_s.upcase.include?('REACTIONS_TOO_MANY')
309
+ end
310
+
311
+ def warn_reaction_forwarding_failure(chat_id:, message_id:, error:)
312
+ warn "Failed to mirror reaction to chat_id=#{chat_id} message_id=#{message_id}: #{error.class}: #{error.message}"
313
+ end
314
+
315
+ def extract_reaction_counts(reactions)
316
+ counts = {}
317
+ reaction_types = {}
318
+
319
+ Array(reactions).each do |reaction|
320
+ reaction_type = reaction['type'] || reaction[:type]
321
+ next unless reaction_type
322
+
323
+ key = reaction_type_key(reaction_type)
324
+ next if key.nil?
325
+
326
+ counts[key] = (reaction['total_count'] || reaction[:total_count] || 0).to_i
327
+ reaction_types[key] = reaction_type
328
+ end
329
+
330
+ [counts, reaction_types]
331
+ end
332
+
333
+ def infer_reaction_from_count_diff(previous_counts:, current_counts:, reaction_types:)
334
+ increments = []
335
+ current_counts.each do |key, current_value|
336
+ previous_value = previous_counts.fetch(key, 0)
337
+ diff = current_value - previous_value
338
+ increments << [key, diff, current_value] if diff.positive?
339
+ end
340
+
341
+ if increments.any?
342
+ selected_key = increments.max_by { |(_, diff, current_value)| [diff, current_value] }[0]
343
+ return [reaction_types.fetch(selected_key)]
344
+ end
345
+
346
+ removed_all = previous_counts.values.any?(&:positive?) && current_counts.values.all?(&:zero?)
347
+ return [] if removed_all
348
+
349
+ nil
350
+ end
351
+
352
+ def reaction_type_key(reaction_type)
353
+ type = reaction_type['type'] || reaction_type[:type]
354
+
355
+ case type
356
+ when 'emoji', :emoji
357
+ "emoji:#{reaction_type['emoji'] || reaction_type[:emoji]}"
358
+ when 'custom_emoji', :custom_emoji
359
+ "custom_emoji:#{reaction_type['custom_emoji_id'] || reaction_type[:custom_emoji_id]}"
360
+ when 'paid', :paid
361
+ 'paid'
362
+ else
363
+ nil
364
+ end
365
+ end
366
+
367
+ def store_message_mapping(support_message_id:, user_chat_id:, user_message_id:)
368
+ mapping = { chat_id: user_chat_id, message_id: user_message_id }
369
+ message_map[support_message_id] = mapping
370
+ message_map[support_message_id.to_s] = mapping
371
+ reverse_message_map[reverse_mapping_key(user_chat_id, user_message_id)] = support_message_id
372
+ end
373
+
374
+ def find_message_mapping(support_message_id)
375
+ message_map[support_message_id] ||
376
+ message_map[support_message_id.to_s] ||
377
+ message_map[support_message_id.to_i]
378
+ end
379
+
380
+ def reverse_mapping_key(chat_id, message_id)
381
+ "#{chat_id}:#{message_id}"
382
+ end
383
+
384
+ def same_chat_id?(left, right)
385
+ left == right || left.to_s == right.to_s
386
+ end
387
+
388
+ def debug_log(message)
389
+ return unless ENV['TSB_DEBUG'] == '1'
390
+
391
+ puts "[TelegramSupportBot DEBUG] #{message}"
392
+ end
393
+
394
+ def extract_message_id(result)
395
+ if result.is_a?(Hash)
396
+ return result['message_id'] || result[:message_id] ||
397
+ result.dig('result', 'message_id') || result.dig(:result, :message_id) ||
398
+ result.dig('result', :message_id) || result.dig(:result, 'message_id')
399
+ end
400
+
401
+ return result.message_id if result.respond_to?(:message_id)
402
+
403
+ nil
404
+ end
405
+
174
406
  def send_welcome_message(chat_id:)
175
407
  welcome_text = "Hello! Thank you for adding me to your chat. To configure your system, please use the following support chat ID: <code>#{chat_id}</code>."
176
408
  adapter.send_message(chat_id: chat_id, text: welcome_text, parse_mode: 'HTML')
@@ -183,4 +415,3 @@ module TelegramSupportBot
183
415
  @adapter = nil
184
416
  end
185
417
  end
186
-
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'telegram_support_bot'
5
+ require 'telegram/bot'
6
+
7
+ token = ENV.fetch('TELEGRAM_BOT_TOKEN')
8
+ support_chat_id = Integer(ENV.fetch('SUPPORT_CHAT_ID'))
9
+ adapter = ENV.fetch('TSB_ADAPTER', 'telegram_bot')
10
+ username = ENV['TELEGRAM_BOT_USERNAME']
11
+
12
+ adapter_options = { token: token }
13
+ adapter_options[:username] = username if username && !username.empty?
14
+
15
+ TelegramSupportBot.configure do |config|
16
+ config.adapter = adapter.to_sym
17
+ config.adapter_options = adapter_options
18
+ config.support_chat_id = support_chat_id
19
+ config.welcome_message = 'Hi! How can we help you?'
20
+ end
21
+
22
+ client = Telegram::Bot::Client.new(token)
23
+ offset = 0
24
+
25
+ puts "TelegramSupportBot dev poller started (adapter=#{adapter}, support_chat_id=#{support_chat_id})."
26
+ puts 'Press Ctrl+C to stop.'
27
+
28
+ def extract_result(payload)
29
+ return payload['result'] if payload.is_a?(Hash) && payload.key?('result')
30
+ return payload[:result] if payload.is_a?(Hash) && payload.key?(:result)
31
+
32
+ payload
33
+ end
34
+
35
+ def verify_reaction_update_prerequisites(client:, support_chat_id:)
36
+ me_response = client.get_me
37
+ me_result = extract_result(me_response)
38
+ bot_user_id = me_result['id'] || me_result[:id]
39
+ return puts('Warning: could not verify bot identity via getMe.') unless bot_user_id
40
+
41
+ member_response = client.get_chat_member(chat_id: support_chat_id, user_id: bot_user_id)
42
+ member_result = extract_result(member_response)
43
+ status = member_result['status'] || member_result[:status]
44
+
45
+ unless %w[administrator creator].include?(status)
46
+ puts "Warning: bot status in support chat is #{status.inspect}. message_reaction/message_reaction_count updates require bot admin rights."
47
+ end
48
+ rescue StandardError => e
49
+ puts "Warning: failed to verify reaction prerequisites: #{e.class}: #{e.message}"
50
+ end
51
+
52
+ verify_reaction_update_prerequisites(client: client, support_chat_id: support_chat_id)
53
+
54
+ def debug_update_summary(update)
55
+ return unless ENV['TSB_DEBUG'] == '1'
56
+
57
+ if update['message_reaction']
58
+ chat_id = update.dig('message_reaction', 'chat', 'id')
59
+ message_id = update.dig('message_reaction', 'message_id')
60
+ puts "[TSB POLL DEBUG] update=message_reaction chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
61
+ elsif update['message_reaction_count']
62
+ chat_id = update.dig('message_reaction_count', 'chat', 'id')
63
+ message_id = update.dig('message_reaction_count', 'message_id')
64
+ puts "[TSB POLL DEBUG] update=message_reaction_count chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
65
+ elsif update['message']
66
+ chat_id = update.dig('message', 'chat', 'id')
67
+ message_id = update.dig('message', 'message_id')
68
+ puts "[TSB POLL DEBUG] update=message chat_id=#{chat_id.inspect} message_id=#{message_id.inspect}"
69
+ elsif update['my_chat_member']
70
+ chat_id = update.dig('my_chat_member', 'chat', 'id')
71
+ puts "[TSB POLL DEBUG] update=my_chat_member chat_id=#{chat_id.inspect}"
72
+ else
73
+ puts "[TSB POLL DEBUG] update=other keys=#{update.keys.inspect}"
74
+ end
75
+ end
76
+
77
+ loop do
78
+ params = {
79
+ offset: offset,
80
+ timeout: 25,
81
+ allowed_updates: %w[message message_reaction message_reaction_count my_chat_member]
82
+ }
83
+ response =
84
+ if client.respond_to?(:get_updates)
85
+ client.get_updates(**params)
86
+ elsif client.respond_to?(:api) && client.api.respond_to?(:get_updates)
87
+ client.api.get_updates(**params)
88
+ elsif client.respond_to?(:api) && client.api.respond_to?(:getUpdates)
89
+ client.api.getUpdates(**params)
90
+ else
91
+ raise 'Unsupported Telegram client API for polling'
92
+ end
93
+ updates = response['result'] || response[:result] || []
94
+
95
+ updates.each do |update|
96
+ normalized = update.respond_to?(:to_h) ? update.to_h : update
97
+ debug_update_summary(normalized)
98
+ TelegramSupportBot.process_update(normalized)
99
+ update_id = normalized['update_id'] || normalized[:update_id]
100
+ offset = update_id + 1 if update_id
101
+ end
102
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegram-support-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.06
4
+ version: 0.1.07
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Buslaev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-26 00:00:00.000000000 Z
11
+ date: 2026-02-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |-
14
14
  The telegram_support_bot gem provides Rails applications with an
@@ -28,6 +28,7 @@ extra_rdoc_files: []
28
28
  files:
29
29
  - ".idea/workspace.xml"
30
30
  - ".rspec"
31
+ - CHANGELOG.md
31
32
  - CODE_OF_CONDUCT.md
32
33
  - Gemfile
33
34
  - LICENSE.txt
@@ -41,13 +42,13 @@ files:
41
42
  - lib/telegram_support_bot/auto_away_scheduler.rb
42
43
  - lib/telegram_support_bot/configuration.rb
43
44
  - lib/telegram_support_bot/version.rb
45
+ - script/dev_poll.rb
44
46
  - sig/telegram_support_bot.rbs
45
- - telegram_support_bot.gemspec
46
47
  homepage: https://github.com/austerlitz/telegram_support_bot
47
48
  licenses:
48
49
  - MIT
49
50
  metadata: {}
50
- post_install_message:
51
+ post_install_message:
51
52
  rdoc_options: []
52
53
  require_paths:
53
54
  - lib
@@ -62,8 +63,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  - !ruby/object:Gem::Version
63
64
  version: '0'
64
65
  requirements: []
65
- rubygems_version: 3.2.3
66
- signing_key:
66
+ rubygems_version: 3.5.11
67
+ signing_key:
67
68
  specification_version: 4
68
69
  summary: A Rails gem for integrating a Telegram bot into your application for seamless
69
70
  support desk functionality.
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/telegram_support_bot/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'telegram-support-bot'
7
- spec.version = TelegramSupportBot::VERSION
8
- spec.authors = ['Max Buslaev']
9
- spec.email = ['max@buslaev.net']
10
-
11
- spec.summary = 'A Rails gem for integrating a Telegram bot into your application for seamless
12
- support desk functionality.'
13
- spec.description = 'The telegram_support_bot gem provides Rails applications with an
14
- easy-to-integrate Telegram bot, designed to enhance customer support services.
15
- By leveraging this gem, developers can swiftly add a Telegram-based support desk to their
16
- application, enabling direct communication between users and support agents through Telegram.
17
- Features include automatic message forwarding to a designated secret chat for support agents,
18
- the ability to reply directly from the secret chat to users, and customizable responses
19
- for common queries. This gem simplifies the process of setting up a robust support channel on one of
20
- the most popular messaging platforms, making it an ideal solution for businesses looking to improve
21
- their customer service experience.'
22
- spec.homepage = 'https://github.com/austerlitz/telegram_support_bot'
23
- spec.license = 'MIT'
24
- # spec.required_ruby_version = '>= 2.6.0'
25
-
26
- # Specify which files should be added to the gem when it is released.
27
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
- spec.files = Dir.chdir(__dir__) do
29
- `git ls-files -z`.split("\x0").reject do |f|
30
- (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
31
- end
32
- end
33
- spec.bindir = 'exe'
34
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
- spec.require_paths = ['lib']
36
-
37
- end