esbot/internal/matrix/events.go

147 lines
3.8 KiB
Go

package matrix
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"git.netflux.io/rob/esbot/internal/lang"
"github.com/rs/zerolog"
)
var cmdRegex = regexp.MustCompile("[[:space:]]*@esbot[[:space:]]+(?P<Verb>[a-zA-ZñÑ]+)")
type EventProcessor interface {
ProcessEvents(events []*RawEvent) error
}
type processor struct {
baseURL, user, accessToken string
httpclient *http.Client
processed map[string]bool
logger zerolog.Logger
}
func NewProcessor(baseURL, user, accessToken string, logger zerolog.Logger) EventProcessor {
return &processor{
baseURL: baseURL,
user: user,
accessToken: accessToken,
httpclient: &http.Client{},
processed: make(map[string]bool),
logger: logger,
}
}
func (p *processor) ProcessEvents(events []*RawEvent) error {
for _, e := range events {
if p.processed[e.Id] {
p.logger.Debug().Str("event_id", e.Id).Msg("already processed event")
continue
}
if e.StateKey != "" {
if e.Type == "m.room.member" && e.Content.Membership == "invite" && e.StateKey == p.user {
if err := p.acceptInvite(e); err != nil {
return err
}
}
p.processed[e.Id] = true
continue
}
if !cmdRegex.MatchString(e.Content.Body) {
p.processed[e.Id] = true
continue
}
match := cmdRegex.FindStringSubmatch(e.Content.Body)
if err := p.handleVerb(e, match[1]); err != nil {
return err
}
p.processed[e.Id] = true
}
return nil
}
func (p *processor) acceptInvite(e *RawEvent) error {
url := fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/join", p.baseURL, e.RoomId)
req, _ := p.buildRequest("POST", url, bytes.NewReader([]byte("{}")))
resp, err := p.httpclient.Do(req)
if err != nil {
return fmt.Errorf("http error accepting invitation: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return p.handleErrorResponse(e, resp)
}
p.logger.Info().Str("room_id", e.RoomId).Str("sender", e.Sender).Msg("accepted invite")
return nil
}
func (p *processor) handleVerb(e *RawEvent, q string) error {
verb := lang.GetVerb(strings.ToLower(q))
var sb sendBody
if verb != nil {
sb = sendBody{
Body: verb.String(),
MsgType: "m.notice",
}
} else {
sb = sendBody{
Body: "I don't know this verb. Try another.",
MsgType: "m.notice",
}
}
encodedBody, err := json.Marshal(sb)
if err != nil {
return fmt.Errorf("error encoding message: %v", err)
}
url := fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", p.baseURL, e.RoomId)
req, _ := p.buildRequest("POST", url, bytes.NewReader(encodedBody))
resp, err := p.httpclient.Do(req)
if err != nil {
return fmt.Errorf("http error sending message: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return p.handleErrorResponse(e, resp)
}
return nil
}
func (p *processor) handleErrorResponse(e *RawEvent, resp *http.Response) error {
var er errorResponse
if err := json.NewDecoder(resp.Body).Decode(&er); err != nil {
return fmt.Errorf("could not decode response: %v", err)
}
if er.Code == "M_FORBIDDEN" || er.Code == "M_UNKNOWN_TOKEN" {
p.processed[e.Id] = true
p.logger.Warn().Str("errcode", er.Code).Str("event_id", e.Id).Str("room_id", e.RoomId).Msg("forbidden, ignoring event")
return nil
}
p.logger.Error().Str("errcode", er.Code).Str("error", er.Error).Msg("error sending message")
return fmt.Errorf("server error: %v", er.Error)
}
func (p *processor) buildRequest(method, url string, body io.Reader) (*http.Request, error) {
p.logger.Debug().Str("method", method).Str("url", url).Msg("build HTTP req")
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", p.accessToken))
return req, nil
}