telegram-support-bot 0.1.09 → 0.1.11
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 +24 -0
- data/README.md +71 -1
- data/lib/telegram_support_bot/configuration.rb +2 -0
- data/lib/telegram_support_bot/state_store.rb +22 -2
- data/lib/telegram_support_bot/version.rb +1 -1
- data/lib/telegram_support_bot.rb +159 -47
- metadata +2 -3
- data/.idea/workspace.xml +0 -153
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eafa2876bee8ea437f0be0466f7954aa3f60999249246d00c81bae3dd87c4cf1
|
|
4
|
+
data.tar.gz: 679898610518848e5a7fa589d394eb9aa08ccc878fa2dcb9a906f40c88d72c87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24cb28ce54acf1f5bd0193c4e3932678ca65ba6e3844be6f36cbe46e8797afa76b20e0d3a2f661845c9b611106f661a23a4fd149e25b3b86eb49554d4a950ce0
|
|
7
|
+
data.tar.gz: 74e495bd8365e8831926e9e1d30bc59389ff5ac85acb0adbf445e2b4a20e4cb5cfdfd60e0163b09047c4e0418cdf3d357c5b85e79b42d1ee89cdd18347b71ba5
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.11] - 2026-02-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Optional host callback `on_user_command` for user-chat commands other than `/start`.
|
|
11
|
+
Return `true` to mark a command as handled and skip forwarding to support chat.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- User-chat `/start` detection now recognizes deep-link command forms and bot mentions:
|
|
15
|
+
`/start`, `/start <payload>`, `/start@botname`, `/start@botname <payload>`.
|
|
16
|
+
- Start commands in user chats continue to run welcome/contact onboarding and are not forwarded to the support chat.
|
|
17
|
+
|
|
18
|
+
## [0.1.10] - 2026-02-25
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Multi-bot runtime support in one process with keyed configuration and processing:
|
|
22
|
+
`configure(:bot_key)` and `process_update(update, bot: :bot_key)`.
|
|
23
|
+
- Bot-scoped adapter/state-store/scheduler instances to isolate mappings, reactions, and user profiles per bot key.
|
|
24
|
+
- Redis state namespace isolation for non-default bot keys.
|
|
25
|
+
- Multi-bot isolation test coverage for message mappings, support chat routing, contact profiles, and Redis namespaces.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Backward compatibility preserved: no-arg `configure` and `process_update` continue using `:default`.
|
|
29
|
+
- Development dependency resolution now has Ruby-version-aware constraints for older runtimes.
|
|
30
|
+
|
|
7
31
|
## [0.1.09] - 2026-02-13
|
|
8
32
|
|
|
9
33
|
### Added
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ gem 'telegram-support-bot'
|
|
|
27
27
|
|
|
28
28
|
1. **Create Your Bot** via BotFather on Telegram to get your bot token.
|
|
29
29
|
2. **Deploy Your Application** and set up a controller action for webhook callbacks, directing them
|
|
30
|
-
to `TelegramSupportBot.process_update
|
|
30
|
+
to `TelegramSupportBot.process_update` (or `TelegramSupportBot.process_update(update, bot: :your_key)` for keyed bots).
|
|
31
31
|
3. **Set the Webhook URL** using the Telegram Bot API to your controller action.
|
|
32
32
|
|
|
33
33
|
### Setting Up Your Telegram Bot
|
|
@@ -54,12 +54,59 @@ TelegramSupportBot.configure do |config|
|
|
|
54
54
|
config.require_contact_for_support = false
|
|
55
55
|
# Optional callback to persist/lookup user profile in your app.
|
|
56
56
|
config.on_contact_received = ->(profile) { YourUserMatcher.sync_from_telegram(profile) }
|
|
57
|
+
# Optional callback for user-chat commands other than /start.
|
|
58
|
+
# Return true to mark the command as handled (it will not be forwarded).
|
|
59
|
+
config.on_user_command = ->(command:, args:, chat_id:, message:, bot_username: nil) { false }
|
|
57
60
|
# Recommended in Kubernetes/multi-pod setup:
|
|
58
61
|
# config.state_store = :redis
|
|
59
62
|
# config.state_store_options = { url: ENV.fetch('REDIS_URL'), namespace: 'telegram_support_bot' }
|
|
60
63
|
end
|
|
61
64
|
```
|
|
62
65
|
|
|
66
|
+
### Multiple Bots In One Process
|
|
67
|
+
|
|
68
|
+
Use keyed configuration when one app process serves multiple Telegram bots/chats:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
TelegramSupportBot.configure(:default) do |config|
|
|
72
|
+
config.adapter_options = { token: ENV.fetch('TELEGRAM_DEFAULT_TOKEN') }
|
|
73
|
+
config.support_chat_id = ENV.fetch('TELEGRAM_DEFAULT_SUPPORT_CHAT_ID')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
TelegramSupportBot.configure(:partners) do |config|
|
|
77
|
+
config.adapter_options = { token: ENV.fetch('TELEGRAM_PARTNERS_TOKEN') }
|
|
78
|
+
config.support_chat_id = ENV.fetch('TELEGRAM_PARTNERS_SUPPORT_CHAT_ID')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Route incoming webhook to the matching bot key:
|
|
82
|
+
TelegramSupportBot.process_update(update, bot: :partners)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Backward compatibility is preserved:
|
|
86
|
+
- `TelegramSupportBot.configure { ... }` configures `:default`
|
|
87
|
+
- `TelegramSupportBot.process_update(update)` processes with `:default`
|
|
88
|
+
|
|
89
|
+
Typical webhook routing in Rails:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# legacy/default bot
|
|
93
|
+
post '/v2/telegram/support' => 'api/v2/telegram#support'
|
|
94
|
+
# keyed bot
|
|
95
|
+
post '/v2/telegram/:bot_key/support' => 'api/v2/telegram#support'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
def support
|
|
100
|
+
update = JSON.parse(request.body.read)
|
|
101
|
+
bot_key = params[:bot_key].presence&.to_sym || :default
|
|
102
|
+
TelegramSupportBot.process_update(update, bot: bot_key)
|
|
103
|
+
head :ok
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When using Redis state store, non-default bot keys are namespaced automatically
|
|
108
|
+
(`telegram_support_bot:<bot_key>:...`) so mappings/profiles do not leak across bots.
|
|
109
|
+
|
|
63
110
|
3. **Interact with Users**: Messages to your bot will be forwarded to the support chat, and replies
|
|
64
111
|
in the chat will be sent back to the users.
|
|
65
112
|
|
|
@@ -153,6 +200,29 @@ other user messages until contact is shared.
|
|
|
153
200
|
Support replies are routed by internal message mapping, so users do not need to change Telegram
|
|
154
201
|
forwarding privacy settings to receive replies.
|
|
155
202
|
|
|
203
|
+
## Host Handling For User Commands
|
|
204
|
+
|
|
205
|
+
You can handle user-chat commands in your app before they are forwarded to support:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
TelegramSupportBot.configure do |config|
|
|
209
|
+
config.on_user_command = lambda do |command:, args:, chat_id:, message:, bot_username: nil|
|
|
210
|
+
case command
|
|
211
|
+
when '/help'
|
|
212
|
+
TelegramSupportBot.adapter.send_message(chat_id: chat_id, text: 'How can we help?')
|
|
213
|
+
true
|
|
214
|
+
else
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Behavior:
|
|
222
|
+
- Triggered only for user-chat commands that start with `/` and are not `/start`.
|
|
223
|
+
- Receives `command` (normalized to lowercase), `bot_username` (if present), and `args` (text after command).
|
|
224
|
+
- Return `true` to stop forwarding to support chat; return `false`/`nil` to keep default forwarding.
|
|
225
|
+
|
|
156
226
|
## State Storage (Single Pod vs Multi-Pod)
|
|
157
227
|
|
|
158
228
|
By default, runtime state is stored in-memory (`state_store = :memory`). This is fine for local
|
|
@@ -7,6 +7,7 @@ module TelegramSupportBot
|
|
|
7
7
|
:ignore_non_command_messages, :non_command_message_response,
|
|
8
8
|
:request_contact_on_start, :require_contact_for_support, :contact_request_message,
|
|
9
9
|
:contact_received_message, :contact_invalid_message, :on_contact_received,
|
|
10
|
+
:on_user_command,
|
|
10
11
|
:state_store, :state_store_options, :mapping_ttl_seconds,
|
|
11
12
|
:reaction_count_ttl_seconds, :user_profile_ttl_seconds
|
|
12
13
|
|
|
@@ -25,6 +26,7 @@ module TelegramSupportBot
|
|
|
25
26
|
@contact_received_message = 'Thanks! We have saved your phone number.'
|
|
26
27
|
@contact_invalid_message = 'Please use the button below to share your own phone number.'
|
|
27
28
|
@on_contact_received = nil
|
|
29
|
+
@on_user_command = nil
|
|
28
30
|
@state_store = :memory
|
|
29
31
|
@state_store_options = {}
|
|
30
32
|
@mapping_ttl_seconds = 30 * 24 * 60 * 60
|
|
@@ -27,9 +27,9 @@ module TelegramSupportBot
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def self.build(configuration)
|
|
30
|
+
def self.build(configuration, bot_key: TelegramSupportBot::DEFAULT_BOT_KEY)
|
|
31
31
|
backend = configuration.state_store.to_sym
|
|
32
|
-
options = configuration.state_store_options
|
|
32
|
+
options = normalize_options(configuration.state_store_options)
|
|
33
33
|
|
|
34
34
|
case backend
|
|
35
35
|
when :memory
|
|
@@ -41,6 +41,7 @@ module TelegramSupportBot
|
|
|
41
41
|
)
|
|
42
42
|
when :redis
|
|
43
43
|
require_relative 'state_stores/redis'
|
|
44
|
+
options = isolate_redis_namespace(options, bot_key)
|
|
44
45
|
StateStores::Redis.new(
|
|
45
46
|
mapping_ttl_seconds: configuration.mapping_ttl_seconds,
|
|
46
47
|
reaction_count_ttl_seconds: configuration.reaction_count_ttl_seconds,
|
|
@@ -51,6 +52,25 @@ module TelegramSupportBot
|
|
|
51
52
|
raise ArgumentError, "Unsupported state store backend: #{backend}"
|
|
52
53
|
end
|
|
53
54
|
end
|
|
55
|
+
|
|
56
|
+
def self.normalize_options(options)
|
|
57
|
+
(options || {}).each_with_object({}) do |(key, value), memo|
|
|
58
|
+
memo[key.to_sym] = value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
private_class_method :normalize_options
|
|
62
|
+
|
|
63
|
+
def self.isolate_redis_namespace(options, bot_key)
|
|
64
|
+
normalized_bot_key = (bot_key || TelegramSupportBot::DEFAULT_BOT_KEY).to_sym
|
|
65
|
+
return options if normalized_bot_key == TelegramSupportBot::DEFAULT_BOT_KEY
|
|
66
|
+
|
|
67
|
+
base_namespace = options[:namespace] || StateStores::Redis::DEFAULT_NAMESPACE
|
|
68
|
+
suffix = ":#{normalized_bot_key}"
|
|
69
|
+
namespaced = base_namespace.to_s.end_with?(suffix) ? base_namespace.to_s : "#{base_namespace}#{suffix}"
|
|
70
|
+
|
|
71
|
+
options.merge(namespace: namespaced)
|
|
72
|
+
end
|
|
73
|
+
private_class_method :isolate_redis_namespace
|
|
54
74
|
end
|
|
55
75
|
end
|
|
56
76
|
|
data/lib/telegram_support_bot.rb
CHANGED
|
@@ -10,70 +10,153 @@ require_relative 'telegram_support_bot/adapters/telegram_bot'
|
|
|
10
10
|
require_relative 'telegram_support_bot/adapters/telegram_bot_ruby'
|
|
11
11
|
|
|
12
12
|
module TelegramSupportBot
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
DEFAULT_BOT_KEY = :default
|
|
14
|
+
BOT_CONTEXT_THREAD_KEY = :telegram_support_bot_current_bot_key
|
|
15
15
|
|
|
16
|
+
class << self
|
|
16
17
|
# Provides a method to configure the gem.
|
|
17
|
-
def configure
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
def configure(bot_key = DEFAULT_BOT_KEY)
|
|
19
|
+
key = normalize_bot_key(bot_key)
|
|
20
|
+
config = configuration(key)
|
|
21
|
+
yield(config) if block_given?
|
|
22
|
+
config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configuration(bot_key = nil)
|
|
26
|
+
key = bot_key.nil? ? current_bot_key : normalize_bot_key(bot_key)
|
|
27
|
+
configurations[key] ||= Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configuration=(config)
|
|
31
|
+
configurations[DEFAULT_BOT_KEY] = config
|
|
20
32
|
end
|
|
21
33
|
|
|
22
34
|
# Lazily builds and returns the adapter based on the current configuration.
|
|
23
35
|
# This method initializes the adapter when it's first called.
|
|
24
|
-
def adapter
|
|
25
|
-
|
|
36
|
+
def adapter(bot_key = nil)
|
|
37
|
+
key = bot_key.nil? ? current_bot_key : normalize_bot_key(bot_key)
|
|
38
|
+
adapters[key] ||= AdapterFactory.build(configuration(key).adapter, configuration(key).adapter_options)
|
|
26
39
|
end
|
|
27
40
|
|
|
28
|
-
def state_store
|
|
29
|
-
|
|
41
|
+
def state_store(bot_key = nil)
|
|
42
|
+
key = bot_key.nil? ? current_bot_key : normalize_bot_key(bot_key)
|
|
43
|
+
state_stores[key] ||= StateStore.build(configuration(key), bot_key: key)
|
|
30
44
|
end
|
|
31
45
|
|
|
32
|
-
def message_map
|
|
33
|
-
state_store.message_map
|
|
46
|
+
def message_map(bot_key = nil)
|
|
47
|
+
state_store(bot_key).message_map
|
|
34
48
|
end
|
|
35
49
|
|
|
36
|
-
def reverse_message_map
|
|
37
|
-
state_store.reverse_message_map
|
|
50
|
+
def reverse_message_map(bot_key = nil)
|
|
51
|
+
state_store(bot_key).reverse_message_map
|
|
38
52
|
end
|
|
39
53
|
|
|
40
|
-
def reaction_count_state
|
|
41
|
-
state_store.reaction_count_state
|
|
54
|
+
def reaction_count_state(bot_key = nil)
|
|
55
|
+
state_store(bot_key).reaction_count_state
|
|
42
56
|
end
|
|
43
57
|
|
|
44
|
-
def user_profiles
|
|
45
|
-
state_store.user_profiles
|
|
58
|
+
def user_profiles(bot_key = nil)
|
|
59
|
+
state_store(bot_key).user_profiles
|
|
46
60
|
end
|
|
47
61
|
|
|
48
|
-
def user_profile(chat_id)
|
|
49
|
-
|
|
62
|
+
def user_profile(chat_id, bot: nil)
|
|
63
|
+
profiles = user_profiles(bot)
|
|
64
|
+
profiles[chat_id] || profiles[chat_id.to_s] || profiles[chat_id.to_i]
|
|
50
65
|
end
|
|
51
66
|
|
|
52
|
-
def scheduler
|
|
53
|
-
|
|
67
|
+
def scheduler(bot_key = nil)
|
|
68
|
+
key = bot_key.nil? ? current_bot_key : normalize_bot_key(bot_key)
|
|
69
|
+
schedulers[key] ||= AutoAwayScheduler.new(adapter(key), configuration(key))
|
|
54
70
|
end
|
|
55
71
|
|
|
56
|
-
def process_update(update)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
def process_update(update, bot: DEFAULT_BOT_KEY)
|
|
73
|
+
with_bot_context(bot) do
|
|
74
|
+
# Handle different types of updates
|
|
75
|
+
if update['message']
|
|
76
|
+
# Process standard messages
|
|
77
|
+
process_message(update['message'])
|
|
78
|
+
elsif update['message_reaction']
|
|
79
|
+
process_message_reaction(update['message_reaction'])
|
|
80
|
+
elsif update['message_reaction_count']
|
|
81
|
+
process_message_reaction_count(update['message_reaction_count'])
|
|
82
|
+
elsif update['my_chat_member']
|
|
83
|
+
# Handle the bot being added to or removed from a chat
|
|
84
|
+
handle_my_chat_member_update(update['my_chat_member'])
|
|
85
|
+
# Add other update types as needed
|
|
86
|
+
else
|
|
87
|
+
# Log or handle unknown update types
|
|
88
|
+
puts "Received an unknown type of update: #{update}"
|
|
89
|
+
end
|
|
72
90
|
end
|
|
73
91
|
end
|
|
74
92
|
|
|
93
|
+
# Reset the adapter instance (useful for testing or reconfiguration).
|
|
94
|
+
def reset_adapter!(bot_key = nil)
|
|
95
|
+
reset_registry!(adapters, bot_key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reset_state_store!(bot_key = nil)
|
|
99
|
+
reset_registry!(state_stores, bot_key)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reset_scheduler!(bot_key = nil)
|
|
103
|
+
reset_registry!(schedulers, bot_key)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def reset_configuration!(bot_key = nil)
|
|
107
|
+
reset_registry!(configurations, bot_key)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def reset!(bot_key = nil)
|
|
111
|
+
reset_adapter!(bot_key)
|
|
112
|
+
reset_state_store!(bot_key)
|
|
113
|
+
reset_scheduler!(bot_key)
|
|
114
|
+
reset_configuration!(bot_key)
|
|
115
|
+
end
|
|
116
|
+
|
|
75
117
|
private
|
|
76
118
|
|
|
119
|
+
def configurations
|
|
120
|
+
@configurations ||= {}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def adapters
|
|
124
|
+
@adapters ||= {}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def state_stores
|
|
128
|
+
@state_stores ||= {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def schedulers
|
|
132
|
+
@schedulers ||= {}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def current_bot_key
|
|
136
|
+
Thread.current[BOT_CONTEXT_THREAD_KEY] || DEFAULT_BOT_KEY
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def with_bot_context(bot_key)
|
|
140
|
+
normalized_bot_key = normalize_bot_key(bot_key)
|
|
141
|
+
previous_bot_key = Thread.current[BOT_CONTEXT_THREAD_KEY]
|
|
142
|
+
Thread.current[BOT_CONTEXT_THREAD_KEY] = normalized_bot_key
|
|
143
|
+
yield
|
|
144
|
+
ensure
|
|
145
|
+
Thread.current[BOT_CONTEXT_THREAD_KEY] = previous_bot_key
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def normalize_bot_key(bot_key)
|
|
149
|
+
(bot_key || DEFAULT_BOT_KEY).to_sym
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def reset_registry!(registry, bot_key)
|
|
153
|
+
if bot_key.nil?
|
|
154
|
+
registry.clear
|
|
155
|
+
else
|
|
156
|
+
registry.delete(normalize_bot_key(bot_key))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
77
160
|
def process_message(message)
|
|
78
161
|
chat_id = message.dig('chat', 'id')
|
|
79
162
|
|
|
@@ -90,12 +173,18 @@ module TelegramSupportBot
|
|
|
90
173
|
return
|
|
91
174
|
end
|
|
92
175
|
|
|
93
|
-
|
|
176
|
+
command_data = parse_command(message['text'])
|
|
177
|
+
|
|
178
|
+
if command_data && command_data[:command] == '/start'
|
|
94
179
|
adapter.send_message(chat_id: chat_id, text: configuration.welcome_message)
|
|
95
180
|
request_contact_from_user(chat_id: chat_id) if should_request_contact?(chat_id)
|
|
96
181
|
return
|
|
97
182
|
end
|
|
98
183
|
|
|
184
|
+
if command_data && handle_user_command(command_data: command_data, message: message, chat_id: chat_id)
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
99
188
|
if configuration.require_contact_for_support && !contact_known_for_user?(chat_id)
|
|
100
189
|
request_contact_from_user(chat_id: chat_id)
|
|
101
190
|
return
|
|
@@ -143,7 +232,9 @@ module TelegramSupportBot
|
|
|
143
232
|
end
|
|
144
233
|
|
|
145
234
|
def process_command(message, chat_id:)
|
|
146
|
-
|
|
235
|
+
command_data = parse_command(message['text'])
|
|
236
|
+
command = command_data && command_data[:command]
|
|
237
|
+
return unless command
|
|
147
238
|
|
|
148
239
|
case command
|
|
149
240
|
when '/start'
|
|
@@ -157,6 +248,35 @@ module TelegramSupportBot
|
|
|
157
248
|
end
|
|
158
249
|
end
|
|
159
250
|
|
|
251
|
+
def handle_user_command(command_data:, message:, chat_id:)
|
|
252
|
+
callback = configuration.on_user_command
|
|
253
|
+
return false unless callback.respond_to?(:call)
|
|
254
|
+
return false if command_data[:command] == '/start'
|
|
255
|
+
|
|
256
|
+
callback.call(
|
|
257
|
+
command: command_data[:command],
|
|
258
|
+
bot_username: command_data[:bot_username],
|
|
259
|
+
args: command_data[:args],
|
|
260
|
+
message: message,
|
|
261
|
+
chat_id: chat_id
|
|
262
|
+
)
|
|
263
|
+
rescue StandardError => error
|
|
264
|
+
warn "Failed to run on_user_command callback: #{error.class}: #{error.message}"
|
|
265
|
+
false
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def parse_command(text)
|
|
269
|
+
return nil unless text.is_a?(String)
|
|
270
|
+
|
|
271
|
+
token, args = text.strip.split(/\s+/, 2)
|
|
272
|
+
return nil unless token&.start_with?('/')
|
|
273
|
+
|
|
274
|
+
command, bot_username = token.split('@', 2)
|
|
275
|
+
return nil if command.nil? || command.empty?
|
|
276
|
+
|
|
277
|
+
{ command: command.downcase, bot_username: bot_username, args: args&.strip }
|
|
278
|
+
end
|
|
279
|
+
|
|
160
280
|
def process_reply_in_support_chat(message)
|
|
161
281
|
reply_to_message = message['reply_to_message']
|
|
162
282
|
reply_to_message_id = reply_to_message['message_id']
|
|
@@ -511,12 +631,4 @@ module TelegramSupportBot
|
|
|
511
631
|
|
|
512
632
|
end
|
|
513
633
|
|
|
514
|
-
# Reset the adapter instance (useful for testing or reconfiguration).
|
|
515
|
-
def self.reset_adapter!
|
|
516
|
-
@adapter = nil
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
def self.reset_state_store!
|
|
520
|
-
@state_store = nil
|
|
521
|
-
end
|
|
522
634
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: telegram-support-bot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Max Buslaev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -40,7 +40,6 @@ executables: []
|
|
|
40
40
|
extensions: []
|
|
41
41
|
extra_rdoc_files: []
|
|
42
42
|
files:
|
|
43
|
-
- ".idea/workspace.xml"
|
|
44
43
|
- ".rspec"
|
|
45
44
|
- CHANGELOG.md
|
|
46
45
|
- CODE_OF_CONDUCT.md
|
data/.idea/workspace.xml
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="AutoImportSettings">
|
|
4
|
-
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
-
</component>
|
|
6
|
-
<component name="ChangeListManager">
|
|
7
|
-
<list default="true" id="edf498b0-8552-42f1-846d-0c79d29ff991" name="Changes" comment="">
|
|
8
|
-
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
9
|
-
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
10
|
-
</list>
|
|
11
|
-
<option name="SHOW_DIALOG" value="false" />
|
|
12
|
-
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
13
|
-
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
14
|
-
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
15
|
-
</component>
|
|
16
|
-
<component name="FileTemplateManagerImpl">
|
|
17
|
-
<option name="RECENT_TEMPLATES">
|
|
18
|
-
<list>
|
|
19
|
-
<option value="Minitest Spec" />
|
|
20
|
-
</list>
|
|
21
|
-
</option>
|
|
22
|
-
</component>
|
|
23
|
-
<component name="Git.Settings">
|
|
24
|
-
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
25
|
-
</component>
|
|
26
|
-
<component name="MarkdownSettingsMigration">
|
|
27
|
-
<option name="stateVersion" value="1" />
|
|
28
|
-
</component>
|
|
29
|
-
<component name="ProjectColorInfo">{
|
|
30
|
-
"associatedIndex": 4
|
|
31
|
-
}</component>
|
|
32
|
-
<component name="ProjectId" id="2ciiVgDyGIIwTjG6xCPIunFNdqZ" />
|
|
33
|
-
<component name="ProjectLevelVcsManager">
|
|
34
|
-
<ConfirmationsSetting value="2" id="Add" />
|
|
35
|
-
</component>
|
|
36
|
-
<component name="ProjectViewState">
|
|
37
|
-
<option name="hideEmptyMiddlePackages" value="true" />
|
|
38
|
-
<option name="showLibraryContents" value="true" />
|
|
39
|
-
</component>
|
|
40
|
-
<component name="PropertiesComponent">{
|
|
41
|
-
"keyToString": {
|
|
42
|
-
"DefaultRubyCreateTestTemplate": "Minitest Spec",
|
|
43
|
-
"RSpec.Unnamed.executor": "Run",
|
|
44
|
-
"Ruby.scratch_61.executor": "Run",
|
|
45
|
-
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
|
46
|
-
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
47
|
-
"git-widget-placeholder": "main",
|
|
48
|
-
"last_opened_file_path": "/home/max/code/tg-sample",
|
|
49
|
-
"node.js.detected.package.eslint": "true",
|
|
50
|
-
"node.js.detected.package.tslint": "true",
|
|
51
|
-
"node.js.selected.package.eslint": "(autodetect)",
|
|
52
|
-
"node.js.selected.package.tslint": "(autodetect)",
|
|
53
|
-
"nodejs_package_manager_path": "npm",
|
|
54
|
-
"ruby.structure.view.model.defaults.configured": "true",
|
|
55
|
-
"settings.editor.selected.configurable": "org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable",
|
|
56
|
-
"vue.rearranger.settings.migration": "true"
|
|
57
|
-
},
|
|
58
|
-
"keyToStringList": {
|
|
59
|
-
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
|
60
|
-
"ruby"
|
|
61
|
-
]
|
|
62
|
-
}
|
|
63
|
-
}</component>
|
|
64
|
-
<component name="RecentsManager">
|
|
65
|
-
<key name="CopyFile.RECENT_KEYS">
|
|
66
|
-
<recent name="$PROJECT_DIR$/spec/telegram_support_bot/adapters" />
|
|
67
|
-
<recent name="$PROJECT_DIR$/spec/support" />
|
|
68
|
-
<recent name="$PROJECT_DIR$/spec" />
|
|
69
|
-
<recent name="$PROJECT_DIR$/lib/telegram_support_bot" />
|
|
70
|
-
<recent name="$PROJECT_DIR$/lib/telegram_support_bot/adapters" />
|
|
71
|
-
</key>
|
|
72
|
-
</component>
|
|
73
|
-
<component name="RunManager" selected="Ruby.scratch_61">
|
|
74
|
-
<configuration name="Unnamed" type="RSpecRunConfigurationType" factoryName="RSpec" nameIsGenerated="true">
|
|
75
|
-
<module name="telegram_support_bot" />
|
|
76
|
-
<predefined_log_file enabled="true" id="RUBY_RSPEC" />
|
|
77
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUBY_ARGS" VALUE="" />
|
|
78
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="WORK DIR" VALUE="" />
|
|
79
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SHOULD_USE_SDK" VALUE="false" />
|
|
80
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ALTERN_SDK_NAME" VALUE="" />
|
|
81
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="myPassParentEnvs" VALUE="true" />
|
|
82
|
-
<EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
|
|
83
|
-
<EXTENSION ID="RubyCoverageRunConfigurationExtension" track_test_folders="true" runner="rcov" ENABLE_BRANCH_COVERAGE="true" ENABLE_FORKED_COVERAGE="true">
|
|
84
|
-
<COVERAGE_PATTERN ENABLED="true">
|
|
85
|
-
<PATTERN REGEXPS="/.rvm/" INCLUDED="false" />
|
|
86
|
-
</COVERAGE_PATTERN>
|
|
87
|
-
</EXTENSION>
|
|
88
|
-
<EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
|
|
89
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TESTS_FOLDER_PATH" VALUE="$MODULE_DIR$/spec" />
|
|
90
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATH" VALUE="" />
|
|
91
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_SCRIPT_PATHS" VALUE="" />
|
|
92
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_RUNNER_PATH" VALUE="" />
|
|
93
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_FILE_MASK" VALUE="**/*_spec.rb" />
|
|
94
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_EXAMPLE_NAME" VALUE="" />
|
|
95
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_EXAMPLE_MATCHES" VALUE="false" />
|
|
96
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="LINE_NUMBER_EXAMPLE_IDS" VALUE="" />
|
|
97
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="TEST_TEST_TYPE" VALUE="ALL_IN_FOLDER" />
|
|
98
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPEC_ARGS" VALUE="" />
|
|
99
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="RUNNER_VERSION" VALUE="" />
|
|
100
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="USE_CUSTOM_SPEC_RUNNER" VALUE="false" />
|
|
101
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="DRB" VALUE="false" />
|
|
102
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="ZEUS" VALUE="false" />
|
|
103
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="SPRING" VALUE="false" />
|
|
104
|
-
<RSPEC_RUN_CONFIG_SETTINGS_ID NAME="FULL_BACKTRACE" VALUE="false" />
|
|
105
|
-
<method v="2" />
|
|
106
|
-
</configuration>
|
|
107
|
-
<configuration name="scratch_61" type="RubyRunConfigurationType" factoryName="Ruby" temporary="true">
|
|
108
|
-
<RUBY_RUN_CONFIG NAME="RUBY_ARGS" VALUE="" />
|
|
109
|
-
<RUBY_RUN_CONFIG NAME="WORK DIR" VALUE="$PROJECT_DIR$" />
|
|
110
|
-
<RUBY_RUN_CONFIG NAME="SHOULD_USE_SDK" VALUE="false" />
|
|
111
|
-
<RUBY_RUN_CONFIG NAME="ALTERN_SDK_NAME" VALUE="" />
|
|
112
|
-
<RUBY_RUN_CONFIG NAME="myPassParentEnvs" VALUE="true" />
|
|
113
|
-
<EXTENSION ID="BundlerRunConfigurationExtension" BUNDLE_MODE="AUTO" bundleExecEnabled="true" />
|
|
114
|
-
<EXTENSION ID="org.jetbrains.plugins.ruby.rails.run.RailsRunConfigurationExtension" SCRATCH_USE_RAILS_RUNNER="false" />
|
|
115
|
-
<RUBY_RUN_CONFIG NAME="SCRIPT_PATH" VALUE="$APPLICATION_CONFIG_DIR$/scratches/scratch_61.rb" />
|
|
116
|
-
<RUBY_RUN_CONFIG NAME="SCRIPT_ARGS" VALUE="" />
|
|
117
|
-
<method v="2" />
|
|
118
|
-
</configuration>
|
|
119
|
-
<list>
|
|
120
|
-
<item itemvalue="RSpec.Unnamed" />
|
|
121
|
-
<item itemvalue="Ruby.scratch_61" />
|
|
122
|
-
</list>
|
|
123
|
-
<recent_temporary>
|
|
124
|
-
<list>
|
|
125
|
-
<item itemvalue="Ruby.scratch_61" />
|
|
126
|
-
</list>
|
|
127
|
-
</recent_temporary>
|
|
128
|
-
</component>
|
|
129
|
-
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
130
|
-
<component name="SpringUtil" SPRING_PRE_LOADER_OPTION="true" RAKE_SPRING_PRE_LOADER_OPTION="true" RAILS_SPRING_PRE_LOADER_OPTION="true" />
|
|
131
|
-
<component name="TaskManager">
|
|
132
|
-
<task active="true" id="Default" summary="Default task">
|
|
133
|
-
<changelist id="edf498b0-8552-42f1-846d-0c79d29ff991" name="Changes" comment="" />
|
|
134
|
-
<created>1708600824931</created>
|
|
135
|
-
<option name="number" value="Default" />
|
|
136
|
-
<option name="presentableId" value="Default" />
|
|
137
|
-
<updated>1708600824931</updated>
|
|
138
|
-
<workItem from="1708600827139" duration="10157000" />
|
|
139
|
-
<workItem from="1708784534887" duration="682000" />
|
|
140
|
-
<workItem from="1708944130416" duration="99000" />
|
|
141
|
-
<workItem from="1708944249622" duration="627000" />
|
|
142
|
-
<workItem from="1708949675518" duration="80000" />
|
|
143
|
-
<workItem from="1708951183744" duration="310000" />
|
|
144
|
-
</task>
|
|
145
|
-
<servers />
|
|
146
|
-
</component>
|
|
147
|
-
<component name="TypeScriptGeneratedFilesManager">
|
|
148
|
-
<option name="version" value="3" />
|
|
149
|
-
</component>
|
|
150
|
-
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
|
151
|
-
<SUITE FILE_PATH="coverage/telegram_support_bot@Unnamed.rcov" NAME="Unnamed Coverage Results" MODIFIED="1708610177774" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" MODULE_NAME="telegram_support_bot" />
|
|
152
|
-
</component>
|
|
153
|
-
</project>
|