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.
- checksums.yaml +4 -4
- data/CHANGELOG +92 -12
- data/CODE_OF_CONDUCT.md +13 -0
- data/Contributing.md +125 -517
- data/Readme.md +10 -61
- data/Starts_HallofFame.md +27 -21
- data/lib/api/client.rb +63 -56
- data/lib/core/bot.rb +32 -22
- data/lib/core/context.rb +47 -18
- data/lib/core/rate_limit.rb +100 -0
- data/lib/core/scene.rb +110 -43
- data/lib/plugins/file_extract.rb +97 -0
- data/lib/session/scene_middleware.rb +22 -0
- data/lib/telegem.rb +3 -1
- metadata +26 -84
- data/docs-src/Bot-registration_.PNG +0 -0
- data/docs-src/bot.md +0 -464
- data/docs-src/context|ctx|.md +0 -531
- data/docs-src/ctx.md +0 -399
- data/docs-src/getting-started.md +0 -328
- data/docs-src/keyboard_inline.md +0 -413
- data/docs-src/scene.md +0 -509
- data/docs-src/webhook.md +0 -341
- /data/{docs-src โ lib/plugins}/.gitkeep +0 -0
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
|
-
    
|
|
5
|
+
    
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
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
|
|
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
##
|
|
51
|
-
|
|
52
|
-
To add new contributors to this list:
|
|
59
|
+
## ๐ Project Metrics
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
| Metric | Badge | Live Status |
|
|
62
|
+
|--------|-------|-------------|
|
|
63
|
+
| **Stars** |  | [View on GitLab](https://gitlab.com/ruby-telegem/telegem) |
|
|
64
|
+
| **Contributors** |  | [See all contributors](https://gitlab.com/ruby-telegem/telegem/-/graphs/main) |
|
|
65
|
+
| **Latest Release** |  | [Releases](https://gitlab.com/ruby-telegem/telegem/-/releases) |
|
|
66
|
+
| **Pipeline Status** |  | [CI/CD](https://gitlab.com/ruby-telegem/telegem/-/pipelines) |
|
|
67
|
+
| **Open Issues** |  | [Issues](https://gitlab.com/ruby-telegem/telegem/-/issues) |
|
|
68
|
+
| **Open MRs** |  | [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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|