telegem 0.2.5 → 1.0.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/.replit +13 -0
- data/Contributing.md +553 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +11 -0
- data/LICENSE +21 -0
- data/Readme.md +353 -0
- data/Test-Projects/.gitkeep +0 -0
- data/Test-Projects/bot_test1.rb +75 -0
- data/Test-Projects/pizza_test_bot_guide.md +163 -0
- data/docs/.gitkeep +0 -0
- data/docs/Api.md +419 -0
- data/docs/Cookbook.md +407 -0
- data/docs/How_to_use.md +571 -0
- data/docs/QuickStart.md +258 -0
- data/docs/Usage.md +717 -0
- data/lib/api/client.rb +89 -116
- data/lib/core/bot.rb +103 -92
- data/lib/core/composer.rb +36 -18
- data/lib/core/context.rb +180 -177
- data/lib/core/scene.rb +81 -71
- data/lib/session/memory_store.rb +1 -1
- data/lib/session/middleware.rb +20 -36
- data/lib/telegem.rb +57 -54
- data/lib/webhook/.gitkeep +0 -0
- data/lib/webhook/server.rb +193 -0
- metadata +38 -35
- data/telegem.gemspec +0 -43
- data/webhook/server.rb +0 -86
data/docs/Cookbook.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
|
|
2
|
+
# 🍳 Telegem Cookbook
|
|
3
|
+
|
|
4
|
+
Quick copy-paste recipes for common bot tasks.
|
|
5
|
+
Each recipe is standalone and ready to use!
|
|
6
|
+
|
|
7
|
+
## 📋 Table of Contents
|
|
8
|
+
- [Sending Media](#-sending-media)
|
|
9
|
+
- [Handling Files](#-handling-files)
|
|
10
|
+
- [Building Forms](#-building-forms)
|
|
11
|
+
- [Admin Commands](#-admin-commands)
|
|
12
|
+
- [Database Patterns](#-database-patterns)
|
|
13
|
+
- [Utility Helpers](#-utility-helpers)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📸 Sending Media
|
|
18
|
+
|
|
19
|
+
### Send Photo from URL
|
|
20
|
+
```ruby
|
|
21
|
+
bot.command('cat') do |ctx|
|
|
22
|
+
ctx.reply "Here's a random cat! 🐱"
|
|
23
|
+
ctx.photo("https://cataas.com/cat")
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Send Photo from File
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
bot.command('logo') do |ctx|
|
|
31
|
+
File.open("logo.png", "rb") do |file|
|
|
32
|
+
ctx.photo(file, caption: "Our logo!")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Send Multiple Photos (Album)
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
bot.command('album') do |ctx|
|
|
41
|
+
photos = [
|
|
42
|
+
"https://example.com/photo1.jpg",
|
|
43
|
+
"https://example.com/photo2.jpg",
|
|
44
|
+
InputFile.new(File.open("local.jpg"))
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Send as media group
|
|
48
|
+
ctx.api.call('sendMediaGroup', {
|
|
49
|
+
chat_id: ctx.chat.id,
|
|
50
|
+
media: photos.map { |p| { type: 'photo', media: p } }
|
|
51
|
+
})
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
📁 Handling Files
|
|
58
|
+
|
|
59
|
+
Receive and Save Document
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
bot.on(:message) do |ctx|
|
|
63
|
+
if ctx.message.document
|
|
64
|
+
file_id = ctx.message.document.file_id
|
|
65
|
+
|
|
66
|
+
# Get file info
|
|
67
|
+
file = ctx.api.call('getFile', file_id: file_id)
|
|
68
|
+
|
|
69
|
+
# Download file
|
|
70
|
+
file_url = "https://api.telegram.org/file/bot#{ctx.bot.token}/#{file['file_path']}"
|
|
71
|
+
download_and_save(file_url, "uploads/#{file_id}")
|
|
72
|
+
|
|
73
|
+
ctx.reply "✅ File saved!"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
File Size Check
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
|
82
|
+
|
|
83
|
+
bot.on(:message) do |ctx|
|
|
84
|
+
if ctx.message.document&.file_size.to_i > MAX_SIZE
|
|
85
|
+
ctx.reply "⚠️ File too large (max 20MB)"
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
📝 Building Forms
|
|
94
|
+
|
|
95
|
+
Simple Contact Form
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
bot.scene :contact_form do
|
|
99
|
+
step :ask_name do |ctx|
|
|
100
|
+
ctx.reply "What's your name?"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
step :ask_email do |ctx|
|
|
104
|
+
ctx.session[:name] = ctx.message.text
|
|
105
|
+
ctx.reply "What's your email?"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
step :ask_message do |ctx|
|
|
109
|
+
ctx.session[:email] = ctx.message.text
|
|
110
|
+
ctx.reply "What's your message?"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
step :submit do |ctx|
|
|
114
|
+
ctx.session[:message] = ctx.message.text
|
|
115
|
+
|
|
116
|
+
# Send to admin
|
|
117
|
+
admin_message = <<~MSG
|
|
118
|
+
📨 New Contact Form:
|
|
119
|
+
|
|
120
|
+
Name: #{ctx.session[:name]}
|
|
121
|
+
Email: #{ctx.session[:email]}
|
|
122
|
+
Message: #{ctx.session[:message]}
|
|
123
|
+
MSG
|
|
124
|
+
|
|
125
|
+
ctx.api.call('sendMessage', {
|
|
126
|
+
chat_id: ADMIN_ID,
|
|
127
|
+
text: admin_message
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
ctx.reply "✅ Message sent! We'll reply soon."
|
|
131
|
+
ctx.leave_scene
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
👑 Admin Commands
|
|
139
|
+
|
|
140
|
+
Admin Middleware
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
ADMIN_IDS = [123456, 789012].freeze
|
|
144
|
+
|
|
145
|
+
class AdminOnly
|
|
146
|
+
def call(ctx, next_middleware)
|
|
147
|
+
if ADMIN_IDS.include?(ctx.from.id)
|
|
148
|
+
next_middleware.call(ctx)
|
|
149
|
+
else
|
|
150
|
+
ctx.reply "⛔ Admin only"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
bot.use AdminOnly.new
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Broadcast to All Users
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
bot.command('broadcast') do |ctx|
|
|
162
|
+
# Get all user IDs from database
|
|
163
|
+
user_ids = User.pluck(:telegram_id)
|
|
164
|
+
|
|
165
|
+
ctx.reply "Broadcasting to #{user_ids.size} users..."
|
|
166
|
+
|
|
167
|
+
Async do
|
|
168
|
+
user_ids.each do |user_id|
|
|
169
|
+
begin
|
|
170
|
+
ctx.api.call('sendMessage', {
|
|
171
|
+
chat_id: user_id,
|
|
172
|
+
text: "📢 Announcement: #{ctx.message.text.sub('/broadcast ', '')}"
|
|
173
|
+
})
|
|
174
|
+
sleep(0.1) # Rate limiting
|
|
175
|
+
rescue => e
|
|
176
|
+
logger.error("Failed to send to #{user_id}: #{e.message}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
ctx.reply "✅ Broadcast sent!"
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
🗄️ Database Patterns
|
|
188
|
+
|
|
189
|
+
ActiveRecord Integration
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class User < ActiveRecord::Base
|
|
193
|
+
def self.from_telegram(ctx)
|
|
194
|
+
find_or_create_by(telegram_id: ctx.from.id) do |user|
|
|
195
|
+
user.username = ctx.from.username
|
|
196
|
+
user.first_name = ctx.from.first_name
|
|
197
|
+
user.last_name = ctx.from.last_name
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
bot.command('profile') do |ctx|
|
|
203
|
+
user = User.from_telegram(ctx)
|
|
204
|
+
|
|
205
|
+
profile = <<~PROFILE
|
|
206
|
+
👤 Your Profile:
|
|
207
|
+
|
|
208
|
+
ID: #{user.telegram_id}
|
|
209
|
+
Name: #{user.first_name} #{user.last_name}
|
|
210
|
+
Joined: #{user.created_at.strftime('%Y-%m-%d')}
|
|
211
|
+
Messages: #{user.messages_count}
|
|
212
|
+
PROFILE
|
|
213
|
+
|
|
214
|
+
ctx.reply profile
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Redis Session Store
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
require 'redis'
|
|
222
|
+
require 'json'
|
|
223
|
+
|
|
224
|
+
class RedisSessionStore
|
|
225
|
+
def initialize(redis = Redis.new)
|
|
226
|
+
@redis = redis
|
|
227
|
+
@prefix = "telegem:session"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def get(user_id)
|
|
231
|
+
data = @redis.get("#{@prefix}:#{user_id}")
|
|
232
|
+
data ? JSON.parse(data, symbolize_names: true) : {}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def set(user_id, data)
|
|
236
|
+
@redis.setex("#{@prefix}:#{user_id}", 3600, data.to_json)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Use it
|
|
241
|
+
bot = Telegem.new(token, session_store: RedisSessionStore.new)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
🛠️ Utility Helpers
|
|
247
|
+
|
|
248
|
+
Formatting Helper
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
module Formatters
|
|
252
|
+
def self.markdown(text)
|
|
253
|
+
# Escape MarkdownV2 special characters
|
|
254
|
+
text.gsub(/[_*[\]()~`>#+\-=|{}.!]/, '\\\\\0')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.html(text)
|
|
258
|
+
# Simple HTML escaping
|
|
259
|
+
text.gsub('&', '&')
|
|
260
|
+
.gsub('<', '<')
|
|
261
|
+
.gsub('>', '>')
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
bot.command('bold') do |ctx|
|
|
266
|
+
text = ctx.message.text.sub('/bold ', '')
|
|
267
|
+
ctx.reply "*#{Formatters.markdown(text)}*", parse_mode: 'MarkdownV2'
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Pagination Helper
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class Paginator
|
|
275
|
+
def initialize(items, per_page: 5)
|
|
276
|
+
@items = items
|
|
277
|
+
@per_page = per_page
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def page(number)
|
|
281
|
+
start = (number - 1) * @per_page
|
|
282
|
+
@items.slice(start, @per_page)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def total_pages
|
|
286
|
+
(@items.size.to_f / @per_page).ceil
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
bot.command('list') do |ctx|
|
|
291
|
+
items = (1..50).to_a # Your data
|
|
292
|
+
paginator = Paginator.new(items)
|
|
293
|
+
page_num = ctx.session[:page] || 1
|
|
294
|
+
|
|
295
|
+
# Show page
|
|
296
|
+
page_items = paginator.page(page_num)
|
|
297
|
+
ctx.reply "Page #{page_num}/#{paginator.total_pages}\n#{page_items.join(', ')}"
|
|
298
|
+
|
|
299
|
+
# Add navigation buttons
|
|
300
|
+
keyboard = Telegem::Markup.inline do
|
|
301
|
+
row callback("⬅️ Prev", "page_#{page_num-1}") if page_num > 1
|
|
302
|
+
row callback("Next ➡️", "page_#{page_num+1}") if page_num < paginator.total_pages
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
ctx.reply "Navigate:", reply_markup: keyboard
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
🚀 Deployment Recipes
|
|
312
|
+
|
|
313
|
+
Dockerfile
|
|
314
|
+
|
|
315
|
+
```dockerfile
|
|
316
|
+
FROM ruby:3.2-alpine
|
|
317
|
+
|
|
318
|
+
WORKDIR /app
|
|
319
|
+
|
|
320
|
+
# Install dependencies
|
|
321
|
+
RUN apk add --no-cache build-base git
|
|
322
|
+
|
|
323
|
+
# Install gems
|
|
324
|
+
COPY Gemfile Gemfile.lock ./
|
|
325
|
+
RUN bundle install --jobs=4 --retry=3
|
|
326
|
+
|
|
327
|
+
# Copy app
|
|
328
|
+
COPY . .
|
|
329
|
+
|
|
330
|
+
# Run bot
|
|
331
|
+
CMD ["ruby", "bot.rb"]
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Docker Compose
|
|
335
|
+
|
|
336
|
+
```yaml
|
|
337
|
+
version: '3.8'
|
|
338
|
+
services:
|
|
339
|
+
bot:
|
|
340
|
+
build: .
|
|
341
|
+
environment:
|
|
342
|
+
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
343
|
+
- REDIS_URL=redis://redis:6379
|
|
344
|
+
- DATABASE_URL=postgres://postgres:password@db:5432/bot
|
|
345
|
+
depends_on:
|
|
346
|
+
- redis
|
|
347
|
+
- db
|
|
348
|
+
|
|
349
|
+
redis:
|
|
350
|
+
image: redis:alpine
|
|
351
|
+
ports:
|
|
352
|
+
- "6379:6379"
|
|
353
|
+
|
|
354
|
+
db:
|
|
355
|
+
image: postgres:15-alpine
|
|
356
|
+
environment:
|
|
357
|
+
- POSTGRES_PASSWORD=password
|
|
358
|
+
- POSTGRES_DB=bot
|
|
359
|
+
volumes:
|
|
360
|
+
- postgres_data:/var/lib/postgresql/data
|
|
361
|
+
|
|
362
|
+
volumes:
|
|
363
|
+
postgres_data:
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
🆘 Troubleshooting
|
|
369
|
+
|
|
370
|
+
"Token Invalid" Error
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
# Check your token format
|
|
374
|
+
token = ENV['TELEGRAM_BOT_TOKEN']
|
|
375
|
+
|
|
376
|
+
# Should be: 1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ
|
|
377
|
+
# Has two parts separated by colon
|
|
378
|
+
if token.nil? || token.split(':').size != 2
|
|
379
|
+
puts "❌ Invalid token format!"
|
|
380
|
+
exit 1
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
"Bot Not Responding"
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
# Add logging middleware
|
|
388
|
+
bot.use do |ctx, next_middleware|
|
|
389
|
+
puts "📨 Received: #{ctx.message&.text}"
|
|
390
|
+
next_middleware.call(ctx)
|
|
391
|
+
puts "✅ Handled"
|
|
392
|
+
end
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
🎯 Quick Search
|
|
398
|
+
|
|
399
|
+
Looking for something specific?
|
|
400
|
+
|
|
401
|
+
• Photos → Send Photo from URL
|
|
402
|
+
• Files → Receive and Save Document
|
|
403
|
+
• Database → ActiveRecord Integration
|
|
404
|
+
• Admin → Admin Middleware
|
|
405
|
+
• Deploy → Dockerfile
|
|
406
|
+
|
|
407
|
+
---
|