diff --git a/cmd/bots/discord/discord.go b/cmd/bots/discord/discord.go index c6ad5012..73188cdd 100644 --- a/cmd/bots/discord/discord.go +++ b/cmd/bots/discord/discord.go @@ -68,14 +68,14 @@ func runDiscordBot() error { ctx, done := signal.NotifyContext(context.Background(), os.Interrupt) defer done() - discordBotEnv, err := initial.InitDiscordBot(discordBotToken, discordChatID, zap) + pairRequest := make(chan pairResponses.RequestPair) + pairResponse := make(chan pairResponses.ResponsePair) + + discordBotEnv, err := initial.InitDiscordBot(discordBotToken, discordChatID, zap, pairRequest, pairResponse) if err != nil { return errors.Wrap(err, "failed to init discord bot") } - - pairRequest := make(chan pairResponses.RequestPair) - pairResponse := make(chan pairResponses.ResponsePair) - handlers.InitDscHandlers(discordBotEnv, pairRequest, pairResponse) + handlers.InitDscHandlers(discordBotEnv, pairRequest, pairResponse, zap) go func() { err := pubsub.StartSubMessagingClient(ctx, nanomsgPubSubURL, discordBotEnv, zap) diff --git a/cmd/bots/internal/common/environment.go b/cmd/bots/internal/common/environment.go index 3c11294c..e5d0630f 100644 --- a/cmd/bots/internal/common/environment.go +++ b/cmd/bots/internal/common/environment.go @@ -38,11 +38,11 @@ var ( templateFiles embed.FS ) -type expectedExtension string +type ExpectedExtension string const ( - Html expectedExtension = ".html" - Markdown expectedExtension = ".md" + Html ExpectedExtension = ".html" + Markdown ExpectedExtension = ".md" ) var errUnknownAlertType = errors.New("received unknown alert type") @@ -79,22 +79,25 @@ func (s *subscriptions) MapR(f func()) { } type DiscordBotEnvironment struct { - ChatID string - Bot *discordgo.Session - subSocket protocol.Socket - Subscriptions subscriptions - Zap *zap.Logger + ChatID string + Bot *discordgo.Session + subSocket protocol.Socket + Subscriptions subscriptions + zap *zap.Logger + requestType chan<- pair.RequestPair + responsePairType <-chan pair.ResponsePair } -func NewDiscordBotEnvironment(bot *discordgo.Session, chatID string, zap *zap.Logger) *DiscordBotEnvironment { - return &DiscordBotEnvironment{Bot: bot, ChatID: chatID, Subscriptions: subscriptions{subs: make(map[entities.AlertType]string), mu: new(sync.RWMutex)}, Zap: zap} +func NewDiscordBotEnvironment(bot *discordgo.Session, chatID string, zap *zap.Logger, requestType chan<- pair.RequestPair, + responsePairType <-chan pair.ResponsePair) *DiscordBotEnvironment { + return &DiscordBotEnvironment{Bot: bot, ChatID: chatID, Subscriptions: subscriptions{subs: make(map[entities.AlertType]string), mu: new(sync.RWMutex)}, zap: zap, requestType: requestType, responsePairType: responsePairType} } func (dscBot *DiscordBotEnvironment) Start() error { - dscBot.Zap.Info("Discord bot started") + dscBot.zap.Info("Discord bot started") err := dscBot.Bot.Open() if err != nil { - dscBot.Zap.Error("failed to open discord bot", zap.Error(err)) + dscBot.zap.Error("failed to open discord bot", zap.Error(err)) return err } return nil @@ -107,36 +110,41 @@ func (dscBot *DiscordBotEnvironment) SetSubSocket(subSocket protocol.Socket) { func (dscBot *DiscordBotEnvironment) SendMessage(msg string) { _, err := dscBot.Bot.ChannelMessageSend(dscBot.ChatID, msg) if err != nil { - dscBot.Zap.Error("failed to send a message to discord", zap.Error(err)) + dscBot.zap.Error("failed to send a message to discord", zap.Error(err)) } } func (dscBot *DiscordBotEnvironment) SendAlertMessage(msg []byte) { if len(msg) == 0 { - dscBot.Zap.Error("received empty alert message") + dscBot.zap.Error("received empty alert message") return } alertType := entities.AlertType(msg[0]) _, ok := entities.AlertTypes[alertType] if !ok { - dscBot.Zap.Sugar().Errorf("failed to construct message, unknown alert type %c, %v", byte(alertType), errUnknownAlertType) + dscBot.zap.Sugar().Errorf("failed to construct message, unknown alert type %c, %v", byte(alertType), errUnknownAlertType) _, err := dscBot.Bot.ChannelMessageSend(dscBot.ChatID, errUnknownAlertType.Error()) if err != nil { - dscBot.Zap.Error("failed to send a message to discord", zap.Error(err)) + dscBot.zap.Error("failed to send a message to discord", zap.Error(err)) } return } - messageToBot, err := constructMessage(alertType, msg[1:], Markdown) + nodes, err := messaging.RequestAllNodes(dscBot.requestType, dscBot.responsePairType) if err != nil { - dscBot.Zap.Error("failed to construct message", zap.Error(err)) + dscBot.zap.Error("failed to get nodes list", zap.Error(err)) + } + + messageToBot, err := constructMessage(alertType, msg[1:], Markdown, nodes) + if err != nil { + dscBot.zap.Error("failed to construct message", zap.Error(err)) return } _, err = dscBot.Bot.ChannelMessageSend(dscBot.ChatID, messageToBot) if err != nil { - dscBot.Zap.Error("failed to send a message to discord", zap.Error(err)) + dscBot.zap.Error("failed to send a message to discord", zap.Error(err)) } } @@ -151,7 +159,7 @@ func (dscBot *DiscordBotEnvironment) SubscribeToAllAlerts() error { return err } dscBot.Subscriptions.Add(alertType, alertName) - dscBot.Zap.Sugar().Infof("subscribed to %s", alertName) + dscBot.zap.Sugar().Infof("subscribed to %s", alertName) } return nil @@ -167,16 +175,18 @@ func (dscBot *DiscordBotEnvironment) IsEligibleForAction(chatID string) bool { } type TelegramBotEnvironment struct { - ChatID int64 - Bot *telebot.Bot - Mute bool // If it used elsewhere, should be protected by mutex - subSocket protocol.Socket - subscriptions subscriptions - Zap *zap.Logger + ChatID int64 + Bot *telebot.Bot + Mute bool // If it used elsewhere, should be protected by mutex + subSocket protocol.Socket + subscriptions subscriptions + Zap *zap.Logger + requestType chan<- pair.RequestPair + responsePairType <-chan pair.ResponsePair } -func NewTelegramBotEnvironment(bot *telebot.Bot, chatID int64, mute bool, zap *zap.Logger) *TelegramBotEnvironment { - return &TelegramBotEnvironment{Bot: bot, ChatID: chatID, Mute: mute, subscriptions: subscriptions{subs: make(map[entities.AlertType]string), mu: new(sync.RWMutex)}, Zap: zap} +func NewTelegramBotEnvironment(bot *telebot.Bot, chatID int64, mute bool, zap *zap.Logger, requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair) *TelegramBotEnvironment { + return &TelegramBotEnvironment{Bot: bot, ChatID: chatID, Mute: mute, subscriptions: subscriptions{subs: make(map[entities.AlertType]string), mu: new(sync.RWMutex)}, Zap: zap, requestType: requestType, responsePairType: responsePairType} } func (tgEnv *TelegramBotEnvironment) Start() error { @@ -218,7 +228,12 @@ func (tgEnv *TelegramBotEnvironment) SendAlertMessage(msg []byte) { return } - messageToBot, err := constructMessage(alertType, msg[1:], Html) + nodes, err := messaging.RequestAllNodes(tgEnv.requestType, tgEnv.responsePairType) + if err != nil { + tgEnv.Zap.Error("failed to get nodes list", zap.Error(err)) + } + + messageToBot, err := constructMessage(alertType, msg[1:], Html, nodes) if err != nil { tgEnv.Zap.Error("failed to construct message", zap.Error(err)) return @@ -398,15 +413,11 @@ func ScheduleNodesStatus( responsePairType <-chan pair.ResponsePair, bot messaging.Bot, zapLogger *zap.Logger) error { _, err := taskScheduler.ScheduleWithCron(func(ctx context.Context) { - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + nodes, err := messaging.RequestAllNodes(requestType, responsePairType) if err != nil { zapLogger.Error("failed to get nodes list", zap.Error(err)) } - additionalUrls, err := messaging.RequestNodesList(requestType, responsePairType, true) - if err != nil { - zapLogger.Error("failed to get additional nodes list", zap.Error(err)) - } - urls = append(urls, additionalUrls...) + urls := messaging.NodesToUrls(nodes) nodesStatus, err := messaging.RequestNodesStatus(requestType, responsePairType, urls) if err != nil { @@ -417,12 +428,12 @@ func ScheduleNodesStatus( statusCondition := StatusCondition{AllNodesAreOk: false, NodesNumber: 0, Height: ""} switch bot.(type) { case *TelegramBotEnvironment: - handledNodesStatus, statusCondition, err = HandleNodesStatus(nodesStatus, Html) + handledNodesStatus, statusCondition, err = HandleNodesStatus(nodesStatus, Html, nodes) if err != nil { zapLogger.Error("failed to handle nodes status", zap.Error(err)) } case *DiscordBotEnvironment: - handledNodesStatus, statusCondition, err = HandleNodesStatus(nodesStatus, Markdown) + handledNodesStatus, statusCondition, err = HandleNodesStatus(nodesStatus, Markdown, nodes) if err != nil { zapLogger.Error("failed to handle nodes status", zap.Error(err)) } @@ -494,7 +505,7 @@ func sortNodesStatuses(statuses []NodeStatus) { }) } -func executeTemplate(template string, data any, extension expectedExtension) (string, error) { +func executeTemplate(template string, data any, extension ExpectedExtension) (string, error) { switch extension { case Html: tmpl, err := htmlTemplate.ParseFS(templateFiles, template+string(extension)) @@ -521,6 +532,212 @@ func executeTemplate(template string, data any, extension expectedExtension) (st default: return "", errors.New("unknown message type to execute a template") } +} + +func replaceNodeWithAlias(node string, nodesAlias map[string]string) string { + if alias, ok := nodesAlias[node]; ok { + return alias + } + node = strings.ReplaceAll(node, entities.HttpsScheme+"://", "") + node = strings.ReplaceAll(node, entities.HttpScheme+"://", "") + return node +} + +type heightStatementGroup struct { + Nodes []string + Height int +} + +type heightStatement struct { + HeightDifference int + FirstGroup heightStatementGroup + SecondGroup heightStatementGroup +} + +type stateHashStatementGroup struct { + BlockID string + Nodes []string + StateHash string +} + +type stateHashStatement struct { + SameHeight int + LastCommonStateHashExist bool + ForkHeight int + ForkBlockID string + ForkStateHash string + FirstGroup stateHashStatementGroup + SecondGroup stateHashStatementGroup +} + +type fixedStatement struct { + PreviousAlert string +} + +func executeAlertTemplate(alertType entities.AlertType, alertJson []byte, extension ExpectedExtension, allNodes []entities.Node) (string, error) { + nodesAliases := make(map[string]string) + for _, n := range allNodes { + if n.Alias != "" { + nodesAliases[n.URL] = n.Alias + } + } + var msg string + switch alertType { + case entities.UnreachableAlertType: + var unreachableAlert entities.UnreachableAlert + err := json.Unmarshal(alertJson, &unreachableAlert) + if err != nil { + return "", err + } + + unreachableAlert.Node = replaceNodeWithAlias(unreachableAlert.Node, nodesAliases) + + msg, err = executeTemplate("templates/alerts/unreachable_alert", unreachableAlert, extension) + if err != nil { + return "", err + } + case entities.IncompleteAlertType: + var incompleteAlert entities.IncompleteAlert + err := json.Unmarshal(alertJson, &incompleteAlert) + if err != nil { + return "", err + } + incompleteStatement := incompleteAlert.NodeStatement + incompleteStatement.Node = replaceNodeWithAlias(incompleteStatement.Node, nodesAliases) + + msg, err = executeTemplate("templates/alerts/incomplete_alert", incompleteStatement, extension) + if err != nil { + return "", err + } + case entities.InvalidHeightAlertType: + var invalidHeightAlert entities.InvalidHeightAlert + err := json.Unmarshal(alertJson, &invalidHeightAlert) + if err != nil { + return "", err + } + invalidHeightStatement := invalidHeightAlert.NodeStatement + + invalidHeightStatement.Node = replaceNodeWithAlias(invalidHeightStatement.Node, nodesAliases) + + msg, err = executeTemplate("templates/alerts/invalid_height_alert", invalidHeightStatement, extension) + if err != nil { + return "", err + } + case entities.HeightAlertType: + var heightAlert entities.HeightAlert + err := json.Unmarshal(alertJson, &heightAlert) + if err != nil { + return "", err + } + + for i := range heightAlert.MaxHeightGroup.Nodes { + heightAlert.MaxHeightGroup.Nodes[i] = replaceNodeWithAlias(heightAlert.MaxHeightGroup.Nodes[i], nodesAliases) + } + for i := range heightAlert.OtherHeightGroup.Nodes { + heightAlert.OtherHeightGroup.Nodes[i] = replaceNodeWithAlias(heightAlert.OtherHeightGroup.Nodes[i], nodesAliases) + } + + heightStatement := heightStatement{ + HeightDifference: heightAlert.MaxHeightGroup.Height - heightAlert.OtherHeightGroup.Height, + FirstGroup: heightStatementGroup{ + Nodes: heightAlert.MaxHeightGroup.Nodes, + Height: heightAlert.MaxHeightGroup.Height, + }, + SecondGroup: heightStatementGroup{ + Nodes: heightAlert.OtherHeightGroup.Nodes, + Height: heightAlert.OtherHeightGroup.Height, + }, + } + + msg, err = executeTemplate("templates/alerts/height_alert", heightStatement, extension) + if err != nil { + return "", err + } + case entities.StateHashAlertType: + var stateHashAlert entities.StateHashAlert + err := json.Unmarshal(alertJson, &stateHashAlert) + if err != nil { + return "", err + } + + for i := range stateHashAlert.FirstGroup.Nodes { + stateHashAlert.FirstGroup.Nodes[i] = replaceNodeWithAlias(stateHashAlert.FirstGroup.Nodes[i], nodesAliases) + } + for i := range stateHashAlert.SecondGroup.Nodes { + stateHashAlert.SecondGroup.Nodes[i] = replaceNodeWithAlias(stateHashAlert.SecondGroup.Nodes[i], nodesAliases) + } + + stateHashStatement := stateHashStatement{ + SameHeight: stateHashAlert.CurrentGroupsBucketHeight, + LastCommonStateHashExist: stateHashAlert.LastCommonStateHashExist, + ForkHeight: stateHashAlert.LastCommonStateHashHeight, + ForkBlockID: stateHashAlert.LastCommonStateHash.BlockID.String(), + ForkStateHash: stateHashAlert.LastCommonStateHash.SumHash.Hex(), + + FirstGroup: stateHashStatementGroup{ + BlockID: stateHashAlert.FirstGroup.StateHash.BlockID.String(), + Nodes: stateHashAlert.FirstGroup.Nodes, + StateHash: stateHashAlert.FirstGroup.StateHash.SumHash.Hex(), + }, + SecondGroup: stateHashStatementGroup{ + BlockID: stateHashAlert.SecondGroup.StateHash.BlockID.String(), + Nodes: stateHashAlert.SecondGroup.Nodes, + StateHash: stateHashAlert.SecondGroup.StateHash.SumHash.Hex(), + }, + } + + msg, err = executeTemplate("templates/alerts/state_hash_alert", stateHashStatement, extension) + if err != nil { + return "", err + } + case entities.AlertFixedType: + var alertFixed entities.AlertFixed + err := json.Unmarshal(alertJson, &alertFixed) + if err != nil { + return "", err + } + + // TODO there is no alias here right now, but AlertFixed needs to be changed. Make previous alert to look like a number + + fixedStatement := fixedStatement{ + PreviousAlert: alertFixed.Fixed.Message(), + } + + msg, err = executeTemplate("templates/alerts/alert_fixed", fixedStatement, extension) + if err != nil { + return "", err + } + case entities.BaseTargetAlertType: + var baseTargetAlert entities.BaseTargetAlert + err := json.Unmarshal(alertJson, &baseTargetAlert) + + for i := range baseTargetAlert.BaseTargetValues { + baseTargetAlert.BaseTargetValues[i].Node = replaceNodeWithAlias(baseTargetAlert.BaseTargetValues[i].Node, nodesAliases) + } + + if err != nil { + return "", err + } + + msg, err = executeTemplate("templates/alerts/base_target_alert", baseTargetAlert, extension) + if err != nil { + return "", err + } + case entities.InternalErrorAlertType: + var internalErrorAlert entities.InternalErrorAlert + err := json.Unmarshal(alertJson, &internalErrorAlert) + if err != nil { + return "", err + } + msg, err = executeTemplate("templates/alerts/internal_error_alert", internalErrorAlert, extension) + if err != nil { + return "", err + } + default: + return "", errors.New("unknown alert type") + } + + return msg, nil } @@ -530,7 +747,7 @@ type StatusCondition struct { Height string } -func HandleNodesStatusError(nodesStatusResp *pair.NodesStatusResponse, extension expectedExtension) (string, StatusCondition, error) { +func HandleNodesStatusError(nodesStatusResp *pair.NodesStatusResponse, extension ExpectedExtension) (string, StatusCondition, error) { statusCondition := StatusCondition{AllNodesAreOk: false, NodesNumber: 0, Height: ""} var differentHeightsNodes []NodeStatus @@ -568,15 +785,21 @@ func HandleNodesStatusError(nodesStatusResp *pair.NodesStatusResponse, extension return fmt.Sprintf("%s\n\n%s", nodesStatusResp.ErrMessage, msg), statusCondition, nil } return nodesStatusResp.ErrMessage, statusCondition, nil + } -func HandleNodesStatus(nodesStatusResp *pair.NodesStatusResponse, extension expectedExtension) (string, StatusCondition, error) { +func HandleNodesStatus(nodesStatusResp *pair.NodesStatusResponse, + extension ExpectedExtension, allNodes []entities.Node) (string, StatusCondition, error) { statusCondition := StatusCondition{AllNodesAreOk: false, NodesNumber: 0, Height: ""} - // remove all https and http prefixes + nodesAliases := make(map[string]string) + for _, n := range allNodes { + if n.Alias != "" { + nodesAliases[n.URL] = n.Alias + } + } for i := range nodesStatusResp.NodesStatus { - nodesStatusResp.NodesStatus[i].Url = strings.ReplaceAll(nodesStatusResp.NodesStatus[i].Url, entities.HttpsScheme+"://", "") - nodesStatusResp.NodesStatus[i].Url = strings.ReplaceAll(nodesStatusResp.NodesStatus[i].Url, entities.HttpScheme+"://", "") + nodesStatusResp.NodesStatus[i].Url = replaceNodeWithAlias(nodesStatusResp.NodesStatus[i].Url, nodesAliases) } if nodesStatusResp.ErrMessage != "" { @@ -642,7 +865,7 @@ func HandleNodesStatus(nodesStatusResp *pair.NodesStatusResponse, extension expe return msg, statusCondition, nil } -func HandleNodeStatement(nodeStatementResp *pair.NodeStatementResponse, extension expectedExtension) (string, error) { +func HandleNodeStatement(nodeStatementResp *pair.NodeStatementResponse, extension ExpectedExtension) (string, error) { nodeStatementResp.NodeStatement.Node = strings.ReplaceAll(nodeStatementResp.NodeStatement.Node, entities.HttpsScheme+"://", "") nodeStatementResp.NodeStatement.Node = strings.ReplaceAll(nodeStatementResp.NodeStatement.Node, entities.HttpScheme+"://", "") @@ -671,51 +894,20 @@ func HandleNodeStatement(nodeStatementResp *pair.NodeStatementResponse, extensio return msg, nil } -func constructMessage(alertType entities.AlertType, alertJson []byte, extension expectedExtension) (string, error) { +func constructMessage(alertType entities.AlertType, alertJson []byte, extension ExpectedExtension, allNodes []entities.Node) (string, error) { alert := generalMessaging.Alert{} err := json.Unmarshal(alertJson, &alert) if err != nil { return "", errors.Wrap(err, "failed to unmarshal json") } - prettyAlert := makeMessagePretty(alertType, alert) - - msg, err := executeTemplate("templates/alert", prettyAlert, extension) + msg, err := executeAlertTemplate(alertType, alertJson, extension, allNodes) if err != nil { - return "", err + return "", errors.Errorf("failed to execute an alert template, %v", err) } return msg, nil } -func makeMessagePretty(alertType entities.AlertType, alert generalMessaging.Alert) generalMessaging.Alert { - alert.Details = strings.ReplaceAll(alert.Details, entities.HttpScheme+"://", "") - alert.Details = strings.ReplaceAll(alert.Details, entities.HttpsScheme+"://", "") - // simple alert is skipped because it needs to be deleted - switch alertType { - case entities.UnreachableAlertType, entities.InvalidHeightAlertType, entities.StateHashAlertType, entities.HeightAlertType: - alert.AlertDescription += fmt.Sprintf(" %s", commonMessages.ErrorOrDeleteMsg) - case entities.InternalErrorAlertType: - alert.AlertDescription += fmt.Sprintf(" %s", commonMessages.WarnMsg) - case entities.IncompleteAlertType: - alert.AlertDescription += fmt.Sprintf(" %s", commonMessages.QuestionMsg) - case entities.AlertFixedType: - alert.AlertDescription += fmt.Sprintf(" %s", commonMessages.OkMsg) - default: - - } - switch alert.Level { - case entities.InfoLevel: - alert.Level += fmt.Sprintf(" %s", commonMessages.InfoMsg) - case entities.WarnLevel: - alert.Level += fmt.Sprintf(" %s", commonMessages.WarnMsg) - case entities.ErrorLevel: - alert.Level += fmt.Sprintf(" %s", commonMessages.ErrorOrDeleteMsg) - default: - } - - return alert -} - func FindAlertTypeByName(alertName string) (entities.AlertType, bool) { for key, val := range entities.AlertTypes { if val == alertName { diff --git a/cmd/bots/internal/common/init/init.go b/cmd/bots/internal/common/init/init.go index bd9c4dd3..d02dcd18 100644 --- a/cmd/bots/internal/common/init/init.go +++ b/cmd/bots/internal/common/init/init.go @@ -7,6 +7,7 @@ import ( tele "gopkg.in/telebot.v3" "nodemon/cmd/bots/internal/common" "nodemon/cmd/bots/internal/telegram/config" + "nodemon/pkg/messaging/pair" ) func InitTgBot(behavior string, @@ -15,6 +16,8 @@ func InitTgBot(behavior string, botToken string, chatID int64, logger *zap.Logger, + requestType chan<- pair.RequestPair, + responsePairType <-chan pair.ResponsePair, ) (*common.TelegramBotEnvironment, error) { botSettings, err := config.NewTgBotSettings(behavior, webhookLocalAddress, publicURL, botToken) if err != nil { @@ -27,7 +30,7 @@ func InitTgBot(behavior string, logger.Sugar().Debugf("telegram chat id for sending alerts is %d", chatID) - tgBotEnv := common.NewTelegramBotEnvironment(bot, chatID, false, logger) + tgBotEnv := common.NewTelegramBotEnvironment(bot, chatID, false, logger, requestType, responsePairType) return tgBotEnv, nil } @@ -35,6 +38,8 @@ func InitDiscordBot( botToken string, chatID string, logger *zap.Logger, + requestType chan<- pair.RequestPair, + responsePairType <-chan pair.ResponsePair, ) (*common.DiscordBotEnvironment, error) { bot, err := discordgo.New("Bot " + botToken) if err != nil { @@ -43,6 +48,6 @@ func InitDiscordBot( logger.Sugar().Debugf("discord chat id for sending alerts is %s", chatID) bot.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent - dscBotEnv := common.NewDiscordBotEnvironment(bot, chatID, logger) + dscBotEnv := common.NewDiscordBotEnvironment(bot, chatID, logger, requestType, responsePairType) return dscBotEnv, nil } diff --git a/cmd/bots/internal/common/messaging/handlers.go b/cmd/bots/internal/common/messaging/handlers.go index 1825b11c..76fc38d3 100644 --- a/cmd/bots/internal/common/messaging/handlers.go +++ b/cmd/bots/internal/common/messaging/handlers.go @@ -43,6 +43,26 @@ func AddNewNodeHandler( return fmt.Sprintf("New node '%s' was added", updatedUrl), nil } +func UpdateAliasHandler( + chatID string, + bot Bot, + requestType chan<- pair.RequestPair, + url string, + alias string) (string, error) { + + if !bot.IsEligibleForAction(chatID) { + return insufficientPermissionMsg, InsufficientPermissionsError + } + + updatedUrl, err := entities.CheckAndUpdateURL(url) + if err != nil { + return incorrectUrlMsg, IncorrectUrlError + } + requestType <- &pair.UpdateNodeRequest{Url: updatedUrl, Alias: alias} + + return fmt.Sprintf("Node '%s' was updated with alias %s", updatedUrl, alias), nil +} + func RemoveNodeHandler( chatID string, bot Bot, @@ -78,18 +98,51 @@ func RequestNodesStatus( } -func RequestNodesList(requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair, specific bool) ([]string, error) { +func RequestNodesUrls(requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair, specific bool) ([]string, error) { requestType <- &pair.NodesListRequest{Specific: specific} responsePair := <-responsePairType nodesList, ok := responsePair.(*pair.NodesListResponse) if !ok { return nil, errors.New("failed to convert response interface to the node list type") } - urls := nodesList.Urls + urls := NodesToUrls(nodesList.Nodes) sort.Strings(urls) return urls, nil } +func RequestNodes(requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair, specific bool) ([]entities.Node, error) { + requestType <- &pair.NodesListRequest{Specific: specific} + responsePair := <-responsePairType + nodesList, ok := responsePair.(*pair.NodesListResponse) + if !ok { + return nil, errors.New("failed to convert response interface to the node list type") + } + return nodesList.Nodes, nil +} + +func RequestAllNodes(requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair) ([]entities.Node, error) { + regularNodes, err := RequestNodes(requestType, responsePairType, false) + if err != nil { + return nil, err + } + specificNodes, err := RequestNodes(requestType, responsePairType, true) + if err != nil { + return nil, err + } + var nodes []entities.Node + nodes = append(nodes, regularNodes...) + nodes = append(nodes, specificNodes...) + return nodes, nil +} + +func NodesToUrls(nodes []entities.Node) []string { + urls := make([]string, len(nodes)) + for i, n := range nodes { + urls[i] = n.URL + } + return urls +} + func RequestNodeStatement(requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair, node string, height int) (*pair.NodeStatementResponse, error) { requestType <- &pair.NodeStatementRequest{Url: node, Height: height} responsePair := <-responsePairType diff --git a/cmd/bots/internal/common/messaging/pair/client.go b/cmd/bots/internal/common/messaging/pair/client.go index 1a77ac21..3133e725 100644 --- a/cmd/bots/internal/common/messaging/pair/client.go +++ b/cmd/bots/internal/common/messaging/pair/client.go @@ -77,6 +77,22 @@ func StartPairMessagingClient(ctx context.Context, nanomsgURL string, requestPai logger.Error("failed to send message", zap.Error(err)) } + case *pair.UpdateNodeRequest: + message.WriteByte(byte(pair.RequestUpdateNode)) + node := entities.Node{ + URL: r.Url, + Enabled: true, + Alias: r.Alias, + } + nodeInfo, err := json.Marshal(node) + if err != nil { + logger.Error("failed to marshal node's info") + } + message.Write(nodeInfo) + err = pairSocket.Send(message.Bytes()) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } case *pair.DeleteNodeRequest: message.WriteByte(byte(pair.RequestDeleteNodeT)) diff --git a/cmd/bots/internal/common/templates/alert.html b/cmd/bots/internal/common/templates/alert.html deleted file mode 100644 index 42c3a667..00000000 --- a/cmd/bots/internal/common/templates/alert.html +++ /dev/null @@ -1,5 +0,0 @@ -Alert type: {{ .AlertDescription}} - -Level: {{ .Level}} - -Details: {{ .Details}} diff --git a/cmd/bots/internal/common/templates/alert.md b/cmd/bots/internal/common/templates/alert.md deleted file mode 100644 index 0d47cf78..00000000 --- a/cmd/bots/internal/common/templates/alert.md +++ /dev/null @@ -1,7 +0,0 @@ -```yaml -Alert type: {{ .AlertDescription}} - -Level: {{ .Level}} - -Details: {{ .Details}} -``` \ No newline at end of file diff --git a/cmd/bots/internal/common/templates/alerts/alert_fixed.html b/cmd/bots/internal/common/templates/alerts/alert_fixed.html new file mode 100644 index 00000000..3277b07c --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/alert_fixed.html @@ -0,0 +1,5 @@ +Alert type: Resolved ✅ + +Level: Info ℹ + +Details: The issue has been resolved: {{ .PreviousAlert}} diff --git a/cmd/bots/internal/common/templates/alerts/alert_fixed.md b/cmd/bots/internal/common/templates/alerts/alert_fixed.md new file mode 100644 index 00000000..07a11106 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/alert_fixed.md @@ -0,0 +1,7 @@ +```yaml +Alert type: Resolved ✅ + +Level: Info ℹ + +Details: The issue has been resolved, {{ .PreviousAlert}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/base_target_alert.html b/cmd/bots/internal/common/templates/alerts/base_target_alert.html new file mode 100644 index 00000000..a87c3fb7 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/base_target_alert.html @@ -0,0 +1,12 @@ +Alert type: Base Target + +Level: Error ❌ + +Details: Base target is greater than the threshold value. The threshold value is {{ .Threshold}} + +{{ with .BaseTargetValues }} +{{range .}} + Node: {{ .Node}} + Base Target: {{ .BaseTarget}} +{{end}} +{{end}} diff --git a/cmd/bots/internal/common/templates/alerts/base_target_alert.md b/cmd/bots/internal/common/templates/alerts/base_target_alert.md new file mode 100644 index 00000000..d5ee1475 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/base_target_alert.md @@ -0,0 +1,14 @@ +```yaml +Alert type: Base Target + +Level: Error ❌ + +Details: Base target is greater than the threshold value. The threshold value is {{ .Threshold }} + +{{ with .BaseTargetValues }} +{{ range . }} +Node: {{ .Node}} +Base Target: {{ .BaseTarget}} +{{end}} +{{end}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/height_alert.html b/cmd/bots/internal/common/templates/alerts/height_alert.html new file mode 100644 index 00000000..d2a76f6f --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/height_alert.html @@ -0,0 +1,19 @@ +Alert type: Height ❌ + +Level: Error ❌ + +Details: Some node(s) are {{ .HeightDifference}} blocks behind + +{{ with .FirstGroup }} +First group with height {{ .Height}}: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + +{{ with .SecondGroup }} +Second group with height {{ .Height}}: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} diff --git a/cmd/bots/internal/common/templates/alerts/height_alert.md b/cmd/bots/internal/common/templates/alerts/height_alert.md new file mode 100644 index 00000000..7ffa879f --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/height_alert.md @@ -0,0 +1,21 @@ +```yaml +Alert type: Height ❌ + +Level: Error ❌ + +Details: Some node(s) are {{ .HeightDifference}} blocks behind + +{{ with .FirstGroup }} +First group with height {{ .Height}}: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + +{{ with .SecondGroup }} +Second group with height {{ .Height}}: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/incomplete_alert.html b/cmd/bots/internal/common/templates/alerts/incomplete_alert.html new file mode 100644 index 00000000..266f1b2c --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/incomplete_alert.html @@ -0,0 +1,5 @@ +Alert type: Incomplete ❔ + +Level: Warning ❗ + +Details: Incomplete statement for node {{ .Node}} {{ .Version}} at height {{ .Height}} diff --git a/cmd/bots/internal/common/templates/alerts/incomplete_alert.md b/cmd/bots/internal/common/templates/alerts/incomplete_alert.md new file mode 100644 index 00000000..58687b59 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/incomplete_alert.md @@ -0,0 +1,7 @@ +```yaml +Alert type: Incomplete ❔ + +Level: Warning ❗ + +Details: Incomplete statement for node {{ .Node}} {{ .Version}} at height {{ .Height}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/internal_error_alert.html b/cmd/bots/internal/common/templates/alerts/internal_error_alert.html new file mode 100644 index 00000000..979491fa --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/internal_error_alert.html @@ -0,0 +1,5 @@ +Alert type: InternalErrorAlert ❗️ + +Level: Warning ❗ + +Details: An internal error has occurred: {{ .Error}} diff --git a/cmd/bots/internal/common/templates/alerts/internal_error_alert.md b/cmd/bots/internal/common/templates/alerts/internal_error_alert.md new file mode 100644 index 00000000..d7c59ce1 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/internal_error_alert.md @@ -0,0 +1,7 @@ +```yaml +Alert type: InternalErrorAlert ❗️ + +Level: Warning ❗ + +Details: An internal error has occurred, {{ .Error}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/invalid_height_alert.html b/cmd/bots/internal/common/templates/alerts/invalid_height_alert.html new file mode 100644 index 00000000..4a417fc9 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/invalid_height_alert.html @@ -0,0 +1,5 @@ +Alert type: Invalid Height ❌ + +Level: Warning ❗ + +Details: Node {{ .Node}} {{ .Version}} has an invalid height {{ .Height}} diff --git a/cmd/bots/internal/common/templates/alerts/invalid_height_alert.md b/cmd/bots/internal/common/templates/alerts/invalid_height_alert.md new file mode 100644 index 00000000..4f1274f7 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/invalid_height_alert.md @@ -0,0 +1,7 @@ +```yaml +Alert type: Invalid Height ❌ + +Level: Warning ❗ + +Details: Node {{ .Node}} {{ .Version}} has an invalid height {{ .Height}} +``` diff --git a/cmd/bots/internal/common/templates/alerts/state_hash_alert.html b/cmd/bots/internal/common/templates/alerts/state_hash_alert.html new file mode 100644 index 00000000..14081739 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/state_hash_alert.html @@ -0,0 +1,31 @@ +Alert type: State Hash ❌ + +Level: Error ❌ + +Details: Nodes have different state hashes at the same height {{ .SameHeight}} + +{{ with .FirstGroup }} +First group: +BlockID: {{ .BlockID}} +State Hash: {{ .StateHash}} +Nodes: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + +{{ with .SecondGroup }} +Second group: +BlockID: {{ .BlockID}} +State Hash: {{ .StateHash}} +Nodes: +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + +{{ if .LastCommonStateHashExist }} +Fork occurred after block {{ .ForkHeight}} +BlockID: {{ .ForkBlockID}} +State Hash: {{ .ForkStateHash}} +{{ end }} diff --git a/cmd/bots/internal/common/templates/alerts/state_hash_alert.md b/cmd/bots/internal/common/templates/alerts/state_hash_alert.md new file mode 100644 index 00000000..bd285bb6 --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/state_hash_alert.md @@ -0,0 +1,33 @@ +```yaml +Alert type: State Hash ❌ + +Level: Error ❌ + +Details: Nodes have different state hashes at the same height {{ .SameHeight}} + +{{ with .FirstGroup }} +BlockID (First group): {{ .BlockID}} +State Hash (First group): {{ .StateHash}} + +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + + +{{ with .SecondGroup }} +BlockID (Second group): {{ .BlockID}} +State Hash (Second group): {{ .StateHash}} + +{{range .Nodes}} +{{.}} +{{end}} +{{end}} + + +{{ if .LastCommonStateHashExist }} +Fork occurred after block {{ .ForkHeight}} +BlockID: {{ .ForkBlockID}} +State Hash: {{ .ForkStateHash}} +{{ end }} +``` diff --git a/cmd/bots/internal/common/templates/alerts/unreachable_alert.html b/cmd/bots/internal/common/templates/alerts/unreachable_alert.html new file mode 100644 index 00000000..e00d9cfd --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/unreachable_alert.html @@ -0,0 +1,5 @@ +Alert type: Unreachable ❌ + +Level: Error ❌ + +Details: Node {{ .Node}} is unreachable diff --git a/cmd/bots/internal/common/templates/alerts/unreachable_alert.md b/cmd/bots/internal/common/templates/alerts/unreachable_alert.md new file mode 100644 index 00000000..266aa03d --- /dev/null +++ b/cmd/bots/internal/common/templates/alerts/unreachable_alert.md @@ -0,0 +1,7 @@ +```yaml +Alert type: Unreachable ❌ + +Level: Error ❌ + +Details: Node {{ .Node}} is unreachable +``` diff --git a/cmd/bots/internal/common/templates_test.go b/cmd/bots/internal/common/templates_test.go new file mode 100644 index 00000000..3d2b992d --- /dev/null +++ b/cmd/bots/internal/common/templates_test.go @@ -0,0 +1,230 @@ +package common + +import ( + "encoding/binary" + "testing" + + "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/proto" + "nodemon/pkg/entities" +) + +var formats = []ExpectedExtension{Html, Markdown} + +func TestBaseTargetTemplate(t *testing.T) { + data := &entities.BaseTargetAlert{ + Timestamp: 100, + BaseTargetValues: []entities.BaseTargetValue{ + { + Node: "test1", + BaseTarget: 150, + }, + { + Node: "test2", + BaseTarget: 510, + }, + { + Node: "test1", + BaseTarget: 150, + }, + { + Node: "test2", + BaseTarget: 510, + }, + { + Node: "test1", + BaseTarget: 150, + }, + { + Node: "test2", + BaseTarget: 510, + }, + }, + Threshold: 101, + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/base_target_alert", data, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestUnreachableTemplate(t *testing.T) { + data := &entities.UnreachableAlert{ + Timestamp: 100, + Node: "node", + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/unreachable_alert", data, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestAlertFixed(t *testing.T) { + unreachable := &entities.UnreachableAlert{ + Timestamp: 100, + Node: "blabla", + } + + data := &entities.AlertFixed{ + Timestamp: 100, + Fixed: unreachable, + } + + fixedStatement := fixedStatement{ + PreviousAlert: data.Fixed.Message(), + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/alert_fixed", fixedStatement, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestHeightTemplate(t *testing.T) { + heightAlert := &entities.HeightAlert{ + Timestamp: 100, + MaxHeightGroup: entities.HeightGroup{ + Height: 2, + Nodes: entities.Nodes{"node 3", "node 4"}, + }, + OtherHeightGroup: entities.HeightGroup{ + Height: 1, + Nodes: entities.Nodes{"node 1", "node 2"}, + }, + } + + heightStatement := heightStatement{ + HeightDifference: heightAlert.MaxHeightGroup.Height - heightAlert.OtherHeightGroup.Height, + FirstGroup: heightStatementGroup{ + Nodes: heightAlert.MaxHeightGroup.Nodes, + Height: heightAlert.MaxHeightGroup.Height, + }, + SecondGroup: heightStatementGroup{ + Nodes: heightAlert.OtherHeightGroup.Nodes, + Height: heightAlert.OtherHeightGroup.Height, + }, + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/height_alert", heightStatement, f) + + if err != nil { + t.Fatal(err) + } + } +} + +type shInfo struct { + id proto.BlockID + sh proto.StateHash +} + +func sequentialBlockID(i int) proto.BlockID { + d := crypto.Digest{} + binary.BigEndian.PutUint64(d[:8], uint64(i)) + return proto.NewBlockIDFromDigest(d) +} + +func sequentialStateHash(blockID proto.BlockID, i int) proto.StateHash { + d := crypto.Digest{} + binary.BigEndian.PutUint64(d[:8], uint64(i)) + return proto.StateHash{ + BlockID: blockID, + SumHash: d, + FieldsHashes: proto.FieldsHashes{}, + } +} + +func generateStateHashes(o, n int) []shInfo { + r := make([]shInfo, n) + for i := 0; i < n; i++ { + id := sequentialBlockID(o + i + 1) + sh := sequentialStateHash(id, o+i+101) + r[i] = shInfo{id: id, sh: sh} + } + return r +} + +func TestStateHashTemplate(t *testing.T) { + + shInfo := generateStateHashes(1, 5) + stateHashAlert := &entities.StateHashAlert{ + CurrentGroupsBucketHeight: 100, + LastCommonStateHashExist: true, + LastCommonStateHashHeight: 1, + LastCommonStateHash: shInfo[0].sh, + FirstGroup: entities.StateHashGroup{ + Nodes: entities.Nodes{"a"}, + StateHash: shInfo[0].sh, + }, + SecondGroup: entities.StateHashGroup{ + Nodes: entities.Nodes{"b"}, + StateHash: shInfo[0].sh, + }, + } + + stateHashStatement := stateHashStatement{ + SameHeight: stateHashAlert.CurrentGroupsBucketHeight, + LastCommonStateHashExist: stateHashAlert.LastCommonStateHashExist, + ForkHeight: stateHashAlert.LastCommonStateHashHeight, + ForkBlockID: stateHashAlert.LastCommonStateHash.BlockID.String(), + ForkStateHash: stateHashAlert.LastCommonStateHash.SumHash.Hex(), + + FirstGroup: stateHashStatementGroup{ + BlockID: stateHashAlert.FirstGroup.StateHash.BlockID.String(), + Nodes: stateHashAlert.FirstGroup.Nodes, + StateHash: stateHashAlert.FirstGroup.StateHash.SumHash.Hex(), + }, + SecondGroup: stateHashStatementGroup{ + BlockID: stateHashAlert.SecondGroup.StateHash.BlockID.String(), + Nodes: stateHashAlert.SecondGroup.Nodes, + StateHash: stateHashAlert.SecondGroup.StateHash.SumHash.Hex(), + }, + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/state_hash_alert", stateHashStatement, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestIncompleteTemplate(t *testing.T) { + data := &entities.IncompleteAlert{ + NodeStatement: entities.NodeStatement{Node: "a", Version: "1", Height: 1}, + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/incomplete_alert", data, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestInternalErrorTemplate(t *testing.T) { + data := &entities.InternalErrorAlert{ + Error: "error", + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/internal_error_alert", data, f) + if err != nil { + t.Fatal(err) + } + } +} + +func TestInvalidHeightTemplate(t *testing.T) { + data := &entities.InvalidHeightAlert{ + NodeStatement: entities.NodeStatement{Node: "a", Version: "1", Height: 1}, + } + for _, f := range formats { + _, err := executeTemplate("templates/alerts/invalid_height_alert", data, f) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/cmd/bots/internal/discord/handlers/handlers.go b/cmd/bots/internal/discord/handlers/handlers.go index 9b4d4e59..9c9d0e79 100644 --- a/cmd/bots/internal/discord/handlers/handlers.go +++ b/cmd/bots/internal/discord/handlers/handlers.go @@ -13,7 +13,7 @@ import ( "nodemon/pkg/messaging/pair" ) -func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair) { +func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair, logger *zap.Logger) { environment.Bot.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.ID == s.State.User.ID { return @@ -21,35 +21,31 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan if m.Content == "/ping" { _, err := s.ChannelMessageSend(environment.ChatID, "Pong!") if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } } if m.Content == "/help" { _, err := s.ChannelMessageSend(environment.ChatID, messages.HelpInfoText) if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } } if m.Content == "/status" { - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + nodes, err := messaging.RequestAllNodes(requestType, responsePairType) if err != nil { - environment.Zap.Error("failed to get a list of nodes", zap.Error(err)) + logger.Error("failed to get nodes list", zap.Error(err)) } - additionalUrls, err := messaging.RequestNodesList(requestType, responsePairType, true) - if err != nil { - environment.Zap.Error("failed to get a list of nodes", zap.Error(err)) - } - urls = append(urls, additionalUrls...) + urls := messaging.NodesToUrls(nodes) nodesStatus, err := messaging.RequestNodesStatus(requestType, responsePairType, urls) if err != nil { - environment.Zap.Error("failed to request nodes status", zap.Error(err)) + logger.Error("failed to request nodes status", zap.Error(err)) } - msg, statusCondition, err := common.HandleNodesStatus(nodesStatus, common.Markdown) + msg, statusCondition, err := common.HandleNodesStatus(nodesStatus, common.Markdown, nodes) if err != nil { - environment.Zap.Error("failed to handle nodes status", zap.Error(err)) + logger.Error("failed to handle nodes status", zap.Error(err)) } if statusCondition.AllNodesAreOk { msg = fmt.Sprintf("%d %s", statusCondition.NodesNumber, msg) @@ -58,7 +54,7 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan msg = fmt.Sprintf("```yaml\n%s\n```", msg) _, err = s.ChannelMessageSend(environment.ChatID, msg) if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } } @@ -67,7 +63,7 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan if url == "" { _, err := s.ChannelMessageSend(environment.ChatID, "Please provide a URL to add") if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } return } @@ -75,7 +71,7 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan if err != nil { _, err := s.ChannelMessageSend(environment.ChatID, "Failed to add a node, "+err.Error()) if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } } } @@ -85,7 +81,7 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan if url == "" { _, err := s.ChannelMessageSend(environment.ChatID, "Please provide a URL to remove") if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } return } @@ -93,7 +89,7 @@ func InitDscHandlers(environment *common.DiscordBotEnvironment, requestType chan if err != nil { _, err := s.ChannelMessageSend(environment.ChatID, "Failed to remove a node, "+err.Error()) if err != nil { - environment.Zap.Error("failed to send a message to discord", zap.Error(err)) + logger.Error("failed to send a message to discord", zap.Error(err)) } } } diff --git a/cmd/bots/internal/telegram/handlers/handlers.go b/cmd/bots/internal/telegram/handlers/handlers.go index 088bc573..a036bdff 100644 --- a/cmd/bots/internal/telegram/handlers/handlers.go +++ b/cmd/bots/internal/telegram/handlers/handlers.go @@ -119,7 +119,7 @@ func InitTgHandlers(environment *common.TelegramBotEnvironment, requestType chan if err != nil { return nil } - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + urls, err := messaging.RequestNodesUrls(requestType, responsePairType, false) if err != nil { return errors.Wrap(err, "failed to request nodes list buttons") } @@ -154,6 +154,44 @@ func InitTgHandlers(environment *common.TelegramBotEnvironment, requestType chan ) } return RemoveNodeHandler(c, environment, requestType, responsePairType, args[0]) + + }) + + environment.Bot.Handle("/add_alias", func(c tele.Context) error { + args := c.Args() + if len(args) != 2 { + return c.Send( + messages.AliasWrongFormat, + &tele.SendOptions{ + ParseMode: tele.ModeDefault, + }, + ) + } + return UpdateAliasHandler(c, environment, requestType, args[0], args[1]) + }) + + environment.Bot.Handle("/aliases", func(c tele.Context) error { + nodes, err := messaging.RequestAllNodes(requestType, responsePairType) + if err != nil { + environment.Zap.Error("failed to request nodes list", zap.Error(err)) + return errors.Wrap(err, "failed to get nodes list") + } + var msg string + for _, n := range nodes { + if n.Alias != "" { + msg += fmt.Sprintf("Node: %s\nAlias: %s\n\n", n.URL, n.Alias) + } + } + if msg == "" { + msg = "No aliases have been found" + } + + return c.Send( + msg, + &tele.SendOptions{ + ParseMode: tele.ModeHTML, + }, + ) }) environment.Bot.Handle("/subscribe", func(c tele.Context) error { @@ -276,17 +314,11 @@ func InitTgHandlers(environment *common.TelegramBotEnvironment, requestType chan }) environment.Bot.Handle("/status", func(c tele.Context) error { - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + nodes, err := messaging.RequestAllNodes(requestType, responsePairType) if err != nil { - environment.Zap.Error("failed to request nodes list buttons", zap.Error(err)) - return err - } - additionalUrls, err := messaging.RequestNodesList(requestType, responsePairType, true) - if err != nil { - environment.Zap.Error("failed to request list of specific nodes", zap.Error(err)) - return err + environment.Zap.Error("failed to get nodes list", zap.Error(err)) } - urls = append(urls, additionalUrls...) + urls := messaging.NodesToUrls(nodes) nodesStatus, err := messaging.RequestNodesStatus(requestType, responsePairType, urls) if err != nil { @@ -294,7 +326,7 @@ func InitTgHandlers(environment *common.TelegramBotEnvironment, requestType chan return err } - msg, statusCondition, err := common.HandleNodesStatus(nodesStatus, common.Html) + msg, statusCondition, err := common.HandleNodesStatus(nodesStatus, common.Html, nodes) if err != nil { environment.Zap.Error("failed to handle status of nodes", zap.Error(err)) return err @@ -320,7 +352,7 @@ func EditPool( requestType chan<- pair.RequestPair, responsePairType <-chan pair.ResponsePair) error { - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + urls, err := messaging.RequestNodesUrls(requestType, responsePairType, false) if err != nil { return errors.Wrap(err, "failed to request nodes list buttons") } diff --git a/cmd/bots/internal/telegram/handlers/text.go b/cmd/bots/internal/telegram/handlers/text.go index d5fd3b68..5bf3a891 100644 --- a/cmd/bots/internal/telegram/handlers/text.go +++ b/cmd/bots/internal/telegram/handlers/text.go @@ -34,7 +34,7 @@ func AddNewNodeHandler( if err != nil { return nil } - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + urls, err := messaging.RequestNodesUrls(requestType, responsePairType, false) if err != nil { return errors.Wrap(err, "failed to request nodes list buttons") } @@ -73,7 +73,7 @@ func RemoveNodeHandler( return err } - urls, err := messaging.RequestNodesList(requestType, responsePairType, false) + urls, err := messaging.RequestNodesUrls(requestType, responsePairType, false) if err != nil { return errors.Wrap(err, "failed to request nodes list buttons") } @@ -89,6 +89,28 @@ func RemoveNodeHandler( ) } +func UpdateAliasHandler( + c tele.Context, + environment *common.TelegramBotEnvironment, + requestType chan<- pair.RequestPair, + url string, + alias string) error { + + response, err := messaging.UpdateAliasHandler(strconv.FormatInt(c.Chat().ID, 10), environment, requestType, url, alias) + if err != nil { + if errors.Is(err, messaging.IncorrectUrlError) || errors.Is(err, messaging.InsufficientPermissionsError) { + return c.Send( + response, + &tele.SendOptions{ParseMode: tele.ModeDefault}, + ) + } + return errors.Wrap(err, "failed to update a node") + } + return c.Send( + response, + &tele.SendOptions{ParseMode: tele.ModeHTML}) +} + func SubscribeHandler( c tele.Context, environment *common.TelegramBotEnvironment, diff --git a/cmd/bots/internal/telegram/messages/base_messages.go b/cmd/bots/internal/telegram/messages/base_messages.go index f5d10f94..60261e11 100644 --- a/cmd/bots/internal/telegram/messages/base_messages.go +++ b/cmd/bots/internal/telegram/messages/base_messages.go @@ -16,7 +16,9 @@ const ( "/add node - to add a node to the list\n" + "/remove node - to remove a node from the list\n" + "/subscribe alert name - to subscribe to a specific alert\n" + - "/unsubscribe alert name - to unsubscribe from a specific alert" + "/unsubscribe alert name - to unsubscribe from a specific alert" + + "/add_alias node alias" + + "/aliases - to see the matching list with aliases" MuteText = "Say no more..." + messages.SleepingMsg PongText = "Pong!" + messages.PongMsg diff --git a/cmd/bots/internal/telegram/messages/error_messages.go b/cmd/bots/internal/telegram/messages/error_messages.go index 42a9b3e2..650175d4 100644 --- a/cmd/bots/internal/telegram/messages/error_messages.go +++ b/cmd/bots/internal/telegram/messages/error_messages.go @@ -5,6 +5,7 @@ const ( AddedLessThanOne = "You should add a node" RemovedMoreThanOne = "You can remove only one node at a time" RemovedLessThanOne = "You should remove a node" + AliasWrongFormat = "Format: /add_alias " SubscribedToMoreThanOne = "You can subscribe to only one node at a time" SubscribedToLessThanOne = "You should subscribe to a node" UnsubscribedFromMoreThanOne = "You can unsubscribe from only one node at a time" diff --git a/cmd/bots/telegram/telegram.go b/cmd/bots/telegram/telegram.go index 71155f3d..e4e73fa5 100644 --- a/cmd/bots/telegram/telegram.go +++ b/cmd/bots/telegram/telegram.go @@ -76,13 +76,14 @@ func runTelegramBot() error { ctx, done := signal.NotifyContext(context.Background(), os.Interrupt) defer done() - tgBotEnv, err := initial.InitTgBot(behavior, webhookLocalAddress, publicURL, tgBotToken, tgChatID, zap) + pairRequest := make(chan pairResponses.RequestPair) + pairResponse := make(chan pairResponses.ResponsePair) + + tgBotEnv, err := initial.InitTgBot(behavior, webhookLocalAddress, publicURL, tgBotToken, tgChatID, zap, pairRequest, pairResponse) if err != nil { zap.Fatal("failed to initialize telegram bot", zapLogger.Error(err)) } - pairRequest := make(chan pairResponses.RequestPair) - pairResponse := make(chan pairResponses.ResponsePair) handlers.InitTgHandlers(tgBotEnv, pairRequest, pairResponse) go func() { diff --git a/pkg/entities/node.go b/pkg/entities/node.go index 96153a48..004f83a7 100644 --- a/pkg/entities/node.go +++ b/pkg/entities/node.go @@ -15,6 +15,7 @@ const ( type Node struct { URL string `json:"url"` Enabled bool `json:"enabled"` + Alias string `json:"alias"` } func CheckAndUpdateURL(s string) (string, error) { diff --git a/pkg/messaging/pair/request_types.go b/pkg/messaging/pair/request_types.go index 5f37d585..13d59e8e 100644 --- a/pkg/messaging/pair/request_types.go +++ b/pkg/messaging/pair/request_types.go @@ -7,6 +7,7 @@ const ( RequestSpecificNodeListT RequestInsertNewNodeT RequestInsertSpecificNewNodeT + RequestUpdateNode RequestDeleteNodeT RequestNodesStatus RequestNodeStatement diff --git a/pkg/messaging/pair/requests.go b/pkg/messaging/pair/requests.go index ca4b1898..ec9be8e7 100644 --- a/pkg/messaging/pair/requests.go +++ b/pkg/messaging/pair/requests.go @@ -15,6 +15,11 @@ type InsertNewNodeRequest struct { Specific bool } +type UpdateNodeRequest struct { + Url string + Alias string +} + type DeleteNodeRequest struct { Url string } @@ -28,6 +33,8 @@ func (nl *NodesListRequest) requestMarker() {} func (nl *InsertNewNodeRequest) requestMarker() {} +func (nl *UpdateNodeRequest) requestMarker() {} + func (nl *DeleteNodeRequest) requestMarker() {} func (nl *NodesStatusRequest) requestMarker() {} diff --git a/pkg/messaging/pair/responses.go b/pkg/messaging/pair/responses.go index 8566132d..4408d68a 100644 --- a/pkg/messaging/pair/responses.go +++ b/pkg/messaging/pair/responses.go @@ -8,7 +8,7 @@ import ( type ResponsePair interface{ responseMarker() } type NodesListResponse struct { - Urls []string `json:"urls"` + Nodes []entities.Node `json:"nodes"` } type NodesStatusResponse struct { diff --git a/pkg/messaging/pair/server.go b/pkg/messaging/pair/server.go index aea4698b..89d028e5 100644 --- a/pkg/messaging/pair/server.go +++ b/pkg/messaging/pair/server.go @@ -59,12 +59,7 @@ func StartPairMessagingServer(ctx context.Context, nanomsgURL string, ns *nodes. return err } } - var nodeList NodesListResponse - - nodeList.Urls = make([]string, len(nodes)) - for i, node := range nodes { - nodeList.Urls[i] = node.URL - } + nodeList := NodesListResponse{Nodes: nodes} response, err := json.Marshal(nodeList) if err != nil { logger.Error("Failed to marshal node list to json", zap.Error(err)) @@ -76,16 +71,27 @@ func StartPairMessagingServer(ctx context.Context, nanomsgURL string, ns *nodes. case RequestInsertNewNodeT: url := msg[1:] - err := ns.InsertIfNew(string(url)) + err := ns.InsertIfNew(string(url), false) if err != nil { logger.Error("Failed to insert a new node to storage", zap.Error(err)) } case RequestInsertSpecificNewNodeT: url := msg[1:] - err := ns.InsertSpecificIfNew(string(url)) + err := ns.InsertIfNew(string(url), true) + if err != nil { + logger.Error("Failed to insert a new specific node to storage", zap.Error(err)) + } + case RequestUpdateNode: + node := entities.Node{} + err := json.Unmarshal(msg[1:], &node) + if err != nil { + logger.Error("Failed to update a specific node", zap.Error(err)) + } + err = ns.Update(node) if err != nil { logger.Error("Failed to insert a new specific node to storage", zap.Error(err)) } + case RequestDeleteNodeT: url := msg[1:] err := ns.Delete(string(url)) diff --git a/pkg/messaging/pubsub/server.go b/pkg/messaging/pubsub/server.go index 05375637..c83ad42d 100644 --- a/pkg/messaging/pubsub/server.go +++ b/pkg/messaging/pubsub/server.go @@ -12,7 +12,6 @@ import ( _ "go.nanomsg.org/mangos/v3/transport/all" "go.uber.org/zap" "nodemon/pkg/entities" - "nodemon/pkg/messaging" ) func StartPubMessagingServer(ctx context.Context, nanomsgURL string, alerts <-chan entities.Alert, logger *zap.Logger) error { @@ -41,14 +40,9 @@ func StartPubMessagingServer(ctx context.Context, nanomsgURL string, alerts <-ch case alert := <-alerts: logger.Sugar().Infof("Alert has been generated: %v", alert) - jsonAlert, err := json.Marshal( - messaging.Alert{ - AlertDescription: alert.ShortDescription(), - Level: alert.Level(), - Details: alert.Message(), - }) + jsonAlert, err := json.Marshal(alert) if err != nil { - logger.Error("Failed to marshal alert to json", zap.Error(err)) + logger.Error("Failed to marshal an alert to json", zap.Error(err)) } message := &bytes.Buffer{} diff --git a/pkg/storing/nodes/nodes.go b/pkg/storing/nodes/nodes.go index 7ac06673..ce0560d4 100644 --- a/pkg/storing/nodes/nodes.go +++ b/pkg/storing/nodes/nodes.go @@ -16,21 +16,21 @@ const ( specificNodesTableName = "specific_nodes" ) -type node struct { +type nodeRecord struct { ID int `json:"id"` entities.Node } -func (n *node) GetID() int { +func (n *nodeRecord) GetID() int { return n.ID } -func (n *node) SetID(id int) { +func (n *nodeRecord) SetID(id int) { n.ID = id } // AfterFind required by Hare function. Don't ask why. -func (n *node) AfterFind(_ *hare.Database) error { +func (n *nodeRecord) AfterFind(_ *hare.Database) error { return nil } @@ -71,49 +71,86 @@ func (cs *Storage) Close() error { } func (cs *Storage) Nodes(specific bool) ([]entities.Node, error) { - return cs.queryNodes(func(_ node) bool { return true }, 0, specific) + nodesRecord, err := cs.queryNodes(func(_ nodeRecord) bool { return true }, 0, specific) + if err != nil { + return nil, err + } + return nodesFromRecords(nodesRecord), nil } func (cs *Storage) EnabledNodes() ([]entities.Node, error) { - return cs.queryNodes(func(n node) bool { return n.Enabled }, 0, false) + nodesRecords, err := cs.queryNodes(func(n nodeRecord) bool { return n.Enabled }, 0, false) + if err != nil { + return nil, err + } + return nodesFromRecords(nodesRecords), nil } func (cs *Storage) EnabledSpecificNodes() ([]entities.Node, error) { - return cs.queryNodes(func(n node) bool { return n.Enabled }, 0, true) + nodesRecords, err := cs.queryNodes(func(n nodeRecord) bool { return n.Enabled }, 0, true) + if err != nil { + return nil, err + } + return nodesFromRecords(nodesRecords), nil } -func (cs *Storage) InsertIfNew(url string) error { - ids, err := cs.queryNodes(func(n node) bool { return n.URL == url }, 0, false) +// Update handles both specific and non-specific nodes +func (cs *Storage) Update(nodeToUpdate entities.Node) error { + specific := false + ids, err := cs.queryNodes(func(n nodeRecord) bool { return n.URL == nodeToUpdate.URL }, 0, false) if err != nil { return err } if len(ids) == 0 { - id, err := cs.db.Insert(nodesTableName, &node{Node: entities.Node{ - URL: url, - Enabled: true, - }}) + // look for the url in the specific nodes table + ids, err = cs.queryNodes(func(n nodeRecord) bool { return n.URL == nodeToUpdate.URL }, 0, true) if err != nil { return err } - cs.zap.Sugar().Infof("New node #%d at '%s' was stored", id, url) + specific = true + if len(ids) == 0 { + return errors.Errorf("nodeRecord %s was not found in the storage", nodeToUpdate.URL) + } + } + if len(ids) != 1 { + return errors.Errorf("failed to update a nodeRecord in the storage, multiple nodes were found") + } + tableName := nodesTableName + if specific { + tableName = specificNodesTableName + } + + pulledRecord := ids[0] + err = cs.db.Update(tableName, &nodeRecord{Node: entities.Node{ + URL: pulledRecord.URL, + Enabled: pulledRecord.Enabled, + Alias: nodeToUpdate.Alias, + }, ID: pulledRecord.ID}) + if err != nil { + return err } + cs.zap.Sugar().Infof("New nodeRecord '%s' was updated with alias %s", pulledRecord.URL, nodeToUpdate.Alias) return nil } -func (cs *Storage) InsertSpecificIfNew(url string) error { - ids, err := cs.queryNodes(func(n node) bool { return n.URL == url }, 0, true) +func (cs *Storage) InsertIfNew(url string, specific bool) error { + ids, err := cs.queryNodes(func(n nodeRecord) bool { return n.URL == url }, 0, false) if err != nil { return err } + tableName := nodesTableName + if specific { + tableName = specificNodesTableName + } if len(ids) == 0 { - id, err := cs.db.Insert(specificNodesTableName, &node{Node: entities.Node{ + id, err := cs.db.Insert(tableName, &nodeRecord{Node: entities.Node{ URL: url, Enabled: true, }}) if err != nil { return err } - cs.zap.Sugar().Infof("New node #%d at '%s' was stored", id, url) + cs.zap.Sugar().Infof("New nodeRecord #%d at '%s' was stored", id, url) } return nil } @@ -124,7 +161,7 @@ func (cs *Storage) Delete(url string) error { return err } for _, id := range ids { - var n node + var n nodeRecord if err := cs.db.Find(nodesTableName, id, &n); err != nil { return err } @@ -141,23 +178,46 @@ func (cs *Storage) Delete(url string) error { return nil } -func (cs *Storage) queryNodes(queryFn func(n node) bool, limit int, specific bool) ([]entities.Node, error) { +func (cs *Storage) FindAlias(url string) (string, error) { + ids, err := cs.queryNodes(func(n nodeRecord) bool { return n.URL == url }, 0, false) + if err != nil { + return "", err + } + if len(ids) == 0 { + // look for the url in the specific nodes table + ids, err = cs.queryNodes(func(n nodeRecord) bool { return n.URL == url }, 0, true) + if err != nil { + return "", err + } + if len(ids) == 0 { + return "", errors.Errorf("nodeRecord %s was not found in the storage", url) + } + } + if len(ids) != 1 { + return "", errors.Errorf("failed to update a nodeRecord in the storage, multiple nodes were found") + } + + return ids[0].Alias, nil +} + +func (cs *Storage) queryNodes(queryFn func(n nodeRecord) bool, limit int, specific bool) ([]nodeRecord, error) { table := nodesTableName if specific { table = specificNodesTableName } - var results []entities.Node + var results []nodeRecord ids, err := cs.db.IDs(table) if err != nil { return nil, err } for _, id := range ids { - var n node + var n nodeRecord if err := cs.db.Find(table, id, &n); err != nil { return nil, err } if queryFn(n) { - results = append(results, n.Node) + n.ID = id + results = append(results, n) } if limit != 0 && limit == len(results) { break @@ -171,10 +231,18 @@ func (cs *Storage) populate(nodes string) error { if err != nil { return err } - err = cs.InsertIfNew(url) + err = cs.InsertIfNew(url, false) if err != nil { return err } } return nil } + +func nodesFromRecords(records []nodeRecord) []entities.Node { + var nodes []entities.Node + for _, r := range records { + nodes = append(nodes, r.Node) + } + return nodes +}