prologic-saltyim/service.go

176 lines
4.4 KiB
Go

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
}