telegram-support-bot 0.1.13 → 0.1.15

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: 0e2776425f88d7b3255ed8130fbc382325c5c50234963b4cc4d626092e16defd
4
- data.tar.gz: 7529852a1df10a48cf1b1381fb0d04822a9fc70875340e0cae3afb36da0430d2
3
+ metadata.gz: 628391b6587ec2b35dc5ce2984276eb42f4f5d5e992205ef06682f26ad41b8cf
4
+ data.tar.gz: 9edfd943aee7356088dede4f647609ee9f9c6cc34d9ba82a3bbdb61feef241fa
5
5
  SHA512:
6
- metadata.gz: cc195cf56e0bdd9299d65342ab7619d9bc091727a09c66f3d4e56a4dde5689b3083cf0cc0531ab7784f85bb600b9b097c5ab056f62f617b2a420b47b980f54b5
7
- data.tar.gz: d931f2c70b662d7167cc670a3e2b8eb6f88cfb8b3036dbdcd2f95f50b5c643762f1309921e0c593ce1b68ce38ff579e648bdbd751ad825074690a82a50705a76
6
+ metadata.gz: 98fb134361de2705a30e239708f317565aaa50728a1a6516516ea6e4728bb216b1c6e2a7195996a8337fe46d85b8ddb77b3185122ada7f9b7088666742d64d0e
7
+ data.tar.gz: 205c10c6546720fabe6d1167846409f9f08cfd9fa264ee8e2218f973c032556d5742766c069cb3a1fedced5e2db6489a220601bf315a5f91d4841349daa27f45
data/CHANGELOG.md CHANGED
@@ -4,11 +4,19 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [0.1.13] - 2026-02-26
7
+ ## [0.1.15] - 2026-02-26
8
8
 
9
9
  ### Added
10
10
  - Optional `forward_start_to_support` configuration to forward the first user `/start`
11
11
  message to support chat, so the team can proactively start the conversation.
12
+ - Update-level deduplication by Telegram `update_id` to prevent repeated replies/forwards
13
+ when webhook deliveries are retried.
14
+
15
+ ### Fixed
16
+ - Initial `/start` forwarding is now fail-safe: errors in forwarding/persisting first-message
17
+ marker no longer crash update processing and block subsequent updates.
18
+ - Redis-backed marker storage for update/start dedup now writes JSON objects (not scalar booleans),
19
+ fixing `JSON::GeneratorError: only generation of JSON objects or arrays allowed` on stricter runtimes.
12
20
 
13
21
  ## [0.1.12] - 2026-02-26
14
22
 
data/README.md CHANGED
@@ -54,6 +54,9 @@ TelegramSupportBot.configure do |config|
54
54
  config.forward_start_to_support = false
55
55
  # Optional: block forwarding until contact is shared.
56
56
  config.require_contact_for_support = false
57
+ # Optional: deduplicate repeated Telegram deliveries by update_id.
58
+ # Keep > 0 (default: 24h) to avoid repeated /start replies/forwards on retries.
59
+ config.processed_update_ttl_seconds = 24 * 60 * 60
57
60
  # Optional callback to persist/lookup user profile in your app.
58
61
  config.on_contact_received = ->(profile) { YourUserMatcher.sync_from_telegram(profile) }
59
62
  # Optional callback for user-chat commands other than /start.
@@ -245,6 +248,7 @@ TelegramSupportBot.configure do |config|
245
248
  # config.mapping_ttl_seconds = 30 * 24 * 60 * 60
246
249
  # config.reaction_count_ttl_seconds = 7 * 24 * 60 * 60
247
250
  # config.user_profile_ttl_seconds = nil
251
+ # config.processed_update_ttl_seconds = 24 * 60 * 60
248
252
  end
