telegram-support-bot 0.1.13 → 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 +7 -1
- data/README.md +4 -0
- data/lib/telegram_support_bot/configuration.rb +2 -1
- data/lib/telegram_support_bot/state_store.rb +2 -0
- data/lib/telegram_support_bot/state_stores/memory.rb +26 -0
- data/lib/telegram_support_bot/state_stores/redis.rb +28 -1
- data/lib/telegram_support_bot/version.rb +1 -1
- data/lib/telegram_support_bot.rb +27 -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,11 +4,17 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
## [0.1.
|
|
7
|
+
## [0.1.14] - 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.
|
|
12
18
|
|
|
13
19
|
## [0.1.12] - 2026-02-26
|
|
14
20
|
|
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,
|
|
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)
|
data/lib/telegram_support_bot.rb
CHANGED
|
@@ -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
|
|
|
@@ -518,12 +527,30 @@ module TelegramSupportBot
|
|
|
518
527
|
return unless forward_message_to_support_chat(message, chat_id: chat_id)
|
|
519
528
|
|
|
520
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)
|
|
521
532
|
end
|
|
522
533
|
|
|
523
534
|
def start_forwarded_to_support?(chat_id)
|
|
524
535
|
!start_forwarded_users[chat_id].nil?
|
|
525
536
|
end
|
|
526
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
|
+
|
|
527
554
|
def contact_known_for_user?(chat_id)
|
|
528
555
|
!user_profile(chat_id).nil?
|
|
529
556
|
end
|