147 lines
3.8 KiB
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
|
|
}
|