saltyim is the Go library and reference client and broker implementation for Salty IM it contains a command-line client (cli), a terminal user interface (tui), builtin server/broker and a Mobile / Desktop App PWA (progressive web app) https://salty.im/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
saltyim/service.go

162 lines
3.6 KiB

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
}