cantino/huginn

View on GitHub
app/models/agents/telegram_agent.rb

Summary

Maintainability
B
6 hrs
Test Coverage
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