Skip to content

Commit

Permalink
feat: send slack new position alerts (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
sol-mocha authored Jun 8, 2023
1 parent feec418 commit 7d7dc60
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 69 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ require (
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/slack-go/slack v0.12.2 // indirect
github.com/streamingfast/logging v0.0.0-20220813175024-b4fbb0e893df // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
Expand Down Expand Up @@ -1285,6 +1286,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
Expand Down
156 changes: 109 additions & 47 deletions pkg/service/alert/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"strconv"
"time"

"github.com/disgoorg/disgo/rest"

"github.com/AlekSi/pointer"
"github.com/dcaf-labs/drip/pkg/service/config"
"github.com/dcaf-labs/drip/pkg/service/utils"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/disgo/webhook"
"github.com/disgoorg/snowflake/v2"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)

type Service interface {
Expand All @@ -27,12 +29,10 @@ func NewAlertService(
appConfig config.AppConfig,
) (Service, error) {
logrus.WithField("discordWebhookID", appConfig.GetDiscordWebhookID()).Info("initiating alert service")
service := serviceImpl{}
service := serviceImpl{
network: appConfig.GetNetwork(),
}
if appConfig.GetDiscordWebhookID() != "" && appConfig.GetDiscordWebhookAccessToken() != "" {
service = serviceImpl{
network: appConfig.GetNetwork(),
enabled: true,
}
webhookID, err := strconv.ParseInt(appConfig.GetDiscordWebhookID(), 10, 64)
if err != nil {
return nil, err
Expand All @@ -43,7 +43,10 @@ func NewAlertService(
RepliedUser: false,
}),
)
service.client = client
service.discordClient = &client
}
if appConfig.GetSlackWebhookURL() != "" {
service.slackWebhookURL = pointer.ToString(appConfig.GetSlackWebhookURL())
}
if err := service.SendInfo(context.Background(), "Info", "Initialized alert service"); err != nil {
return nil, err
Expand All @@ -52,33 +55,37 @@ func NewAlertService(
}

type serviceImpl struct {
network config.Network
enabled bool
client webhook.Client
network config.Network
discordClient *webhook.Client
slackWebhookURL *string
}

func (a serviceImpl) SendError(ctx context.Context, err error) error {
if !a.enabled {
logrus.WithError(err).Info("alert service disabled, skipping error alert")
return nil
}
return a.send(ctx, discord.Embed{
func (a serviceImpl) SendError(ctx context.Context, err error) (retErr error) {
if sendErr := a.sendDiscord(ctx, discord.Embed{
Title: "Error",
Description: err.Error(),
Color: int(InfoColor),
})
}); sendErr != nil {
retErr = multierror.Append(retErr, sendErr)
}
if sendErr := a.sendSlack(ctx, slack.NewTextBlockObject(slack.PlainTextType, err.Error(), false, false)); sendErr != nil {
retErr = multierror.Append(retErr, sendErr)
}
return retErr
}

func (a serviceImpl) SendInfo(ctx context.Context, title string, message string) error {
if !a.enabled {
logrus.WithField("msg", message).Info("alert service disabled, skipping info alert")
return nil
}
return a.send(ctx, discord.Embed{
func (a serviceImpl) SendInfo(ctx context.Context, title string, message string) (retErr error) {
if sendErr := a.sendDiscord(ctx, discord.Embed{
Title: title,
Description: message,
Color: int(InfoColor),
})
}); sendErr != nil {
retErr = multierror.Append(retErr, sendErr)
}
if sendErr := a.sendSlack(ctx, slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, message, false, false))); sendErr != nil {
retErr = multierror.Append(retErr, sendErr)
}
return retErr
}

type NewPositionAlert struct {
Expand All @@ -100,13 +107,25 @@ type NewPositionAlert struct {
func (a serviceImpl) SendNewPositionAlert(
ctx context.Context,
alertParams NewPositionAlert,
) error {
) (err error) {
log := logrus.WithField("msg", "new position").WithField("position", alertParams.Position)
if !a.enabled {
log.Info("alert service disabled, skipping info alert")
return nil
log.Info("attempting to send discord notification")
if sendErr := a.sendNewDiscordPositionAlert(ctx, alertParams); sendErr != nil {
err = multierror.Append(err, sendErr)
}
log.Info("attempting to send slack notification")
if sendErr := a.sendNewSlackPositionAlert(ctx, alertParams); sendErr != nil {
err = multierror.Append(err, sendErr)
}
log.Info("attempting to send notification")
return err
}

func (a serviceImpl) sendNewDiscordPositionAlert(
ctx context.Context,
alertParams NewPositionAlert,
) error {
log := logrus.WithField("msg", "new position").WithField("position", alertParams.Position)
log.Info("attempting to sendDiscord notification")

granularityStr := getGranularityString(alertParams.Granularity)
tokenA := utils.GetWithDefault(alertParams.TokenASymbol, alertParams.TokenAMint)
Expand Down Expand Up @@ -145,38 +164,81 @@ func (a serviceImpl) SendNewPositionAlert(
Build()
embeds = append(embeds, tokenBEmbed)

return a.send(ctx, embeds...)
return a.sendDiscord(ctx, embeds...)
}

func (a serviceImpl) sendNewSlackPositionAlert(
ctx context.Context,
alertParams NewPositionAlert,
) error {
log := logrus.WithField("msg", "new position").WithField("position", alertParams.Position)
log.Info("attempting to send slack notification")
headerText := slack.NewTextBlockObject(slack.PlainTextType, "New Position! :money_mouth_face:", true, false)
headerBlock := slack.NewHeaderBlock(headerText)

granularityStr := getGranularityString(alertParams.Granularity)
tokenA := utils.GetWithDefault(alertParams.TokenASymbol, alertParams.TokenAMint)
tokenAImage := utils.GetWithDefault(alertParams.TokenAIconURL, "https://static.vecteezy.com/system/resources/previews/004/141/669/non_2x/no-photo-or-blank-image-icon-loading-images-or-missing-image-mark-image-not-available-or-image-coming-soon-sign-simple-nature-silhouette-in-frame-isolated-illustration-vector.jpg")
tokenB := utils.GetWithDefault(alertParams.TokenBSymbol, alertParams.TokenBMint)
tokenBImage := utils.GetWithDefault(alertParams.TokenBIconURL, "https://static.vecteezy.com/system/resources/previews/004/141/669/non_2x/no-photo-or-blank-image-icon-loading-images-or-missing-image-mark-image-not-available-or-image-coming-soon-sign-simple-nature-silhouette-in-frame-isolated-illustration-vector.jpg")
usdValue := utils.GetWithDefault(alertParams.USDValue, 0)

summaryBlock := slack.NewContextBlock("summary",
slack.NewTextBlockObject(slack.MarkdownType, fmt.Sprintf("%f %s", alertParams.ScaledDripAmount, tokenA), false, false),
slack.NewImageBlockElement(tokenAImage, "token a image"),
slack.NewTextBlockObject(slack.MarkdownType, "to", false, false),
slack.NewTextBlockObject(slack.MarkdownType, tokenB, false, false),
slack.NewImageBlockElement(tokenBImage, "token b image"),
slack.NewTextBlockObject(slack.MarkdownType, fmt.Sprintf("%s, for a total of %d swaps, valued at *%f USD*", granularityStr, alertParams.NumberOfSwaps, usdValue), false, false),
)
return a.sendSlack(ctx, headerBlock, summaryBlock)
}

func getGranularityString(granularity uint64) (granularityStr string) {
minuteInS := uint64(time.Minute.Seconds())
hourInS := uint64(time.Hour.Seconds())
dayInS := uint64((time.Hour * 24).Seconds())
if uint64(granularity/minuteInS) <= 1 {
granularityStr = "Every Minute"
granularityStr = "every Minute"
} else if granularity > minuteInS && granularity < hourInS {
granularityStr = fmt.Sprintf("Every %d Minutes", uint64(granularity/minuteInS))
granularityStr = fmt.Sprintf("every %d Minutes", uint64(granularity/minuteInS))
} else if uint64(granularity/hourInS) <= 1 {
granularityStr = "Every Hour"
granularityStr = "every Hour"
} else if granularity > hourInS && granularity < dayInS {
granularityStr = fmt.Sprintf("Every %d Hours", uint64(granularity/hourInS))
granularityStr = fmt.Sprintf("every %d Hours", uint64(granularity/hourInS))
} else if uint64(granularity/dayInS) <= 1 {
granularityStr = "Every Day"
granularityStr = "every Day"
} else {
granularityStr = fmt.Sprintf("Every %d Days", uint64(granularity/dayInS))
granularityStr = fmt.Sprintf("every %d Days", uint64(granularity/dayInS))
}
return granularityStr
}

func (a serviceImpl) send(ctx context.Context, embeds ...discord.Embed) error {
_, err := a.client.CreateMessage(
discord.NewWebhookMessageCreateBuilder().
SetAvatarURL("https://pbs.twimg.com/profile_images/1512938686702403603/DDObiFjj_400x400.jpg").
SetEmbeds(embeds...).
Build(),
// delay each request by 2 seconds
rest.WithDelay(2*time.Second),
rest.WithCtx(ctx),
)
func (a serviceImpl) sendDiscord(ctx context.Context, embeds ...discord.Embed) (err error) {
if a.discordClient != nil {
_, err = (*a.discordClient).CreateMessage(
discord.NewWebhookMessageCreateBuilder().
SetAvatarURL("https://pbs.twimg.com/profile_images/1512938686702403603/DDObiFjj_400x400.jpg").
SetEmbeds(embeds...).
Build(),
// delay each request by 2 seconds
rest.WithDelay(2*time.Second),
rest.WithCtx(ctx),
)
} else {
logrus.Info("alert service disabled, skipping info alert")
}
return err
}
func (a serviceImpl) sendSlack(ctx context.Context, blocks ...slack.Block) (err error) {
if a.slackWebhookURL != nil {
return slack.PostWebhookContext(ctx, *a.slackWebhookURL, &slack.WebhookMessage{
//Text: "hello",
Blocks: &slack.Blocks{BlockSet: blocks},
})
} else {
logrus.Info("alert service disabled, skipping info alert")
}
return err
}

Expand Down
49 changes: 27 additions & 22 deletions pkg/service/alert/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,34 @@ func Test_SendNewPositionAlert(t *testing.T) {

config.LoadEnv()

t.Run("should send position alert", func(t *testing.T) {
webhookID, err := strconv.ParseInt(os.Getenv("DISCORD_WEBHOOK_ID"), 10, 64)
t.Run("should sendDiscord position alert", func(t *testing.T) {
discordWebhookID, err := strconv.ParseInt(os.Getenv("DISCORD_WEBHOOK_ID"), 10, 64)
assert.NoError(t, err)
accessToken := os.Getenv("DISCORD_ACCESS_TOKEN")
client := webhook.New(snowflake.ID(webhookID), accessToken,
discordAccessToken := os.Getenv("DISCORD_ACCESS_TOKEN")
discordClient := webhook.New(snowflake.ID(discordWebhookID), discordAccessToken,
webhook.WithLogger(logrus.New()),
webhook.WithDefaultAllowedMentions(discord.AllowedMentions{
RepliedUser: false,
}),
)

slackWebhookURL := os.Getenv("SLACK_WEBHOOK_URL")

newAlertService := serviceImpl{
network: config.MainnetNetwork,
enabled: true,
client: client,
network: config.MainnetNetwork,
discordClient: &discordClient,
slackWebhookURL: &slackWebhookURL,
}
ctx := context.Background()
assert.NoError(t, newAlertService.SendInfo(ctx, "TEST", "MSG"))

assert.NoError(t,
newAlertService.SendNewPositionAlert(ctx, NewPositionAlert{
TokenASymbol: pointer.ToString("SAMO"),
TokenAIconURL: pointer.ToString("https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/logo.png"),
TokenAMint: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
TokenBSymbol: pointer.ToString("USDC"),
TokenBIconURL: pointer.ToString("https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png "),
TokenASymbol: pointer.ToString("SAMO"),
TokenAIconURL: pointer.ToString("https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/logo.png"),
TokenAMint: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
TokenBSymbol: pointer.ToString("USDC"),
TokenBIconURL: pointer.ToString("https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png"),
TokenBMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
ScaledTokenADepositAmount: 10000,
ScaledDripAmount: 10,
Expand All @@ -61,43 +66,43 @@ func Test_SendNewPositionAlert(t *testing.T) {
}{
{
input: 10,
output: "Every Minute",
output: "every Minute",
},
{
input: 60,
output: "Every Minute",
output: "every Minute",
},
{
input: 90,
output: "Every Minute",
output: "every Minute",
},
{
input: 110,
output: "Every Minute",
output: "every Minute",
},
{
input: 120,
output: "Every 2 Minutes",
output: "every 2 Minutes",
},
{
input: 3600,
output: "Every Hour",
output: "every Hour",
},
{
input: 4000,
output: "Every Hour",
output: "every Hour",
},
{
input: 86400,
output: "Every Day",
output: "every Day",
},
{
input: 120400,
output: "Every Day",
output: "every Day",
},
{
input: 172800,
output: "Every 2 Days",
output: "every 2 Days",
},
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/service/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type AppConfig interface {
GetServerPort() int
GetDiscordWebhookID() string
GetDiscordWebhookAccessToken() string
GetSlackWebhookURL() string
GetShouldByPassAdminAuth() bool
GetShouldBackfillDripAccounts() bool
}
Expand All @@ -59,10 +60,15 @@ type appConfig struct {
Port int `yaml:"port" env:"PORT"`
DiscordWebhookID string `yaml:"discordWebhookID" env:"DISCORD_WEBHOOK_ID"`
DiscordWebhookAccessToken string `yaml:"discordWebhookAccessToken" env:"DISCORD_ACCESS_TOKEN"`
SlackWebhookURL string `yaml:"slackWebhookURL" env:"SLACK_WEBHOOK_URL"`
ShouldByPassAdminAuth bool `yaml:"shouldBypassAdminAuth" env:"SHOULD_BYPASS_ADMIN_AUTH" env-default:"false"`
ShouldBackfillDripAccounts bool `yaml:"shouldBackfillDripAccounts" env:"SHOULD_BACKFILL_DRIP_ACCOUNTS" env-default:"true"`
}

func (a appConfig) GetSlackWebhookURL() string {
return a.SlackWebhookURL
}

func (a appConfig) GetShouldByPassAdminAuth() bool {
return a.ShouldByPassAdminAuth
}
Expand Down
Loading

0 comments on commit 7d7dc60

Please sign in to comment.