package matrix import ( "bytes" "encoding/json" "fmt" "io" "net/http" "regexp" "github.com/rs/zerolog" ) var cmdRegex = regexp.MustCompile("[[:space:]]*!(?P[a-zA-Z]+)[[:space:]]+(?P[a-zA-Z]+)") type EventProcessor interface { ProcessEvents(events []*RawEvent) error } type processor struct { accessToken string httpclient *http.Client processed map[string]bool logger zerolog.Logger } func NewProcessor(accessToken string, logger zerolog.Logger) EventProcessor { return &processor{ 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.Content.Membership == "invite" { 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) cmd := match[1] arg := match[2] switch cmd { case "verb": if err := p.handleVerb(e, cmd, arg); err != nil { return err } default: p.logger.Debug().Str("command", cmd).Str("arg", arg).Msg("unrecognized command") } p.processed[e.Id] = true } return nil } func (p *processor) acceptInvite(e *RawEvent) error { url := fmt.Sprintf("https://synapse.local/_matrix/client/r0/rooms/%s/join", 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) } 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, cmd, arg string) error { replyBody := sendBody{ Body: fmt.Sprintf("rcvd command %q, arg %q", cmd, arg), MsgType: "text", } encodedBody, err := json.Marshal(replyBody) if err != nil { return fmt.Errorf("error encoding message: %v", err) } url := fmt.Sprintf("https://synapse.local/_matrix/client/r0/rooms/%s/send/m.room.message", 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) } if resp.StatusCode != http.StatusOK { return p.handleErrorResponse(e, resp) } return nil } func (p *processor) handleErrorResponse(e *RawEvent, resp *http.Response) error { defer resp.Body.Close() 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("synapse error: %v", er.Error) } func (p *processor) buildRequest(url, method string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(url, method, 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 }