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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15900469360f291d85f69a938dc8f25cc9826ec0fab4105c954e0f58aea48868
4
- data.tar.gz: 441423cab220f879327d6474bafb069c8fa099966f7cc45c6ac2896944a52238
3
+ metadata.gz: eafa2876bee8ea437f0be0466f7954aa3f60999249246d00c81bae3dd87c4cf1
4
+ data.tar.gz: 679898610518848e5a7fa589d394eb9aa08ccc878fa2dcb9a906f40c88d72c87
5
5
  SHA512:
6
- metadata.gz: 5fa0913384b4a0ecc87dd7c37cc546fc6d8a5e937c9154b7bdeb91d80ed3070cb33d4f5bc6d9d051fe5d84b865bfc904638528e7259cb4a366f5e978e448ecf3
7
- data.tar.gz: c844cfaf107d7bed62378b7dfd170d28ee8a4e81f17802c7924cc8629a0dc3485242b5966dbc0371bdddffa2bbd5e861068e0970ebb5d5561c7da1b3a663b06d
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TelegramSupportBot
4
- VERSION = "0.1.09"
4
+ VERSION = "0.1.11"
5
5
  end
@@ -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
- class << self
14
- attr_accessor :configuration
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
- self.configuration ||= Configuration.new
19
- yield(configuration) if block_given?
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
- @adapter ||= AdapterFactory.build(configuration.adapter, configuration.adapter_options)
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
- @state_store ||= StateStore.build(configuration)
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
- user_profiles[chat_id] || user_profiles[chat_id.to_s] || user_profiles[chat_id.to_i]
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
- @scheduler ||= AutoAwayScheduler.new(adapter, configuration)
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
- # Handle different types of updates
58
- if update['message']
59
- # Process standard messages
60
- process_message(update['message'])
61
- elsif update['message_reaction']
62
- process_message_reaction(update['message_reaction'])
63
- elsif update['message_reaction_count']
64
- process_message_reaction_count(update['message_reaction_count'])
65
- elsif update['my_chat_member']
66
- # Handle the bot being added to or removed from a chat
67
- handle_my_chat_member_update(update['my_chat_member'])
68
- # Add other update types as needed
69
- else
70
- # Log or handle unknown update types
71
- puts "Received an unknown type of update: #{update}"
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
- if message['text'] == '/start'
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
- command = message['text'].split(/[ \@]/).first.downcase # Extract the command, normalize to lowercase
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.09
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-13 00:00:00.000000000 Z
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
- &quot;associatedIndex&quot;: 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
- &quot;keyToString&quot;: {
42
- &quot;DefaultRubyCreateTestTemplate&quot;: &quot;Minitest Spec&quot;,
43
- &quot;RSpec.Unnamed.executor&quot;: &quot;Run&quot;,
44
- &quot;Ruby.scratch_61.executor&quot;: &quot;Run&quot;,
45
- &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
46
- &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
47
- &quot;git-widget-placeholder&quot;: &quot;main&quot;,
48
- &quot;last_opened_file_path&quot;: &quot;/home/max/code/tg-sample&quot;,
49
- &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
50
- &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
51
- &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
52
- &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
53
- &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
54
- &quot;ruby.structure.view.model.defaults.configured&quot;: &quot;true&quot;,
55
- &quot;settings.editor.selected.configurable&quot;: &quot;org.jetbrains.plugins.ruby.settings.RubyActiveModuleSdkConfigurable&quot;,
56
- &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
57
- },
58
- &quot;keyToStringList&quot;: {
59
- &quot;com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File&quot;: [
60
- &quot;ruby&quot;
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>