telegem 2.1.0 → 3.0.1

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +2 -2
  3. data/Starts_HallofFame.md +65 -0
  4. data/Test-Projects/bot.md +145 -0
  5. data/Test-Projects/bot1.rb +91 -0
  6. data/bin/telegem-ssl +44 -0
  7. data/docs-src/Bot-registration_.PNG +0 -0
  8. data/docs-src/bot.md +349 -180
  9. data/docs-src/ctx.md +399 -0
  10. data/docs-src/webhook.md +341 -0
  11. data/lib/api/client.rb +72 -27
  12. data/lib/core/bot.rb +81 -149
  13. data/lib/telegem.rb +1 -1
  14. data/lib/webhook/server.rb +149 -290
  15. data/public/index.html +481 -0
  16. metadata +32 -24
  17. data/Test-Projects/Movie-tracker-bot/Gemfile +0 -2
  18. data/Test-Projects/Movie-tracker-bot/bot.rb +0 -62
  19. data/Test-Projects/Movie-tracker-bot/handlers/.gitkeep +0 -0
  20. data/Test-Projects/Movie-tracker-bot/handlers/add_1_.rb +0 -160
  21. data/Test-Projects/Movie-tracker-bot/handlers/add_2_.rb +0 -139
  22. data/Test-Projects/Movie-tracker-bot/handlers/premium.rb +0 -13
  23. data/Test-Projects/Movie-tracker-bot/handlers/report.rb +0 -31
  24. data/Test-Projects/Movie-tracker-bot/handlers/search.rb +0 -150
  25. data/Test-Projects/Movie-tracker-bot/handlers/sponsor.rb +0 -14
  26. data/Test-Projects/Movie-tracker-bot/handlers/start.rb +0 -48
  27. data/Test-Projects/Movie-tracker-bot/handlers/watch.rb +0 -210
  28. data/Test-Projects/Test-submitted-by-marvel/.gitkeep +0 -0
  29. data/Test-Projects/Test-submitted-by-marvel/Marvel-bot.md +0 -3
  30. data/Test-Projects/bot_test1.rb +0 -75
  31. data/Test-Projects/pizza_test_bot_guide.md +0 -163
  32. data/docs-src/understanding-ctx.md +0 -581
  33. /data/{Test-Projects → bin}/.gitkeep +0 -0
  34. /data/{Test-Projects/Movie-tracker-bot → public}/.gitkeep +0 -0
@@ -1,326 +1,185 @@
1
- # lib/webhook/server.rb - V2.0.0 (CLOUD-READY)
1
+ # lib/telegem/webhook/server.rb
2
+ require 'async/http/server'
3
+ require 'async/http/endpoint'
4
+ require 'openssl'
5
+ require 'yaml'
2
6
  require 'json'
3
- require 'rack'
7
+ require 'securerandom'
8
+ require 'uri'
4
9
 
5
10
  module Telegem
6
11
  module Webhook
7
12
  class Server
8
- attr_reader :bot, :port, :host, :logger, :running, :secret_token
9
-
10
- def initialize(bot, port: nil, host: '0.0.0.0', logger: nil, max_threads: 10, secret_token: nil)
13
+ attr_reader :bot, :port, :host, :logger, :secret_token, :running, :server, :ssl_mode
14
+
15
+ def initialize(bot, port: nil, host: '0.0.0.0', secret_token: nil, logger: nil, ssl: nil)
11
16
  @bot = bot
12
17
  @port = port || ENV['PORT'] || 3000
13
18
  @host = host
19
+ @secret_token = secret_token || ENV['WEBHOOK_SECRET_TOKEN'] || SecureRandom.hex(16)
14
20
  @logger = logger || Logger.new($stdout)
