golang

Уведомления через Telegram-бота при помощи почтового триггера Yandex Cloud Functions

  • четверг, 29 февраля 2024 г. в 00:00:19
https://habr.com/ru/companies/ppr/articles/796889/

Привет, Хабр!

Возможно, многие сталкивались с задачей: есть сервер с некими cronjob-ами, результат выполнения которых хотелось бы мониторить определенному числу лиц, но при этом сервер находится в каком-нибудь intranet и не имеет доступа к сети интернет. Вот и у нас однажды возникла подобная проблема. Единственным доступным средством коммуникации с внешним миром у сервера был почтовый шлюз, через который можно было отправить электронную почту. До некоторого времени задача решалась отправкой нескольких копий письма, но со временем стало понятно, что гораздо  удобнее читать уведомления в Telegram. Такую настройку мы произвели с помощью Yandex Cloud Functions.

Инструкция

Для решения задачи мы воспользовались непосредственно самими функциями с триггером для запуска — почтой.

По документации Яндекса для начала создаем сервисный аккаунт с правами

  • functions.functionInvoker

  • iam.serviceAccounts.user

Он будет использоваться триггером для запуска функции.

Далее пишем нашу функцию, Яндекс предоставляет для этого множество сред разработки, но мы выбрали go, т.к. на данный момент пытаемся его активно внедрять в свою работу.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "html"
    "io/ioutil"
    "log"
    "net/http"
)

const telegramAPIBaseURL = "https://api.telegram.org/bot"
const chatId = 123456789
const botToken = "456346346346:Skdklsdf939fcjkDJDs3"

type Message struct {
    ReceivedAt  string     `json:"received_at"`
    Headers     []Header   `json:"headers"`
    Attachments Attachment `json:"attachments"`
    Message     string     `json:"message"`
}

type Header struct {
    Name   string   `json:"name"`
    Values []string `json:"values"`
}

type Attachment struct {
    BucketId string   `json:"bucket_id"`
    Keys     []string `json:"keys"`
}

type Email struct {
    Messages []Message `json:"messages"`
}

type Request struct {
    Message string `json:"message"`
    Number  int    `json:"number"`
}

type ResponseBody struct {
    Context context.Context `json:"context"`
    Request interface{}     `json:"request"`
    Error   string          `json:"error"`
}

func Handler(ctx context.Context, email *Email) ([]byte, error) {
    var eMessages []string
    var errorMess string
    for _, message := range email.Messages {
        eMessages = append(eMessages, message.Message)
        err := sendMessageToTelegram(message.Message, chatId)
        if err != nil {
            log.Println(err)
            errorMess = err.Error()
        }
    }

    body, err := json.Marshal(&ResponseBody{
        Context: ctx,
        Request: eMessages,
        Error:   errorMess,
    })

    if err != nil {
        return nil, err
    }

    // Тело ответа необходимо вернуть в виде массива байтов
    return body, nil

}

// sendMessageToTelegram отправляет сообщение в Telegram чат.
func sendMessageToTelegram(messageText string, chtid int) error {
    // Telegram API endpoint для отправки сообщений
    url := telegramAPIBaseURL + botToken + "/sendMessage"

    // Подготовка данных для отправки
    message := map[string]interface{}{
        "chat_id":    chtid,
        "text":       html.EscapeString(messageText),
    }
    messageBytes, err := json.Marshal(message)
    if err != nil {
        return fmt.Errorf("error marshaling message: %w", err)
    }

    // Создание HTTP запроса
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(messageBytes))
    if err != nil {
        return fmt.Errorf("error creating request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")

    // Отправка запроса
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("error sending request to Telegram: %w", err)
    }
    defer resp.Body.Close()

    // Проверка на успешный ответ от Telegram
    if resp.StatusCode != http.StatusOK {
        responseData, _ := ioutil.ReadAll(resp.Body)
        return fmt.Errorf("error from Telegram, status code: %d, response: %s", resp.StatusCode, string(responseData))
    }

    return nil
}

В коде есть две константы botToken(токен бота в telegram) и chatId (идентификатор чата). Получить botToken можно создав нового бота отца всех ботов

Далее получаем chatId

Для этого нужно:

  1. Добавить вашего бота в существующий чат или создать новый чат, в котором вы хотите получать уведомления. 

  2. Написать боту любое сообщение в этом чате.

  3. Перейти по ссылке api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates и найти в ответе заветный chatId

Сам код, в общем-то, должен просто разбирать сообщение от триггера и отправлять нужный контент в telegram. Сообщение от триггера выглядит примерно так:

  "messages":[
      {
         "received_at":"2022-09-15 14:42:23.983842092 +0000 UTC m=+260285.403254765",
         "headers":[
            {
               "name":"X-Yandex-Fwd",
               "values":[
                  "1"
               ]
            },
			...
            {
               "name":"Authentication-Results",
               "values":[
                  "myt6-22bd3499f8ff.qloud-c.yandex.net; dkim=pass header.i=@yandex.ru"
               ]
            }
           
         ],
         "attachments":{
            "bucket_id":"trigger-bucket-id",
            "keys":[
               "attachement-object-key1",
               "attachement-object-key2"
            ]
         },
         "message":"<div>This is example body for documentation</div>\r\n"
      }
   ]

Далее сохраняем нашу функцию, ждем окончания сборки и тестируем. Переходим из редактора на вкладку “тестирование”, и в поле “входные данные” вставляем пример сообщения:

После выполнения, ответ отобразится в данном поле: 

А в telegram должно прийти сообщение из поля message

Осталось только создать триггер запуска. В этом же разделе функций переходим в триггеры

и создаем новый. Для этого указываем имя, тип (почта), вашу функцию, версию функции и сервисный аккаунт который создали ранее:

После создания заходим в наш триггер и смотрим адрес электронной почты, на которую нужно отправить письмо для срабатывания триггера:

Теперь можно протестировать и сам триггер. Для этого отправляем произвольное письмо на сгенерированную почту и видим, что в telegram почти моментально прилетело исходное сообщение.

Иногда требуется отправлять одно и то же сообщение в несколько чатов, и если текст сообщения одинаковый, то оно может прийти только в первый чат. Чтобы исправить эту ситуацию, нужно добавить задержку между отправками, например так:

// Задержка в секундах
	delay := time.Second * 1
	
	...
	err := sendMessageToTelegram(message.Message, chatId)
	...
	
	time.Sleep(delay)

Заключение

На момент написания первые 1 000 000 запусков функции Яндекс предоставлял бесплатно. С учетом того, что нам требуется всего несколько запусков в сутки, это очень удобное решение.

В целом, в статье описан крайне примитивный вариант использования и самих функций, и триггера на отправку. Можно модернизировать решение задачи, например, реализовать сохранение вложений в бакет, разбор вложения функций, более таргетированную рассылку в чаты и т.д.