telegem 0.1.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.
@@ -0,0 +1,294 @@
1
+ module Telegem
2
+ module Markup
3
+ class Keyboard
4
+ attr_reader :buttons, :options
5
+
6
+ def initialize(buttons = [], **options)
7
+ @buttons = buttons
8
+ @options = {
9
+ resize_keyboard: true,
10
+ one_time_keyboard: false,
11
+ selective: false
12
+ }.merge(options)
13
+ end
14
+
15
+ # Create from array
16
+ def self.[](*rows)
17
+ new(rows)
18
+ end
19
+
20
+ # Builder pattern
21
+ def self.build(&block)
22
+ builder = Builder.new
23
+ builder.instance_eval(&block) if block_given?
24
+ builder.keyboard
25
+ end
26
+
27
+ # Add a row
28
+ def row(*buttons)
29
+ @buttons << buttons.flatten
30
+ self
31
+ end
32
+
33
+ # Add a button
34
+ def button(text, **options)
35
+ last_row = @buttons.last || []
36
+
37
+ if last_row.is_a?(Array)
38
+ last_row << { text: text }.merge(options)
39
+ else
40
+ @buttons << [{ text: text }.merge(options)]
41
+ end
42
+ self
43
+ end
44
+
45
+ # Chainable options
46
+ def resize(resize = true)
47
+ @options[:resize_keyboard] = resize
48
+ self
49
+ end
50
+
51
+ def one_time(one_time = true)
52
+ @options[:one_time_keyboard] = one_time
53
+ self
54
+ end
55
+
56
+ def selective(selective = true)
57
+ @options[:selective] = selective
58
+ self
59
+ end
60
+
61
+ # Convert to Telegram format
62
+ def to_h
63
+ {
64
+ keyboard: @buttons.map { |row| row.is_a?(Array) ? row : [row] },
65
+ **@options
66
+ }
67
+ end
68
+
69
+ def to_json(*args)
70
+ to_h.to_json(*args)
71
+ end
72
+
73
+ # Remove keyboard
74
+ def self.remove(selective: false)
75
+ {
76
+ remove_keyboard: true,
77
+ selective: selective
78
+ }
79
+ end
80
+
81
+ # Force reply
82
+ def self.force_reply(selective: false, input_field_placeholder: nil)
83
+ markup = {
84
+ force_reply: true,
85
+ selective: selective
86
+ }
87
+ markup[:input_field_placeholder] = input_field_placeholder if input_field_placeholder
88
+ markup
89
+ end
90
+ end
91
+
92
+ class InlineKeyboard
93
+ attr_reader :buttons
94
+
95
+ def initialize(buttons = [])
96
+ @buttons = buttons
97
+ end
98
+
99
+ # Create from array
100
+ def self.[](*rows)
101
+ new(rows)
102
+ end
103
+
104
+ # Builder pattern
105
+ def self.build(&block)
106
+ builder = InlineBuilder.new
107
+ builder.instance_eval(&block) if block_given?
108
+ builder.keyboard
109
+ end
110
+
111
+ # Add a row
112
+ def row(*buttons)
113
+ @buttons << buttons.flatten
114
+ self
115
+ end
116
+
117
+ # Add a button
118
+ def button(text, **options)
119
+ last_row = @buttons.last || []
120
+
121
+ if last_row.is_a?(Array)
122
+ last_row << { text: text }.merge(options)
123
+ else
124
+ @buttons << [{ text: text }.merge(options)]
125
+ end
126
+ self
127
+ end
128
+
129
+ # URL button
130
+ def url(text, url)
131
+ button(text, url: url)
132
+ end
133
+
134
+ # Callback button
135
+ def callback(text, data)
136
+ button(text, callback_data: data)
137
+ end
138
+
139
+ # Web app button
140
+ def web_app(text, url)
141
+ button(text, web_app: { url: url })
142
+ end
143
+
144
+ # Login button
145
+ def login(text, url, **options)
146
+ button(text, login_url: { url: url, **options })
147
+ end
148
+
149
+ # Switch inline query button
150
+ def switch_inline(text, query = "")
151
+ button(text, switch_inline_query: query)
152
+ end
153
+
154
+ # Switch inline query current chat button
155
+ def switch_inline_current(text, query = "")
156
+ button(text, switch_inline_query_current_chat: query)
157
+ end
158
+
159
+ # Convert to Telegram format
160
+ def to_h
161
+ {
162
+ inline_keyboard: @buttons.map { |row| row.is_a?(Array) ? row : [row] }
163
+ }
164
+ end
165
+
166
+ def to_json(*args)
167
+ to_h.to_json(*args)
168
+ end
169
+ end
170
+
171
+ # Builder DSL for keyboards
172
+ class Builder
173
+ attr_reader :keyboard
174
+
175
+ def initialize
176
+ @keyboard = Keyboard.new
177
+ end
178
+
179
+ def row(*buttons, &block)
180
+ if block_given?
181
+ sub_builder = Builder.new
182
+ sub_builder.instance_eval(&block)
183
+ @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
184
+ else
185
+ @keyboard.row(*buttons)
186
+ end
187
+ self
188
+ end
189
+
190
+ def button(text, **options)
191
+ @keyboard.button(text, **options)
192
+ self
193
+ end
194
+
195
+ def method_missing(name, *args, &block)
196
+ if @keyboard.respond_to?(name)
197
+ @keyboard.send(name, *args, &block)
198
+ else
199
+ super
200
+ end
201
+ end
202
+
203
+ def respond_to_missing?(name, include_private = false)
204
+ @keyboard.respond_to?(name) || super
205
+ end
206
+ end
207
+
208
+ # Builder DSL for inline keyboards
209
+ class InlineBuilder
210
+ attr_reader :keyboard
211
+
212
+ def initialize
213
+ @keyboard = InlineKeyboard.new
214
+ end
215
+
216
+ def row(*buttons, &block)
217
+ if block_given?
218
+ sub_builder = InlineBuilder.new
219
+ sub_builder.instance_eval(&block)
220
+ @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
221
+ else
222
+ @keyboard.row(*buttons)
223
+ end
224
+ self
225
+ end
226
+
227
+ def button(text, **options)
228
+ @keyboard.button(text, **options)
229
+ self
230
+ end
231
+
232
+ def url(text, url)
233
+ @keyboard.url(text, url)
234
+ self
235
+ end
236
+
237
+ def callback(text, data)
238
+ @keyboard.callback(text, data)
239
+ self
240
+ end
241
+
242
+ def web_app(text, url)
243
+ @keyboard.web_app(text, url)
244
+ self
245
+ end
246
+
247
+ def login(text, url, **options)
248
+ @keyboard.login(text, url, **options)
249
+ self
250
+ end
251
+
252
+ def switch_inline(text, query = "")
253
+ @keyboard.switch_inline(text, query)
254
+ self
255
+ end
256
+
257
+ def switch_inline_current(text, query = "")
258
+ @keyboard.switch_inline_current(text, query)
259
+ self
260
+ end
261
+
262
+ def method_missing(name, *args, &block)
263
+ if @keyboard.respond_to?(name)
264
+ @keyboard.send(name, *args, &block)
265
+ else
266
+ super
267
+ end
268
+ end
269
+
270
+ def respond_to_missing?(name, include_private = false)
271
+ @keyboard.respond_to?(name) || super
272
+ end
273
+ end
274
+
275
+ # Shortcuts for common use
276
+ class << self
277
+ def keyboard(&block)
278
+ Keyboard.build(&block)
279
+ end
280
+
281
+ def inline(&block)
282
+ InlineKeyboard.build(&block)
283
+ end
284
+
285
+ def remove(**options)
286
+ Keyboard.remove(**options)
287
+ end
288
+
289
+ def force_reply(**options)
290
+ Keyboard.force_reply(**options)
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,49 @@
1
+ module Telegem
2
+ module Session
3
+ class MemoryStore
4
+ def initialize
5
+ @store = {}
6
+ @mutex = Mutex.new
7
+ end
8
+
9
+ def get(key)
10
+ @mutex.synchronize do
11
+ @store[key.to_s]
12
+ end
13
+ end
14
+
15
+ def set(key, value)
16
+ @mutex.synchronize do
17
+ @store[key.to_s] = value
18
+ end
19
+ end
20
+
21
+ def delete(key)
22
+ @mutex.synchronize do
23
+ @store.delete(key.to_s)
24
+ end
25
+ end
26
+
27
+ def clear
28
+ @mutex.synchronize do
29
+ @store.clear
30
+ end
31
+ end
32
+
33
+ def keys
34
+ @mutex.synchronize do
35
+ @store.keys
36
+ end
37
+ end
38
+
39
+ def size
40
+ @mutex.synchronize do
41
+ @store.size
42
+ end
43
+ end
44
+
45
+ def empty?
46
+ @mutex.synchronize do
47
+ @store.empty?
48
+ end
49
+ end
@@ -0,0 +1,53 @@
1
+ module Telegem
2
+ module Session
3
+ class Middleware
4
+ def initialize(store = nil)
5
+ @store = store || MemoryStore.new
6
+ end
7
+
8
+ def call(ctx, next_middleware)
9
+ user_id = get_user_id(ctx)
10
+ return next_middleware.call(ctx) unless user_id
11
+
12
+ ctx.session = @store.get(user_id) || {}
13
+
14
+ begin
15
+ result = next_middleware.call(ctx)
16
+ # Handle async result
17
+ result.is_a?(Async::Task) ? result : Async::Task.new(result)
18
+ ensure
19
+ @store.set(user_id, ctx.session)
20
+ end
21
+ end
22
+ private
23
+
24
+ def get_user_id(ctx)
25
+ return nil unless ctx.from
26
+
27
+ ctx.from.id
28
+ end
29
+ end
30
+
31
+ class MemoryStore
32
+ def initialize
33
+ @store = {}
34
+ end
35
+
36
+ def get(key)
37
+ @store[key]
38
+ end
39
+
40
+ def set(key, value)
41
+ @store[key] = value
42
+ end
43
+
44
+ def delete(key)
45
+ @store.delete(key)
46
+ end
47
+
48
+ def clear
49
+ @store.clear
50
+ end
51
+ end
52
+ end
53
+ end
data/telegem.rb ADDED
@@ -0,0 +1,55 @@
1
+ module Telegem
2
+ VERSION = '0.1.0'.freeze
3
+
4
+ # Define module structure
5
+ module API; end
6
+ module Core; end
7
+ module Session; end
8
+ module Markup; end
9
+ module Webhook; end
10
+ module Types; end
11
+
12
+ # Shortcut for creating a new bot
13
+ def self.new(token, **options)
14
+ require_relative 'lib/core/bot'
15
+ Core::Bot.new(token, **options)
16
+ end
17
+
18
+ # Configure global settings
19
+ def self.configure(&block)
20
+ yield(config) if block_given?
21
+ config
22
+ end
23
+
24
+ def self.config
25
+ @config ||= Configuration.new
26
+ end
27
+
28
+ class Configuration
29
+ attr_accessor :logger, :default_adapter, :default_concurrency,
30
+ :default_session_store, :default_webhook_port
31
+
32
+ def initialize
33
+ @logger = Logger.new($stdout)
34
+ @default_adapter = :async_http
35
+ @default_concurrency = 10
36
+ @default_session_store = :memory
37
+ @default_webhook_port = 3000
38
+ end
39
+ end
40
+
41
+ # Error base class
42
+ class Error < StandardError; end
43
+ end
44
+
45
+ # Load all components
46
+ require_relative 'lib/api/client'
47
+ require_relative 'lib/api/types'
48
+ require_relative 'lib/core/bot'
49
+ require_relative 'lib/core/context'
50
+ require_relative 'lib/core/composer'
51
+ require_relative 'lib/core/scene'
52
+ require_relative 'lib/session/middleware'
53
+ require_relative 'lib/session/memory_store'
54
+ require_relative 'lib/markup/keyboard'
55
+ require_relative 'webhook/server'
data/version.rb ADDED
@@ -0,0 +1,4 @@
1
+ # version.rb
2
+ module Telegem
3
+ VERSION = '0.1.0'.freeze
4
+ end
data/webhook/server.rb ADDED
@@ -0,0 +1,86 @@
1
+ module Telegem
2
+ module Webhook
3
+ class Server
4
+ attr_reader :bot, :endpoint, :server, :logger
5
+
6
+ def initialize(bot, endpoint: nil, logger: nil)
7
+ @bot = bot
8
+ @endpoint = endpoint || Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
9
+ @logger = logger || Logger.new($stdout)
10
+ @server = nil
11
+ @running = false
12
+ end
13
+
14
+ def run
15
+ Async do |task|
16
+ @server = Async::HTTP::Server.new(app, @endpoint)
17
+ @running = true
18
+
19
+ @logger.info "Starting webhook server on #{@endpoint}"
20
+ @logger.info "Set your Telegram webhook to: #{webhook_url}"
21
+
22
+ @server.run
23
+ rescue => e
24
+ @logger.error "Webhook server error: #{e.message}"
25
+ raise
26
+ ensure
27
+ @running = false
28
+ end
29
+ end
30
+
31
+ def stop
32
+ Async do
33
+ @server&.close
34
+ @running = false
35
+ @logger.info "Webhook server stopped"
36
+ end
37
+ end
38
+
39
+ def running?
40
+ @running
41
+ end
42
+
43
+ def webhook_url
44
+ "#{@endpoint.url}/webhook/#{@bot.token}"
45
+ end
46
+
47
+ def app
48
+ proc do |req|
49
+ handle_request(req)
50
+ end
51
+ end
52
+
53
+ def process_webhook_request(req)
54
+ Async do
55
+ body = req.body.read
56
+ data = JSON.parse(body)
57
+ await @bot.process(data)
58
+ [200, {}, ["OK"]]
59
+ rescue JSON::ParserError => e
60
+ @logger.error "Invalid JSON in webhook request: #{e.message}"
61
+ [400, {}, ["Bad Request"]]
62
+ rescue => e
63
+ @logger.error "Error processing webhook request: #{e.message}"
64
+ [500, {}, ["Internal Server Error"]]
65
+ end
66
+ end
67
+
68
+ def handle_request(req)
69
+ Async do
70
+ case req.path
71
+ when "/webhook/#{@bot.token}"
72
+ process_webhook_request(req)
73
+ when "/health"
74
+ [200, {}, ["OK"]]
75
+ else
76
+ [404, {}, ["Not Found"]]
77
+ end
78
+ end
79
+ end
80
+
81
+ def call(req)
82
+ handle_request(req)
83
+ end
84
+ end
85
+ end
86
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: telegem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Phantom
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.60'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.60'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mime-types
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.4'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ description: A Telegraf-inspired Telegram Bot framework with async I/O
83
+ email:
84
+ - ynghosted@icloud.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/api/client.rb
90
+ - lib/api/types.rb
91
+ - lib/core/bot.rb
92
+ - lib/core/composer.rb
93
+ - lib/core/context.rb
94
+ - lib/core/scene.rb
95
+ - lib/markup/keyboard.rb
96
+ - lib/session/memory_store.rb
97
+ - lib/session/middleware.rb
98
+ - telegem.rb
99
+ - version.rb
100
+ - webhook/server.rb
101
+ homepage: https://gitlab.com/ruby-telegem/telegem
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ - webhook
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '3.0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.6.9
121
+ specification_version: 4
122
+ summary: Modern, async Telegram Bot API for Ruby
123
+ test_files: []