package mrktplaats import ( "context" "encoding/json" "fmt" "net/url" "time" ) // MessagingService handles conversations and messages. type MessagingService struct { t *transport } // Conversation represents a messaging thread. type Conversation struct { ID string `json:"id"` ItemID string `json:"itemId"` SellerID int `json:"sellerId"` OtherParticipant Participant `json:"otherParticipant"` UnreadMessagesCount int `json:"unreadMessagesCount"` Title string `json:"title"` ImageURL string `json:"imageUrl"` Embedded conversationEmbed `json:"_embedded"` } // LatestMessage returns the latest message in the conversation. func (c *Conversation) LatestMessage() *Message { return c.Embedded.LatestMessage } type conversationEmbed struct { LatestMessage *Message `json:"mc:latest-message"` } // Participant is the other user in a conversation. type Participant struct { ID int `json:"id"` Name string `json:"name"` IsReviewable bool `json:"isReviewable"` } // Message represents a single message in a conversation. type Message struct { ID string `json:"id"` SenderID int `json:"senderId"` Text string `json:"text"` ReceivedDate time.Time `json:"receivedDate"` IsRead bool `json:"isRead"` MessageType string `json:"messageType"` IsMarkedAsFraud bool `json:"isMarkedAsFraud"` Actions []any `json:"actions"` } // ConversationsResponse is the response from listing conversations. type ConversationsResponse struct { ConversationsWithUnreadMessagesCount int `json:"conversationsWithUnreadMessagesCount"` Limit int `json:"limit"` Offset int `json:"offset"` TotalCount int `json:"totalCount"` UnreadMessagesCount int `json:"unreadMessagesCount"` Conversations []Conversation } // UnmarshalJSON handles the HAL _embedded format for conversations. func (r *ConversationsResponse) UnmarshalJSON(data []byte) error { type plain struct { ConversationsWithUnreadMessagesCount int `json:"conversationsWithUnreadMessagesCount"` Limit int `json:"limit"` Offset int `json:"offset"` TotalCount int `json:"totalCount"` UnreadMessagesCount int `json:"unreadMessagesCount"` Embedded struct { Conversations []Conversation `json:"mc:conversations"` } `json:"_embedded"` } var p plain if err := json.Unmarshal(data, &p); err != nil { return err } r.ConversationsWithUnreadMessagesCount = p.ConversationsWithUnreadMessagesCount r.Limit = p.Limit r.Offset = p.Offset r.TotalCount = p.TotalCount r.UnreadMessagesCount = p.UnreadMessagesCount r.Conversations = p.Embedded.Conversations return nil } // MessagesResponse is the response from fetching a conversation's messages. type MessagesResponse struct { Offset int `json:"offset"` Limit int `json:"limit"` TotalCount int `json:"totalCount"` Messages []Message } // UnmarshalJSON handles the deeply nested HAL format for messages. func (r *MessagesResponse) UnmarshalJSON(data []byte) error { var raw struct { Embedded struct { Messages struct { Offset int `json:"offset"` Limit int `json:"limit"` TotalCount int `json:"totalCount"` Embedded struct { Message []Message `json:"mc:message"` } `json:"_embedded"` } `json:"mc:messages"` } `json:"_embedded"` } if err := json.Unmarshal(data, &raw); err != nil { return err } r.Offset = raw.Embedded.Messages.Offset r.Limit = raw.Embedded.Messages.Limit r.TotalCount = raw.Embedded.Messages.TotalCount r.Messages = raw.Embedded.Messages.Embedded.Message return nil } // ServerTime returns the current server datetime. func (s *MessagingService) ServerTime(ctx context.Context) (time.Time, error) { var resp struct { DateTime time.Time `json:"dateTime"` } if err := s.t.get(ctx, "/app/messaging/v1/datetime", nil, &resp); err != nil { return time.Time{}, err } return resp.DateTime, nil } // ConversationsOptions configures a conversations list request. type ConversationsOptions struct { Offset int Limit int Latitude float64 Longitude float64 } // Conversations lists the authenticated user's conversations. func (s *MessagingService) Conversations(ctx context.Context, opts *ConversationsOptions) (*ConversationsResponse, error) { params := url.Values{} if opts != nil { if opts.Offset > 0 { params.Set("offset", fmt.Sprintf("%d", opts.Offset)) } if opts.Limit > 0 { params.Set("limit", fmt.Sprintf("%d", opts.Limit)) } if opts.Latitude != 0 { params.Set("latitude", fmt.Sprintf("%f", opts.Latitude)) } if opts.Longitude != 0 { params.Set("longitude", fmt.Sprintf("%f", opts.Longitude)) } } var resp ConversationsResponse if err := s.t.get(ctx, "/app/messaging/v1/conversations/android", params, &resp); err != nil { return nil, err } return &resp, nil } // Messages retrieves the messages in a conversation. func (s *MessagingService) Messages(ctx context.Context, conversationID string, offset, limit int) (*MessagesResponse, error) { path := fmt.Sprintf("/app/messaging/v1/conversations/android/%s/messages", conversationID) params := url.Values{ "offset": {fmt.Sprintf("%d", offset)}, "limit": {fmt.Sprintf("%d", limit)}, } var resp MessagesResponse if err := s.t.get(ctx, path, params, &resp); err != nil { return nil, err } return &resp, nil } // StartConversation sends an initial message to the seller of a listing // using the ASQ (Ask Seller a Question) enquiry endpoint. // sellerID is the numeric seller ID from ListingDetail.SellerInformation.ID. // Returns nil on success (the API responds with 204 No Content). func (s *MessagingService) StartConversation(ctx context.Context, itemID, sellerID, text string) error { body := AskQuestionBody{ AdURN: itemID, BidValue: -1.0, Body: text, RecipientID: sellerID, } return s.t.postJSON(ctx, "/app/enquiry/v1/question", nil, nil, body, nil) } // SendMessage sends a text message in an existing conversation. func (s *MessagingService) SendMessage(ctx context.Context, conversationID, text string) (string, error) { path := fmt.Sprintf("/app/messaging/v1/conversations/android/%s/text", conversationID) body := struct { Text string `json:"text"` Actions any `json:"actions"` }{ Text: text, } var resp struct { ID string `json:"id"` } if err := s.t.postJSON(ctx, path, nil, nil, body, &resp); err != nil { return "", err } return resp.ID, nil } // EmailSubscriptions returns the user's messaging email notification settings. func (s *MessagingService) EmailSubscriptions(ctx context.Context) (json.RawMessage, error) { var resp json.RawMessage if err := s.t.get(ctx, "/app/messaging/v1/subscriptions/email", nil, &resp); err != nil { return nil, err } return resp, nil }