app/models/agents/telegram_agent.rb
require 'httmultiparty'
module Agents
class TelegramAgent < Agent
include FormConfigurable
cannot_be_scheduled!
cannot_create_events!
no_bulk_receive!
can_dry_run!
description <<~MD
The Telegram Agent receives and collects events and sends them via [Telegram](https://telegram.org/).
It is assumed that events have either a `text`, `photo`, `audio`, `document`, `video` or `group` key. You can use the EventFormattingAgent if your event does not provide these keys.
The value of `text` key is sent as a plain text message. You can also tell Telegram how to parse the message with `parse_mode`, set to either `html`, `markdown` or `markdownv2`.
The value of `photo`, `audio`, `document` and `video` keys should be a url whose contents will be sent to you.
The value of `group` key should be a list and must consist of 2-10 objects representing an [InputMedia](https://core.telegram.org/bots/api#inputmedia) from the [Telegram Bot API](https://core.telegram.org/bots/api#inputmedia). Be careful: the `caption` field is not covered by the "long message" setting.#{' '}
**Setup**
* Obtain an `auth_token` by [creating a new bot](https://telegram.me/botfather).
* If you would like to send messages to a public channel:
* Add your bot to the channel as an administrator
* If you would like to send messages to a group:
* Add the bot to the group
* If you would like to send messages privately to yourself:
* Open a conservation with the bot by visiting https://telegram.me/YourHuginnBot
* Send a message to the bot, group or channel.
* Select the `chat_id` from the dropdown.
**Options**
* `caption`: caption for a media content (0-1024 characters), applied only for `photo`, `audio`, `document`, or `video`
* `disable_notification`: send a message silently in a channel
* `disable_web_page_preview`: disable link previews for links in a text message
* `long_message`: truncate (default) or split text messages and captions that exceed Telegram API limits. Markdown and HTML tags can't span across messages and, if not opened or closed properly, will render as plain text.
* `parse_mode`: parse policy of a text message
See the official [Telegram Bot API documentation](https://core.telegram.org/bots/api#available-methods) for detailed info.
MD
def default_options
{
auth_token: 'xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
chat_id: 'xxxxxxxx'
}
end
form_configurable :auth_token, roles: :validatable
form_configurable :chat_id, roles: :completable
form_configurable :caption
form_configurable :disable_notification, type: :array, values: ['', 'true', 'false']
form_configurable :disable_web_page_preview, type: :array, values: ['', 'true', 'false']
form_configurable :long_message, type: :array, values: ['', 'split', 'truncate']
form_configurable :parse_mode, type: :array, values: ['', 'html', 'markdown', 'markdownv2']
def validate_auth_token
HTTMultiParty.post(telegram_bot_uri('getMe'))['ok']
end
def complete_chat_id
response = HTTMultiParty.post(telegram_bot_uri('getUpdates'))
return [] unless response['ok']
response['result'].map { |update| update_to_complete(update) }.uniq
end
def validate_options
errors.add(:base, 'auth_token is required') unless options['auth_token'].present?
errors.add(:base, 'chat_id is required') unless options['chat_id'].present?
errors.add(:base,
'caption should be 1024 characters or less') if interpolated['caption'].present? && interpolated['caption'].length > 1024 && (!interpolated['long_message'].present? || interpolated['long_message'] != 'split')
errors.add(:base,
"disable_notification has invalid value: should be 'true' or 'false'") if interpolated['disable_notification'].present? && !%w[
true false
].include?(interpolated['disable_notification'])
errors.add(:base,
"disable_web_page_preview has invalid value: should be 'true' or 'false'") if interpolated['disable_web_page_preview'].present? && !%w[
true false
].include?(interpolated['disable_web_page_preview'])
errors.add(:base,
"long_message has invalid value: should be 'split' or 'truncate'") if interpolated['long_message'].present? && !%w[
split truncate
].include?(interpolated['long_message'])
errors.add(:base,
"parse_mode has invalid value: should be 'html', 'markdown' or 'markdownv2'") if interpolated['parse_mode'].present? && !%w[
html markdown markdownv2
].include?(interpolated['parse_mode'])
end
def working?
received_event_without_error? && !recent_error_logs?
end
def receive(incoming_events)
incoming_events.each do |event|
receive_event event
end
end
private
TELEGRAM_ACTIONS = {
text: :sendMessage,
photo: :sendPhoto,
audio: :sendAudio,
document: :sendDocument,
video: :sendVideo,
group: :sendMediaGroup,
}.freeze
def configure_params(params)
params[:chat_id] = interpolated['chat_id']
params[:disable_notification] =
interpolated['disable_notification'] if interpolated['disable_notification'].present?
if params.has_key?(:text)
params[:disable_web_page_preview] =
interpolated['disable_web_page_preview'] if interpolated['disable_web_page_preview'].present?
params[:parse_mode] = interpolated['parse_mode'] if interpolated['parse_mode'].present?
elsif !params.has_key?(:media)
params[:caption] = interpolated['caption'] if interpolated['caption'].present?
end
params
end
def receive_event(event)
interpolate_with event do
messages_send = TELEGRAM_ACTIONS.count do |field, _method|
payload = event.payload[field]
next unless payload.present?
if field == :group
send_telegram_messages field, configure_params(media: payload)
else
send_telegram_messages field, configure_params(field => payload)
end
true
end
error("No valid key found in event #{event.payload.inspect}") if messages_send.zero?
end
end
def send_message(field, params)
response = HTTMultiParty.post telegram_bot_uri(TELEGRAM_ACTIONS[field]),
body: params.to_json,
headers: { 'Content-Type' => 'application/json' }
unless response['ok']
error(response)
end
end
def send_telegram_messages(field, params)
if interpolated['long_message'] == 'split'
if field == :text
params[:text].scan(/\G\s*(?:\w{4096}|.{1,4096}(?=\b|\z))/m) do |message|
message.strip!
send_message field, configure_params(field => message) unless message.blank?
end
else
caption_array = (params[:caption].presence || '').scan(/\G\s*\K(?:\w{1024}|.{1,1024}(?=\b|\z))/m).map(&:strip)
params[:caption] = caption_array.shift
send_message field, params
caption_array.each do |caption|
send_message(:text, configure_params(text: caption)) unless caption.blank?
end
end
else
params[:caption] = params[:caption][0..1023] if params[:caption]
params[:text] = params[:text][0..4095] if params[:text]
send_message field, params
end
end
def telegram_bot_uri(method)
"https://api.telegram.org/bot#{interpolated['auth_token']}/#{method}"
end
def update_to_complete(update)
chat = (update['message'] || update.fetch('channel_post', {})).fetch('chat', {})
{ id: chat['id'], text: chat['title'] || "#{chat['first_name']} #{chat['last_name']}" }
end
end
end