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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +7 -0
- data/lib/telegram_support_bot/configuration.rb +4 -2
- data/lib/telegram_support_bot/state_store.rb +2 -0
- data/lib/telegram_support_bot/state_stores/memory.rb +52 -0
- data/lib/telegram_support_bot/state_stores/redis.rb +53 -1
- data/lib/telegram_support_bot/version.rb +1 -1
- data/lib/telegram_support_bot.rb +48 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1aa04c608a30e8eb69bd222ab37e804224d4850472bc183d52dee74b397557c1
|
|
4
|
+
data.tar.gz: a84baa26e47cb70191bc6b5736e86de0187efc799f1735b47b31114c03d7066d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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)
|
data/lib/telegram_support_bot.rb
CHANGED
|
@@ -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
|