package saltyim import ( "bytes" "context" "fmt" "strings" "sync" "github.com/avast/retry-go" "github.com/keys-pub/keys" log "github.com/sirupsen/logrus" "go.yarn.social/lextwt" ) // Service is an object that implements an async responder (bot) that responds to // textual callbacks (commands) or events. This can be used to implement automated // users, bots or services. type Service struct { mu sync.RWMutex me Addr id *Identity cli *Client state string textFns map[string]MessageTextHandlerFunc eventFns map[string]MessageEventHandlerFunc } // MessageTextHandlerFunc defines a function type to handle an inbound message to a service type MessageTextHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyText) error // MessageEventHandlerFunc defines a function type to handle an inbound event to a service type MessageEventHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyEvent) error // NewService constructs a new service with the provided address, identity and state func NewService(me Addr, id *Identity, state string) (*Service, error) { svc := &Service{ me: me, id: id, state: state, textFns: make(map[string]MessageTextHandlerFunc), eventFns: make(map[string]MessageEventHandlerFunc), } svc.TextFunc("ping", func(ctx context.Context, svc *Service, key *keys.EdX25519PublicKey, msg *lextwt.SaltyText) error { return svc.Respond(msg.User.String(), "pong") }) return svc, nil } // SetClient sets the client instance to used for this service func (svc *Service) SetClient(cli *Client) { svc.mu.Lock() defer svc.mu.Unlock() svc.cli = cli } func (svc *Service) String() string { svc.mu.RLock() defer svc.mu.RUnlock() buf := &bytes.Buffer{} fmt.Fprintln(buf, "Bot: ", svc.me) for k := range svc.textFns { fmt.Fprintln(buf, " - TextCmd: ", k) } for k := range svc.eventFns { fmt.Fprintln(buf, " - EventCmd: ", k) } return buf.String() } // Respond is a convenitne method to respond to a user with the provided message func (svc *Service) Respond(user, msg string) error { if svc.cli == nil { return fmt.Errorf("service not connected") } return svc.cli.Send(user, msg) } // Run runs the service tunil the context is done, if an error occurred at any point // an error is returned. func (svc *Service) Run(ctx context.Context) error { // create the service user's client in a loop until successful // TODO: Should this timeout? Use a context? if err := retry.Do(func() error { cli, err := NewClient( WithAddr(svc.me), WithIdentity(svc.id), WithStateFromFile(svc.state), ) if err != nil { return err } if err := cli.me.Refresh(); err != nil { return err } svc.SetClient(cli) return nil }, retry.LastErrorOnly(true), retry.OnRetry(func(n uint, err error) { log.Debugf("retrying service user setup (try #%d): %s", n, err) }), ); err != nil { return fmt.Errorf("error setting up service user %s: %w", svc.me, err) } defer func() { if err := svc.cli.State().Save(svc.state); err != nil { log.WithError(err).Warnf("error saving state: %s", svc.state) } }() log.Debugf("listening for service requests as %s", svc.me) msgch := svc.cli.Subscribe(ctx) for { select { case <-ctx.Done(): return nil case msg := <-msgch: if err := svc.handle(ctx, msg); err != nil { log.WithError(err).Println("failed to handle message") } } } } func (svc *Service) handle(ctx context.Context, msg Message) error { decoded, err := lextwt.ParseSalty(msg.Text) if err != nil { return err } switch m := decoded.(type) { case *lextwt.SaltyText: fields := strings.Fields(m.LiteralText()) svc.mu.RLock() defer svc.mu.RUnlock() if fn, ok := svc.textFns[strings.ToUpper(fields[0])]; ok { err = fn(ctx, svc, msg.Key, m) } case *lextwt.SaltyEvent: svc.mu.RLock() defer svc.mu.RUnlock() if fn, ok := svc.eventFns[strings.ToUpper(m.Command)]; ok { err = fn(ctx, svc, msg.Key, m) } } return err } // TextFunc registers a handler for processing textual callbacks (commands) func (svc *Service) TextFunc(name string, fn MessageTextHandlerFunc) { svc.mu.Lock() defer svc.mu.Unlock() svc.textFns[strings.ToUpper(name)] = fn } // EventFunc registers a handler for processing event callbacks (events) func (svc *Service) EventFunc(name string, fn MessageEventHandlerFunc) { svc.mu.Lock() defer svc.mu.Unlock() svc.eventFns[strings.ToUpper(name)] = fn }