Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ jobs:
fi

- name: Run tests with coverage
run: go test -short -race -coverprofile=coverage.out ./...
# -coverpkg scopes coverage to the library package only, excluding
# examples/ and tests/integration/ which have no unit tests and
# would otherwise drag the reported percentage down.
run: go test -short -race -coverprofile=coverage.out -coverpkg=github.com/tirthpatell/threads-go ./...

- name: Extract coverage percentage
if: github.ref == 'refs/heads/main' && matrix.go-version == '1.24'
Expand Down
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thank you for contributing! This guide helps you get started with development.

## Prerequisites

- Go 1.21 or later
- Go 1.21 or later (tested on 1.21-1.24)
- Git
- Threads API credentials (for testing)

Expand All @@ -25,7 +25,10 @@ export THREADS_REDIRECT_URI="your-redirect-uri"
export THREADS_ACCESS_TOKEN="your-token"

# Run tests
go test ./...
go test -short ./...
go test -short -race ./...

# Run integration tests (requires credentials)
go test ./tests/integration/...
```

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Tirth Patel
Copyright (c) 2024-present Tirth Patel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
69 changes: 63 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tirthpatell/2c608589294aed9aa900256daeec0fd4/raw/coverage.json)](https://github.com/tirthpatell/threads-go/actions)

Production-ready Go client for the Threads API with complete endpoint coverage, OAuth 2.0 authentication, rate limiting, and comprehensive error handling.
Unofficial, production-ready Go client for the [Threads API](https://developers.facebook.com/docs/threads) with complete endpoint coverage, OAuth 2.0 authentication, rate limiting, and comprehensive error handling. Not affiliated with or endorsed by Meta.

Requires Go 1.21+. Tested on Go 1.21-1.24.

## Features

- Complete API coverage (posts, users, replies, insights, locations)
- Complete API coverage (posts, users, replies, insights, locations, search)
- GIF attachments (GIPHY), ghost posts, and reply approvals
- Fluent `ContainerBuilder` for advanced post creation
- OAuth 2.0 flow and existing token support
- Intelligent rate limiting with exponential backoff
- Type-safe error handling
- Type-safe error handling with transient error detection
- Thread-safe concurrent usage
- Comprehensive test coverage

Expand Down Expand Up @@ -56,8 +60,9 @@ authURL := client.GetAuthURL(config.Scopes)
// Redirect user to authURL

// Exchange authorization code for token
err = client.ExchangeCodeForToken("auth-code-from-callback")
err = client.GetLongLivedToken() // Convert to long-lived token
ctx := context.Background()
err = client.ExchangeCodeForToken(ctx, "auth-code-from-callback")
err = client.GetLongLivedToken(ctx) // Convert to long-lived token
```

