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 +4 -4
- data/CHANGELOG.md +9 -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 +45 -2
- 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: 628391b6587ec2b35dc5ce2984276eb42f4f5d5e992205ef06682f26ad41b8cf
|
|
4
|
+
data.tar.gz: 9edfd943aee7356088dede4f647609ee9f9c6cc34d9ba82a3bbdb61feef241fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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,
|
|
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
|
|
|
@@ -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] =
|
|
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
|
-
|
|
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)
|