322 lines
7.1 KiB
Go
322 lines
7.1 KiB
Go
package telegramhook
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.company.lan/gopkg/logrus"
|
|
)
|
|
|
|
// TelegramHook to send logs via the Telegram API.
|
|
type TelegramHook struct {
|
|
client *http.Client
|
|
mu sync.RWMutex
|
|
appName string
|
|
authToken string
|
|
chatId string
|
|
threadId string
|
|
level logrus.Level
|
|
sync bool
|
|
}
|
|
|
|
// Option defines a method for additional configuration when instantiating TelegramHook
|
|
type Option func(*TelegramHook)
|
|
|
|
// Timeout sets http call timeout for telegram client
|
|
func WithTimeout(timeout time.Duration) Option {
|
|
return func(h *TelegramHook) {
|
|
if timeout > 0 {
|
|
h.client.Timeout = timeout
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithLevel set level
|
|
func WithLevel(level logrus.Level) Option {
|
|
return func(h *TelegramHook) {
|
|
h.SetLevel(level)
|
|
}
|
|
}
|
|
|
|
// WithSync sets logging to telegram as synchronous
|
|
func WithSync(sync bool) Option {
|
|
return func(h *TelegramHook) {
|
|
h.SetSync(sync)
|
|
}
|
|
}
|
|
|
|
// NewTelegramHook creates a new instance of a hook targeting the Telegram API.
|
|
func NewTelegramHook(appName, authToken, chatId, threadId string, options ...Option) (*TelegramHook, error) {
|
|
client := &http.Client{}
|
|
return NewTelegramHookWithClient(appName, authToken, chatId, threadId, client, options...)
|
|
}
|
|
|
|
// NewTelegramHookWithClient creates a new instance of a hook targeting the Telegram API with custom http.Client.
|
|
func NewTelegramHookWithClient(appName, authToken, chatId, threadId string, client *http.Client, options ...Option) (*TelegramHook, error) {
|
|
h := TelegramHook{
|
|
client: client,
|
|
appName: appName,
|
|
authToken: authToken,
|
|
chatId: chatId,
|
|
threadId: threadId,
|
|
level: logrus.ErrorLevel,
|
|
sync: false,
|
|
}
|
|
|
|
for _, opt := range options {
|
|
opt(&h)
|
|
}
|
|
|
|
// Verify the API token is valid and correct before continuing
|
|
if err := h.verifyToken(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &h, nil
|
|
}
|
|
|
|
// apiRequest encapsulates the request structure we are sending to the Telegram API.
|
|
type apiRequest struct {
|
|
ChatId string `json:"chat_id"`
|
|
ThreadId string `json:"message_thread_id,omitempty"`
|
|
Text string `json:"text"`
|
|
ParseMode string `json:"parse_mode,omitempty"`
|
|
}
|
|
|
|
// apiResponse encapsulates the response structure received from the Telegram API.
|
|
type apiResponse struct {
|
|
Ok bool `json:"ok"`
|
|
ErrorCode *int `json:"error_code,omitempty"`
|
|
Desc *string `json:"description,omitempty"`
|
|
Result *any `json:"result,omitempty"`
|
|
}
|
|
|
|
// verifyToken issues a test request to the Telegram API to ensure the provided token is correct and valid.
|
|
func (h *TelegramHook) verifyToken() error {
|
|
endpoint, _ := url.JoinPath(h.ApiEndpoint(), "getMe")
|
|
|
|
res, err := h.client.Get(endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
apiRes := apiResponse{}
|
|
if err := json.NewDecoder(res.Body).Decode(&apiRes); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !apiRes.Ok {
|
|
// Received an error from the Telegram API
|
|
msg := "Received error response from Telegram API"
|
|
|
|
if apiRes.ErrorCode != nil {
|
|
msg = fmt.Sprintf("%s (error code %d)", msg, *apiRes.ErrorCode)
|
|
}
|
|
|
|
if apiRes.Desc != nil {
|
|
msg = fmt.Sprintf("%s: %s", msg, *apiRes.Desc)
|
|
}
|
|
|
|
j, _ := json.MarshalIndent(apiRes, "", "\t")
|
|
return fmt.Errorf("%s\n%s", msg, j)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendMessage issues the provided message to the Telegram API.
|
|
func (h *TelegramHook) sendMessage(msg string) error {
|
|
apiReq := apiRequest{
|
|
ChatId: h.ChatId(),
|
|
ThreadId: h.ThreadId(),
|
|
Text: msg,
|
|
ParseMode: "HTML",
|
|
}
|
|
b, err := json.Marshal(apiReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
endpoint, _ := url.JoinPath(h.ApiEndpoint(), "sendMessage")
|
|
|
|
res, err := h.client.Post(endpoint, "application/json", bytes.NewReader(b))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Encountered error when issuing request to Telegram API, %v", err)
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
apiRes := apiResponse{}
|
|
if err := json.NewDecoder(res.Body).Decode(&apiRes); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !apiRes.Ok {
|
|
// Received an error from the Telegram API
|
|
msg := "Received error response from Telegram API"
|
|
|
|
if apiRes.ErrorCode != nil {
|
|
msg = fmt.Sprintf("%s (error code %d)", msg, *apiRes.ErrorCode)
|
|
}
|
|
|
|
if apiRes.Desc != nil {
|
|
msg = fmt.Sprintf("%s: %s", msg, *apiRes.Desc)
|
|
}
|
|
|
|
return errors.New(msg)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createMessage crafts an HTML-formatted message to send to the Telegram API.
|
|
func (h *TelegramHook) createMessage(entry *logrus.Entry) string {
|
|
var msg string
|
|
|
|
switch entry.Level {
|
|
case logrus.PanicLevel:
|
|
msg = "<b>PANIC</b>"
|
|
case logrus.FatalLevel:
|
|
msg = "<b>FATAL</b>"
|
|
case logrus.ErrorLevel:
|
|
msg = "<b>ERROR</b>"
|
|
case logrus.WarnLevel:
|
|
msg = "<b>WARNING</b>"
|
|
case logrus.InfoLevel:
|
|
msg = "<b>INFO</b>"
|
|
case logrus.DebugLevel:
|
|
msg = "<b>DEBUG</b>"
|
|
}
|
|
|
|
msg = strings.Join([]string{msg, h.AppName()}, "@")
|
|
msg = strings.Join([]string{msg, entry.Message}, " - ")
|
|
|
|
if len(entry.Data) > 0 {
|
|
msg = strings.Join([]string{msg, "<pre>"}, "\n")
|
|
for k, v := range entry.Data {
|
|
msg = strings.Join([]string{msg, html.EscapeString(fmt.Sprintf("\t%s: %+v", k, v))}, "\n")
|
|
}
|
|
msg = strings.Join([]string{msg, "</pre>"}, "\n")
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// Levels returns the log levels that the hook should be enabled for.
|
|
func (h *TelegramHook) Levels() []logrus.Level {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return logrus.AllLevels[:h.level+1]
|
|
}
|
|
|
|
// Fire emits a log message to the Telegram API.
|
|
func (h *TelegramHook) Fire(entry *logrus.Entry) error {
|
|
msg := h.createMessage(entry)
|
|
|
|
if h.Sync() {
|
|
if err := h.sendMessage(msg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Unable to send message, %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
go h.sendMessage(msg)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApiEndpoint
|
|
func (h *TelegramHook) ApiEndpoint() string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return fmt.Sprintf("https://api.telegram.org/bot%s", h.authToken)
|
|
}
|
|
|
|
// AppName
|
|
func (h *TelegramHook) AppName() string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.appName
|
|
}
|
|
|
|
func (h *TelegramHook) SetAppName(appName string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.appName = appName
|
|
}
|
|
|
|
// AuthToken
|
|
func (h *TelegramHook) AuthToken() string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.authToken
|
|
}
|
|
|
|
func (h *TelegramHook) SetAuthToken(authToken string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.authToken = authToken
|
|
}
|
|
|
|
// ChatId
|
|
func (h *TelegramHook) ChatId() string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.chatId
|
|
}
|
|
|
|
func (h *TelegramHook) SetChatId(chatId string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.chatId = chatId
|
|
}
|
|
|
|
// ThreadId
|
|
func (h *TelegramHook) ThreadId() string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.threadId
|
|
}
|
|
|
|
func (h *TelegramHook) SetThreadId(threadId string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.threadId = threadId
|
|
}
|
|
|
|
// Level
|
|
func (h *TelegramHook) Level() logrus.Level {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.level
|
|
}
|
|
|
|
func (h *TelegramHook) SetLevel(level logrus.Level) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.level = level
|
|
}
|
|
|
|
// Sync
|
|
func (h *TelegramHook) Sync() bool {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.sync
|
|
}
|
|
|
|
func (h *TelegramHook) SetSync(sync bool) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.sync = sync
|
|
}
|