telegem 3.3.0 → 3.4.0
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/.rubocop.yml +57 -0
- data/CHANGELOG.md +121 -0
- data/Gemfile +1 -1
- data/README.md +147 -0
- data/bin/telegem-ssl +71 -25
- data/contributing.md +375 -0
- data/docs/api.md +663 -0
- data/docs/bot.md +332 -0
- data/docs/changelog.md +182 -0
- data/docs/context.md +554 -0
- data/docs/core_concepts.md +218 -0
- data/docs/deployment.md +702 -0
- data/docs/error_handling.md +435 -0
- data/docs/examples.md +752 -0
- data/docs/getting_started.md +151 -0
- data/docs/handlers.md +580 -0
- data/docs/keyboards.md +446 -0
- data/docs/middleware.md +536 -0
- data/docs/plugins.md +384 -0
- data/docs/scenes.md +517 -0
- data/docs/sessions.md +544 -0
- data/docs/testing.md +612 -0
- data/docs/troubleshooting.md +574 -0
- data/docs/types.md +538 -0
- data/docs/webhooks.md +456 -0
- data/lib/api/client.rb +38 -10
- data/lib/api/types.rb +433 -172
- data/lib/core/composer.rb +3 -3
- data/lib/core/context.rb +17 -11
- data/lib/plugins/cc +3 -0
- data/lib/plugins/file_extract.rb +2 -2
- data/lib/plugins/translate.rb +43 -0
- data/lib/session/memory_store.rb +90 -103
- data/lib/session/redis.rb +91 -0
- data/lib/telegem.rb +4 -4
- data/lib/webhook/server.rb +5 -3
- metadata +51 -35
- data/CHANGELOG +0 -95
- data/Contributing.md +0 -161
- data/Readme.md +0 -302
- data/examples/.gitkeep +0 -0
- data/public/.gitkeep +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Getting Started with Telegem
|
|
2
|
+
|
|
3
|
+
This guide will walk you through creating your first Telegram bot with Telegem.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Ruby 3.0 or higher
|
|
8
|
+
- A Telegram account
|
|
9
|
+
- Basic knowledge of Ruby
|
|
10
|
+
|
|
11
|
+
## Step 1: Create a Telegram Bot
|
|
12
|
+
|
|
13
|
+
1. Open Telegram and search for [@BotFather](https://t.me/botfather)
|
|
14
|
+
2. Send `/newbot` and follow the instructions
|
|
15
|
+
3. Save your bot token (something like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
|
|
16
|
+
|
|
17
|
+
## Step 2: Install Telegem
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install telegem
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
source 'https://rubygems.org'
|
|
27
|
+
|
|
28
|
+
gem 'telegem'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Step 3: Create Your First Bot
|
|
32
|
+
|
|
33
|
+
Create a file called `bot.rb`:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require 'telegem'
|
|
37
|
+
|
|
38
|
+
# Initialize bot with your token
|
|
39
|
+
bot = Telegem.new('YOUR_BOT_TOKEN')
|
|
40
|
+
|
|
41
|
+
# Handle /start command
|
|
42
|
+
bot.command('start') do |ctx|
|
|
43
|
+
ctx.reply("Hello, #{ctx.from.first_name}! 👋")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Handle /help command
|
|
47
|
+
bot.command('help') do |ctx|
|
|
48
|
+
ctx.reply("I'm your friendly Telegem bot! Send /start to begin.")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Handle any text message
|
|
52
|
+
bot.hears(/.+/) do |ctx|
|
|
53
|
+
ctx.reply("You said: #{ctx.message.text}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Start the bot
|
|
57
|
+
puts "🤖 Bot is running..."
|
|
58
|
+
bot.start_polling
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Step 4: Run Your Bot
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ruby bot.rb
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Step 5: Test Your Bot
|
|
68
|
+
|
|
69
|
+
1. Open Telegram
|
|
70
|
+
2. Find your bot by username
|
|
71
|
+
3. Send `/start` and see the response
|
|
72
|
+
|
|
73
|
+
## Understanding the Code
|
|
74
|
+
|
|
75
|
+
### Bot Initialization
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
bot = Telegem.new('YOUR_BOT_TOKEN')
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This creates a new bot instance with your token.
|
|
82
|
+
|
|
83
|
+
### Command Handlers
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
bot.command('start') do |ctx|
|
|
87
|
+
# Handle /start command
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Commands are messages starting with `/`. The `ctx` object contains information about the update.
|
|
92
|
+
|
|
93
|
+
### Text Message Handlers
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
bot.hears(/.+/) do |ctx|
|
|
97
|
+
# Handle any text message
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`hears` matches messages using regular expressions.
|
|
102
|
+
|
|
103
|
+
### Context Object
|
|
104
|
+
|
|
105
|
+
The `ctx` (context) object provides access to:
|
|
106
|
+
|
|
107
|
+
- `ctx.message` - The message object
|
|
108
|
+
- `ctx.from` - The user who sent the message
|
|
109
|
+
- `ctx.chat` - The chat where the message was sent
|
|
110
|
+
- `ctx.reply(text)` - Send a reply
|
|
111
|
+
|
|
112
|
+
## Next Steps
|
|
113
|
+
|
|
114
|
+
- Learn about [handlers](handlers.md) for more routing options
|
|
115
|
+
- Explore [middleware](middleware.md) for request processing
|
|
116
|
+
- Check out [scenes](scenes.md) for multi-step conversations
|
|
117
|
+
- See [examples](examples.md) for more complex bots
|
|
118
|
+
|
|
119
|
+
## Common Issues
|
|
120
|
+
|
|
121
|
+
### "Bot token is invalid"
|
|
122
|
+
|
|
123
|
+
- Double-check your token from @BotFather
|
|
124
|
+
- Make sure there are no extra spaces
|
|
125
|
+
|
|
126
|
+
### "Connection refused"
|
|
127
|
+
|
|
128
|
+
- Check your internet connection
|
|
129
|
+
- Verify the bot token is correct
|
|
130
|
+
|
|
131
|
+
### Bot doesn't respond
|
|
132
|
+
|
|
133
|
+
- Make sure the bot is running (`ruby bot.rb`)
|
|
134
|
+
- Check that you've started a conversation with the bot first
|
|
135
|
+
- Look at the console output for errors
|
|
136
|
+
|
|
137
|
+
## Development Tips
|
|
138
|
+
|
|
139
|
+
- Use `puts` statements for debugging
|
|
140
|
+
- Check the [API reference](api.md) for available methods
|
|
141
|
+
- Join the [Telegram Bot Community](https://t.me/botcommunity) for help
|
|
142
|
+
|
|
143
|
+
## Production Deployment
|
|
144
|
+
|
|
145
|
+
For production use, consider:
|
|
146
|
+
|
|
147
|
+
- [Webhook deployment](webhooks.md) instead of polling
|
|
148
|
+
- [Session storage](sessions.md) for user data
|
|
149
|
+
- [Error handling](error_handling.md) for reliability
|
|
150
|
+
- [Rate limiting](middleware.md) for performance</content>
|
|
151
|
+
<parameter name="filePath">/home/slick/telegem/docs/getting_started.md
|
data/docs/handlers.md
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# Handlers and Routing
|
|
2
|
+
|
|
3
|
+
Handlers are functions that process specific types of updates from Telegram. Telegem provides a flexible routing system to match updates to handlers.
|
|
4
|
+
|
|
5
|
+
## Handler Types
|
|
6
|
+
|
|
7
|
+
### Command Handlers
|
|
8
|
+
|
|
9
|
+
Commands are messages starting with `/`. They automatically handle bot mentions.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
bot.command('start') do |ctx|
|
|
13
|
+
ctx.reply("Welcome!")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
bot.command('help') do |ctx|
|
|
17
|
+
ctx.reply("Available commands: /start, /help")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Commands with arguments
|
|
21
|
+
bot.command('echo') do |ctx|
|
|
22
|
+
args = ctx.command_args
|
|
23
|
+
ctx.reply(args || "No arguments provided")
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Command matching:
|
|
28
|
+
- `/start` matches
|
|
29
|
+
- `/start@mybot` matches (if bot username is @mybot)
|
|
30
|
+
- `start` doesn't match (no slash)
|
|
31
|
+
|
|
32
|
+
### Text Pattern Handlers
|
|
33
|
+
|
|
34
|
+
Match messages using strings or regular expressions.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# Exact string match
|
|
38
|
+
bot.hears('hello') do |ctx|
|
|
39
|
+
ctx.reply("Hi there!")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Case-insensitive match
|
|
43
|
+
bot.hears(/^hello$/i) do |ctx|
|
|
44
|
+
ctx.reply("Hello to you!")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Any message containing word
|
|
48
|
+
bot.hears(/bot/i) do |ctx|
|
|
49
|
+
ctx.reply("You mentioned bot!")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Catch-all for text messages
|
|
53
|
+
bot.hears(/.+/) do |ctx|
|
|
54
|
+
ctx.reply("You said: #{ctx.message.text}")
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Update Type Handlers
|
|
59
|
+
|
|
60
|
+
Handle specific update types directly.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Callback queries (button presses)
|
|
64
|
+
bot.callback_query do |ctx|
|
|
65
|
+
data = ctx.data
|
|
66
|
+
ctx.answer_callback_query("Button pressed: #{data}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Inline queries (inline search)
|
|
70
|
+
bot.inline_query do |ctx|
|
|
71
|
+
query = ctx.query
|
|
72
|
+
# Return search results
|
|
73
|
+
results = search_results_for(query)
|
|
74
|
+
ctx.answer_inline_query(results)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Poll answers
|
|
78
|
+
bot.poll_answer do |ctx|
|
|
79
|
+
answer = ctx.poll_answer
|
|
80
|
+
# Process poll response
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Chat join requests
|
|
84
|
+
bot.chat_join_request do |ctx|
|
|
85
|
+
# Approve or deny join request
|
|
86
|
+
ctx.approve_chat_join_request()
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Media Handlers
|
|
91
|
+
|
|
92
|
+
Specialized handlers for different media types.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# Photos
|
|
96
|
+
bot.photo do |ctx|
|
|
97
|
+
ctx.reply("Nice photo! 📸")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Documents
|
|
101
|
+
bot.document do |ctx|
|
|
102
|
+
filename = ctx.message.document.file_name
|
|
103
|
+
ctx.reply("Received document: #{filename}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Audio files
|
|
107
|
+
bot.audio do |ctx|
|
|
108
|
+
title = ctx.message.audio.title
|
|
109
|
+
ctx.reply("Playing: #{title}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Videos
|
|
113
|
+
bot.video do |ctx|
|
|
114
|
+
ctx.reply("Video received!")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Voice messages
|
|
118
|
+
bot.voice do |ctx|
|
|
119
|
+
duration = ctx.message.voice.duration
|
|
120
|
+
ctx.reply("Voice message (#{duration}s)")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Stickers
|
|
124
|
+
bot.sticker do |ctx|
|
|
125
|
+
emoji = ctx.message.sticker.emoji
|
|
126
|
+
ctx.reply("Sticker: #{emoji}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Locations
|
|
130
|
+
bot.location do |ctx|
|
|
131
|
+
lat = ctx.message.location.latitude
|
|
132
|
+
lng = ctx.message.location.longitude
|
|
133
|
+
ctx.reply("Location: #{lat}, #{lng}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Contacts
|
|
137
|
+
bot.contact do |ctx|
|
|
138
|
+
contact = ctx.message.contact
|
|
139
|
+
ctx.reply("Contact: #{contact.first_name}")
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Generic Handlers
|
|
144
|
+
|
|
145
|
+
Use `on()` for any update type with optional filters.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Handle all messages
|
|
149
|
+
bot.on(:message) do |ctx|
|
|
150
|
+
puts "Message received"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Messages in private chats only
|
|
154
|
+
bot.on(:message, chat_type: 'private') do |ctx|
|
|
155
|
+
ctx.reply("This is private")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Messages containing specific text
|
|
159
|
+
bot.on(:message, text: /urgent/i) do |ctx|
|
|
160
|
+
ctx.reply("🚨 Urgent message!")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Edited messages
|
|
164
|
+
bot.on(:edited_message) do |ctx|
|
|
165
|
+
ctx.reply("Message edited")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Channel posts
|
|
169
|
+
bot.on(:channel_post) do |ctx|
|
|
170
|
+
# Handle channel posts
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Handler Priority and Order
|
|
175
|
+
|
|
176
|
+
Handlers are checked in registration order. More specific handlers should be registered first.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Bad: catch-all first
|
|
180
|
+
bot.hears(/.+/) do |ctx|
|
|
181
|
+
ctx.reply("Catch-all")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
bot.command('start') do |ctx| # Never reached
|
|
185
|
+
ctx.reply("Start")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Good: specific first
|
|
189
|
+
bot.command('start') do |ctx|
|
|
190
|
+
ctx.reply("Start")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
bot.hears(/.+/) do |ctx|
|
|
194
|
+
ctx.reply("Catch-all")
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Filters
|
|
199
|
+
|
|
200
|
+
Use filters to match specific conditions.
|
|
201
|
+
|
|
202
|
+
### Chat Type Filters
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
bot.on(:message, chat_type: 'private') do |ctx|
|
|
206
|
+
# Private messages only
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
bot.on(:message, chat_type: 'group') do |ctx|
|
|
210
|
+
# Group messages only
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
bot.on(:message, chat_type: 'supergroup') do |ctx|
|
|
214
|
+
# Supergroup messages only
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### User Filters
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Messages from specific user
|
|
222
|
+
bot.on(:message, user_id: 123456) do |ctx|
|
|
223
|
+
ctx.reply("Hello admin!")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Messages from bots
|
|
227
|
+
bot.on(:message, is_bot: true) do |ctx|
|
|
228
|
+
ctx.reply("Bot detected")
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Content Filters
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
# Messages with photos
|
|
236
|
+
bot.on(:message, has_photo: true) do |ctx|
|
|
237
|
+
ctx.reply("Photo received")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Messages with documents
|
|
241
|
+
bot.on(:message, has_document: true) do |ctx|
|
|
242
|
+
ctx.reply("Document received")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Forwarded messages
|
|
246
|
+
bot.on(:message, forwarded: true) do |ctx|
|
|
247
|
+
ctx.reply("Forwarded message")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Reply messages
|
|
251
|
+
bot.on(:message, reply: true) do |ctx|
|
|
252
|
+
ctx.reply("This is a reply")
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Custom Filters
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# Custom filter function
|
|
260
|
+
bot.on(:message, ->(ctx) { ctx.from&.username == 'admin' }) do |ctx|
|
|
261
|
+
ctx.reply("Admin command")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Multiple conditions
|
|
265
|
+
bot.on(:message, chat_type: 'group', text: /admin/i) do |ctx|
|
|
266
|
+
# Admin commands in groups
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Handler Context
|
|
271
|
+
|
|
272
|
+
### Match Data
|
|
273
|
+
|
|
274
|
+
For regex handlers, match data is available.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
bot.hears(/hello (\w+)/) do |ctx|
|
|
278
|
+
name = ctx.match[1] # Captured group
|
|
279
|
+
ctx.reply("Hello #{name}!")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
bot.hears(/^\/greet (\w+) (\w+)/) do |ctx|
|
|
283
|
+
first_name = ctx.match[1]
|
|
284
|
+
last_name = ctx.match[2]
|
|
285
|
+
ctx.reply("Greetings #{first_name} #{last_name}!")
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### State and Session
|
|
290
|
+
|
|
291
|
+
Handlers have access to state and session.
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
bot.hears(/set name (.+)/) do |ctx|
|
|
295
|
+
name = ctx.match[1]
|
|
296
|
+
ctx.session[:name] = name
|
|
297
|
+
ctx.reply("Name set to #{name}")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
bot.hears('my name') do |ctx|
|
|
301
|
+
name = ctx.session[:name] || 'unknown'
|
|
302
|
+
ctx.reply("Your name is #{name}")
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Dynamic Handlers
|
|
307
|
+
|
|
308
|
+
Register handlers at runtime.
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# Add command dynamically
|
|
312
|
+
bot.command('dynamic') do |ctx|
|
|
313
|
+
ctx.reply("Dynamic command!")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Conditional handlers
|
|
317
|
+
if ENV['ADMIN_MODE']
|
|
318
|
+
bot.command('admin') do |ctx|
|
|
319
|
+
# Admin commands
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Handler factories
|
|
324
|
+
def create_counter_handler(name)
|
|
325
|
+
bot.command(name) do |ctx|
|
|
326
|
+
ctx.session[name] ||= 0
|
|
327
|
+
ctx.session[name] += 1
|
|
328
|
+
ctx.reply("#{name}: #{ctx.session[name]}")
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
create_counter_handler('count1')
|
|
333
|
+
create_counter_handler('count2')
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Handler Removal
|
|
337
|
+
|
|
338
|
+
Handlers cannot be removed individually. To change handlers:
|
|
339
|
+
|
|
340
|
+
1. Create a new bot instance
|
|
341
|
+
2. Use conditional registration
|
|
342
|
+
3. Use middleware to filter
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# Conditional handler
|
|
346
|
+
bot.use do |ctx, next_middleware|
|
|
347
|
+
if should_skip_handler?(ctx)
|
|
348
|
+
return # Skip handler
|
|
349
|
+
end
|
|
350
|
+
next_middleware.call(ctx)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
bot.command('conditional') do |ctx|
|
|
354
|
+
# Only reached if middleware allows
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Error Handling in Handlers
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
bot.command('risky') do |ctx|
|
|
362
|
+
begin
|
|
363
|
+
risky_operation(ctx.text)
|
|
364
|
+
ctx.reply("Success!")
|
|
365
|
+
rescue => e
|
|
366
|
+
ctx.logger.error("Handler error: #{e.message}")
|
|
367
|
+
ctx.reply("Something went wrong")
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Async Handlers
|
|
373
|
+
|
|
374
|
+
For long-running operations, use async.
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
bot.command('long_task') do |ctx|
|
|
378
|
+
ctx.reply("Processing...")
|
|
379
|
+
|
|
380
|
+
Async do
|
|
381
|
+
result = long_running_operation()
|
|
382
|
+
ctx.reply("Done: #{result}")
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Handler Best Practices
|
|
388
|
+
|
|
389
|
+
### 1. Keep Handlers Small
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
# Bad
|
|
393
|
+
bot.command('process') do |ctx|
|
|
394
|
+
# 50 lines of processing code
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Good
|
|
398
|
+
bot.command('process') do |ctx|
|
|
399
|
+
result = process_data(ctx.text)
|
|
400
|
+
ctx.reply(result)
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 2. Use Appropriate Handler Types
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# Use command for commands
|
|
408
|
+
bot.command('start')
|
|
409
|
+
|
|
410
|
+
# Use hears for text patterns
|
|
411
|
+
bot.hears(/hello/)
|
|
412
|
+
|
|
413
|
+
# Use on() for complex conditions
|
|
414
|
+
bot.on(:message, chat_type: 'private', has_photo: true)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### 3. Validate Input
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
bot.command('calculate') do |ctx|
|
|
421
|
+
number = ctx.command_args&.to_i
|
|
422
|
+
if number.nil? || number < 0
|
|
423
|
+
ctx.reply("Please provide a positive number")
|
|
424
|
+
return
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
result = calculate(number)
|
|
428
|
+
ctx.reply("Result: #{result}")
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### 4. Handle Edge Cases
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
bot.photo do |ctx|
|
|
436
|
+
unless ctx.message.photo&.any?
|
|
437
|
+
ctx.reply("No photo found")
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Process photo
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 5. Use Sessions for State
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
bot.hears('start quiz') do |ctx|
|
|
449
|
+
ctx.session[:quiz_active] = true
|
|
450
|
+
ctx.session[:question] = 1
|
|
451
|
+
ctx.reply("Quiz started! Question 1...")
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
bot.hears(/.+/) do |ctx|
|
|
455
|
+
if ctx.session[:quiz_active]
|
|
456
|
+
# Handle quiz answer
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 6. Log Important Actions
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
bot.command('delete') do |ctx|
|
|
465
|
+
ctx.logger.info("User #{ctx.from.id} deleting data")
|
|
466
|
+
delete_user_data(ctx.from.id)
|
|
467
|
+
ctx.reply("Data deleted")
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Common Patterns
|
|
472
|
+
|
|
473
|
+
### Menu Systems
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
bot.command('menu') do |ctx|
|
|
477
|
+
keyboard = Telegem.keyboard do
|
|
478
|
+
row "📊 Stats", "⚙️ Settings"
|
|
479
|
+
row "❓ Help", "🚪 Exit"
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
ctx.reply("Choose option:", reply_markup: keyboard)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
bot.hears('Stats') do |ctx|
|
|
486
|
+
stats = get_user_stats(ctx.from.id)
|
|
487
|
+
ctx.reply("Your stats: #{stats}")
|
|
488
|
+
end
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Admin Commands
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
ADMIN_IDS = [123456, 789012]
|
|
495
|
+
|
|
496
|
+
bot.command('admin') do |ctx|
|
|
497
|
+
unless ADMIN_IDS.include?(ctx.from.id)
|
|
498
|
+
ctx.reply("Access denied")
|
|
499
|
+
return
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Admin functionality
|
|
503
|
+
ctx.reply("Admin panel")
|
|
504
|
+
end
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Rate Limiting
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
bot.use do |ctx, next_middleware|
|
|
511
|
+
user_id = ctx.from&.id
|
|
512
|
+
if user_id
|
|
513
|
+
key = "rate_limit:#{user_id}"
|
|
514
|
+
count = ctx.session[key] ||= 0
|
|
515
|
+
|
|
516
|
+
if count > 10
|
|
517
|
+
ctx.reply("Too many requests")
|
|
518
|
+
return
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
ctx.session[key] = count + 1
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
next_middleware.call(ctx)
|
|
525
|
+
end
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Command Aliases
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
['start', 'begin', 'hello'].each do |cmd|
|
|
532
|
+
bot.command(cmd) do |ctx|
|
|
533
|
+
ctx.reply("Welcome!")
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Fallback Handlers
|
|
539
|
+
|
|
540
|
+
```ruby
|
|
541
|
+
# Handle unknown commands
|
|
542
|
+
bot.on(:message, ->(ctx) { ctx.message&.text&.start_with?('/') }) do |ctx|
|
|
543
|
+
ctx.reply("Unknown command. Try /help")
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Handle non-text messages
|
|
547
|
+
bot.on(:message) do |ctx|
|
|
548
|
+
unless ctx.message&.text
|
|
549
|
+
ctx.reply("I only understand text messages")
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## Testing Handlers
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
# Test helper
|
|
558
|
+
def simulate_message(bot, text, from_id: 123)
|
|
559
|
+
update = Telegem::Types::Update.new({
|
|
560
|
+
update_id: 1,
|
|
561
|
+
message: {
|
|
562
|
+
message_id: 1,
|
|
563
|
+
from: { id: from_id, first_name: 'Test' },
|
|
564
|
+
chat: { id: from_id, type: 'private' },
|
|
565
|
+
date: Time.now.to_i,
|
|
566
|
+
text: text
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
ctx = Telegem::Core::Context.new(update, bot)
|
|
571
|
+
bot.process_update(update)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Usage
|
|
575
|
+
simulate_message(bot, '/start')
|
|
576
|
+
simulate_message(bot, 'hello world')
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Understanding handlers is crucial for building interactive bots. Choose the right handler type and use filters to create precise routing logic.</content>
|
|
580
|
+
<parameter name="filePath">/home/slick/telegem/docs/handlers.md
|