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[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 }