### Environment Variables
Expand Down Expand Up @@ -101,7 +106,7 @@ imagePost, err := client.CreateImagePost(ctx, &threads.ImagePostContent{

// Get posts
post, err := client.GetPost(ctx, threads.PostID("123"))
posts, err := client.GetUserPosts(ctx, threads.UserID("456"), &threads.PostsOptions{Limit: 25})
posts, err := client.GetUserPosts(ctx, threads.UserID("456"), &threads.PaginationOptions{Limit: 25})

// Delete post
err = client.DeletePost(ctx, threads.PostID("123"))
Expand Down Expand Up @@ -166,6 +171,56 @@ tagResults, err := client.KeywordSearch(ctx, "#technology", &threads.SearchOptio
locations, err := client.SearchLocations(ctx, "New York", nil, nil)
```

### GIF Posts

```go
// Create a post with a GIF attachment (GIPHY)
gifPost, err := client.CreateTextPost(ctx, &threads.TextPostContent{
Text: "Check out this GIF!",
GIFAttachment: &threads.GIFAttachment{
GIFID: "giphy-gif-id",
Provider: threads.GIFProviderGiphy,
},
})
```

> Note: Tenor support is deprecated and will be sunset on March 31, 2026. Use GIPHY instead.

### Ghost Posts

```go
// Create a ghost post (auto-expiring)
ghost, err := client.CreateTextPost(ctx, &threads.TextPostContent{
Text: "This will disappear!",
IsGhostPost: true,
})

// Retrieve a user's ghost posts
ghosts, err := client.GetUserGhostPosts(ctx, userID, nil)
```

### Reply Approvals

```go
// Approve or ignore pending replies
err = client.ApprovePendingReply(ctx, threads.PostID("reply-id"))
err = client.IgnorePendingReply(ctx, threads.PostID("reply-id"))
```

### Container Builder

For advanced post creation, use the fluent `ContainerBuilder`:

```go
builder := threads.NewContainerBuilder().
SetMediaType("TEXT").
SetText("Hello from the builder!").
SetReplyControl(threads.ReplyControlFollowersOnly).
SetTopicTag("golang").
SetLocationID("location-id")
params := builder.Build()
```

### Pagination & Iterators

For large datasets, use iterators to automatically handle pagination:
Expand Down Expand Up @@ -231,6 +286,8 @@ case threads.IsRateLimitError(err):
case threads.IsValidationError(err):
validationErr := err.(*threads.ValidationError)
log.Printf("Invalid %s: %s", validationErr.Field, err.Error())
case threads.IsTransientError(err):
// Safe to retry — transient API error
}
```

Expand Down
2 changes: 1 addition & 1 deletion examples/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ THREADS_CLIENT_SECRET=your_app_secret_here
THREADS_REDIRECT_URI=https://your-domain.com/callback

# Optional: Set to true to enable debug logging
DEBUG=false
THREADS_DEBUG=false
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ cd post-creation && go run main.go
- Text, image, video posts
- Carousel posts (multiple media)
- Quote posts and reposts
- GIF posts (GIPHY)
- Ghost posts (auto-expiring)
- Advanced options (reply controls, tags)

### Reply Management (`reply-management/`)
Expand All @@ -70,6 +72,7 @@ cd reply-management && go run main.go
- Create and retrieve replies
- Conversation threading
- Reply moderation (hide/unhide)
- Reply approvals (approve/ignore pending replies)
- Pagination and sorting

### Insights & Analytics (`insights/`)
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-usage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ post, err := client.CreateImagePost(ctx, &threads.ImagePostContent{

fmt.Println("Quote post:")
fmt.Printf(`
post, err := client.CreateQuotePost(ctx, &threads.QuotePostContent{
post, err := client.CreateTextPost(ctx, &threads.TextPostContent{
Text: "Adding my thoughts to this",
QuotedPostID: "existing_post_id",
})
Expand Down
76 changes: 71 additions & 5 deletions examples/post-creation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,21 @@ func main() {
createRepost(client)
fmt.Println()

// Example 8: Error handling
fmt.Println("Example 8: Error Handling")
fmt.Println("========================")
// Example 8: GIF post
fmt.Println("Example 8: GIF Post")
fmt.Println("===================")
createGIFPost(client)
fmt.Println()

// Example 9: Ghost post
fmt.Println("Example 9: Ghost Post")
fmt.Println("=====================")
createGhostPost(client)
fmt.Println()

// Example 10: Error handling
fmt.Println("Example 10: Error Handling")
fmt.Println("=========================")
demonstrateErrorHandling(client)
fmt.Println()

Expand Down Expand Up @@ -244,7 +256,7 @@ func createQuotePost(_ *threads.Client) {
fmt.Printf(" Reply control: %s\n", content.ReplyControl)

// Uncomment to actually create (requires real post ID):
// post, err := client.CreateQuotePost(content)
// post, err := client.CreateTextPost(ctx, content)
// if err != nil {
// fmt.Printf(" Failed to create quote post: %v\n", err)
// handlePostError(err)
Expand All @@ -267,7 +279,7 @@ func createRepost(_ *threads.Client) {
fmt.Printf(" Post ID to repost: %s\n", postIDToRepost)

// Uncomment to actually create (requires real post ID):
// post, err := client.RepostPost(postIDToRepost)
// post, err := client.RepostPost(ctx, threads.ConvertToPostID(postIDToRepost))
// if err != nil {
// fmt.Printf(" Failed to create repost: %v\n", err)
// handlePostError(err)
Expand All @@ -279,6 +291,60 @@ func createRepost(_ *threads.Client) {
fmt.Println(" Replace 'example_post_id_to_repost' with a real post ID to create reposts")
}

func createGIFPost(client *threads.Client) {
ctx := context.Background()
content := &threads.TextPostContent{
Text: "Check out this GIF!",
GIFAttachment: &threads.GIFAttachment{
GIFID: "your-giphy-gif-id",
Provider: threads.GIFProviderGiphy,
},
}

fmt.Printf(" GIF post structure:\n")
fmt.Printf(" Text: %s\n", content.Text)
fmt.Printf(" GIF ID: %s\n", content.GIFAttachment.GIFID)
fmt.Printf(" Provider: %s\n", content.GIFAttachment.Provider)

// Uncomment to actually create (requires real GIPHY GIF ID):
// post, err := client.CreateTextPost(ctx, content)
// if err != nil {
// fmt.Printf(" Failed to create GIF post: %v\n", err)
// handlePostError(err)
// return
// }
// fmt.Println(" GIF post created successfully!")
// printPostInfo(post)
_ = ctx

fmt.Println(" Note: Tenor is deprecated (sunset March 31, 2026). Use GIFProviderGiphy instead.")
}

func createGhostPost(client *threads.Client) {
ctx := context.Background()
content := &threads.TextPostContent{
Text: "This is a ghost post -- it will auto-expire!",
IsGhostPost: true,
}

fmt.Printf(" Ghost post structure:\n")
fmt.Printf(" Text: %s\n", content.Text)
fmt.Printf(" Is Ghost Post: %t\n", content.IsGhostPost)

// Uncomment to actually create:
// post, err := client.CreateTextPost(ctx, content)
// if err != nil {
// fmt.Printf(" Failed to create ghost post: %v\n", err)
// handlePostError(err)
// return
// }
// fmt.Println(" Ghost post created successfully!")
// printPostInfo(post)
_ = ctx

fmt.Println(" Ghost posts auto-expire and can be retrieved with client.GetUserGhostPosts()")
}

func demonstrateErrorHandling(client *threads.Client) {
ctx := context.Background()
fmt.Println("Testing various error scenarios...")
Expand Down
27 changes: 25 additions & 2 deletions examples/reply-management/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ func main() {
getUserReplyHistory(client, me.ID)
fmt.Println()

// Example 7: Advanced reply options
fmt.Println("Example 7: Advanced Reply Features")
// Example 7: Reply approvals
fmt.Println("Example 7: Reply Approvals")
fmt.Println("==========================")
demonstrateReplyApprovals(client)
fmt.Println()

// Example 8: Advanced reply options
fmt.Println("Example 8: Advanced Reply Features")
fmt.Println("==================================")
demonstrateAdvancedReplyFeatures(client, originalPost.ID)
fmt.Println()
Expand Down Expand Up @@ -366,6 +372,23 @@ func getUserReplyHistory(client *threads.Client, userID string) {
}
}

func demonstrateReplyApprovals(_ *threads.Client) {
fmt.Println(" Reply approvals allow you to review replies before they appear")
fmt.Println()
fmt.Println(" Usage:")
fmt.Println(" // Approve a pending reply")
fmt.Println(" err := client.ApprovePendingReply(ctx, threads.PostID(\"reply-id\"))")
fmt.Println()
fmt.Println(" // Ignore a pending reply")
fmt.Println(" err := client.IgnorePendingReply(ctx, threads.PostID(\"reply-id\"))")
fmt.Println()
fmt.Println(" // Enable reply approvals when creating a post")
fmt.Println(" content := &threads.TextPostContent{")
fmt.Println(" Text: \"Post with reply approvals\",")
fmt.Println(" EnableReplyApprovals: true,")
fmt.Println(" }")
}

func demonstrateAdvancedReplyFeatures(client *threads.Client, postID string) {
fmt.Println(" Demonstrating advanced reply features...")

Expand Down
Loading