Merge pull request #233 from c9s/refactor/notifier

This commit is contained in:
Yo-An Lin 2021-05-12 14:58:51 +08:00 committed by GitHub
commit ab51007b62
9 changed files with 109 additions and 60 deletions

View File

@ -359,8 +359,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
case "$session": case "$session":
defaultTradeUpdateHandler := func(trade types.Trade) { defaultTradeUpdateHandler := func(trade types.Trade) {
text := util.Render(TemplateTradeReport, trade) environ.Notify(&trade)
environ.Notify(text, &trade)
} }
for name := range environ.sessions { for name := range environ.sessions {
session := environ.sessions[name] session := environ.sessions[name]
@ -369,8 +368,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
channel, ok := environ.SessionChannelRouter.Route(name) channel, ok := environ.SessionChannelRouter.Route(name)
if ok { if ok {
session.Stream.OnTradeUpdate(func(trade types.Trade) { session.Stream.OnTradeUpdate(func(trade types.Trade) {
text := util.Render(TemplateTradeReport, trade) environ.NotifyTo(channel, &trade)
environ.NotifyTo(channel, text, &trade)
}) })
} else { } else {
session.Stream.OnTradeUpdate(defaultTradeUpdateHandler) session.Stream.OnTradeUpdate(defaultTradeUpdateHandler)
@ -390,12 +388,11 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
// use same handler for each session // use same handler for each session
handler := func(trade types.Trade) { handler := func(trade types.Trade) {
text := util.Render(TemplateTradeReport, trade)
channel, ok := environ.RouteObject(&trade) channel, ok := environ.RouteObject(&trade)
if ok { if ok {
environ.NotifyTo(channel, text, &trade) environ.NotifyTo(channel, &trade)
} else { } else {
environ.Notify(text, &trade) environ.Notify(&trade)
} }
} }
for _, session := range environ.sessions { for _, session := range environ.sessions {

View File

@ -1,15 +1,15 @@
package bbgo package bbgo
type Notifier interface { type Notifier interface {
NotifyTo(channel, format string, args ...interface{}) NotifyTo(channel string, obj interface{}, args ...interface{})
Notify(format string, args ...interface{}) Notify(obj interface{}, args ...interface{})
} }
type NullNotifier struct{} type NullNotifier struct{}
func (n *NullNotifier) NotifyTo(channel, format string, args ...interface{}) {} func (n *NullNotifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {}
func (n *NullNotifier) Notify(format string, args ...interface{}) {} func (n *NullNotifier) Notify(obj interface{}, args ...interface{}) {}
type Notifiability struct { type Notifiability struct {
notifiers []Notifier notifiers []Notifier
@ -18,7 +18,7 @@ type Notifiability struct {
ObjectChannelRouter *ObjectChannelRouter `json:"-"` ObjectChannelRouter *ObjectChannelRouter `json:"-"`
} }
// RouteSession routes symbol name to channel // RouteSymbol routes symbol name to channel
func (m *Notifiability) RouteSymbol(symbol string) (channel string, ok bool) { func (m *Notifiability) RouteSymbol(symbol string) (channel string, ok bool) {
if m.SymbolChannelRouter != nil { if m.SymbolChannelRouter != nil {
return m.SymbolChannelRouter.Route(symbol) return m.SymbolChannelRouter.Route(symbol)
@ -47,14 +47,14 @@ func (m *Notifiability) AddNotifier(notifier Notifier) {
m.notifiers = append(m.notifiers, notifier) m.notifiers = append(m.notifiers, notifier)
} }
func (m *Notifiability) Notify(format string, args ...interface{}) { func (m *Notifiability) Notify(obj interface{}, args ...interface{}) {
for _, n := range m.notifiers { for _, n := range m.notifiers {
n.Notify(format, args...) n.Notify(obj, args...)
} }
} }
func (m *Notifiability) NotifyTo(channel, format string, args ...interface{}) { func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interface{}) {
for _, n := range m.notifiers { for _, n := range m.notifiers {
n.NotifyTo(channel, format, args...) n.NotifyTo(channel, obj, args...)
} }
} }

View File

@ -147,6 +147,4 @@ type TradeReporter struct {
*Notifiability *Notifiability
} }
const TemplateTradeReport = `:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`
const TemplateOrderReport = `:handshake: {{ .Symbol }} {{ .Side }} Order Update @ {{ .Price }}` const TemplateOrderReport = `:handshake: {{ .Symbol }} {{ .Side }} Order Update @ {{ .Price }}`

View File

@ -35,32 +35,26 @@ func New(token, channel string, options ...NotifyOption) *Notifier {
return notifier return notifier
} }
func (n *Notifier) Notify(format string, args ...interface{}) { func (n *Notifier) Notify(obj interface{}, args ...interface{}) {
n.NotifyTo(n.channel, format, args...) n.NotifyTo(n.channel, obj, args...)
} }
func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) { func filterSlackAttachments(args []interface{}) (slackAttachments []slack.Attachment, pureArgs []interface{}) {
if len(channel) == 0 { var firstAttachmentOffset = -1
channel = n.channel
}
var slackAttachments []slack.Attachment
var slackArgsOffset = -1
for idx, arg := range args { for idx, arg := range args {
switch a := arg.(type) { switch a := arg.(type) {
// concrete type assert first // concrete type assert first
case slack.Attachment: case slack.Attachment:
if slackArgsOffset == -1 { if firstAttachmentOffset == -1 {
slackArgsOffset = idx firstAttachmentOffset = idx
} }
slackAttachments = append(slackAttachments, a) slackAttachments = append(slackAttachments, a)
case SlackAttachmentCreator: case SlackAttachmentCreator:
if slackArgsOffset == -1 { if firstAttachmentOffset == -1 {
slackArgsOffset = idx firstAttachmentOffset = idx
} }
slackAttachments = append(slackAttachments, a.SlackAttachment()) slackAttachments = append(slackAttachments, a.SlackAttachment())
@ -68,19 +62,45 @@ func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
} }
} }
var nonSlackArgs = args pureArgs = args
if slackArgsOffset > -1 { if firstAttachmentOffset > -1 {
nonSlackArgs = args[:slackArgsOffset] pureArgs = args[:firstAttachmentOffset]
}
return
}
func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {
if len(channel) == 0 {
channel = n.channel
}
slackAttachments, pureArgs := filterSlackAttachments(args)
var opts []slack.MsgOption
switch a := obj.(type) {
case string:
opts = append(opts, slack.MsgOptionText(fmt.Sprintf(a, pureArgs...), true),
slack.MsgOptionAttachments(slackAttachments...))
case slack.Attachment:
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...))
case SlackAttachmentCreator:
// convert object to slack attachment (if supported)
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...))
default:
log.Errorf("slack notifier error, unsupported object: %T %+v", a, a)
} }
go func() { go func() {
_, _, err := n.client.PostMessageContext(context.Background(), channel, _, _, err := n.client.PostMessageContext(context.Background(), channel, opts...)
slack.MsgOptionText(fmt.Sprintf(format, nonSlackArgs...), true),
slack.MsgOptionAttachments(slackAttachments...))
if err != nil { if err != nil {
log.WithError(err). log.WithError(err).
WithField("channel", channel). WithField("channel", channel).
Errorf("slack error: %s", err.Error()) Errorf("slack api error: %s", err.Error())
} }
}() }()

View File

@ -12,7 +12,9 @@ type Notifier struct {
type NotifyOption func(notifier *Notifier) type NotifyOption func(notifier *Notifier)
// start bot daemon
// New
// TODO: register interaction with channel, so that we can route message to the specific telegram bot
func New(interaction *Interaction, options ...NotifyOption) *Notifier { func New(interaction *Interaction, options ...NotifyOption) *Notifier {
notifier := &Notifier{ notifier := &Notifier{
interaction: interaction, interaction: interaction,
@ -25,14 +27,12 @@ func New(interaction *Interaction, options ...NotifyOption) *Notifier {
return notifier return notifier
} }
func (n *Notifier) Notify(format string, args ...interface{}) { func (n *Notifier) Notify(obj interface{}, args ...interface{}) {
n.NotifyTo("", format, args...) n.NotifyTo("", obj, args...)
} }
func (n *Notifier) NotifyTo(_, format string, args ...interface{}) { func filterPlaintextMessages(args []interface{}) (texts []string, pureArgs []interface{}) {
var textArgsOffset = -1 var textArgsOffset = -1
var texts []string
for idx, arg := range args { for idx, arg := range args {
switch a := arg.(type) { switch a := arg.(type) {
@ -40,21 +40,44 @@ func (n *Notifier) NotifyTo(_, format string, args ...interface{}) {
texts = append(texts, a.PlainText()) texts = append(texts, a.PlainText())
textArgsOffset = idx textArgsOffset = idx
case types.Stringer:
texts = append(texts, a.String())
textArgsOffset = idx
} }
} }
var simpleArgs = args pureArgs = args
if textArgsOffset > -1 { if textArgsOffset > -1 {
simpleArgs = args[:textArgsOffset] pureArgs = args[:textArgsOffset]
} }
log.Infof(format, simpleArgs...) return texts, pureArgs
}
func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {
var texts, pureArgs = filterPlaintextMessages(args)
var message string
switch a := obj.(type) {
case string:
log.Infof(a, pureArgs...)
message = fmt.Sprintf(a, pureArgs...)
case types.Stringer:
message = a.String()
case types.PlainText:
message = a.PlainText()
default:
log.Errorf("unsupported notification format: %T %+v", a, a)
}
message := fmt.Sprintf(format, simpleArgs...)
n.interaction.SendToOwner(message) n.interaction.SendToOwner(message)
for _, text := range texts { for _, text := range texts {
n.interaction.SendToOwner(text) n.interaction.SendToOwner(text)
} }
} }

View File

@ -110,7 +110,7 @@ func (s *TradeService) Sync(ctx context.Context, exchange types.Exchange, symbol
trade.Side, trade.Side,
trade.Price, trade.Price,
trade.Quantity, trade.Quantity,
trade.MakerOrTakerLabel(), trade.Liquidity(),
trade.Time.String()) trade.Time.String())
if err := s.Insert(trade); err != nil { if err := s.Insert(trade); err != nil {

View File

@ -3,3 +3,7 @@ package types
type PlainText interface { type PlainText interface {
PlainText() string PlainText() string
} }
type Stringer interface {
String() string
}

View File

@ -81,6 +81,8 @@ func (trade Trade) PlainText() string {
util.FormatFloat(trade.QuoteQuantity, 2)) util.FormatFloat(trade.QuoteQuantity, 2))
} }
var slackTradeTextTemplate = ":handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}"
func (trade Trade) SlackAttachment() slack.Attachment { func (trade Trade) SlackAttachment() slack.Attachment {
var color = "#DC143C" var color = "#DC143C"
@ -88,25 +90,27 @@ func (trade Trade) SlackAttachment() slack.Attachment {
color = "#228B22" color = "#228B22"
} }
liquidity := trade.Liquidity()
text := util.Render(slackTradeTextTemplate, trade)
return slack.Attachment{ return slack.Attachment{
Text: fmt.Sprintf("*%s* Trade %s", trade.Symbol, trade.Side), Text: text,
Color: color,
// Pretext: "", // Pretext: "",
// Text: "", Color: color,
Fields: []slack.AttachmentField{ Fields: []slack.AttachmentField{
{Title: "Exchange", Value: trade.Exchange, Short: true}, {Title: "Exchange", Value: trade.Exchange, Short: true},
{Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true}, {Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true},
{Title: "Quote", Value: util.FormatFloat(trade.Quantity, 4), Short: true}, {Title: "Quantity", Value: util.FormatFloat(trade.Quantity, 4), Short: true},
{Title: "QuoteQuantity", Value: util.FormatFloat(trade.QuoteQuantity, 2)}, {Title: "QuoteQuantity", Value: util.FormatFloat(trade.QuoteQuantity, 2)},
{Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true}, {Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true},
{Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true}, {Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true},
{Title: "Liquidity", Value: liquidity, Short: true},
}, },
// Footer: tradingCtx.TradeStartTime.Format(time.RFC822), // Footer: tradingCtx.TradeStartTime.Format(time.RFC822),
// FooterIcon: "", // FooterIcon: "",
} }
} }
func (trade Trade) MakerOrTakerLabel() (o string) { func (trade Trade) Liquidity() (o string) {
if trade.IsMaker { if trade.IsMaker {
o += "MAKER" o += "MAKER"
} else { } else {

View File

@ -2,21 +2,24 @@ package util
import ( import (
"bytes" "bytes"
"github.com/sirupsen/logrus"
"text/template" "text/template"
"github.com/sirupsen/logrus"
) )
func Render(tpl string, args interface{}) string { func Render(tpl string, args interface{}) string {
var buf = bytes.NewBuffer(nil) var buf = bytes.NewBuffer(nil)
tmpl, err := template.New("tmp").Parse(tpl) tmpl, err := template.New("tmp").Parse(tpl)
if err != nil { if err != nil {
logrus.WithError(err).Error("template error") logrus.WithError(err).Error("template parse error")
return "" return ""
} }
err = tmpl.Execute(buf, args) err = tmpl.Execute(buf, args)
if err != nil { if err != nil {
logrus.WithError(err).Error("template error") logrus.WithError(err).Error("template execute error")
return "" return ""
} }
return buf.String() return buf.String()
} }