telegem 3.0.6 โ†’ 3.1.3

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.
data/Readme.md CHANGED
@@ -2,7 +2,14 @@ Telegem ๐Ÿค–โšก
2
2
 
3
3
  Modern, blazing-fast async Telegram Bot API for Ruby - Inspired by Telegraf, built for performance.
4
4
 
5
- ![Gem Version](https://badge.fury.io/rb/telegem.svg) ![GitLab](https://img.shields.io/badge/gitlab-telegem-orange) ![Ruby Version](https://img.shields.io/badge/Ruby-3.0+-red.svg) ![License](https://img.shields.io/badge/License-MIT-blue.svg) ![Async I/O](https://img.shields.io/badge/Async-I/O-green.svg)
5
+ ![Gem Version](https://badge.fury.io/rb/telegem.svg) ![GitLab](https://img.shields.io/badge/gitlab-telegem-orange) ![Ruby Version](https://img.shields.io/badge/Ruby-3.0+-red.svg) ![License](https://img.shields.io/badge/License-MIT-blue.svg) ![Async I/O](https://img.shields.io/badge/Async-I/O-green.svg)
6
+
7
+ ![GitLab stars](https://img.shields.io/gitlab/stars/ruby-telegem/telegem?style=for-the-badge&logo=gitlab&color=orange)
8
+ ![GitLab contributors](https://img.shields.io/gitlab/contributors/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
9
+ ![GitLab last commit](https://img.shields.io/gitlab/last-commit/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
10
+ ![GitLab license](https://img.shields.io/gitlab/license/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
11
+
12
+
6
13
 
7
14
  Blazing-fast, modern Telegram Bot framework for Ruby. Inspired by Telegraf.js, built for performance with true async/await patterns.
8
15
 
@@ -63,7 +70,7 @@ Interactive Example
63
70
  ```ruby
64
71
  # Pizza ordering bot example
65
72
  bot.command('order') do |ctx|
66
- keyboard = Telegem::Markup.keyboard do
73
+ keyboard = Telegem.keyboard do
67
74
  row "๐Ÿ• Margherita", "๐Ÿ• Pepperoni"
68
75
  row "๐Ÿฅค Drinks", "๐Ÿฐ Dessert"
69
76
  row "๐Ÿ“ž Support", "โŒ Cancel"
@@ -75,11 +82,6 @@ end
75
82
 
76
83
  ---
77
84
 
78
- ๐Ÿ“ธ See It in Action
79
-
80
- <img src="https://i.postimg.cc/W3fdnx45/DA5D1EC7-F2E2-4243-87AB-841F5467F70C.png">
81
-
82
- Example bot with interactive keyboard and scene-based flow
83
85
 
84
86
  ---
85
87
 
@@ -108,22 +110,7 @@ Perfect For:
108
110
 
109
111
  ๐Ÿ“š Documentation
110
112
 
111
- Getting Started
112
113
 
113
- 1. How to Use - Beginner-friendly tutorial
114
- 2. Usage Guide - Advanced patterns & best practices
115
- 3. Cookbook - Copy-paste recipes for common tasks
116
- 4. API Reference - Complete method documentation
117
-
118
- Quick Links
119
-
120
- ยท [Creating Your First Bot](https://gitlab.com/ruby-telegem/telegem/-/blob/main/docs/QuickStart.md)
121
- ยท [Understanding Context (ctx)](https://gitlab.com/ruby-telegem/telegem/-/blob/main/docs/How_to_use.md)
122
- ยท [Building Scenes](https://gitlab.com/ruby-telegem/telegem/-/blob/main/docs/Usage.md)
123
- ยท Middleware Patterns
124
- ยท Deployment Guide
125
-
126
- ---
127
114
 
128
115
  ๐Ÿงฉ Advanced Features
129
116
 
@@ -217,26 +204,6 @@ CMD ["ruby", "bot.rb"]
217
204
 
218
205
  ---
219
206
 
220
- ๐Ÿงช Testing
221
-
222
- ```ruby
223
- # Unit test scenes
224
- describe RegistrationScene do
225
- it "asks for name on enter" do
226
- ctx = mock_context
227
- scene = bot.scenes[:registration]
228
- expect(ctx).to receive(:reply).with("What's your name?")
229
- scene.enter(ctx)
230
- end
231
- end
232
-
233
- # Integration testing
234
- bot.command('test') { |ctx| ctx.reply("Working!") }
235
-
236
- update = mock_update(text: '/test')
237
- bot.process(update)
238
- # Verify reply sent
239
- ```
240
207
 
241
208
  ---
242
209
 
@@ -259,19 +226,9 @@ my_bot/
259
226
 
260
227
  ---
261
228
 
262
- ๐Ÿค Contributing
229
+ [๐Ÿค contributing](https://gitlab.com/ruby-telegem/telegem/-/blob/main/Contributing.md)
263
230
 
264
- We love contributions! Whether you're fixing bugs, adding features, or improving documentation, all help is welcome.
265
231
 
266
- How to Contribute:
267
-
268
- 1. Read CONTRIBUTING.md for detailed guidelines
269
- 2. Fork the repository on GitLab
270
- 3. Create a feature branch (git checkout -b feature/amazing-thing)
271
- 4. Make your changes and add tests
272
- 5. Run tests (rake spec)
273
- 6. Commit with clear messages (git commit -m 'Add amazing thing')
274
- 7. Push and open a Merge Request
275
232
 
276
233
  Development Setup:
277
234
 
@@ -298,14 +255,7 @@ Coming Soon
298
255
  - More Session Stores - PostgreSQL, MySQL, MongoDB
299
256
  - Built-in Analytics - Usage tracking & insights
300
257
  - Admin Dashboard - Web interface for bot management
301
- - i18n Support - Built-in internationalization
302
-
303
- In Progress
304
258
 
305
- - httpx(Async) - Non-blocking I/O
306
- - Scene System - Multi-step conversations
307
- - Middleware Pipeline - Extensible architecture
308
- - Webhook Server - Production deployment
309
259
 
310
260
  ---
311
261
 
@@ -346,7 +296,6 @@ gem install telegem
346
296
  ruby -r telegem -e "puts 'Welcome to Telegem! ๐Ÿš€'"
347
297
  ```
348
298
 
349
- Check out docs/ for comprehensive guides, or jump right into examples/ to see real bots in action!
350
299
 
351
300
  ---
352
301
 
data/Starts_HallofFame.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # ๐Ÿ† Hall of Fame
2
2
 
3
+ ![GitLab stars](https://img.shields.io/gitlab/stars/ruby-telegem/telegem?style=for-the-badge&logo=gitlab&color=orange)
4
+ ![GitLab contributors](https://img.shields.io/gitlab/contributors/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
5
+ ![GitLab last commit](https://img.shields.io/gitlab/last-commit/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
6
+ ![GitLab license](https://img.shields.io/gitlab/license/ruby-telegem/telegem?style=for-the-badge&logo=gitlab)
7
+
3
8
  This page honors the individuals who have actively engaged with and supported the Telegem project from its earliest days. Your participation is the foundation of this community.
4
9
 
5
10
  Thank you for being part of the journey. โœจ
@@ -8,6 +13,7 @@ Thank you for being part of the journey. โœจ
8
13
 
9
14
  ## ๐Ÿง‘โ€๐Ÿ’ป Active Contributors & Early Community
10
15
 
16
+ ### ๐ŸŒŸ Founding Contributors
11
17
  **Adeniyi Ayonide** ([@adeniyiayomide712](https://gitlab.com/adeniyiayomide712))
12
18
  *First community member to open an issue, providing crucial early feedback.*
13
19
 
@@ -17,49 +23,49 @@ Thank you for being part of the journey. โœจ
17
23
  **Billy Ricch** ([@billyricch40](https://gitlab.com/billyricch40))
18
24
  *Active community participant and contributor to project discussions.*
19
25
 
26
+ ### ๐Ÿ› ๏ธ Technical Contributors
20
27
  **Damola Amadu** ([@Dambzboy](https://gitlab.com/Dambzboy))
21
28
  *Key supporter and participant in the project's growth.*
22
29
 
23
30
  **Kakashi Osaose** ([@kakashiosaose](https://gitlab.com/kakashiosaose))
24
31
  *Valued community member and technical contributor.*
25
32
 
26
- **Olonade Solomon** ([@Timijay789](https://gitlab.com/Timijay789))
27
- *Active participant in development discussions and testing.*
28
-
29
33
  **Sergey Kojin** ([@skojin](https://gitlab.com/skojin))
30
34
  *Technical contributor providing important implementation feedback.*
31
35
 
32
- **Peter Boling** ([@pboling](https://gitlab.com/pboling))
36
+ ### ๐Ÿงช Testing & Feedback
37
+ **Olonade Solomon** ([@Timijay789](https://gitlab.com/Timijay789))
38
+ *Active participant in development discussions and testing.*
33
39
 
40
+ **Peter Boling** ([@pboling](https://gitlab.com/pboling))
41
+ *Contributor providing valuable insights and feedback.*
34
42
 
35
43
  ---
36
44
 
37
45
  ## ๐Ÿ“œ Contribution Philosophy
38
46
 
39
47
  This Hall of Fame recognizes **active participation and contribution** to the Telegem project. We value:
40
- * **Code contributions** through merged Merge Requests
41
- * **Quality issue reports** that improve the project
42
- * **Technical discussions** that shape implementation
43
- * **Community support** that helps other users
44
- * **Documentation improvements** that help everyone
48
+
49
+ * **Code contributions** through merged Merge Requests
50
+ * **Quality issue reports** that improve the project
51
+ * **Technical discussions** that shape implementation
52
+ * **Community support** that helps other users
53
+ * **Documentation improvements** that help everyone
45
54
 
46
55
  While starring the repository is appreciated, this list specifically honors those who have engaged in active dialogue, reporting, or contribution to the project's development.
47
56
 
48
57
  ---
49
58
 
50
- ## ๐Ÿ”„ For Maintainers: How to Update
51
-
52
- To add new contributors to this list:
59
+ ## ๐Ÿ“Š Project Metrics
53
60
 
54
- 1. **Format:** Follow the existing structure:
55
- ```markdown
56
- **Name** ([@username](link-to-profile))
57
- *Brief description of their contribution or role.*
58
- ```
59
- 2. **Categories:** As the community grows, consider organizing by contribution type (Core Contributors, Documentation, etc.)
60
- 3. **Verification:** Ensure the individual has made meaningful contributions beyond passive following
61
- 4. **Date:** Update the "Last Updated" field below
61
+ | Metric | Badge | Live Status |
62
+ |--------|-------|-------------|
63
+ | **Stars** | ![GitLab stars](https://img.shields.io/gitlab/stars/ruby-telegem/telegem) | [View on GitLab](https://gitlab.com/ruby-telegem/telegem) |
64
+ | **Contributors** | ![GitLab contributors](https://img.shields.io/gitlab/contributors/ruby-telegem/telegem) | [See all contributors](https://gitlab.com/ruby-telegem/telegem/-/graphs/main) |
65
+ | **Latest Release** | ![GitLab release](https://img.shields.io/gitlab/v/release/ruby-telegem/telegem) | [Releases](https://gitlab.com/ruby-telegem/telegem/-/releases) |
66
+ | **Pipeline Status** | ![pipeline status](https://gitlab.com/ruby-telegem/telegem/badges/main/pipeline.svg) | [CI/CD](https://gitlab.com/ruby-telegem/telegem/-/pipelines) |
67
+ | **Open Issues** | ![GitLab issues](https://img.shields.io/gitlab/issues/open/ruby-telegem/telegem) | [Issues](https://gitlab.com/ruby-telegem/telegem/-/issues) |
68
+ | **Open MRs** | ![GitLab MRs](https://img.shields.io/gitlab/merge-requests/open/ruby-telegem/telegem) | [Merge Requests](https://gitlab.com/ruby-telegem/telegem/-/merge_requests) |
62
69
 
63
70
  ---
64
71
 
65
- **Last Updated: 2025-31-12**
data/lib/api/client.rb CHANGED
@@ -27,64 +27,71 @@ module Telegem
27
27
  }
28
28
  )
29
29
  end
30
- def call(method, params = {})
31
- url = "#{BASE_URL}/bot#{@token}/#{method}"
32
- @logger.debug("Api call #{method}") if @logger
33
- response = @http.post(url, json: params.compact)
34
- json = response.json
35
- if json && json['ok']
36
- json['result']
37
- else
38
- raise APIError.new(json ? json['description']: "Api Error")
39
- end
40
- end
41
- def call!(method, params = {}, &callback)
42
- url = "#{BASE_URL}/bot#{@token}/#{method}"
43
-
44
- if callback
45
- @http.on_response_completed do |request, response|
46
- begin
47
- if response.status == 200
48
- json = response.json
49
- if json && json['ok']
50
- callback.call(json['result'], nil)
51
- @logger.debug("API Response: #{json}") if @logger
52
- else
53
- error_msg = json ? json['description'] : "No JSON response"
54
- error_code = json['error_code'] if json
55
- callback.call(nil, APIError.new("API Error: #{error_msg}", error_code))
56
- end
57
- else
58
- callback.call(nil, NetworkError.new("HTTP #{response.status}"))
59
- end
60
- rescue JSON::ParserError
61
- callback.call(nil, NetworkError.new("Invalid JSON response"))
62
- rescue => e
63
- callback.call(nil, e)
64
- end
65
- end
66
-
67
- @http.on_request_error do |request, error|
68
- callback.call(nil, error)
69
- end
70
- end
71
-
72
- @http.post(url, json: params.compact)
73
-
74
- end
75
- def upload(method, params)
30
+
31
+ def call(method, params = {})
76
32
  url = "#{BASE_URL}/bot#{@token}/#{method}"
33
+ @logger.debug("Api call #{method}") if @logger
34
+ response = @http.post(url, json: params.compact)
35
+ json = response.json
36
+ if json && json['ok']
37
+ json['result']
38
+ else
39
+ raise APIError.new(json ? json['description'] : "Api Error")
40
+ end
41
+ end
42
+
43
+ def call!(method, params = {}, &callback)
44
+ url = "#{BASE_URL}/bot#{@token}/#{method}"
45
+ return unless callback
77
46
 
78
- form = params.map do |key, value|
79
- if file_object?(value)
80
- [key.to_s, HTTPX::FormData::File.new(value)]
81
- else
82
- [key.to_s, value.to_s]
83
- end
84
- end
85
- response = @http.post(url, form: form).await
86
- response.json
47
+ @http.post(url, json: params.compact) do |response|
48
+ begin
49
+ if response.status == 200
50
+ json = response.json
51
+ if json && json['ok']
52
+ @logger.debug("#{json}") if @logger
53
+ callback.call(json['result'], nil)
54
+ else
55
+ error_msg = json ? json['description'] : "NO JSON Response"
56
+ error_code = json['error_code'] if json
57
+ callback.call(nil, APIError.new("API ERROR #{error_msg}", error_code))
58
+ end
59
+ else
60
+ callback.call(nil, NetworkError.new("HTTP #{response.status}"))
61
+ end
62
+ rescue JSON::ParserError
63
+ callback.call(nil, NetworkError.new("Invalid Json response"))
64
+ rescue => e
65
+ callback.call(nil, e)
66
+ end
67
+ end
68
+ end
69
+
70
+ def upload(method, params)
71
+ url = "#{BASE_URL}/bot#{@token}/#{method}"
72
+ response = @http.post(url, form: params)
73
+ response.json
87
74
  end
75
+
76
+ def download(file_id, destination_path = nil)
77
+ file_info = call('getFile', file_id: file_id)
78
+ return nil unless file_info && file_info['file_path']
79
+ file_path = file_info['file_path']
80
+ download_url = "#{BASE_URL}/file/bot#{@token}/#{file_path}"
81
+ @logger.debug("downloading.. #{download_url}") if @logger
82
+ response = @http.get(download_url)
83
+ if response.status == 200
84
+ if destination_path
85
+ File.binwrite(destination_path, response.body.to_s)
86
+ @logger.debug("saved to #{destination_path}") if @logger
87
+ destination_path
88
+ else
89
+ response.body.to_s
90
+ end
91
+ else
92
+ raise NetworkError.new("Download failed : #{response.status}")
93
+ end
94
+ end
88
95
 
89
96
  def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
90
97
  params = { timeout: timeout, limit: limit }
@@ -116,4 +123,4 @@ end
116
123
 
117
124
  class NetworkError < APIError; end
118
125
  end
119
- end
126
+ end
data/lib/core/bot.rb CHANGED
@@ -189,22 +189,24 @@ end
189
189
  private
190
190
 
191
191
  def poll_loop
192
- last_pool_time = Time.now
193
192
  while @running
194
- elapsed_time = Time.now - last_pool_time
195
- sleep(0.5 - elapsed_time) if elapsed_time < 0.5
196
-
197
- fetch_updates do |result, error|
198
- if error
199
- @logger.error "polling error #{error.message}"
200
- sleep(2)
201
- elsif result && result['ok']
202
- handle_updates_response(result)
203
- end
204
- end
205
- sleep 0.1
206
- end
207
- end
193
+ begin
194
+ Async do |task|
195
+ fetch_updates do |result, error|
196
+ if error
197
+ @logger.error "Polling error #{error.message}"
198
+ task.sleep(0.2)
199
+ elsif result && result['ok']
200
+ handle_updates_response(result)
201
+ end
202
+ end
203
+ end.wait
204
+ rescue => e
205
+ @logger.error "poll loop error #{e.message}"
206
+ end
207
+ sleep 0.5
208
+ end
209
+ end
208
210
 
209
211
  def fetch_updates(&completion_callback)
210
212
  params = {
@@ -255,7 +257,7 @@ end
255
257
  if update.message&.text && @logger
256
258
  user = update.message.from
257
259
  cmd = update.message.text.split.first
258
- @logger.info("#{cmd} - #{user.username || user.first_name}")
260
+ @logger.info("#{cmd} - #{user.username}")
259
261
  end
260
262
 
261
263
  ctx = Context.new(update, self)
@@ -287,12 +289,20 @@ end
287
289
  end
288
290
  end
289
291
 
290
- unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
291
- chain.use(Session::Middleware.new(@session_store))
292
- end
293
-
294
- chain
295
- end
292
+ unless @middleware.any? { |m, _, _| m.to_s =~ /Scene/ }
293
+ begin
294
+ require_relative '../session/scene_middleware'
295
+ chain.use(Telegem::Scene::Middleware.new)
296
+ rescue LoadError => e
297
+ @logger.debug("Scene middleware not available: #{e.message}") if @logger
298
+ end
299
+ end
300
+ unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
301
+ chain.use(Session::Middleware.new(@session_store))
302
+ end
303
+
304
+ chain
305
+ end
296
306
 
297
307
  def dispatch_to_handlers(ctx)
298
308
  update_type = detect_update_type(ctx.update)
data/lib/core/context.rb CHANGED
@@ -151,6 +151,10 @@ module Telegem
151
151
  end
152
152
  end
153
153
 
154
+ def download_file(file_id, destination_path = nil)
155
+ @bot.api.download(file_id, destination_path)
156
+ end
157
+
154
158
  def sticker(sticker, **options)
155
159
  return nil unless chat
156
160
 
@@ -321,7 +325,43 @@ module Telegem
321
325
  def uploading_document(**options)
322
326
  send_chat_action('upload_document', **options)
323
327
  end
324
-
328
+ def scene
329
+ session[:telegem_scene]&.[](:id)
330
+ end
331
+ def ask(question, **options)
332
+ scene_data = session[:telegem_scene]
333
+ if scene_data
334
+ scene_data[:waiting_for_response] = true
335
+ scene_data[:last_question] = question
336
+ end
337
+ reply(question, **options)
338
+ end
339
+ def scene_data
340
+ @session[:telegem_scene]&.[](:data) || {}
341
+ end
342
+ def current_scene
343
+ @session[:telegem_scene]&.[](:id)
344
+ end
345
+ def in_scene?
346
+ !current_scene.nil?
347
+ end
348
+ def leave_scene(**options)
349
+ scene_data = @session[:telegem_scene]
350
+ return unless scene_data
351
+ scene_id = scene_data[:id].to_sym
352
+ scene = @bot.scenes[scene_id]
353
+ result = scene&.leave(self, options[:reason] || :manual)
354
+ @session.delete(:telegem_scene)
355
+ @scene = nil
356
+ result
357
+ end
358
+ def next_step(step_name = nil)
359
+ scene_data = @session[:telegem_scene]
360
+ return unless scene_data
361
+ scene_id = scene_data[:id].to_sym
362
+ scene = @bot.scenes[scene_id]
363
+ scene&.next_step(self, step_name)
364
+ end
325
365
  def with_typing(&block)
326
366
  typing_request = typing
327
367
 
@@ -339,23 +379,12 @@ module Telegem
339
379
  end
340
380
 
341
381
  def enter_scene(scene_name, **options)
342
- return nil unless @bot.scenes[scene_name]
343
-
344
- @scene = scene_name
345
- @bot.scenes[scene_name].enter(self, **options)
346
- end
347
-
348
- def leave_scene(**options)
349
- return nil unless @scene && @bot.scenes[@scene]
350
-
351
- scene_name = @scene
352
- @scene = nil
353
- @bot.scenes[scene_name].leave(self, **options)
354
- end
355
-
356
- def current_scene
357
- @bot.scenes[@scene] if @scene
358
- end
382
+ scene = @bot.scenes[scene_name]
383
+ return nil unless scene
384
+ leave_scene if in_scene?
385
+ scene.enter(self, options[:step], options.except(:step))
386
+ scene_name
387
+ end
359
388
 
360
389
  def logger
361
390
  @bot.logger
@@ -0,0 +1,100 @@
1
+
2
+ module Telegem
3
+ class RateLimit
4
+ def initialize(**options)
5
+ @options = {
6
+ global: { max: 30, per: 1 }, # 30 reqs/second globally
7
+ user: { max: 5, per: 10 }, # 5 reqs/10 seconds per user
8
+ chat: { max: 20, per: 60 } # 20 reqs/minute per chat
9
+ }.merge(options)
10
+
11
+ @counters = {
12
+ global: Telegem::Session::MemoryStore.new,
13
+ user: Telegem::Session::MemoryStore.new,
14
+ chat: Telegem::Session::MemoryStore.new
15
+ }
16
+ end
17
+
18
+ def call(ctx, next_middleware)
19
+ return next_middleware.call(ctx) unless should_rate_limit?(ctx)
20
+
21
+ if limit_exceeded?(ctx)
22
+ ctx.logger&.warn("Rate limit exceeded for #{ctx.from&.id}")
23
+ return rate_limit_response(ctx)
24
+ end
25
+
26
+ increment_counters(ctx)
27
+ next_middleware.call(ctx)
28
+ end
29
+
30
+ private
31
+
32
+ def should_rate_limit?(ctx)
33
+
34
+ return false if ctx.update.poll?
35
+ return false if ctx.update.chat_member?
36
+ return true
37
+ end
38
+
39
+ def limit_exceeded?(ctx)
40
+ global_limit?(ctx) || user_limit?(ctx) || chat_limit?(ctx)
41
+ end
42
+
43
+ def global_limit?(ctx)
44
+ check_limit(:global, "global", ctx)
45
+ end
46
+
47
+ def user_limit?(ctx)
48
+ return false unless ctx.from&.id
49
+ check_limit(:user, "user:#{ctx.from.id}", ctx)
50
+ end
51
+
52
+ def chat_limit?(ctx)
53
+ return false unless ctx.chat&.id
54
+ check_limit(:chat, "chat:#{ctx.chat.id}", ctx)
55
+ end
56
+
57
+ def check_limit(type, key, ctx)
58
+ limit = @options[type]
59
+ return false unless limit
60
+
61
+ counter = @counters[type].get(key) || 0
62
+ counter >= limit[:max]
63
+ end
64
+
65
+ def increment_counters(ctx)
66
+ now = Time.now.to_i
67
+
68
+
69
+ if @options[:global]
70
+ key = "global"
71
+ cleanup_counter(:global, key, now)
72
+ @counters[:global].increment(key, 1, ttl: @options[:global][:per])
73
+ end
74
+
75
+
76
+ if @options[:user] && ctx.from&.id
77
+ key = "user:#{ctx.from.id}"
78
+ cleanup_counter(:user, key, now)
79
+ @counters[:user].increment(key, 1, ttl: @options[:user][:per])
80
+ end
81
+
82
+
83
+ if @options[:chat] && ctx.chat&.id
84
+ key = "chat:#{ctx.chat.id}"
85
+ cleanup_counter(:chat, key, now)
86
+ @counters[:chat].increment(key, 1, ttl: @options[:chat][:per])
87
+ end
88
+ end
89
+
90
+ def cleanup_counter(type, key, now)
91
+
92
+ end
93
+
94
+ def rate_limit_response(ctx)
95
+
96
+ ctx.reply("โณ Please wait a moment before sending another request.") rescue nil
97
+ nil
98
+ end
99
+ end
100
+ end