15
- @max_threads = max_threads
16
- @thread_pool = Concurrent::FixedThreadPool.new(max_threads)
17
- @secret_token = secret_token || generate_secret_token
18
- @server = nil
19
- @running = false
20
- @webhook_path = "/webhook"
21
- end
22
-
23
- # Production server with Puma
24
- def run
25
- return if @running
26
-
27
- @logger.info "🚀 Starting Telegem webhook server on #{@host}:#{@port}"
28
- @logger.info "📝 Set Telegram webhook to: #{webhook_url}"
29
- @logger.info "🔐 Secret token: #{@secret_token}"
30
- @logger.info "✅ Using Puma (production-ready)"
31
- @logger.info "☁️ Cloud-ready: #{cloud_platform}"
32
-
33
- @running = true
34
-
35
- app = build_rack_app
36
- start_puma_server(app)
37
-
38
- self
39
- end
40
-
41
- def stop
42
- return unless @running
43
-
44
- @logger.info "🛑 Stopping webhook server..."
45
21
  @running = false
46
-
47
- @server&.stop
48
- @thread_pool.shutdown
49
- @thread_pool.wait_for_termination(5)
50
-
51
- @logger.info "✅ Webhook server stopped"
52
- end
53
-
54
- alias_method :shutdown, :stop
55
-
56
- def running?
57
- @running
22
+ @server = nil
23
+
24
+ @ssl_mode, @ssl_context = determine_ssl_mode(ssl)
25
+ log_configuration
26
+ validate_ssl_setup
58
27
  end
59
-
60
- def webhook_url
61
- # For cloud platforms, use their URL
62
- if cloud_url = detect_cloud_url
63
- "#{cloud_url}#{@webhook_path}"
64
- elsif ENV['WEBHOOK_URL']
65
- ENV['WEBHOOK_URL']
66
- else
67
- # Local development
68
- "https://#{@host}:#{@port}#{@webhook_path}"
28
+
29
+ def determine_ssl_mode(ssl_options)
30
+ return [:none, nil] if ssl_options == false
31
+
32
+ if File.exist?('.telegem-ssl')
33
+ config = YAML.load_file('.telegem-ssl')
34
+ cert_path = config['cert_path']
35
+ key_path = config['key_path']
36
+
37
+ if cert_path && key_path && File.exist?(cert_path) && File.exist?(key_path)
38
+ return [:cli, load_certificate_files(cert_path, key_path)]
39
+ end
40
+ end
41
+
42
+ if ssl_options && ssl_options[:cert_path] && ssl_options[:key_path]
43
+ return [:manual, load_certificate_files(ssl_options[:cert_path], ssl_options[:key_path])]
44
+ end
45
+
46
+ if ENV['TELEGEM_WEBHOOK_URL'] && URI(ENV['TELEGEM_WEBHOOK_URL']).scheme == 'https'
47
+ return [:cloud, nil]
69
48
  end
49
+
50
+ [:none, nil]
70
51
  end
71
-
72
- def set_webhook(**options)
73
- params = {
74
- url: webhook_url,
75
- secret_token: @secret_token,
76
- drop_pending_updates: true
77
- }.merge(options)
78
-
79
- @bot.api.call!('setWebhook', params)
52
+
53
+ def load_certificate_files(cert_path, key_path)
54
+ ctx = OpenSSL::SSL::SSLContext.new
55
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
56
+ ctx.key = OpenSSL::PKey::RSA.new(File.read(key_path))
57
+ ctx
58
+ rescue
59
+ nil
80
60
  end
81
-
82
- def delete_webhook
83
- @bot.api.call!('deleteWebhook', {})
61
+
62
+ def validate_ssl_setup
63
+ case @ssl_mode
64
+ when :cli, :manual
65
+ raise "SSL certificate files not found or invalid" if @ssl_context.nil?
66
+ when :cloud
67
+ url = URI(ENV['TELEGEM_WEBHOOK_URL'])
68
+ raise "TELEGEM_WEBHOOK_URL must be HTTPS" unless url.scheme == 'https'
69
+ end
84
70
  end
