telegem 0.1.6 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca0f4841ae7f22222a39ad5a3664843a908f522ac988cf9da42367b8edaa31e2
4
- data.tar.gz: dd000aa5a145b4912bedc5b4e5eaf071a9fb6198a30279b43edf81492dab160e
3
+ metadata.gz: dc3dc81c673fa1128b108e02c3401f4bac483d7173759b4c901c3db625fa8dd8
4
+ data.tar.gz: 4cb6de9fa5d9b8c5babbbd83ff86b5b7f6c0161628e74dbb7416ee5eeaa10e70
5
5
  SHA512:
6
- metadata.gz: 5cff4090a99a1d98996b25303e80ca516e415d383cbe123657a9606aeddf6fa3b211891ad568c0bfff85d1fd6b3865a61416ab3e9f03862c86603fb8aac79437
7
- data.tar.gz: 3fcb8b5de79185dc660833dd2ae20ecdac3034633f84e14a770c29df408631ff310ca3349e7383333eb8ec2cee5cd001df6b918451144e4e93a8051e75ad2486
6
+ metadata.gz: 32211bca88485aa5d19b41c8bc400b1af8b790de8ec4cf9761537a2f59de299e4ee6c5dca2de3e5561f58fe0280238c1452cff10429aa876d2fad2426556819d
7
+ data.tar.gz: fef73c4f8d976614ec815cee3c230409bf471564f38a46acb5fc7093c80d2154186a621f68068098cc1072cdb144184c92a683be83e42107f7ea409ea3175b5e
data/lib/api/client.rb CHANGED
@@ -1,156 +1,163 @@
1
- module Telegem
2
- module API
3
- class Client
4
- BASE_URL = 'https://api.telegram.org'
5
-
6
- attr_reader :token, :logger
7
-
8
- def initialize(token, endpoint: nil, logger: nil)
9
- @token = token
10
- @logger = logger || Logger.new($stdout)
11
- @endpoint = endpoint || Async::HTTP::Endpoint.parse("#{BASE_URL}/bot#{token}")
12
- @client = nil
13
- @semaphore = Async::Semaphore.new(30)
14
- end
15
-
16
- def call(method, params = {})
17
- Async do |task|
18
- @semaphore.async do
19
- make_request(method, clean_params(params))
20
- end
21
- end
22
- end
23
-
24
- def upload(method, params)
25
- Async do |task|
26
- @semaphore.async do
27
- make_multipart_request(method, params)
28
- end
29
- end
30
- end
31
-
32
- def get_updates(offset: nil, timeout: 30, limit: 100)
33
- params = { timeout: timeout, limit: limit }
34
- params[:offset] = offset if offset
35
- call('getUpdates', params)
36
- end
37
-
38
- def close
39
- @client&.close
40
- end
41
-
42
- private
43
-
44
- def make_request(method, params)
45
- with_client do |client|
46
- headers = { 'content-type' => 'application/json' }
47
- body = params.to_json
48
-
49
- response = client.post("/bot#{@token}/#{method}", headers, body)
50
- handle_response(response)
51
- end
52
- end
53
-
54
- def make_multipart_request(method, params)
55
- with_client do |client|
56
- form = build_multipart(params)
57
- headers = form.headers
58
-
59
- response = client.post("/bot#{@token}/#{method}", headers, form.body)
60
- handle_response(response)
61
- end
62
- end
63
-
64
- def with_client(&block)
65
- @client ||= Async::HTTP::Client.new(@endpoint)
66
- yield @client
67
- end
68
-
69
- def clean_params(params)
70
- params.reject { |_, v| v.nil? }
71
- end
72
-
73
- def build_multipart(params)
74
- # Build multipart form data for file uploads
75
- boundary = SecureRandom.hex(16)
76
- parts = []
77
-
78
- params.each do |key, value|
79
- if file?(value)
80
- parts << part_from_file(key, value, boundary)
81
- else
82
- parts << part_from_field(key, value, boundary)
83
- end
84
- end
85
-
86
- parts << "--#{boundary}--\r\n"
87
-
88
- body = parts.join
89
- headers = {
90
- 'content-type' => "multipart/form-data; boundary=#{boundary}",
91
- 'content-length' => body.bytesize.to_s
92
- }
93
-
94
- OpenStruct.new(headers: headers, body: body)
95
- end
96
-
97
- def file?(value)
98
- value.is_a?(File) ||
99
- value.is_a?(StringIO) ||
100
- (value.is_a?(String) && File.exist?(value))
101
- end
102
-
103
- def part_from_file(key, file, boundary)
104
- filename = File.basename(file.path) if file.respond_to?(:path)
105
- filename ||= "file"
106
-
107
- mime_type = MIME::Types.type_for(filename).first || 'application/octet-stream'
108
-
109
- content = if file.is_a?(String)
110
- File.read(file)
111
- else
112
- file.read
113
- end
114
-
115
- <<~PART
116
- --#{boundary}\r
117
- Content-Disposition: form-data; name="#{key}"; filename="#{filename}"\r
118
- Content-Type: #{mime_type}\r
119
- \r
120
- #{content}\r
121
- PART
122
- end
123
-
124
- def part_from_field(key, value, boundary)
125
- <<~PART
126
- --#{boundary}\r
127
- Content-Disposition: form-data; name="#{key}"\r
128
- \r
129
- #{value}\r
130
- PART
131
- end
132
-
133
- def handle_response(response)
134
- body = response.read
135
- json = JSON.parse(body)
136
-
137
- if json['ok']
138
- json['result']
139
- else
140
- raise APIError.new(json['description'], json['error_code'])
141
- end
142
- rescue JSON::ParserError
143
- raise APIError, "Invalid JSON response: #{body[0..100]}"
144
- end
1
+ require 'async'
2
+ require 'async/http'
3
+ require 'json'
4
+ require 'logger'
5
+ require 'mime/types'
6
+ require 'securerandom'
7
+ require 'ostruct'
8
+
9
+ module Telegem
10
+ module API
11
+ class Client
12
+ BASE_URL = 'https://api.telegram.org'
13
+
14
+ attr_reader :token, :logger
15
+
16
+ def initialize(token, endpoint: nil, logger: nil)
17
+ @token = token
18
+ @logger = logger || Logger.new($stdout)
19
+ @endpoint = endpoint || Async::HTTP::Endpoint.parse("#{BASE_URL}/bot#{token}")
20
+ @client = nil
21
+ @semaphore = Async::Semaphore.new(30)
22
+ end
23
+
24
+ def call(method, params = {})
25
+ Async do |task|
26
+ @semaphore.async do
27
+ make_request(method, clean_params(params))
145
28
  end
