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.
220 lines
7.1 KiB
Go
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
|
|
}
|