package main import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) type MatrixClient struct { HomeserverURL string AccessToken string RoomID string Limit int httpClient *http.Client } func newMatrixClientFromEnv() *MatrixClient { hs := strings.TrimRight(readEnv("MATRIX_HOMESERVER_URL", ""), "/") token := readEnv("MATRIX_ACCESS_TOKEN", "") roomID := readEnv("MATRIX_ROOM_ID", "") if hs == "" || token == "" || roomID == "" { return nil } limit := mustInt(readEnv("MATRIX_LIMIT", "25"), 25) if limit < 1 { limit = 1 } if limit > 100 { limit = 100 } return &MatrixClient{ HomeserverURL: hs, AccessToken: token, RoomID: roomID, Limit: limit, httpClient: &http.Client{ Timeout: 6 * time.Second, }, } } func (m *MatrixClient) FetchRecentMessages(ctx context.Context) ([]MatrixMessage, error) { filter := fmt.Sprintf(`{"room":{"rooms":["%s"],"timeline":{"limit":%d}}}`, m.RoomID, m.Limit) endpoint := m.HomeserverURL + "/_matrix/client/v3/sync?timeout=0&filter=" + url.QueryEscape(filter) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+m.AccessToken) resp, err := m.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, errors.New("matrix sync failed: " + strconv.Itoa(resp.StatusCode)) } var syncResp struct { Rooms struct { Join map[string]struct { Timeline struct { Events []struct { Type string `json:"type"` Sender string `json:"sender"` OriginServerTS int64 `json:"origin_server_ts"` Content struct { MsgType string `json:"msgtype"` Body string `json:"body"` } `json:"content"` } `json:"events"` } `json:"timeline"` } `json:"join"` } `json:"rooms"` } if err := json.NewDecoder(resp.Body).Decode(&syncResp); err != nil { return nil, err } room, ok := syncResp.Rooms.Join[m.RoomID] if !ok { return []MatrixMessage{}, nil } out := make([]MatrixMessage, 0, len(room.Timeline.Events)) for _, ev := range room.Timeline.Events { if ev.Type != "m.room.message" { continue } if ev.Content.MsgType != "m.text" && ev.Content.MsgType != "m.notice" { continue } body := strings.TrimSpace(ev.Content.Body) if body == "" { continue } ts := time.UnixMilli(ev.OriginServerTS).Local().Format("02.01.2006 15:04") out = append(out, MatrixMessage{ Sender: ev.Sender, Body: body, Timestamp: ts, }) } return out, nil }