tgbot 0.1.6 → 0.2.2

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.
@@ -1,5 +1,291 @@
1
1
  require 'tgbot/version'
2
- require 'tgbot/core'
3
- require 'tgbot/update'
4
- require 'tgbot/runner'
5
- require 'tgbot/dsl'
2
+
3
+ require 'http'
4
+ require 'mimemagic'
5
+ require 'ostruct'
6
+ require 'json'
7
+ require 'psych'
8
+
9
+ class Tgbot
10
+ APIDOC = Psych.load_file File.join __dir__, 'api.yaml'
11
+
12
+ def self.run(*args, &blk)
13
+ bot = new(*args)
14
+ bot.instance_exec(bot, &blk) if blk
15
+ bot.run
16
+ end
17
+
18
+ attr_accessor :debug
19
+
20
+ def initialize(token, proxy: nil, debug: false)
21
+ @prefix = "/bot#{token}"
22
+ @client = HTTP.persistent "https://api.telegram.org"
23
+ if proxy
24
+ addr, port = *proxy.split(':')
25
+ @client = @client.via(addr, port.to_i)
26
+ end
27
+ @debug = debug
28
+ @commands = []
29
+ @start = @finish = nil
30
+ end
31
+
32
+ def start &blk
33
+ @start = blk
34
+ end
35
+
36
+ def finish &blk
37
+ @finish = blk
38
+ end
39
+
40
+ def on pattern=nil, name: nil, before_all: false, after_all: false, &blk
41
+ if before_all && after_all
42
+ raise ArgumentError, 'before_all and after_all can\'t both be true'
43
+ end
44
+ @commands << [pattern, name, before_all, after_all, blk]
45
+ end
46
+
47
+ def debug msg
48
+ STDERR.puts "#{Time.now.strftime "%FT%T"} #{msg}" if @debug
49
+ end
50
+
51
+ class Update
52
+ attr_reader :bot, :data
53
+
54
+ def initialize bot, data
55
+ @bot = bot
56
+ @data = data
57
+ end
58
+
59
+ def interrupt!
60
+ @bot.instance_variable_get(:@tasks).clear
61
+ end
62
+
63
+ alias done! interrupt!
64
+
65
+ def retry! n=1
66
+ @retried ||= n
67
+ return if @retried <= 0
68
+ @bot.instance_variable_get(:@updates) << self
69
+ @retried -= 1
70
+ end
71
+
72
+ def match? pattern
73
+ return true if pattern.nil?
74
+ return false if text.nil?
75
+ !!match(pattern)
76
+ end
77
+
78
+ def match pattern
79
+ return nil if pattern.nil? || text.nil?
80
+ text.match(pattern)
81
+ end
82
+
83
+ def message
84
+ @data.to_h.find { |k, v| k.match? /message|post/ }&.last
85
+ end
86
+
87
+ def text
88
+ message&.text&.gsub(/(\/\w+)(@\w+)/, '\1')
89
+ end
90
+
91
+ def reply *things, media: true, style: nil, **options
92
+ meth = :send_message
93
+ payload = { chat_id: message&.chat.id }
94
+ things.each do |x|
95
+ if IO === x
96
+ magic = MimeMagic.by_magic(x)
97
+ case
98
+ when magic.audio?
99
+ meth = :send_audio
100
+ payload[:audio] = x
101
+ when magic.image?
102
+ meth = :send_photo
103
+ payload[:photo] = x
104
+ when magic.video?
105
+ meth = :send_video
106
+ payload[:video] = x
107
+ else
108
+ meth = :send_document
109
+ payload[:document] = x
110
+ end
111
+ else
112
+ payload[:text] = x.to_s
113
+ payload[:parse_mode] = 'Markdown'
114
+ end
115
+ end
116
+ payload = payload.merge options
117
+ case style
118
+ when :at
119
+ if payload[:text] && message.from
120
+ from = message.from
121
+ if payload[:parse_mode].match? /Markdown/i
122
+ prefix = "[#{from.first_name}](tg://user?id=#{from.id}) "
123
+ elsif payload[:parse_mode].match? /HTML/i
124
+ prefix = "<a href=\"tg://user?id=#{from.id}\">#{from.first_name}</a> "
125
+ else
126
+ prefix = ''
127
+ end
128
+ payload[:text] = prefix + payload[:text]
129
+ end
130
+ when nil
131
+ if !payload[:reply_to_message_id]
132
+ payload[:reply_to_message_id] = message_id
133
+ end
134
+ end
135
+ @bot.send meth, payload
136
+ end
137
+
138
+ def message_id
139
+ message&.message_id
140
+ end
141
+
142
+ def method_missing meth, *args, &blk
143
+ return @data[meth] if args.empty? && @data[meth]
144
+ @bot.send(meth, *args, &blk)
145
+ end
146
+ end
147
+
148
+ def run &blk
149
+ @offset = 0
150
+ @timeout = 2
151
+ @updates = []
152
+ instance_exec(self, &@start) if @start
153
+ loop do
154
+ while x = @updates.shift
155
+ u = Update === x ? x : Update.new(self, x)
156
+ @offset = u.update_id if u.update_id && @offset < u.update_id
157
+ @tasks = @commands.select { |pattern, *| u.match? pattern }
158
+ .group_by { |e| e[2] ? :before : e[3] ? :after : nil }
159
+ .values_at(:before, nil, :after).compact.flatten(1)
160
+ while t = @tasks.shift
161
+ debug ">> #{t[1] || t[0]}"
162
+ u.instance_exec(u.match(t[0]), u, t, &t[4])
163
+ end
164
+ end
165
+ res = get_updates offset: @offset + 1, limit: 7, timeout: 15
166
+ if res.ok
167
+ @updates.push *res.result
168
+ else
169
+ debug "#{res.error_code}: #{res.description}"
170
+ end
171
+ end
172
+ rescue HTTP::ConnectionError
173
+ debug "connect failed, check proxy?"
174
+ retry
175
+ rescue Interrupt
176
+ instance_exec(self, &@finish) if @finish
177
+ ensure
178
+ @client.close
179
+ end
180
+
181
+ def api_version
182
+ search_in_doc(/changes/i, '')[0].desc[0].content[/\d+\.\d+/]
183
+ end
184
+
185
+ private
186
+
187
+ def method_missing meth, *args, &blk
188
+ meth = meth.to_s.split('_').map.
189
+ with_index { |x, i| i.zero? ? x : (x[0].upcase + x[1..-1]) }.join
190
+ payload = make_payload meth, *args
191
+ debug "/#{meth} #{payload}"
192
+ result = @client.post("#@prefix/#{meth}", form: payload).to_s
193
+ result = json_to_ostruct(result)
194
+ debug "=> #{result}"
195
+ blk ? blk.call(result) : result
196
+ end
197
+
198
+ def json_to_ostruct json
199
+ JSON.parse(json, object_class: OpenStruct)
200
+ end
201
+
202
+ def make_payload meth, *args
203
+ defaults, schema = meth_info meth
204
+ payload = {}
205
+ args.each do |arg|
206
+ if field = defaults.find { |k, _| k.any? { |l| check_match l, arg } }&.last
207
+ defaults.delete_if { |_, v| v == field }
208
+ payload[field] = arg
209
+ end
210
+ if Hash === arg
211
+ defaults.delete_if { |_, v| arg.keys.include?(v) || arg.keys.include?(v.to_sym) }
212
+ payload = payload.merge arg
213
+ end
214
+ end
215
+ if !defaults.empty?
216
+ debug "should 400: #{defaults.values.join(', ')} not specified"
217
+ end
218
+ check_type payload, schema
219
+ end
220
+
221
+ def meth_info meth
222
+ unless table = (search_in_doc '', /^#{meth}$/)[0]&.table
223
+ debug "don't find type of #{meth}"
224
+ return {}, {}
225
+ end
226
+ defaults = table.select { |e| e.Required.match? /Yes/i }
227
+ .map { |e|
228
+ [e.Type.split(/\s+or\s+/).flat_map { |s|
229
+ string_to_native_types s, false
230
+ }.compact, e.Parameter]
231
+ }.reject { |(ts, _para)| ts.empty? }.to_h
232
+ schema = table
233
+ .map { |e|
234
+ [e.Parameter, e.Type.split(/\s+or\s+/).map { |s|
235
+ string_to_native_types s
236
+ }]
237
+ }.to_h
238
+ return defaults, schema
239
+ end
240
+
241
+ def string_to_native_types s, keep_unknown=true
242
+ if s['Array of ']
243
+ return [string_to_native_types(s[9..-1], keep_unknown)]
244
+ end
245
+ case s
246
+ when 'String' then String
247
+ when 'Integer' then Integer
248
+ when 'Boolean' then [true, false]
249
+ else
250
+ keep_unknown ? s : nil
251
+ end
252
+ end
253
+
254
+ def check_match k, arg
255
+ if Array === k
256
+ if k.size > 1
257
+ k.any? { |t| t === arg }
258
+ else
259
+ Array === arg && arg.all? { |a| check_match k[0], a }
260
+ end
261
+ else
262
+ return true if String === k # unknown types, like "User"
263
+ k === arg
264
+ end
265
+ end
266
+
267
+ def check_type payload, schema
268
+ filtered = {}
269
+ payload.each do |field, value|
270
+ row = schema[field.to_s]
271
+ if row&.any? { |k| check_match k, value }
272
+ filtered[field] = value
273
+ else
274
+ debug "check_type failed at #{field} :: #{row&.join(' | ')} = #{value.inspect}"
275
+ end
276
+ end
277
+ filtered
278
+ end
279
+
280
+ def search_in_doc *hints
281
+ doc = [APIDOC]
282
+ hints.each do |hint|
283
+ if nxt = doc.flat_map { |x| x['children'] }.select { |x| x['name'][hint] }
284
+ doc = nxt
285
+ else
286
+ return nil
287
+ end
288
+ end
289
+ json_to_ostruct JSON.generate doc
290
+ end
291
+ end
@@ -1,3 +1,3 @@
1
- module Tgbot
2
- VERSION = '0.1.6'.freeze
1
+ class Tgbot
2
+ VERSION = '0.2.2'.freeze
3
3
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["hyrious@outlook.com"]
11
11
 
12
12
  spec.summary = 'Telegram Bot API'
13
- spec.description = 'Telegram Bot API Wrapper'
13
+ spec.description = 'A deadly simple Telegram Bot API wrapper.'
14
14
  spec.homepage = 'https://github.com/hyrious/tgbot'
15
15
  spec.license = "MIT"
16
16
 
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "bundler"
25
25
  spec.add_development_dependency "rake"
26
26
 
27
- spec.add_dependency 'faraday'
27
+ spec.add_dependency 'http'
28
28
  spec.add_dependency 'mimemagic'
29
- spec.required_ruby_version = '>= 2.4.0'
29
+ spec.required_ruby_version = '>= 2.6.0'
30
30
  end
@@ -0,0 +1,32 @@
1
+ require 'http'
2
+ require 'nokogiri'
3
+
4
+ raw = HTTP.via('127.0.0.1', 1080).get('https://core.telegram.org/bots/api').to_s
5
+ nodes = Nokogiri::HTML(raw).at('#dev_page_content').children
6
+ ret = [{ name: 'Telegram Bot API', children: [] }]
7
+ uplevels = [2]
8
+ nodes.each do |node|
9
+ case node.name
10
+ when 'h3', 'h4'
11
+ n = node.name[/\d/].to_i
12
+ while uplevels[-1] >= n
13
+ ret.pop
14
+ uplevels.pop
15
+ end
16
+ ret[-1][:children] << (x = { name: node.content, children: [] })
17
+ ret << x
18
+ uplevels << n
19
+ when 'h6', 'p', 'blockquote', 'pre'
20
+ (ret[-1]['desc'] ||= []) << { name: node.name, content: node.content.strip }
21
+ when 'ul', 'ol'
22
+ (ret[-1]['desc'] ||= []) << { name: node.name, content: node.css('li').map(&:content).map(&:strip) }
23
+ when 'table'
24
+ keys = node.css('th').map(&:content)
25
+ ret[-1]['table'] = node.css('tbody > tr').map do |tr|
26
+ keys.zip(tr.css('td').map(&:content)).to_h
27
+ end
28
+ end
29
+ end
30
+ ret = JSON.parse JSON.generate ret[0]
31
+ require 'psych'
32
+ open(File.expand_path('../lib/api.yaml', __dir__), 'wb') { |f| Psych.dump ret, f, line_width: 1<<30 }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tgbot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - hyrious
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-03-01 00:00:00.000000000 Z
11
+ date: 2019-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: faraday
42
+ name: http
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -66,7 +66,7 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- description: Telegram Bot API Wrapper
69
+ description: A deadly simple Telegram Bot API wrapper.
70
70
  email:
71
71
  - hyrious@outlook.com
72
72
  executables: []
@@ -80,25 +80,11 @@ files:
80
80
  - Rakefile
81
81
  - bin/console
82
82
  - bin/setup
83
- - example.rb
84
- - lib/methods.json
83
+ - lib/api.yaml
85
84
  - lib/tgbot.rb
86
- - lib/tgbot/core.rb
87
- - lib/tgbot/dsl.rb
88
- - lib/tgbot/runner.rb
89
- - lib/tgbot/update.rb
90
85
  - lib/tgbot/version.rb
91
- - lib/types.json
92
86
  - tgbot.gemspec
93
- - tools/gen_methods_json.rb
94
- - tools/gen_methods_txt.rb
95
- - tools/gen_types_json.rb
96
- - tools/gen_types_txt.rb
97
- - tools/methods.json
98
- - tools/methods.txt
99
- - tools/types.json
100
- - tools/types.txt
101
- - usage.md
87
+ - tool/gen.rb
102
88
  homepage: https://github.com/hyrious/tgbot
103
89
  licenses:
104
90
  - MIT
@@ -111,15 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
97
  requirements:
112
98
  - - ">="
113
99
  - !ruby/object:Gem::Version
114
- version: 2.4.0
100
+ version: 2.6.0
115
101
  required_rubygems_version: !ruby/object:Gem::Requirement
116
102
  requirements:
117
103
  - - ">="
118
104
  - !ruby/object:Gem::Version
119
105
  version: '0'
120
106
  requirements: []
121
- rubyforge_project:
122
- rubygems_version: 2.7.3
107
+ rubygems_version: 3.0.1
123
108
  signing_key:
124
109
  specification_version: 4
125
110
  summary: Telegram Bot API
data/example.rb DELETED
@@ -1,110 +0,0 @@
1
- require 'json'
2
- require 'faraday'
3
- require './helper'
4
- save_pid
5
- require 'tgbot'
6
- Garage = load_data.shuffle
7
- Cache = {}
8
- TOKEN =
9
- Tgbot.run TOKEN, proxy: 'http://127.0.0.1:1080' do |bot|
10
-
11
- bot.start do
12
- log "\e[33m#{bot.name}\e[32m, at your service.", 2
13
- end
14
- bot.finish do
15
- log "byebye.", 2
16
- end
17
-
18
- bot.get 'start' do
19
- send_message(<<~EOF, parse_mode: 'Markdown')
20
- ```
21
- start : 显示此帮助信息
22
- drive : 随机返回一张车库里的图
23
- 对该图回复 “原图” : 返回原图
24
- exchange 100 CNY to JPY : 汇率转换
25
- register : 添加自定义功能(会先提交给作者)
26
- ```
27
- EOF
28
- end
29
-
30
- bot.get 'drive' do
31
- pic = Garage.pop
32
- log ">> Sending #{File.basename(pic)} to @#{message.from.username} ##{id}", 6
33
- bytes = File.size pic
34
- size = hsize bytes
35
- reply "正在发车 (#{size} #{htime(bytes / 30720)})"
36
- x = reply_photo pic, caption: File.basename(pic, '.*')
37
- if self.done = x['ok']
38
- Cache["drive_#{x['result']['message_id']}"] = pic
39
- end
40
- self.done! if self.count > 1
41
- end
42
- bot.get '原图' do
43
- x = message&.reply_to_message&.message_id
44
- pic = Cache["drive_#{x}"]
45
- unless pic
46
- reply '没找到原图,重开'
47
- next
48
- end
49
- log ">> Sending original #{File.basename(pic)} to @#{message.from.username} ##{id}", 6
50
- reply_document pic
51
- end
52
-
53
- bot.get 'exchange' do
54
- x = text&.match /([-+]?[1-9]\d*(\.\d+)?)\s*([A-Z]+)\s*to\s*([A-Z]+)/
55
- unless x
56
- reply 'Usage: exchange 100 CNY to JPY'
57
- next
58
- end
59
- n, f, t = x.values_at 1, 3, 4
60
- n = Float(n) rescue next
61
- Cache["exchange_#{f}"] ||= JSON.parse Faraday.get("http://api.fixer.io/latest?base=#{f}").body
62
- next unless Cache["exchange_#{f}"] && !Cache["exchange_#{f}"]['error']
63
- next unless Cache["exchange_#{f}"]['rates'][t]
64
- n *= Cache["exchange_#{f}"]['rates'][t]
65
- t = Cache["exchange_#{f}"]['date']
66
- reply "#{'%.2f' % n} (#{t})"
67
- end
68
-
69
- bot.before do |update|
70
- log ">> Processing ##{update.id}"
71
- log "@#{update.message&.from&.first_name}: #{update.text}", 3
72
- end
73
- bot.after do |update|
74
- if update.done?
75
- log "=> Success ##{update.id}", 2
76
- else
77
- log "?> Retry ##{update.id}", 3
78
- end
79
- end
80
-
81
- bot.get 'register' do
82
- e = message&.entities&.find { |e| e.type == 'pre' }
83
- if e.nil?
84
- send_message(<<~EOF)
85
- register <功能名>
86
- ```
87
- get /command/ do |matched|
88
- # your code here
89
- end
90
- ```
91
- EOF
92
- next
93
- end
94
- open 'register.rb', 'a' do |f|
95
- f.puts text[e.offset, e.length]
96
- end
97
- reply '脚本已备分'
98
- end
99
-
100
- bot.get 'coin' do
101
- send_message Array.new(text&.match(/\d+/)&.to_s.to_i || 1){ ['🌞', '🌚'].sample }.join
102
- end
103
- bot.get 'roll' do
104
- send_message rand(text&.match(/\d+/)&.to_s.to_i.nonzero? || 100).to_s
105
- end
106
-
107
- end
108
-
109
- save_data Garage
110
- delete_pid