telegram-support-bot 0.1.12 → 0.1.14

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: f542e3e9a21dc2d7d9c45c9b65b19aec360bad95b60f0e4f999f3a9f4c532700
4
- data.tar.gz: 0136afb1b7bea4ce86afcd4f28b3af38f56743f4a2a4a87c29bfd33287f792e4
3
+ metadata.gz: 1aa04c608a30e8eb69bd222ab37e804224d4850472bc183d52dee74b397557c1
4
+ data.tar.gz: a84baa26e47cb70191bc6b5736e86de0187efc799f1735b47b31114c03d7066d
5
5
  SHA512:
6
- metadata.gz: 7599da9dab831b489bd6f687bf9d779fc5b3582a1a5546eea8d3c00e4febce46bddccc6aaa118177ad20e296f95fb1ff8f8bc54f013e30e41e462c9e3f8aa576
7
- data.tar.gz: 00f33f8a9eed3902cbde9584d06a781880f6efe9a0cbd6f1405ac2450e82da1e0605a3c0eed25520e9c6c46e8291e40495276cd08cc59a0e92c7494023c35a13
6
+ metadata.gz: a4e14a5cc0d07a7f05764eaf0577eb965ef9db5e86e276d74196ed3e08c9f0956315e0bec3e9b5b5b2ec1b94f7429186e299b16a80473fff24731122ba481263
7
+ data.tar.gz: b875c18abaef4ad4dc5ce57d2e94635f354ca3e69d055fc2e9a0f8bdf626527dcd3ba16a021decd58d30746d0dfc7900f21aff707223955ccba8661d051ddb36
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.14] - 2026-02-26
8
+
9
+ ### Added
10
+ - Optional `forward_start_to_support` configuration to forward the first user `/start`
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
+
7
19
  ## [0.1.12] - 2026-02-26
8
20
 
9
21
  ### Fixed
data/README.md CHANGED
@@ -50,8 +50,13 @@ TelegramSupportBot.configure do |config|
50
50
  config.non_command_message_response = 'I only respond to commands. Please use /start.'
51
51
  # Optional: ask users to share their phone once for account lookup.
52
52
  config.request_contact_on_start = true
53
+ # Optional: forward the user's first /start message to support chat.
54
+ config.forward_start_to_support = false
53
55
  # Optional: block forwarding until contact is shared.
54
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
55
60
  # Optional callback to persist/lookup user profile in your app.
56
61
  config.on_contact_received = ->(profile) { YourUserMatcher.sync_from_telegram(profile) }
57
62
  # Optional callback for user-chat commands other than /start.
@@ -222,6 +227,7 @@ Behavior:
222
227
  - Triggered only for user-chat commands that start with `/` and are not `/start`.
223
228
  - Receives `command` (normalized to lowercase), `bot_username` (if present), and `args` (text after command).
224
229
  - Return `true` to stop forwarding to support chat; return `false`/`nil` to keep default forwarding.
230
+ - `/start` can be forwarded once per user to support chat when `forward_start_to_support = true`.
225
231
 
226
232
  ## State Storage (Single Pod vs Multi-Pod)
227
233
 
@@ -242,6 +248,7 @@ TelegramSupportBot.configure do |config|
242
248
  # config.mapping_ttl_seconds = 30 * 24 * 60 * 60
243
249
  # config.reaction_count_ttl_seconds = 7 * 24 * 60 * 60
244
250
  # config.user_profile_ttl_seconds = nil
251
+ # config.processed_update_ttl_seconds = 24 * 60 * 60
245
252
  end