29
+ end
30
+ end
31
+
32
+ def upload(method, params)
33
+ Async do |task|
34
+ @semaphore.async do
35
+ make_multipart_request(method, params)
36
+ end
37
+ end
38
+ end
39
+
40
+ def get_updates(offset: nil, timeout: 30, limit: 100)
41
+ params = { timeout: timeout, limit: limit }
42
+ params[:offset] = offset if offset
43
+ call('getUpdates', params)
44
+ end
45
+
46
+ def close
47
+ @client&.close
48
+ end
49
+
50
+ private
146
51
 
147
- class APIError < StandardError
148
- attr_reader :code
52
+ def make_request(method, params)
53
+ with_client do |client|
54
+ headers = { 'content-type' => 'application/json' }
55
+ body = params.to_json
149
56
 
150
- def initialize(message, code = nil)
151
- super(message)
152
- @code = code
153
- end
57
+ response = client.post("/bot#{@token}/#{method}", headers, body)
58
+ handle_response(response)
59
+ end
60
+ end
61
+
62
+ def make_multipart_request(method, params)
63
+ with_client do |client|
64
+ form = build_multipart(params)
65
+ headers = form.headers
66
+
67
+ response = client.post("/bot#{@token}/#{method}", headers, form.body)
68
+ handle_response(response)
69
+ end
70
+ end
71
+
72
+ def with_client(&block)
73
+ @client ||= Async::HTTP::Client.new(@endpoint)
74
+ yield @client
75
+ end
76
+
77
+ def clean_params(params)
78
+ params.reject { |_, v| v.nil? }
79
+ end
80
+
81
+ def build_multipart(params)
82
+ boundary = SecureRandom.hex(16)
83
+ parts = []
84
+
85
+ params.each do |key, value|
86
+ if file?(value)
87
+ parts << part_from_file(key, value, boundary)
88
+ else
89
+ parts << part_from_field(key, value, boundary)
154
90
  end