249
253
  ```
250
254
 
@@ -9,7 +9,7 @@ module TelegramSupportBot
9
9
  :contact_received_message, :contact_invalid_message, :forward_start_to_support, :on_contact_received,
10
10
  :on_user_command,
11
11
  :state_store, :state_store_options, :mapping_ttl_seconds,
12
- :reaction_count_ttl_seconds, :user_profile_ttl_seconds
12
+ :reaction_count_ttl_seconds, :user_profile_ttl_seconds, :processed_update_ttl_seconds
13
13
 
14
14
  def initialize
15
15
  @adapter = :telegram_bot
@@ -33,6 +33,7 @@ module TelegramSupportBot
33
33
  @mapping_ttl_seconds = 30 * 24 * 60 * 60
34
34
  @reaction_count_ttl_seconds = 7 * 24 * 60 * 60
35
35
  @user_profile_ttl_seconds = nil
36
+ @processed_update_ttl_seconds = 24 * 60 * 60
36
37
  end
37
38
  end
38
39
  end
@@ -37,6 +37,7 @@ module TelegramSupportBot
37
37
  mapping_ttl_seconds: configuration.mapping_ttl_seconds,
38
38
  reaction_count_ttl_seconds: configuration.reaction_count_ttl_seconds,
39
39
  user_profile_ttl_seconds: configuration.user_profile_ttl_seconds,
40
+ processed_update_ttl_seconds: configuration.processed_update_ttl_seconds,
40
41
  **options
41
42
  )
42
43
  when :redis
@@ -46,6 +47,7 @@ module TelegramSupportBot
46
47
  mapping_ttl_seconds: configuration.mapping_ttl_seconds,
47
48
  reaction_count_ttl_seconds: configuration.reaction_count_ttl_seconds,
48
49
  user_profile_ttl_seconds: configuration.user_profile_ttl_seconds,
50
+ processed_update_ttl_seconds: configuration.processed_update_ttl_seconds,
49
51
  **options
50
52
  )
51
53
  else
@@ -12,6 +12,7 @@ module TelegramSupportBot
12
12
  @reaction_count_state = {}
13
13
  @user_profiles = {}
14
14
  @start_forwarded_users = {}
15
+ @processed_updates = {}
15
16
  end
16
17
 
17
18
  def message_map
@@ -59,6 +60,15 @@ module TelegramSupportBot
59
60
  )
60
61
  end
61
62
 
63
+ def processed_updates
64
+ @processed_updates_proxy ||= StateStore::MapProxy.new(
65
+ get_proc: ->(key) { get_processed_update(key) },
66
+ set_proc: ->(key, value) { set_processed_update(key, value) },
67
+ clear_proc: -> { clear_processed_updates },
68
+ size_proc: -> { processed_updates_size }
69
+ )
70
+ end
71
+
62
72
  def get_message_mapping(key)
63
73
  synchronize { @message_map[normalize_key(key)] }
64
74
  end
@@ -139,6 +149,22 @@ module TelegramSupportBot
139
149
  synchronize { @start_forwarded_users.size }
140
150
  end
141
151
 
152
+ def get_processed_update(key)
153
+ synchronize { @processed_updates[normalize_key(key)] }
154
+ end
155
+
156
+ def set_processed_update(key, value)
157
+ synchronize { @processed_updates[normalize_key(key)] = value }
158
+ end
159
+
160
+ def clear_processed_updates
161
+ synchronize { @processed_updates.clear }
162
+ end
163
+
164
+ def processed_updates_size
165
+ synchronize { @processed_updates.size }
166
+ end
167
+
142
168
  private
143
169
 
144
170
  def synchronize(&block)
@@ -8,7 +8,8 @@ module TelegramSupportBot
8
8
  DEFAULT_NAMESPACE = 'telegram_support_bot'
9
9
 
10
10
  def initialize(url: nil, redis: nil, namespace: DEFAULT_NAMESPACE,
11
- mapping_ttl_seconds: nil, reaction_count_ttl_seconds: nil, user_profile_ttl_seconds: nil, **_options)
11
+ mapping_ttl_seconds: nil, reaction_count_ttl_seconds: nil, user_profile_ttl_seconds: nil,
12
+ processed_update_ttl_seconds: nil, **_options)
12
13
  @redis = redis || begin
13
14
  require 'redis'
14
15
  ::Redis.new(url: url)
@@ -19,6 +20,7 @@ module TelegramSupportBot
19
20
  @mapping_ttl_seconds = mapping_ttl_seconds
20
21
  @reaction_count_ttl_seconds = reaction_count_ttl_seconds
21
22
  @user_profile_ttl_seconds = user_profile_ttl_seconds
23
+ @processed_update_ttl_seconds = processed_update_ttl_seconds
22
24
  end
23
25
 
24
26
  def message_map
@@ -66,6 +68,15 @@ module TelegramSupportBot
66
68
  )
67
69
  end
68
70
 
71
+ def processed_updates
72
+ @processed_updates_proxy ||= StateStore::MapProxy.new(
73
+ get_proc: ->(key) { get_processed_update(key) },
74
+ set_proc: ->(key, value) { set_processed_update(key, value) },
75
+ clear_proc: -> { clear_processed_updates },
76
+ size_proc: -> { processed_updates_size }
77
+ )
78
+ end
79
+
69
80
  def get_message_mapping(key)
70
81
  payload = parse_json(@redis.get(map_key(:message_map, key)))
71
82
  symbolize_hash(payload)
@@ -149,6 +160,22 @@ module TelegramSupportBot
149
160
  count_by_prefix(prefix(:start_forwarded_users))
150
161
  end
151
162
 
163
+ def get_processed_update(key)
164
+ parse_json(@redis.get(map_key(:processed_updates, key)))
165
+ end
166
+
167
+ def set_processed_update(key, value)
168
+ write_json(map_key(:processed_updates, key), value, @processed_update_ttl_seconds)
169
+ end
170
+
171
+ def clear_processed_updates
172
+ delete_by_prefix(prefix(:processed_updates))
173
+ end
174
+
175
+ def processed_updates_size
176
+ count_by_prefix(prefix(:processed_updates))
177
+ end
178
+
152
179
  private
153
180
 
154
181
  def prefix(name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TelegramSupportBot
4
- VERSION = "0.1.13"
4
+ VERSION = "0.1.15"
5
5
  end
@@ -63,6 +63,10 @@ module TelegramSupportBot
63
63
  state_store(bot_key).start_forwarded_users
64
64
  end
65
65
 
66
+ def processed_updates(bot_key = nil)
67
+ state_store(bot_key).processed_updates
68
+ end
69
+
66
70
  def user_profile(chat_id, bot: nil)
67
71
  profiles = user_profiles(bot)
68
72
  profiles[chat_id] || profiles[chat_id.to_s] || profiles[chat_id.to_i]
@@ -75,6 +79,9 @@ module TelegramSupportBot
75
79
 
76
80
  def process_update(update, bot: DEFAULT_BOT_KEY)
77
81
  with_bot_context(bot) do
82
+ update_id = update['update_id'] || update[:update_id]
83
+ return if duplicate_update?(update_id)
84
+
78
85
  # Handle different types of updates
79
86
  if update['message']
80
87
  # Process standard messages
@@ -91,6 +98,8 @@ module TelegramSupportBot
91
98
  # Log or handle unknown update types
92
99
  puts "Received an unknown type of update: #{update}"
93
100
  end
101
+
102
+ mark_update_as_processed(update_id)
94
103
  end
95
104
  end
96
105
 
@@ -517,11 +526,45 @@ module TelegramSupportBot
517
526
  return unless should_forward_start_to_support?(chat_id)
518
527
  return unless forward_message_to_support_chat(message, chat_id: chat_id)
519
528
 
520
- start_forwarded_users[chat_id] = true
529
+ start_forwarded_users[chat_id] = start_forwarded_marker_value
530
+ rescue StandardError => error
531
+ warn_start_forwarding_failure(chat_id: chat_id, message_id: message['message_id'], error: error)
521
532
  end
522
533
 
523
534
  def start_forwarded_to_support?(chat_id)
524
- !start_forwarded_users[chat_id].nil?
535
+ marker_present?(start_forwarded_users[chat_id])
536
+ end
537
+
538
+ def duplicate_update?(update_id)
539
+ return false if update_id.nil?
540
+
541
+ marker_present?(processed_updates[update_id])
542
+ end
543
+
544
+ def mark_update_as_processed(update_id)
545
+ return if update_id.nil?
546
+
547
+ processed_updates[update_id] = processed_update_marker_value
548
+ end
549
+
550
+ def marker_present?(value)
551
+ return false if value.nil?
552
+ return value if value == true || value == false
553
+ return value.fetch(:present, value['present']) if value.is_a?(Hash)
554
+
555
+ true
556
+ end
557
+
558
+ def start_forwarded_marker_value
559
+ { present: true }
560
+ end
561
+
562
+ def processed_update_marker_value
563
+ { present: true }
564
+ end
565
+
566
+ def warn_start_forwarding_failure(chat_id:, message_id:, error:)
567
+ warn "Failed to forward initial /start for chat_id=#{chat_id} message_id=#{message_id}: #{error.class}: #{error.message}"
525
568
  end
526
569
 
527
570
  def contact_known_for_user?(chat_id)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegram-support-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Buslaev