// 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': 'Environment: %s' } }, { 'textParagraph': { 'text': 'Message: %s' } }, { 'textParagraph': { 'text': 'Build: %s' } }, { 'textParagraph': { 'text': 'Time: %s' } }, { 'textParagraph': { 'text': 'Hostname: %s' } }, { 'textParagraph': { 'text': 'IP: %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 }