Files
qfixdpl/src/client/notify/google/google_chat.go
Ramiro Paz 0910b1e6c8 fixes
2026-03-12 10:23:19 -03:00

272 lines
6.4 KiB
Go

// Package googlechat provides functionality to send google chat notifications
package googlechat
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/sasha-s/go-deadlock"
"quantex.com/qfixdpl/src/app/version"
common "quantex.com/qfixdpl/src/client/notify"
"quantex.com/qfixdpl/src/common/logger"
"quantex.com/qfixdpl/src/common/tracerr"
"quantex.com/qfixdpl/src/domain"
)
//revive:disable:line-length-limit It's just links
type SpaceID string
// spaces URLs
const googleChatURL = "https://chat.googleapis.com/v1/spaces/"
type Notify struct {
spaces map[domain.MessageChannel]SpaceID
messages map[string]time.Time
m deadlock.Mutex
}
//nolint:lll // It's just a link
func New(chls domain.Channels) *Notify {
if err := common.ValidateChannelConfig(chls); err != nil {
panic(tracerr.Errorf("google config error: %w", err))
}
messages := make(map[string]time.Time)
spaces := make(map[domain.MessageChannel]SpaceID)
spaces[domain.MessageChannelTest] = SpaceID(chls.Test)
spaces[domain.MessageChannelWeb] = SpaceID(chls.Web)
spaces[domain.MessageChannelPanic] = SpaceID(chls.Panic)
spaces[domain.MessageChannelError] = SpaceID(chls.Error)
return &Notify{
spaces: spaces,
messages: messages,
}
}
func (g *Notify) Example() {
wg := sync.WaitGroup{}
wg.Add(3)
go g.SendMsg(domain.MessageChannelTest, "Hello World!", domain.MessageStatusGood, &wg)
go g.SendMsg(domain.MessageChannelTest, "Error", domain.MessageStatusStopper, &wg)
go g.SendMsg(domain.MessageChannelTest, "Warning", domain.MessageStatusWarning, &wg)
wg.Wait()
}
func (g *Notify) SendMsg(chat domain.MessageChannel, text string, status domain.MessageStatus, waitgroup *sync.WaitGroup) {
var space SpaceID
if s, ok := g.spaces[chat]; !ok {
space = g.spaces[domain.MessageChannelError]
err := tracerr.Errorf("error sending google notification, there's no space id for chat: %s", chat)
logger.ErrorWoNotifier("%v", err.Error())
} else {
space = s
}
s := fmt.Sprintf("%s-%s-%s", text, status.String(), string(space))
if ok := g.shouldSendMessage(s); ok {
go common.SendWithRetry(text, status, waitgroup, googleChatURL+string(space), getMessage)
}
}
//nolint:lll // It's just a link
const (
errImg = "https://media.gettyimages.com/id/1359003186/es/foto/illustration-of-a-tick-or-an-x-indicating-right-and-wrong.jpg?s=612x612&w=0&k=20&c=FdYIXI1qCPpVcY6phcIJom8sRhZw4hgYVtwOC6lNlmA="
goodImg = "https://media.gettyimages.com/id/1352723074/es/foto/drawing-of-green-tick-check-mark.jpg?s=612x612&w=0&k=20&c=RgoCJ-n0OpZSClCWhJstoXAfiGrBZhnggUvOp2ooJqI="
warnImg = "https://media.gettyimages.com/id/1407160246/es/vector/icono-de-tri%C3%A1ngulo-de-peligro.jpg?s=612x612&w=0&k=20&c=rI0IYmZmkg62txtNQFhyTbHx5oW311_OupBkbWODfjg="
)
func getImage(status domain.MessageStatus) (string, error) {
switch status {
case domain.MessageStatusGood:
return goodImg, nil
case domain.MessageStatusWarning:
return warnImg, nil
case domain.MessageStatusStopper:
return errImg, nil
}
return "", tracerr.Errorf("unknown Message Status Type")
}
func getMessage(text string, status domain.MessageStatus) string {
image, err := getImage(status)
if err != nil {
err = tracerr.Errorf("error getting image at getMessage, error: %w", err)
logger.ErrorWoNotifier("%v", err.Error())
var e error
image, e = getImage(domain.MessageStatusWarning)
if e != nil {
e = tracerr.Errorf("error getting image at getMessage, error: %w", e)
logger.ErrorWoNotifier("%v", e.Error())
}
}
env := version.Environment()
appVersion := version.Base()
text = strings.ReplaceAll(text, "'", "\"")
msg := `
{
'cardsV2': [{
'cardId': 'createCardMessage',
'card': {
'header': {
'title': 'QFIXDPL',
'subtitle': 'Notification',
'imageUrl': '%s',
'imageType': 'CIRCLE'
},
'sections': [
{
'widgets':[
{
'textParagraph': {
'text': '<b>Environment:</b> %s'
}
},
{
'textParagraph': {
'text': '<b>Message:</b> %s'
}
},
{
'textParagraph': {
'text': '<b>Build:</b> %s'
}
},
{
'textParagraph': {
'text': '<b>Time:</b> %s'
}
},
{
'textParagraph': {
'text': '<b>Hostname:</b> %s'
}
},
{
'textParagraph': {
'text': '<b>IP:</b> %s'
}
}
]
}
]
}
}]
}`
loc, err := time.LoadLocation("America/Argentina/Buenos_Aires")
if err != nil {
err := tracerr.Errorf("error loading timezone %v, setting default UTC", err)
logger.ErrorWoNotifier("%v", err.Error())
loc = time.UTC
}
now := time.Now().In(loc).Format("15:04:05 02-01-2006 -0700")
jsonMsg := fmt.Sprintf(msg, image, env, text, appVersion, now,
getHostname(), getOutboundIP())
return jsonMsg
}
func (g *Notify) shouldSendMessage(s string) (ok bool) {
sha, err := generateShaString(s)
if err != nil {
err = tracerr.Errorf("error generating sha string at shouldSendMessage, error: %w", err)
logger.ErrorWoNotifier("%v", err.Error())
return true
}
g.m.Lock()
defer g.m.Unlock()
if t, ok := g.messages[sha]; ok && time.Since(t) < time.Minute {
return false
}
g.messages[sha] = time.Now()
return true
}
func generateShaString(s string) (sha string, err error) {
hash := sha256.New()
if _, err := hash.Write([]byte(s)); err != nil {
err = tracerr.Errorf("error generating hash at generateShaString, error: %w", err)
logger.ErrorWoNotifier("%v", err.Error())
return sha, err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
//revive:enable:line-length-limit
//nolint:gochecknoglobals // need to be global
var outboundIP string
func getOutboundIP() string {
if outboundIP == "" {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
logger.ErrorWoNotifier("failed to get UDP address")
return ""
}
defer func(conn net.Conn) {
err = conn.Close()
if err != nil {
logger.ErrorWoNotifier("error closing net connection")
}
}(conn)
localAddr, ok := conn.LocalAddr().(*net.UDPAddr)
if !ok {
logger.ErrorWoNotifier("error: conn.LocalAddr() is not a *net.UDPAddr type")
}
outboundIP = localAddr.IP.String()
}
return outboundIP
}
func getHostname() string {
hostname, err := os.Hostname()
if err != nil {
logger.ErrorWoNotifier("failed to get hostname")
return ""
}
return hostname
}