85
-
86
- # Quick setup helper
87
- def self.setup(bot, **options)
88
- server = new(bot, **options)
89
- server.run
90
- server.set_webhook
91
- server
71
+
72
+ def log_configuration
73
+ @logger.info("Webhook Server Configuration:")
74
+ @logger.info(" Mode: #{@ssl_mode.to_s.upcase}")
75
+ @logger.info(" Port: #{@port}")
76
+ @logger.info(" Host: #{@host}")
77
+ @logger.info(" Secret: #{@secret_token[0..8]}...")
92
78
  end
93
-
94
- private
95
-
96
- def build_rack_app
97
- Rack::Builder.new do
98
- use Rack::CommonLogger
99
- use Rack::ShowExceptions if ENV['RACK_ENV'] == 'development'
100
-
101
- # Health endpoint (required by Render/Railway/Heroku)
102
- map "/health" do
103
- run ->(env) {
104
- [200, {
105
- 'Content-Type' => 'application/json',
106
- 'Cache-Control' => 'no-cache'
107
- }, [{
108
- status: 'healthy',
109
- timestamp: Time.now.iso8601,
110
- service: 'telegem-webhook',
111
- version: Telegem::VERSION
112
- }.to_json]]
113
- }
114
- end
115
-
116
- # Webhook endpoint
117
- map "/webhook" do
118
- run ->(env) do
119
- server = env['telegem.server']
120
- req = Rack::Request.new(env)
121
-
122
- if req.post?
123
- server.handle_rack_request(req)
124
- else
125
- [405, { 'Content-Type' => 'text/plain' }, ['Method Not Allowed']]
126
- end
127
- end
128
- end
129
-
130
- # Root landing page
131
- map "/" do
132
- run ->(env) do
133
- server = env['telegem.server']
134
- [200, { 'Content-Type' => 'text/html' }, [server.landing_page]]
135
- end
136
- end
79
+
80
+ def run
81
+ return if @running
82
+ @running = true
83
+
84
+ case @ssl_mode
85
+ when :cli, :manual
86
+ endpoint = Async::HTTP::Endpoint.parse("https://#{@host}:#{@port}", ssl_context: @ssl_context)
87
+ @logger.info("Starting HTTPS server with local certificates")
88
+ when :cloud
89
+ endpoint = Async::HTTP::Endpoint.parse("http://#{@host}:#{@port}")
90
+ @logger.info("Starting HTTP server (cloud platform handles SSL)")
91
+ else
92
+ endpoint = Async::HTTP::Endpoint.parse("http://#{@host}:#{@port}")
93
+ @logger.warn("Starting HTTP server (Telegram requires HTTPS)")
137
94
  end
138
- end
139
-
140
- def start_puma_server(app)
141
- require 'puma'
142
-
143
- # Cloud platforms set PORT environment variable
144
- port = ENV['PORT'] || @port
145
-
146
- config = {
147
- Host: @host,
148
- Port: port,
149
- Threads: "0:#{@max_threads}",
150
- workers: ENV['WEB_CONCURRENCY']&.to_i || 1,
151
- daemonize: false,
152
- silent: false,
153
- environment: ENV['RACK_ENV'] || 'production',
154
- # Cloud platforms handle SSL termination
155
- ssl_bind: nil # Let platform handle SSL
156
- }
157
-
158
- @server = Puma::Server.new(app)
159
- @server.add_tcp_listener(@host, port)
160
-
161
- @server.app = ->(env) do
162
- env['telegem.server'] = self
163
- app.call(env)
95
+
96
+ @server = Async::HTTP::Server.for(endpoint) do |request|
97
+ handle_request(request)
164
98
  end
165
-
166
- Thread.new do
167
- begin
168
- @server.run
169
- rescue => e
170
- @logger.error "❌ Puma server error: #{e.message}"
171
- @running = false
172
- raise
173
- end
99
+
100
+ Async do |task|
101
+ @server.run
102
+ task.sleep while @running
174
103
  end