155
91
  end
156
- end
92
+
93
+ parts << "--#{boundary}--\r\n"
94
+
95
+ body = parts.join
96
+ headers = {
97
+ 'content-type' => "multipart/form-data; boundary=#{boundary}",
98
+ 'content-length' => body.bytesize.to_s
99
+ }
100
+
101
+ OpenStruct.new(headers: headers, body: body)
102
+ end
103
+
104
+ def file?(value)
105
+ value.is_a?(File) ||
106
+ value.is_a?(StringIO) ||
107
+ (value.is_a?(String) && File.exist?(value))
108
+ end
109
+
110
+ def part_from_file(key, file, boundary)
111
+ filename = File.basename(file.path) if file.respond_to?(:path)
112
+ filename ||= "file"
113
+
114
+ mime_type = MIME::Types.type_for(filename).first || 'application/octet-stream'
115
+
116
+ content = if file.is_a?(String)
117
+ File.read(file)
118
+ else
119
+ file.read
120
+ end
121
+
122
+ <<~PART
123
+ --#{boundary}\r
124
+ Content-Disposition: form-data; name="#{key}"; filename="#{filename}"\r
125
+ Content-Type: #{mime_type}\r
126
+ \r
127
+ #{content}\r
128
+ PART
129
+ end
130
+
131
+ def part_from_field(key, value, boundary)
132
+ <<~PART
133
+ --#{boundary}\r
134
+ Content-Disposition: form-data; name="#{key}"\r
135
+ \r
136
+ #{value}\r
137
+ PART
138
+ end
139
+
140
+ def handle_response(response)
141
+ body = response.read
142
+ json = JSON.parse(body)
143
+
144
+ if json['ok']
145
+ json['result']
146
+ else
147
+ raise APIError.new(json['description'], json['error_code'])
148
+ end
149
+ rescue JSON::ParserError
150
+ raise APIError, "Invalid JSON response: #{body[0..100]}"
151
+ end
152
+ end
153
+
154
+ class APIError < StandardError
155
+ attr_reader :code
156
+
157
+ def initialize(message, code = nil)
158
+ super(message)
159
+ @code = code
160
+ end
161
+ end
162
+ end
163
+ end
data/lib/core/bot.rb CHANGED
@@ -22,9 +22,10 @@ module Telegem
22
22
  @session_store = options[:session_store] || Session::MemoryStore.new
23
23
  @concurrency = options[:concurrency] || 10
24
24
  @semaphore = Async::Semaphore.new(@concurrency)
25
+ @polling_task = nil
26
+ @running = false
25
27
  end
26
28
 
27
- # DSL Methods
28
29
  def command(name, **options, &block)
29
30
  pattern = /^\/#{Regexp.escape(name)}(?:@\w+)?(?:\s+(.+))?$/i
30
31
 
@@ -59,35 +60,43 @@ module Telegem
59
60
  @scenes[id] = Scene.new(id, &block)
60
61
  end
61
62
 
62
- # Async Polling
63
63
  def start_polling(**options)
64
- Async do |parent|
64
+ return @polling_task if @running
65
+
66
+ @polling_task = Async do |parent|
67
+ @running = true
65
68
  @logger.info "Starting async polling..."
66
69
  offset = nil
67
70
 
68
- loop do
69
- updates = await fetch_updates(offset, **options)
70
-
71
- # Process updates concurrently with limits
72
- updates.each do |update|
73
- parent.async do |child|
74
- @semaphore.async do
75
- await process_update(update)
71
+ begin
72
+ loop do
73
+ updates_task = fetch_updates(offset, **options)
74
+ updates_data = updates_task.wait
75
+ updates = updates_data.map { |data| Types::Update.new(data) }
76
+
77
+ updates.each do |update|
78
+ parent.async do |child|
79
+ @semaphore.async do
80
+ process_update(update)
81
+ end
76
82
  end
77
83
  end
78
- end
79
84
 
80
- offset = updates.last&.update_id.to_i + 1 if updates.any?
85
+ offset = updates.last&.update_id.to_i + 1 if updates.any?
86
+ end
87
+ rescue => e
88
+ handle_error(e)
89
+ raise
90
+ ensure
91
+ @running = false
81
92
  end