246
253
  ```
247
254
 
@@ -6,10 +6,10 @@ module TelegramSupportBot
6
6
  :auto_away_message, :auto_away_interval, :ignore_unknown_commands,
7
7
  :ignore_non_command_messages, :non_command_message_response,
8
8
  :request_contact_on_start, :require_contact_for_support, :contact_request_message,
9
- :contact_received_message, :contact_invalid_message, :on_contact_received,
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
@@ -25,6 +25,7 @@ module TelegramSupportBot
25
25
  @contact_request_message = 'Please share your phone number so we can quickly identify your account.'
26
26
  @contact_received_message = 'Thanks! We have saved your phone number.'
27
27
  @contact_invalid_message = 'Please use the button below to share your own phone number.'
28
+ @forward_start_to_support = false
28
29
  @on_contact_received = nil
29
30
  @on_user_command = nil
30
31
  @state_store = :memory
@@ -32,6 +33,7 @@ module TelegramSupportBot
32
33
  @mapping_ttl_seconds = 30 * 24 * 60 * 60
33
34
  @reaction_count_ttl_seconds = 7 * 24 * 60 * 60
34
35
  @user_profile_ttl_seconds = nil
36
+ @processed_update_ttl_seconds = 24 * 60 * 60
35
37
  end
36
38
  end
37
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
@@ -11,6 +11,8 @@ module TelegramSupportBot
11
11
  @reverse_message_map = {}
12
12
  @reaction_count_state = {}
13
13
  @user_profiles = {}
14
+ @start_forwarded_users = {}
15
+ @processed_updates = {}
14
16
  end
15
17
 
16
18
  def message_map
@@ -49,6 +51,24 @@ module TelegramSupportBot
49
51
  )
50
52
  end
51
53
 
54
+ def start_forwarded_users
55
+ @start_forwarded_users_proxy ||= StateStore::MapProxy.new(
56
+ get_proc: ->(key) { get_start_forwarded_user(key) },
57
+ set_proc: ->(key, value) { set_start_forwarded_user(key, value) },
58
+ clear_proc: -> { clear_start_forwarded_users },
59
+ size_proc: -> { start_forwarded_users_size }
60
+ )
61
+ end
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
+
52
72
  def get_message_mapping(key)
53
73
  synchronize { @message_map[normalize_key(key)] }
54
74
  end
@@ -113,6 +133,38 @@ module TelegramSupportBot
113
133
  synchronize { @user_profiles.size }
114
134
  end
115
135
 
136
+ def get_start_forwarded_user(key)
137
+ synchronize { @start_forwarded_users[normalize_key(key)] }
138
+ end
139
+
140
+ def set_start_forwarded_user(key, value)
141
+ synchronize { @start_forwarded_users[normalize_key(key)] = value }
142
+ end
143
+
144
+ def clear_start_forwarded_users
145
+ synchronize { @start_forwarded_users.clear }
146
+ end
147
+
148
+ def start_forwarded_users_size
149
+ synchronize { @start_forwarded_users.size }
150
+ end
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
+
116
168
  private
117
169
 
118
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
@@ -57,6 +59,24 @@ module TelegramSupportBot
57
59
  )
58
60
  end
59
61
 
62
+ def start_forwarded_users
63
+ @start_forwarded_users_proxy ||= StateStore::MapProxy.new(
64
+ get_proc: ->(key) { get_start_forwarded_user(key) },
65
+ set_proc: ->(key, value) { set_start_forwarded_user(key, value) },
66
+ clear_proc: -> { clear_start_forwarded_users },
67
+ size_proc: -> { start_forwarded_users_size }
68
+ )
69
+ end
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
+
60
80
  def get_message_mapping(key)
61
81
  payload = parse_json(@redis.get(map_key(:message_map, key)))
62
82
  symbolize_hash(payload)
@@ -124,6 +144,38 @@ module TelegramSupportBot
124
144
  count_by_prefix(prefix(:user_profiles))
125
145
  end
126
146
 
147
+ def get_start_forwarded_user(key)
148
+ parse_json(@redis.get(map_key(:start_forwarded_users, key)))
149
+ end
150
+
151
+ def set_start_forwarded_user(key, value)
152
+ write_json(map_key(:start_forwarded_users, key), value, @user_profile_ttl_seconds)
153
+ end
154
+
155
+ def clear_start_forwarded_users
156
+ delete_by_prefix(prefix(:start_forwarded_users))
157
+ end
158
+
159
+ def start_forwarded_users_size
160
+ count_by_prefix(prefix(:start_forwarded_users))
161
+ end
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
+
127
179
  private
128
180
 
129
181
  def prefix(name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TelegramSupportBot
4
- VERSION = "0.1.12"
4
+ VERSION = "0.1.14"
5
5
  end
@@ -59,6 +59,14 @@ module TelegramSupportBot
59
59
  state_store(bot_key).user_profiles
60
60
  end
61
61
 
62
+ def start_forwarded_users(bot_key = nil)
63
+ state_store(bot_key).start_forwarded_users
64
+ end
65
+
66
+ def processed_updates(bot_key = nil)
67
+ state_store(bot_key).processed_updates
68
+ end
69
+
62
70
  def user_profile(chat_id, bot: nil)
63
71
  profiles = user_profiles(bot)
64
72
  profiles[chat_id] || profiles[chat_id.to_s] || profiles[chat_id.to_i]
@@ -71,6 +79,9 @@ module TelegramSupportBot
71
79
 
72
80
  def process_update(update, bot: DEFAULT_BOT_KEY)
73
81
  with_bot_context(bot) do
82
+ update_id = update['update_id'] || update[:update_id]
83
+ return if duplicate_update?(update_id)
84
+
74
85
  # Handle different types of updates
75
86
  if update['message']
76
87
  # Process standard messages
@@ -87,6 +98,8 @@ module TelegramSupportBot
87
98
  # Log or handle unknown update types
88
99
  puts "Received an unknown type of update: #{update}"
89
100
  end
101
+
102
+ mark_update_as_processed(update_id)
90
103
  end
91
104
  end
92
105
 
@@ -178,6 +191,7 @@ module TelegramSupportBot
178
191
  if command_data && command_data[:command] == '/start'
179
192
  adapter.send_message(chat_id: chat_id, text: configuration.welcome_message)
180
193
  request_contact_from_user(chat_id: chat_id) if should_request_contact?(chat_id)
194
+ forward_start_to_support_chat_if_needed(message, chat_id: chat_id)
181
195
  return
182
196
  end
183
197
 
@@ -363,6 +377,7 @@ module TelegramSupportBot
363
377
  end
364
378
  end
365
379
  # scheduler.schedule_auto_away_message(message_id, message_chat_id)
380
+ result
366
381
  end
367
382
 
368
383
  def handle_my_chat_member_update(update)
@@ -503,6 +518,39 @@ module TelegramSupportBot
503
518
  configuration.request_contact_on_start && !contact_known_for_user?(chat_id)
504
519
  end
505
520
 
521
+ def should_forward_start_to_support?(chat_id)
522
+ configuration.forward_start_to_support && !start_forwarded_to_support?(chat_id)
523
+ end
524
+
525
+ def forward_start_to_support_chat_if_needed(message, chat_id:)
526
+ return unless should_forward_start_to_support?(chat_id)
527
+ return unless forward_message_to_support_chat(message, chat_id: chat_id)
528
+
529
+ start_forwarded_users[chat_id] = true
530
+ rescue StandardError => error
531
+ warn_start_forwarding_failure(chat_id: chat_id, message_id: message['message_id'], error: error)
532
+ end
533
+
534
+ def start_forwarded_to_support?(chat_id)
535
+ !start_forwarded_users[chat_id].nil?
536
+ end
537
+
538
+ def duplicate_update?(update_id)
539
+ return false if update_id.nil?
540
+
541
+ !processed_updates[update_id].nil?
542
+ end
543
+
544
+ def mark_update_as_processed(update_id)
545
+ return if update_id.nil?
546
+
547
+ processed_updates[update_id] = true
548
+ end
549
+
550
+ def warn_start_forwarding_failure(chat_id:, message_id:, error:)
551
+ warn "Failed to forward initial /start for chat_id=#{chat_id} message_id=#{message_id}: #{error.class}: #{error.message}"
552
+ end
553
+
506
554
  def contact_known_for_user?(chat_id)
507
555
  !user_profile(chat_id).nil?
508
556
  end
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.12
4
+ version: 0.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Buslaev