class Tgbot

Constants

APIDOC
VERSION

Attributes

debug[RW]

Public Class Methods

new(token, proxy: nil, debug: false) click to toggle source
# File lib/tgbot.rb, line 20
def initialize(token, proxy: nil, debug: false)
  @prefix = "/bot#{token}"
  @client = HTTP.persistent "https://api.telegram.org"
  if proxy
    addr, port = *proxy.split(':')
    @client = @client.via(addr, port.to_i)
  end
  @debug = debug
  @commands = []
  @start = @finish = nil
end
run(*args, &blk) click to toggle source
# File lib/tgbot.rb, line 12
def self.run(*args, &blk)
  bot = new(*args)
  bot.instance_exec(bot, &blk) if blk
  bot.run
end

Public Instance Methods

api_version() click to toggle source
# File lib/tgbot.rb, line 181
def api_version
  search_in_doc(/changes/i, '')[0].desc[0].content[/\d+\.\d+/]
end
finish(&blk) click to toggle source
# File lib/tgbot.rb, line 36
def finish &blk
  @finish = blk
end
on(pattern=nil, name: nil, before_all: false, after_all: false, &blk) click to toggle source
# File lib/tgbot.rb, line 40
def on pattern=nil, name: nil, before_all: false, after_all: false, &blk
  if before_all && after_all
    raise ArgumentError, 'before_all and after_all can\'t both be true'
  end
  @commands << [pattern, name, before_all, after_all, blk]
end
run(&blk) click to toggle source
# File lib/tgbot.rb, line 148
def run &blk
  @offset = 0
  @timeout = 2
  @updates = []
  instance_exec(self, &@start) if @start
  loop do
    while x = @updates.shift
      u = Update === x ? x : Update.new(self, x)
      @offset = u.update_id if u.update_id && @offset < u.update_id
      @tasks = @commands.select { |pattern, *| u.match? pattern }
        .group_by { |e| e[2] ? :before : e[3] ? :after : nil }
        .values_at(:before, nil, :after).compact.flatten(1)
      while t = @tasks.shift
        debug ">> #{t[1] || t[0]}"
        u.instance_exec(u.match(t[0]), u, t, &t[4])
      end
    end
    res = get_updates offset: @offset + 1, limit: 7, timeout: 15
    if res.ok
      @updates.push *res.result
    else
      debug "#{res.error_code}: #{res.description}"
    end
  end
rescue HTTP::ConnectionError
  debug "connect failed, check proxy?"
  retry
rescue Interrupt
  instance_exec(self, &@finish) if @finish
ensure
  @client.close
end
start(&blk) click to toggle source
# File lib/tgbot.rb, line 32
def start &blk
  @start = blk
end

Private Instance Methods

check_match(k, arg) click to toggle source
# File lib/tgbot.rb, line 254
def check_match k, arg
  if Array === k
    if k.size > 1
      k.any? { |t| t === arg }
    else
      Array === arg && arg.all? { |a| check_match k[0], a }
    end
  else
    return true if String === k # unknown types, like "User"
    k === arg
  end
end
check_type(payload, schema) click to toggle source
# File lib/tgbot.rb, line 267
def check_type payload, schema
  filtered = {}
  payload.each do |field, value|
    row = schema[field.to_s]
    if row&.any? { |k| check_match k, value }
      filtered[field] = value
    else
      debug "check_type failed at #{field} :: #{row&.join(' | ')} = #{value.inspect}"
    end
  end
  filtered
end
json_to_ostruct(json) click to toggle source
# File lib/tgbot.rb, line 198
def json_to_ostruct json
  JSON.parse(json, object_class: OpenStruct)
end
make_payload(meth, *args) click to toggle source
# File lib/tgbot.rb, line 202
def make_payload meth, *args
  defaults, schema = meth_info meth
  payload = {}
  args.each do |arg|
    if field = defaults.find { |k, _| k.any? { |l| check_match l, arg } }&.last
      defaults.delete_if { |_, v| v == field }
      payload[field] = arg
    end
    if Hash === arg
      defaults.delete_if { |_, v| arg.keys.include?(v) || arg.keys.include?(v.to_sym) }
      payload = payload.merge arg
    end
  end
  if !defaults.empty?
    debug "should 400: #{defaults.values.join(', ')} not specified"
  end
  check_type payload, schema
end
meth_info(meth) click to toggle source
# File lib/tgbot.rb, line 221
def meth_info meth
  unless table = (search_in_doc '', /^#{meth}$/)[0]&.table
    debug "don't find type of #{meth}"
    return {}, {}
  end
  defaults = table.select { |e| e.Required.match? /Yes/i }
    .map { |e|
      [e.Type.split(/\s+or\s+/).flat_map { |s| 
        string_to_native_types s, false
      }.compact, e.Parameter]
    }.reject { |(ts, _para)| ts.empty? }.to_h
  schema = table
    .map { |e|
      [e.Parameter, e.Type.split(/\s+or\s+/).map { |s|
        string_to_native_types s
      }]
    }.to_h
  return defaults, schema
end
method_missing(meth, *args, &blk) click to toggle source
# File lib/tgbot.rb, line 187
def method_missing meth, *args, &blk
  meth = meth.to_s.split('_').map.
    with_index { |x, i| i.zero? ? x : (x[0].upcase + x[1..-1]) }.join
  payload = make_payload meth, *args
  debug "/#{meth} #{payload}"
  result = @client.post("#@prefix/#{meth}", form: payload).to_s
  result = json_to_ostruct(result)
  debug "=> #{result}"
  blk ? blk.call(result) : result
end
search_in_doc(*hints) click to toggle source
# File lib/tgbot.rb, line 280
def search_in_doc *hints
  doc = [APIDOC]
  hints.each do |hint|
    if nxt = doc.flat_map { |x| x['children'] }.select { |x| x['name'][hint] }
      doc = nxt
    else
      return nil
    end
  end
  json_to_ostruct JSON.generate doc
end
string_to_native_types(s, keep_unknown=true) click to toggle source
# File lib/tgbot.rb, line 241
def string_to_native_types s, keep_unknown=true
  if s['Array of ']
    return [string_to_native_types(s[9..-1], keep_unknown)]
  end
  case s
  when 'String'  then String
  when 'Integer' then Integer
  when 'Boolean' then [true, false]
  else
    keep_unknown ? s : nil
  end
end