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" ) type Service struct { mu sync.RWMutex me *Addr id *Identity cli *Client state string textFns map[string]MessageTextHandlerFunc eventFns map[string]MessageEventHandlerFunc } type MessageTextHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyText) error type MessageEventHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyEvent) error 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 } 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() } func (svc *Service) Respond(user, msg string) error { if svc.cli == nil { return fmt.Errorf("service not connected") } return svc.cli.Send(user, msg) } 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( svc.me, WithStateFromFile(svc.state), WithClientIdentity(WithIdentity(svc.id)), ) 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 } func (svc *Service) TextFunc(name string, fn MessageTextHandlerFunc) { svc.mu.Lock() defer svc.mu.Unlock() svc.textFns[strings.ToUpper(name)] = fn } func (svc *Service) EventFunc(name string, fn MessageEventHandlerFunc) { svc.mu.Lock() defer svc.mu.Unlock() svc.eventFns[strings.ToUpper(name)] = fn }