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.
- checksums.yaml +4 -4
- data/Readme.md +2 -2
- data/Starts_HallofFame.md +65 -0
- data/Test-Projects/bot.md +145 -0
- data/Test-Projects/bot1.rb +91 -0
- data/bin/telegem-ssl +44 -0
- data/docs-src/Bot-registration_.PNG +0 -0
- data/docs-src/bot.md +349 -180
- data/docs-src/ctx.md +399 -0
- data/docs-src/webhook.md +341 -0
- data/lib/api/client.rb +72 -27
- data/lib/core/bot.rb +81 -149
- data/lib/telegem.rb +1 -1
- data/lib/webhook/server.rb +149 -290
- data/public/index.html +481 -0
- metadata +32 -24
- data/Test-Projects/Movie-tracker-bot/Gemfile +0 -2
- data/Test-Projects/Movie-tracker-bot/bot.rb +0 -62
- data/Test-Projects/Movie-tracker-bot/handlers/.gitkeep +0 -0
- data/Test-Projects/Movie-tracker-bot/handlers/add_1_.rb +0 -160
- data/Test-Projects/Movie-tracker-bot/handlers/add_2_.rb +0 -139
- data/Test-Projects/Movie-tracker-bot/handlers/premium.rb +0 -13
- data/Test-Projects/Movie-tracker-bot/handlers/report.rb +0 -31
- data/Test-Projects/Movie-tracker-bot/handlers/search.rb +0 -150
- data/Test-Projects/Movie-tracker-bot/handlers/sponsor.rb +0 -14
- data/Test-Projects/Movie-tracker-bot/handlers/start.rb +0 -48
- data/Test-Projects/Movie-tracker-bot/handlers/watch.rb +0 -210
- data/Test-Projects/Test-submitted-by-marvel/.gitkeep +0 -0
- data/Test-Projects/Test-submitted-by-marvel/Marvel-bot.md +0 -3
- data/Test-Projects/bot_test1.rb +0 -75
- data/Test-Projects/pizza_test_bot_guide.md +0 -163
- data/docs-src/understanding-ctx.md +0 -581
- /data/{Test-Projects → bin}/.gitkeep +0 -0
- /data/{Test-Projects/Movie-tracker-bot → public}/.gitkeep +0 -0
data/lib/webhook/server.rb
CHANGED
|
@@ -1,326 +1,185 @@
|
|
|
1
|
-
# lib/webhook/server.rb
|
|
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 '
|
|
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, :
|
|
9
|
-
|
|
10
|
-
def initialize(bot, port: nil, host: '0.0.0.0',
|
|
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
|
-
|
|
48
|
-
@
|
|
49
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
@
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 =
|
|
121
|
+
body = request.body.read
|
|
190
122
|
update_data = JSON.parse(body)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
242
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
160
|
+
"http://#{@host}:#{@port}#{@secret_token}"
|
|
263
161
|
end
|
|
264
162
|
end
|
|
265
|
-
|
|
266
|
-
def
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
288
|
-
|
|
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
|
-
|
|
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
|