Files
mrktplaats/messaging.go
T
Joren 25410749db Initial Marktplaats SDK scaffold
Clone the twdehands SDK into mrktplaats and retarget naming and defaults for app.marktplaats.nl while keeping the same request/response bodies and endpoint behavior.
2026-04-15 23:45:49 +02:00

220 lines
7.1 KiB
Go

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
}