diff --git a/pkg/livenote/livenote.go b/pkg/livenote/livenote.go index af01cef95..b9f6a1879 100644 --- a/pkg/livenote/livenote.go +++ b/pkg/livenote/livenote.go @@ -1,6 +1,9 @@ package livenote -import "sync" +import ( + "sync" + "time" +) type Object interface { ObjectID() string @@ -13,9 +16,15 @@ type LiveNote struct { ChannelID string `json:"channelId"` + Pin bool `json:"pin"` + + TimeToLive time.Duration `json:"timeToLive"` + Object Object cachedObjID string + + postedTime time.Time } func NewLiveNote(object Object) *LiveNote { @@ -33,6 +42,14 @@ func (n *LiveNote) ObjectID() string { return n.cachedObjID } +func (n *LiveNote) SetTimeToLive(du time.Duration) { + n.TimeToLive = du +} + +func (n *LiveNote) SetPostedTime(tt time.Time) { + n.postedTime = tt +} + func (n *LiveNote) SetObject(object Object) { n.Object = object } @@ -45,6 +62,19 @@ func (n *LiveNote) SetChannelID(channelID string) { n.ChannelID = channelID } +func (n *LiveNote) SetPin(enabled bool) { + n.Pin = enabled +} + +func (n *LiveNote) IsExpired(now time.Time) bool { + if n.postedTime.IsZero() || n.TimeToLive == 0 { + return false + } + + expiryTime := n.postedTime.Add(n.TimeToLive) + return now.After(expiryTime) +} + type Pool struct { notes map[string]*LiveNote mu sync.Mutex @@ -64,6 +94,10 @@ func (p *Pool) Get(obj Object) *LiveNote { for _, note := range p.notes { if note.ObjectID() == objID { + if note.IsExpired(time.Now()) { + return nil + } + return note } } @@ -79,6 +113,10 @@ func (p *Pool) Update(obj Object) *LiveNote { for _, note := range p.notes { if note.ObjectID() == objID { + if note.IsExpired(time.Now()) { + break + } + // update the object inside the note note.SetObject(obj) return note diff --git a/pkg/livenote/options.go b/pkg/livenote/options.go index cf120f46e..f3ce1736d 100644 --- a/pkg/livenote/options.go +++ b/pkg/livenote/options.go @@ -1,5 +1,7 @@ package livenote +import "time" + type Option interface{} type OptionCompare struct { @@ -29,3 +31,21 @@ func Comment(text string, users ...string) *OptionComment { Users: users, } } + +type OptionTimeToLive struct { + Duration time.Duration +} + +func TimeToLive(du time.Duration) *OptionTimeToLive { + return &OptionTimeToLive{Duration: du} +} + +type OptionPin struct { + Value bool +} + +func Pin(value bool) *OptionPin { + return &OptionPin{ + Value: value, + } +} diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index ef938b05f..f8f17492c 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -52,6 +52,8 @@ func (t *notifyTask) addMsgOption(opts ...slack.MsgOption) { // To use this notifier, you need to setup the slack app permissions: // - channels:read // - chat:write +// +// When using "pins", you will need permission: "pins:write" type Notifier struct { ctx context.Context cancel context.CancelFunc @@ -236,6 +238,9 @@ func (n *Notifier) PostLiveNote(obj livenote.Object, opts ...livenote.Option) er var commentHandles []string var comments []string var shouldCompare bool + var shouldPin bool + var ttl time.Duration = 0 + for _, opt := range opts { switch val := opt.(type) { case *livenote.OptionOneTimeMention: @@ -245,6 +250,10 @@ func (n *Notifier) PostLiveNote(obj livenote.Object, opts ...livenote.Option) er commentHandles = append(commentHandles, val.Users...) case *livenote.OptionCompare: shouldCompare = val.Value + case *livenote.OptionPin: + shouldPin = val.Value + case *livenote.OptionTimeToLive: + ttl = val.Duration } } @@ -260,6 +269,10 @@ func (n *Notifier) PostLiveNote(obj livenote.Object, opts ...livenote.Option) er note := n.liveNotePool.Update(obj) curObj = note.Object + if ttl > 0 { + note.SetTimeToLive(ttl) + } + if shouldCompare && prevObj != nil { diffs, err := dynamic.Compare(curObj, prevObj) if err != nil { @@ -331,6 +344,18 @@ func (n *Notifier) PostLiveNote(obj livenote.Object, opts ...livenote.Option) er note.SetChannelID(respCh) note.SetMessageID(respTs) + note.SetPostedTime(time.Now()) + + if shouldPin { + note.SetPin(true) + + if err := n.client.AddPinContext(ctx, respCh, slack.ItemRef{ + Channel: respCh, + Timestamp: respTs, + }); err != nil { + log.WithError(err).Warnf("unable to pin the slack message: %s", respTs) + } + } if len(firstTimeTags) > 0 { n.queueTask(n.ctx, notifyTask{ diff --git a/pkg/strategy/example/livenote/strategy.go b/pkg/strategy/example/livenote/strategy.go index be0aa868a..4e0db4dc3 100644 --- a/pkg/strategy/example/livenote/strategy.go +++ b/pkg/strategy/example/livenote/strategy.go @@ -2,6 +2,7 @@ package livenote import ( "context" + "time" "github.com/sirupsen/logrus" @@ -66,7 +67,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.PostLiveNote(&k, livenote.OneTimeMention(s.UserID), livenote.Comment("please check the deposit"), - livenote.CompareObject(true)) + livenote.CompareObject(true), + livenote.TimeToLive(time.Minute), + // livenote.Pin(true), + ) } // register our kline event handler