148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
package discord
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/certusone/wormhole/node/pkg/vaa"
|
|
"github.com/diamondburned/arikawa/v3/api"
|
|
"github.com/diamondburned/arikawa/v3/discord"
|
|
"go.uber.org/zap"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type DiscordNotifier struct {
|
|
c *api.Client
|
|
chans []discord.Channel
|
|
logger *zap.Logger
|
|
|
|
groupToIDMu sync.RWMutex
|
|
groupToID map[string]string
|
|
}
|
|
|
|
// NewDiscordNotifier returns and initializes a new Discord notifier.
|
|
//
|
|
// During initialization, a list of all guilds and channels is fetched.
|
|
// Newly added guilds and channels won't be detected at runtime.
|
|
func NewDiscordNotifier(botToken string, channelName string, logger *zap.Logger) (*DiscordNotifier, error) {
|
|
c := api.NewClient("Bot " + botToken)
|
|
chans := make([]discord.Channel, 0)
|
|
|
|
guilds, err := c.Guilds(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve guilds: %w", err)
|
|
}
|
|
|
|
for _, guild := range guilds {
|
|
gcn, err := c.Channels(guild.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve channels for %s: %w", guild.ID, err)
|
|
}
|
|
|
|
for _, cn := range gcn {
|
|
if cn.Name == channelName {
|
|
chans = append(chans, cn)
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.Info("notification channels", zap.Any("channels", chans))
|
|
|
|
return &DiscordNotifier{
|
|
c: c,
|
|
chans: chans,
|
|
logger: logger,
|
|
groupToID: make(map[string]string),
|
|
}, nil
|
|
}
|
|
|
|
func wrapCode(in string) string {
|
|
return fmt.Sprintf("`%s`", in)
|
|
}
|
|
|
|
func (d *DiscordNotifier) LookupGroupID(groupName string) (string, error) {
|
|
d.groupToIDMu.RLock()
|
|
if id, ok := d.groupToID[groupName]; ok {
|
|
d.groupToIDMu.RUnlock()
|
|
return id, nil
|
|
}
|
|
d.groupToIDMu.RUnlock()
|
|
|
|
guilds, err := d.c.Guilds(0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to retrieve guilds: %w", err)
|
|
}
|
|
|
|
for _, guild := range guilds {
|
|
gcn, err := d.c.Roles(guild.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to retrieve roles for %s: %w", guild.ID, err)
|
|
}
|
|
|
|
for _, cn := range gcn {
|
|
if cn.Name == groupName {
|
|
m := cn.ID.String()
|
|
|
|
d.groupToIDMu.Lock()
|
|
d.groupToID[groupName] = m
|
|
d.groupToIDMu.Unlock()
|
|
|
|
return m, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to find group %s", groupName)
|
|
}
|
|
|
|
func (d *DiscordNotifier) MissingSignaturesOnTransaction(v *vaa.VAA, hasSigs, wantSigs int, quorum bool, missing []string) error {
|
|
if len(missing) == 0 {
|
|
panic("no missing nodes specified")
|
|
}
|
|
var quorumText string
|
|
if quorum {
|
|
quorumText = fmt.Sprintf("✔️ yes (%d/%d)", hasSigs, wantSigs)
|
|
} else {
|
|
quorumText = fmt.Sprintf("🚨️ **NO** (%d/%d)", hasSigs, wantSigs)
|
|
}
|
|
|
|
var messageText string
|
|
if !quorum {
|
|
messageText = "**NO QUORUM** - Wormhole likely failed to achieve consensus on this message @here"
|
|
}
|
|
|
|
missingText := &bytes.Buffer{}
|
|
for _, m := range missing {
|
|
groupID, err := d.LookupGroupID(m)
|
|
if err != nil {
|
|
d.logger.Error("failed to lookup group id", zap.Error(err), zap.String("name", m))
|
|
groupID = m
|
|
} else {
|
|
groupID = fmt.Sprintf("<@&%s>", groupID)
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(missingText, "- %s\n", groupID); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
for _, cn := range d.chans {
|
|
if _, err := d.c.SendMessage(cn.ID, messageText,
|
|
discord.Embed{
|
|
Title: "Message with missing signatures",
|
|
Fields: []discord.EmbedField{
|
|
{Name: "Message ID", Value: wrapCode(v.MessageID()), Inline: true},
|
|
{Name: "Digest", Value: wrapCode(v.HexDigest()), Inline: true},
|
|
{Name: "Quorum", Value: quorumText, Inline: true},
|
|
{Name: "Source Chain", Value: strings.Title(v.EmitterChain.String()), Inline: false},
|
|
{Name: "Missing Guardians", Value: missingText.String(), Inline: false},
|
|
},
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|