diff --git a/constants.go b/constants.go index 8018f88..2e65cab 100644 --- a/constants.go +++ b/constants.go @@ -43,8 +43,8 @@ const ( // Field Sets for API requests const ( - // Post fields - PostExtendedFields = "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,alt_text,link_attachment_url,has_replies,reply_audience,quoted_post,reposted_post,gif_url" + // Post fields (is_verified and profile_picture_url added December 16, 2025 for replies/mentions) + PostExtendedFields = "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,alt_text,link_attachment_url,has_replies,reply_audience,quoted_post,reposted_post,gif_url,is_verified,profile_picture_url" // Ghost Post fields GhostPostFields = "id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,ghost_post_status,ghost_post_expiration_timestamp" @@ -53,7 +53,8 @@ const ( UserProfileFields = "id,username,name,threads_profile_picture_url,threads_biography,is_verified" // Reply fields (includes additional reply-specific fields) - ReplyFields = "id,media_product_type,media_type,media_url,permalink,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,root_post,replied_to,is_reply,is_reply_owned_by_me,reply_audience,quoted_post,reposted_post,gif_url,alt_text,hide_status,topic_tag" + // is_verified and profile_picture_url added December 16, 2025 (only available on direct replies for conversations) + ReplyFields = "id,media_product_type,media_type,media_url,permalink,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post,has_replies,root_post,replied_to,is_reply,is_reply_owned_by_me,reply_audience,quoted_post,reposted_post,gif_url,alt_text,hide_status,topic_tag,is_verified,profile_picture_url" // Container status fields ContainerStatusFields = "id,status,error_message" @@ -93,3 +94,11 @@ const ( ErrEmptyContainerID = "Container ID is required" ErrEmptySearchQuery = "Search query is required" ) + +// API Error Codes returned by the Threads API +const ( + // ErrCodeLinkLimitExceeded is returned when a post contains more than 5 unique links. + // This error occurs during media container creation (POST /{threads-user-id}/threads). + // Added December 22, 2025. Reduce the number of unique links to 5 or fewer to resolve. + ErrCodeLinkLimitExceeded = "THREADS_API__LINK_LIMIT_EXCEEDED" +) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 872d008..102308c 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -892,6 +892,53 @@ func TestIntegration_ValidationErrors(t *testing.T) { t.Logf("Validation error (expected): %v", err) } }) + + t.Run("TooManyLinks", func(t *testing.T) { + // Try to create post with more than 5 unique links (should fail validation) + // Added December 22, 2025: THREADS_API__LINK_LIMIT_EXCEEDED + content := &threads.TextPostContent{ + Text: "Check out these links: https://example1.com https://example2.com https://example3.com https://example4.com https://example5.com https://example6.com", + } + + _, err := client.CreateTextPost(context.Background(), content) + if err == nil { + t.Error("Expected validation error for too many links (max 5)") + } else { + t.Logf("Validation error (expected): %v", err) + } + }) + + t.Run("TooManyLinksWithLinkAttachment", func(t *testing.T) { + // Try to create post with 5 links in text + 1 different link_attachment (should fail validation) + content := &threads.TextPostContent{ + Text: "Links: https://example1.com https://example2.com https://example3.com https://example4.com https://example5.com", + LinkAttachment: "https://example6.com", // 6th unique link + } + + _, err := client.CreateTextPost(context.Background(), content) + if err == nil { + t.Error("Expected validation error for too many links with link_attachment") + } else { + t.Logf("Validation error (expected): %v", err) + } + }) + + t.Run("LinksWithDuplicateLinkAttachment", func(t *testing.T) { + // 5 unique links in text + link_attachment that duplicates one = should pass (5 unique total) + // This tests that duplicate link_attachment is not double-counted + content := &threads.TextPostContent{ + Text: "Links: https://example1.com https://example2.com https://example3.com https://example4.com https://example5.com", + LinkAttachment: "https://example1.com", // Duplicate, should not count as 6th + } + + // This should NOT fail validation (5 unique links is the limit) + err := client.ValidateTextPostContent(content) + if err != nil { + t.Errorf("Unexpected validation error for 5 unique links with duplicate link_attachment: %v", err) + } else { + t.Log("Validation passed (expected): 5 unique links with duplicate link_attachment") + } + }) } // TestIntegration_GIFPosts tests GIF attachment functionality diff --git a/types.go b/types.go index 7a7e5ad..bbaec81 100644 --- a/types.go +++ b/types.go @@ -75,6 +75,14 @@ type Post struct { TopicTag string `json:"topic_tag,omitempty"` GhostPostStatus string `json:"ghost_post_status,omitempty"` GhostPostExpirationTimestamp Time `json:"ghost_post_expiration_timestamp,omitempty"` + // IsVerified indicates if the post author's profile is verified on Threads. + // Available for replies and mentions. For conversations, only available on direct replies. + // Added December 16, 2025. + IsVerified bool `json:"is_verified,omitempty"` + // ProfilePictureURL is the URL of the post author's profile picture on Threads. + // Available for replies and mentions. For conversations, only available on direct replies. + // Added December 16, 2025. + ProfilePictureURL string `json:"profile_picture_url,omitempty"` } // User represents a Threads user profile with app-scoped data.