2022-03-26 14:43:05 +00:00
|
|
|
package saltyim
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2022-03-28 21:49:01 +00:00
|
|
|
"github.com/avast/retry-go"
|
2022-03-26 14:43:05 +00:00
|
|
|
"github.com/keys-pub/keys"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"go.yarn.social/lextwt"
|
|
|
|
)
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// 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.
|
2022-03-26 14:43:05 +00:00
|
|
|
type Service struct {
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
2023-01-26 22:30:16 +00:00
|
|
|
me Addr
|
2022-04-04 03:42:54 +00:00
|
|
|
id *Identity
|
|
|
|
cli *Client
|
|
|
|
state string
|
2022-03-27 05:25:09 +00:00
|
|
|
|
2022-03-26 14:43:05 +00:00
|
|
|
textFns map[string]MessageTextHandlerFunc
|
|
|
|
eventFns map[string]MessageEventHandlerFunc
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// MessageTextHandlerFunc defines a function type to handle an inbound message to a service
|
2022-03-26 14:43:05 +00:00
|
|
|
type MessageTextHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyText) error
|
2023-01-14 02:46:47 +00:00
|
|
|
|
|
|
|
// MessageEventHandlerFunc defines a function type to handle an inbound event to a service
|
2022-03-26 14:43:05 +00:00
|
|
|
type MessageEventHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyEvent) error
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// NewService constructs a new service with the provided address, identity and state
|
2023-01-26 22:30:16 +00:00
|
|
|
func NewService(me Addr, id *Identity, state string) (*Service, error) {
|
2022-03-26 14:43:05 +00:00
|
|
|
svc := &Service{
|
2022-03-27 05:25:09 +00:00
|
|
|
me: me,
|
|
|
|
id: id,
|
2022-04-04 03:42:54 +00:00
|
|
|
state: state,
|
2022-03-26 14:43:05 +00:00
|
|
|
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 {
|
2022-03-28 21:49:01 +00:00
|
|
|
return svc.Respond(msg.User.String(), "pong")
|
2022-03-26 14:43:05 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return svc, nil
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// SetClient sets the client instance to used for this service
|
2022-03-28 21:49:01 +00:00
|
|
|
func (svc *Service) SetClient(cli *Client) {
|
|
|
|
svc.mu.Lock()
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
|
|
svc.cli = cli
|
|
|
|
}
|
|
|
|
|
2022-03-26 14:43:05 +00:00
|
|
|
func (svc *Service) String() string {
|
|
|
|
svc.mu.RLock()
|
|
|
|
defer svc.mu.RUnlock()
|
2022-03-27 02:11:40 +00:00
|
|
|
|
|
|
|
buf := &bytes.Buffer{}
|
2022-03-27 05:25:09 +00:00
|
|
|
fmt.Fprintln(buf, "Bot: ", svc.me)
|
2022-03-26 14:43:05 +00:00
|
|
|
for k := range svc.textFns {
|
|
|
|
fmt.Fprintln(buf, " - TextCmd: ", k)
|
|
|
|
}
|
|
|
|
for k := range svc.eventFns {
|
|
|
|
fmt.Fprintln(buf, " - EventCmd: ", k)
|
|
|
|
}
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// Respond is a convenitne method to respond to a user with the provided message
|
2022-03-27 05:25:09 +00:00
|
|
|
func (svc *Service) Respond(user, msg string) error {
|
|
|
|
if svc.cli == nil {
|
|
|
|
return fmt.Errorf("service not connected")
|
|
|
|
}
|
|
|
|
return svc.cli.Send(user, msg)
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// Run runs the service tunil the context is done, if an error occurred at any point
|
|
|
|
// an error is returned.
|
2022-03-28 21:49:01 +00:00
|
|
|
func (svc *Service) Run(ctx context.Context) error {
|
2022-03-27 05:25:09 +00:00
|
|
|
// create the service user's client in a loop until successful
|
|
|
|
// TODO: Should this timeout? Use a context?
|
2022-03-28 21:49:01 +00:00
|
|
|
if err := retry.Do(func() error {
|
2022-04-04 03:42:54 +00:00
|
|
|
cli, err := NewClient(
|
2023-01-25 23:05:29 +00:00
|
|
|
WithAddr(svc.me),
|
|
|
|
WithIdentity(svc.id),
|
2022-04-04 03:42:54 +00:00
|
|
|
WithStateFromFile(svc.state),
|
|
|
|
)
|
2022-03-27 05:25:09 +00:00
|
|
|
if err != nil {
|
2022-03-28 21:49:01 +00:00
|
|
|
return err
|
2022-03-27 05:25:09 +00:00
|
|
|
}
|
2022-03-28 21:49:01 +00:00
|
|
|
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)
|
2022-03-27 05:25:09 +00:00
|
|
|
}
|
2022-04-04 03:42:54 +00:00
|
|
|
defer func() {
|
|
|
|
if err := svc.cli.State().Save(svc.state); err != nil {
|
|
|
|
log.WithError(err).Warnf("error saving state: %s", svc.state)
|
|
|
|
}
|
|
|
|
}()
|
2022-03-27 05:25:09 +00:00
|
|
|
|
2022-03-28 21:49:01 +00:00
|
|
|
log.Debugf("listening for service requests as %s", svc.me)
|
|
|
|
|
2023-01-27 23:19:52 +00:00
|
|
|
msgch := svc.cli.Subscribe(ctx)
|
2022-03-28 21:49:01 +00:00
|
|
|
|
2022-03-26 14:43:05 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2022-03-28 21:49:01 +00:00
|
|
|
return nil
|
2022-03-26 14:43:05 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// TextFunc registers a handler for processing textual callbacks (commands)
|
2022-03-26 14:43:05 +00:00
|
|
|
func (svc *Service) TextFunc(name string, fn MessageTextHandlerFunc) {
|
|
|
|
svc.mu.Lock()
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
|
|
svc.textFns[strings.ToUpper(name)] = fn
|
|
|
|
}
|
|
|
|
|
2023-01-14 02:46:47 +00:00
|
|
|
// EventFunc registers a handler for processing event callbacks (events)
|
2022-03-26 14:43:05 +00:00
|
|
|
func (svc *Service) EventFunc(name string, fn MessageEventHandlerFunc) {
|
|
|
|
svc.mu.Lock()
|
|
|
|
defer svc.mu.Unlock()
|
|
|
|
|
|
|
|
svc.eventFns[strings.ToUpper(name)] = fn
|
|
|
|
}
|