Class: AgentsControl::Channels::Telegram::Api

Inherits:
Object
  • Object
show all
Defined in:
lib/agents_control/channels/telegram/api.rb

Overview

A thin Bot API client built on the stdlib.

Deliberately gem-free: net/http covers everything needed, and an extra dependency in a tool installed with a single command costs more than the lines it would save.

The token is part of the URL, so any trace of a request — exception message, log, debug output — must go through redact. Otherwise the secret leaks somewhere nobody meant to put it.

Defined Under Namespace

Classes: TooManyRequests

Constant Summary collapse

HOST =
"api.telegram.org"
CALL_TIMEOUT =

Telegram requires answering a button press within 10 seconds, or it stays stuck in the interface. Regular calls are kept noticeably shorter than that.

8
Error =
Class.new(AgentsControl::Error)
Conflict =

Another process is already reading updates with this same token. This doesn't resolve itself: one instance per token is required.

Class.new(Error)
Unavailable =

The network dropped. Unlike other errors, this is an expected state for a laptop that got closed and carried off; it's fixed by retrying, not by stopping.

Class.new(Error)

Instance Method Summary collapse

Constructor Details

#initialize(token, http: nil) ⇒ Api

Returns a new instance of Api.



50
51
52
53
# File 'lib/agents_control/channels/telegram/api.rb', line 50

def initialize(token, http: nil)
  @token = token
  @http = http || Http.new
end

Instance Method Details

#answer_callback_query(id, text: nil, show_alert: false) ⇒ Object



91
92
93
# File 'lib/agents_control/channels/telegram/api.rb', line 91

def answer_callback_query(id, text: nil, show_alert: false)
  call("answerCallbackQuery", { callback_query_id: id, text: text, show_alert: show_alert })
end

#edit_message_text(chat_id:, message_id:, text:, reply_markup: nil) ⇒ Object



76
77
78
79
80
81
# File 'lib/agents_control/channels/telegram/api.rb', line 76

def edit_message_text(chat_id:, message_id:, text:, reply_markup: nil)
  call("editMessageText", {
         chat_id: chat_id, message_id: message_id, text: text,
         reply_markup: reply_markup && JSON.generate(reply_markup)
       })
end

#get_meObject



55
# File 'lib/agents_control/channels/telegram/api.rb', line 55

def get_me = call("getMe")

#get_updates(offset: nil, timeout: 30) ⇒ Object

timeout here means long polling: the connection stays open until an update shows up. The network timeout has to be noticeably longer, or the client would drop the connection right as the server was about to answer.



61
62
63
64
65
66
67
# File 'lib/agents_control/channels/telegram/api.rb', line 61

def get_updates(offset: nil, timeout: 30)
  call(
    "getUpdates",
    { offset: offset, timeout: timeout, allowed_updates: %w[message callback_query] },
    read_timeout: timeout + 15
  )
end

#redact(text) ⇒ Object

Strip the token out of arbitrary text before logging it.

Matches both the literal value and its inspect-escaped form (control characters like a tab become the two characters \t): URI::InvalidURIError's message is built with inspect, so a plain literal match alone would miss the token whenever it contains anything inspect escapes, and let it straight through into a log.

An empty token is guarded separately: gsub against an empty pattern matches between every character, turning any message into unreadable noise instead of leaving it untouched.



107
108
109
110
111
# File 'lib/agents_control/channels/telegram/api.rb', line 107

def redact(text)
  return text.to_s if @token.to_s.empty?

  text.to_s.gsub(Regexp.union(@token.to_s, escaped_token), "<token>")
end

#send_message(chat_id:, text:, reply_markup: nil, parse_mode: nil) ⇒ Object



69
70
71
72
73
74
# File 'lib/agents_control/channels/telegram/api.rb', line 69

def send_message(chat_id:, text:, reply_markup: nil, parse_mode: nil)
  call("sendMessage", {
         chat_id: chat_id, text: text, parse_mode: parse_mode,
         reply_markup: reply_markup && JSON.generate(reply_markup)
       })
end

#set_my_commands(commands) ⇒ Object

The command menu in the Telegram UI: a button next to the input field instead of hunting through the chat for a list.



85
86
87
88
89
# File 'lib/agents_control/channels/telegram/api.rb', line 85

def set_my_commands(commands)
  payload = commands.map { |name, description| { command: name, description: description } }

  call("setMyCommands", { commands: JSON.generate(payload) })
end