82
- rescue => e
83
- handle_error(e)
84
- raise
85
93
  end
94
+
95
+ @polling_task
86
96
  end
87
97
 
88
- # Webhook Support
89
98
  def webhook(app = nil, &block)
90
- require 'telegem/webhook'
99
+ require_relative '../../webhook/server'
91
100
 
92
101
  if block_given?
93
102
  Webhook::Server.new(self, &block)
@@ -117,7 +126,6 @@ module Telegem
117
126
  end
118
127
  end
119
128
 
120
- # Core Processing
121
129
  def process(update_data)
122
130
  Async do
123
131
  update = Types::Update.new(update_data)
@@ -127,24 +135,29 @@ module Telegem
127
135
 
128
136
  def shutdown
129
137
  @logger.info "Shutting down..."
138
+
139
+ if @polling_task && @polling_task.running?
140
+ @polling_task.stop
141
+ @polling_task = nil
142
+ end
143
+
130
144
  @api.close
145
+ @running = false
131
146
  @logger.info "Bot stopped"
132
147
  end
133
148
 
149
+ def running?
150
+ @running
151
+ end
152
+
134
153
  private
135
154
 
136
155
  def fetch_updates(offset, timeout: 30, limit: 100, allowed_updates: nil)
137
- Async do
138
- params = { timeout: timeout, limit: limit }
139
- params[:offset] = offset if offset
140
- params[:allowed_updates] = allowed_updates if allowed_updates
141
-
142
- updates = await @api.get_updates(**params)
143
- updates.map { |data| Types::Update.new(data) }
144
- rescue API::APIError => e
145
- @logger.error "Failed to fetch updates: #{e.message}"
146
- []
147
- end
156
+ params = { timeout: timeout, limit: limit }
157
+ params[:offset] = offset if offset
158
+ params[:allowed_updates] = allowed_updates if allowed_updates
159
+
160
+ @api.get_updates(**params)
148
161
  end
149
162
 
150
163
  def process_update(update)
@@ -169,7 +182,6 @@ module Telegem
169
182
  def build_middleware_chain
170
183
  chain = Composer.new
171
184
 
172
- # Add user middleware
173
185
  @middleware.each do |middleware_class, args, block|
174
186
  if middleware_class.respond_to?(:new)
175
187
  middleware = middleware_class.new(*args, &block)
@@ -179,7 +191,6 @@ module Telegem
179
191
  end
180
192
  end
181
193
 
182
- # Add session middleware if not already added
183
194
  unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
184
195
  chain.use(Session::Middleware.new(@session_store))
185
196
  end
@@ -196,7 +207,7 @@ module Telegem
196
207
  if matches_filters?(ctx, handler[:filters])
197
208
  result = handler[:handler].call(ctx)
198
209
  result = await(result) if result.is_a?(Async::Task)
199
- break # First matching handler wins
210
+ break
200
211
  end
201
212
  end
202
213
  end
@@ -251,11 +262,6 @@ module Telegem
251
262
  ctx.chat.type == type.to_s
252
263
  end
253
264
 
254
- def webhook_server(**options)
255
- require_relative '../webhook/server'
256
- Webhook::Server.new(self, **options)
257
- end
258
-
259
265
  def matches_command_filter(ctx, command_name)
260
266
  return false unless ctx.message&.command?
261
267
  ctx.message.command_name == command_name.to_s
data/lib/telegem.rb CHANGED
@@ -6,7 +6,7 @@ require 'logger'
6
6
  require 'json'
7
7
 
8
8
  module Telegem
9
- VERSION = '0.1.6'.freeze
9
+ VERSION = '0.2.5'.freeze
10
10
 
11
11
  # Define module structure
12
12
  module API; end
data/telegem.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
 
5
5
  # Read version from lib/telegem.rb
6
6
  version_file = File.read('lib/telegem.rb').match(/VERSION\s*=\s*['"]([^'"]+)['"]/)
7
- spec.version = version_file ? version_file[1] : "0.1.6"
7
+ spec.version = version_file ? version_file[1] : "0.2.5"
8
8
 
9
9
  spec.authors = ["Phantom"]
10
10
  spec.email = ["ynghosted@icloud.com"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phantom