175
-
176
- sleep 1 until @server.running
177
104
  end
178
-
179
- def handle_rack_request(req)
180
- # Validate secret token from Telegram
181
- telegram_token = req.get_header('X-Telegram-Bot-Api-Secret-Token')
182
-
183
- if @secret_token && telegram_token != @secret_token
184
- @logger.warn "⚠️ Invalid secret token from #{req.ip}"
185
- return [403, { 'Content-Type' => 'text/plain' }, ['Forbidden']]
105
+
106
+ def handle_request(request)
107
+ case request.path
108
+ when @secret_token, "/#{@secret_token}"
109
+ handle_webhook_request(request)
110
+ when '/health', '/healthz'
111
+ health_endpoint(request)
112
+ else
113
+ [404, {}, ["Not Found"]]
186
114
  end
187
-
115
+ end
116
+
117
+ def handle_webhook_request(request)
118
+ return [405, {}, ["Method Not Allowed"]] unless request.post?
119
+
188
120
  begin
189
- body = req.body.read
121
+ body = request.body.read
190
122
  update_data = JSON.parse(body)
191
-
192
- # Process in thread pool (not spawn new threads!)
193
- @thread_pool.post do
194
- begin
195
- @bot.process(update_data)
196
- rescue => e
197
- @logger.error "Error processing update: #{e.message}"
198
- end
199
- end
200
-
201
- [200, { 'Content-Type' => 'text/plain' }, ['OK']]
202
- rescue JSON::ParserError
203
- [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']]
204
- rescue => e
205
- @logger.error "Webhook error: #{e.message}"
206
- [500, { 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
123
+ Async { process_webhook_update(update_data) }
124
+ [200, {}, ["OK"]]
125
+ rescue
126
+ [500, {}, ["Internal Server Error"]]
207
127
  end
208
128
  end
209
-
210
- def landing_page
211
- <<~HTML
212
- <!DOCTYPE html>
213
- <html>
214
- <head>
215
- <title>Telegem Webhook Server</title>
216
- <style>
217
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
218
- max-width: 800px; margin: 0 auto; padding: 20px; }
219
- .status { background: #4CAF50; color: white; padding: 5px 10px; border-radius: 3px; }
220
- code { background: #f5f5f5; padding: 2px 5px; border-radius: 3px; }
221
- a { color: #2196F3; text-decoration: none; }
222
- </style>
223
- </head>
224
- <body>
225
- <h1>🤖 Telegem Webhook Server</h1>
226
- <p><strong>Status:</strong> <span class="status">Running</span></p>
227
- <p><strong>Webhook URL:</strong> <code>#{webhook_url}</code></p>
228
- <p><strong>Health Check:</strong> <a href="/health">/health</a></p>
229
- <p><strong>Platform:</strong> #{cloud_platform}</p>
230
- <hr>
231
- <p>To set up your Telegram bot:</p>
232
- <pre><code>bot = Telegem.new('YOUR_TOKEN')
233
- server = bot.webhook
234
- server.run
235
- server.set_webhook</code></pre>
236
- </body>
237
- </html>
238
- HTML
129
+
130
+ def process_webhook_update(update_data)
131
+ @bot.process(update_data)
132
+ rescue => e
133
+ @logger.error("Process error: #{e}")
134
+ end
135
+
136
+ def health_endpoint(request)
137
+ [200, { 'Content-Type' => 'application/json' }, [{
138
+ status: 'ok',
139
+ mode: @ssl_mode.to_s,
140
+ ssl: @ssl_mode != :none
141
+ }.to_json]]
239
142
  end
240
-
241
- def generate_secret_token
242
- SecureRandom.hex(32)
143
+
144
+ def stop
145
+ return unless @running
146
+ @running = false
147
+ @server&.close
148
+ @logger.info("Server stopped")
149
+ @server = nil
243
150
  end
244
-
245
- def detect_cloud_url
246
- # Render
247
- if ENV['RENDER_EXTERNAL_URL']
248
- ENV['RENDER_EXTERNAL_URL']
249
- # Railway
250
- elsif ENV['RAILWAY_STATIC_URL']
251
- ENV['RAILWAY_STATIC_URL']
252
- # Heroku
253
- elsif ENV['HEROKU_APP_NAME']
254
- "https://#{ENV['HEROKU_APP_NAME']}.herokuapp.com"
255
- # Fly.io
256
- elsif ENV['FLY_APP_NAME']
257
- "https://#{ENV['FLY_APP_NAME']}.fly.dev"
258
- # Vercel (if using serverless)
259
- elsif ENV['VERCEL_URL']
260
- "https://#{ENV['VERCEL_URL']}"
151
+
152
+ def webhook_url
153
+ case @ssl_mode
154
+ when :cli, :manual
155
+ "https://#{@host}:#{@port}#{@secret_token}"
156
+ when :cloud
157
+ cloud_url = ENV['TELEGEM_WEBHOOK_URL'].chomp('/')
158
+ "#{cloud_url}#{@secret_token}"
261
159
  else
262
- nil
160
+ "http://#{@host}:#{@port}#{@secret_token}"
263
161
  end
264
162
  end
265
-
266
- def cloud_platform
267
- if ENV['RENDER'] then 'Render'
268
- elsif ENV['RAILWAY'] then 'Railway'
269
- elsif ENV['HEROKU_APP_NAME'] then 'Heroku'
270
- elsif ENV['FLY_APP_NAME'] then 'Fly.io'
271
- elsif ENV['VERCEL'] then 'Vercel'
272
- elsif ENV['DYNO'] then 'Heroku'
273
- else 'Local/Unknown'
274
- end
163
+
164
+ def set_webhook(**options)
165
+ url = webhook_url
166
+ params = { url: url }.merge(options)
167
+ @bot.set_webhook(**params)
168
+ @logger.info("Webhook set to: #{url}")
169
+ url
275
170
  end
276
- end
277
-
278
- # Rack Middleware for existing apps
279
- class Middleware
280
- def initialize(app, bot, secret_token: nil)
281
- @app = app
282
- @bot = bot
283
- @secret_token = secret_token || ENV['TELEGRAM_SECRET_TOKEN']
284
- @thread_pool = Concurrent::FixedThreadPool.new(10)
171
+
172
+ def delete_webhook
173
+ @bot.delete_webhook
174
+ @logger.info("Webhook deleted")
285
175
  end
286
-
287
- def call(env)
288
- req = Rack::Request.new(env)
289
-
290
- if req.post? && req.path == "/webhook"
291
- handle_webhook(req)
292
- else
293
- @app.call(env)
294
- end
176
+
177
+ def get_webhook_info
178
+ @bot.get_webhook_info
295
179
  end
296
-
297
- private
298
-
299
- def handle_webhook(req)
300
- telegram_token = req.get_header('X-Telegram-Bot-Api-Secret-Token')
301
-
302
- if @secret_token && telegram_token != @secret_token
303
- return [403, { 'Content-Type' => 'text/plain' }, ['Forbidden']]
304
- end
305
-
306
- begin
307
- update_data = JSON.parse(req.body.read)
308
-
309
- @thread_pool.post do
310
- begin
311
- @bot.process(update_data)
312
- rescue => e
313
- @bot.logger&.error("Webhook error: #{e.message}")
314
- end
315
- end
316
-
317
- [200, { 'Content-Type' => 'text/plain' }, ['OK']]
318
- rescue JSON::ParserError
319
- [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']]
320
- rescue => e
321
- @bot.logger&.error("Webhook error: #{e.message}")
322
- [500, { 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
323
- end
180
+
181
+ def running?
182
+ @running
324
183
  end
325
184
  end
326
185
  end