package controlplane import ( "encoding/base64" "encoding/json" "errors" "strings" "time" ) const pageCursorVersion = 1 type pageCursorPayload struct { Version int `json:"v"` Kind string `json:"kind"` ChatID string `json:"chat_id,omitempty"` SortTime string `json:"sort_time"` MessageID string `json:"message_id,omitempty"` } type MessagePageCursor struct { ChatID string SourceTime string MessageID string } type ConversationPageCursor struct { SortTime string ChatID string } func EncodeMessagePageCursor(chatID string, sourceTime time.Time, messageID string) string { return encodePageCursor(pageCursorPayload{ Version: pageCursorVersion, Kind: "messages", ChatID: chatID, SortTime: sourceTime.UTC().Format(time.RFC3339Nano), MessageID: messageID, }) } func DecodeMessagePageCursor(raw, chatID string) (*MessagePageCursor, error) { payload, err := decodePageCursor(raw, "messages") if err != nil || payload.ChatID != chatID || payload.MessageID == "" { return nil, errors.New("invalid message cursor") } if _, err := time.Parse(time.RFC3339Nano, payload.SortTime); err != nil { return nil, errors.New("invalid message cursor") } return &MessagePageCursor{ChatID: payload.ChatID, SourceTime: payload.SortTime, MessageID: payload.MessageID}, nil } func EncodeConversationPageCursor(sortTime time.Time, chatID string) string { return encodePageCursor(pageCursorPayload{ Version: pageCursorVersion, Kind: "conversations", ChatID: chatID, SortTime: sortTime.UTC().Format(time.RFC3339Nano), }) } func DecodeConversationPageCursor(raw string) (*ConversationPageCursor, error) { payload, err := decodePageCursor(raw, "conversations") if err != nil || payload.ChatID == "" { return nil, errors.New("invalid conversation cursor") } if _, err := time.Parse(time.RFC3339Nano, payload.SortTime); err != nil { return nil, errors.New("invalid conversation cursor") } return &ConversationPageCursor{SortTime: payload.SortTime, ChatID: payload.ChatID}, nil } func encodePageCursor(payload pageCursorPayload) string { encoded, _ := json.Marshal(payload) return base64.RawURLEncoding.EncodeToString(encoded) } func decodePageCursor(raw, kind string) (pageCursorPayload, error) { if strings.TrimSpace(raw) == "" { return pageCursorPayload{}, errors.New("cursor is empty") } decoded, err := base64.RawURLEncoding.DecodeString(raw) if err != nil { return pageCursorPayload{}, errors.New("cursor is not base64url") } var payload pageCursorPayload if err := json.Unmarshal(decoded, &payload); err != nil || payload.Version != pageCursorVersion || payload.Kind != kind || payload.SortTime == "" { return pageCursorPayload{}, errors.New("cursor payload is invalid") } return payload, nil }