@codybrom/denim
Version:
A Deno/TypeScript library for the Threads API
926 lines (851 loc) • 24.4 kB
text/typescript
// types.ts
// ─── Media & Reply Control ───────────────────────────────────────────────────
/**
* Media types used when creating posts.
*/
export type MediaType = "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL";
/**
* All media type values that can appear in API responses.
* Responses use TEXT_POST (not TEXT) and CAROUSEL_ALBUM (not CAROUSEL).
*/
export type ResponseMediaType =
| "TEXT_POST"
| "IMAGE"
| "VIDEO"
| "CAROUSEL_ALBUM"
| "GIF"
| "REPOST_FACADE"
| "AUDIO";
/**
* Represents the options for controlling who can reply to a post.
*/
export type ReplyControl =
| "everyone"
| "accounts_you_follow"
| "mentioned_only"
| "parent_post_author_only"
| "followers_only";
// ─── Input Types (what callers pass in) ──────────────────────────────────────
/**
* Options for poll attachments when creating a post.
* Properties use snake_case to match the Threads API payload format directly.
*/
export interface PollAttachmentInput {
/** First poll option (required) */
option_a: string;
/** Second poll option (required) */
option_b: string;
/** Third poll option (optional) */
option_c?: string;
/** Fourth poll option (optional) */
option_d?: string;
}
/**
* Represents a text entity for spoiler/styling annotations.
* Properties use snake_case to match the Threads API payload format directly.
*/
export interface TextEntity {
/** The type of entity (e.g., "spoiler") */
entity_type: string;
/** Character offset where the entity starts */
offset: number;
/** Length of the entity in characters */
length: number;
}
/**
* Input for text attachments (long-form text posts).
* Properties use snake_case to match the Threads API payload format directly.
*/
export interface TextAttachmentInput {
/** The plaintext content */
plaintext: string;
/** Optional URL to attach */
link_attachment_url?: string;
/** Optional styled text with formatting info */
text_with_styling_info?: Array<{
offset: number;
length: number;
styling_info: string[];
}>;
}
/**
* Input for GIF attachments.
* Properties use snake_case to match the Threads API payload format directly.
*/
export interface GifAttachment {
/** The GIF ID from the provider */
gif_id: string;
/** The GIF provider */
provider: "TENOR";
}
/**
* Represents a request to post content on Threads.
*/
export interface ThreadsPostRequest {
/** The user ID of the Threads account */
userId: string;
/** The access token for authentication */
accessToken: string;
/** The type of media being posted */
mediaType: MediaType;
/** The text content of the post (optional) */
text?: string;
/** The URL of the image to be posted (optional, for IMAGE type) */
imageUrl?: string;
/** The URL of the video to be posted (optional, for VIDEO type) */
videoUrl?: string;
/** The accessibility text for the image or video (optional) */
altText?: string;
/** The URL to be attached as a link to the post (optional, for text posts only) */
linkAttachment?: string;
/** List of country codes where the post should be visible (optional) */
allowlistedCountryCodes?: string[];
/** Controls who can reply to the post (optional) */
replyControl?: ReplyControl;
/** Array of carousel item IDs (required for CAROUSEL type) */
children?: string[];
/** ID of the post to reply to (optional) */
replyToId?: string;
/** ID of the post to quote (optional) */
quotePostId?: string;
/** Poll options (optional, TEXT posts only) */
pollAttachment?: PollAttachmentInput;
/** Auto-publish text posts without the separate publish step (optional, TEXT only) */
autoPublishText?: boolean;
/** Topic tag for the post (optional, no periods or ampersands) */
topicTag?: string;
/** Whether the media is a spoiler (optional) */
isSpoilerMedia?: boolean;
/** Text entity annotations for spoilers/styling (optional, max 10) */
textEntities?: TextEntity[];
/** Long-form text attachment (optional, TEXT posts only) */
textAttachment?: TextAttachmentInput;
/** GIF attachment (optional, TEXT posts only) */
gifAttachment?: GifAttachment;
/** Whether the post is a ghost post (optional, TEXT only, cannot be used with replyToId) */
isGhostPost?: boolean;
/** Location ID to tag (optional) */
locationId?: string;
}
/**
* Pagination options shared across list endpoints.
*/
export interface PaginationOptions {
/** Start date (Unix timestamp or strtotime-parseable string) */
since?: string | number;
/** End date (Unix timestamp or strtotime-parseable string) */
until?: string | number;
/** Maximum number of results (default 25, max 100) */
limit?: number;
/** Cursor for previous page */
before?: string;
/** Cursor for next page */
after?: string;
}
/**
* Cursor-based pagination options for endpoints that only support before/after.
* Used by media-level reply and conversation endpoints.
*/
export interface CursorPaginationOptions {
/** Cursor for previous page */
before?: string;
/** Cursor for next page */
after?: string;
}
// ─── Response Types (snake_case — matching API reality) ──────────────────────
/**
* Poll attachment as returned by the API.
*/
export interface PollAttachment {
/** First poll option text */
option_a?: string;
/** Second poll option text */
option_b?: string;
/** Third poll option text */
option_c?: string;
/** Fourth poll option text */
option_d?: string;
/** Percentage of votes for option A (0-1) */
option_a_votes_percentage?: number;
/** Percentage of votes for option B (0-1) */
option_b_votes_percentage?: number;
/** Percentage of votes for option C (0-1) */
option_c_votes_percentage?: number;
/** Percentage of votes for option D (0-1) */
option_d_votes_percentage?: number;
/** Total number of votes */
total_votes?: number;
/** Timestamp when the poll expires (ISO 8601) */
expiration_timestamp?: string;
}
/**
* Text attachment as returned by the API.
*/
export interface TextAttachment {
/** The plaintext content */
plaintext?: string;
/** Attached link URL */
link_attachment_url?: string;
/** Styled text with formatting info */
text_with_styling_info?: Array<{
/** Character offset */
offset: number;
/** Length of styled range */
length: number;
/** Styling type (e.g., "bold", "italic") */
styling_info: string[];
}>;
}
/**
* Location as returned by the API.
*/
export interface ThreadsLocation {
/** Location ID */
id: string;
/** Location name */
name?: string;
/** Street address */
address?: string;
/** City */
city?: string;
/** Country */
country?: string;
/** Latitude */
latitude?: number;
/** Longitude */
longitude?: number;
/** Postal code */
postal_code?: string;
}
/**
* Represents a single Threads media object.
* All fields use snake_case to match the API response format.
*/
export interface ThreadsPost {
/** Unique identifier for the media object */
id: string;
/** Type of product where the media is published (e.g., "THREADS") */
media_product_type?: string;
/** Type of media */
media_type?: ResponseMediaType;
/** URL of the media content */
media_url?: string;
/** Permanent link to the post */
permalink?: string;
/** Information about the owner of the post */
owner?: { id: string };
/** Username of the account that created the post */
username?: string;
/** Text content of the post */
text?: string;
/** Timestamp of when the post was created (ISO 8601 format) */
timestamp?: string;
/** Short code identifier for the media */
shortcode?: string;
/** URL of the thumbnail image (for video posts) */
thumbnail_url?: string;
/** List of child posts (for carousel posts) */
children?: { data: Array<{ id: string }> };
/** Indicates if the post is a quote of another post */
is_quote_post?: boolean;
/** Accessibility text for the image or video */
alt_text?: string;
/** URL of the attached link */
link_attachment_url?: string;
/** Indicates if the post has replies */
has_replies?: boolean;
/** Indicates if the post is a reply to another post */
is_reply?: boolean;
/** Indicates if the reply is owned by the current user */
is_reply_owned_by_me?: boolean;
/** Information about the root post (for replies) */
root_post?: { id: string };
/** Information about the post being replied to */
replied_to?: { id: string };
/** Visibility status of the post */
hide_status?:
| "NOT_HUSHED"
| "UNHUSHED"
| "HIDDEN"
| "COVERED"
| "BLOCKED"
| "RESTRICTED";
/** Controls who can reply to the post */
reply_audience?: ReplyControl;
/** The quoted post (for quote posts) */
quoted_post?: { id: string };
/** The reposted post (for reposts) */
reposted_post?: { id: string };
/** URL of the GIF */
gif_url?: string;
/** Poll attachment data */
poll_attachment?: PollAttachment;
/** Topic tag */
topic_tag?: string;
/** Whether the media is a spoiler */
is_spoiler_media?: boolean;
/** Text entity annotations */
text_entities?: TextEntity[];
/** Text attachment for long-form posts */
text_attachment?: TextAttachment;
/** Location ID */
location_id?: string;
/** Location data */
location?: ThreadsLocation;
/** Whether the user is verified */
is_verified?: boolean;
/** URL of the user's profile picture */
profile_picture_url?: string;
/** Ghost post status */
ghost_post_status?: "ACTIVE" | "ARCHIVED";
/** Ghost post expiration timestamp (ISO 8601) */
ghost_post_expiration_timestamp?: string;
/** List of country codes where the post is visible */
allowlisted_country_codes?: string[];
}
/**
* Represents the response structure when retrieving a list of Threads.
*/
export interface ThreadsListResponse {
/** Array of ThreadsPost representing the retrieved posts */
data: ThreadsPost[];
/** Pagination information */
paging?: {
/** Cursors for navigating through pages of results */
cursors: {
/** Cursor for the previous page */
before: string;
/** Cursor for the next page */
after: string;
};
};
}
/**
* Quota configuration.
*/
export interface QuotaConfig {
/** Total allowed quota */
quota_total: number;
/** Duration of the quota period in seconds */
quota_duration: number;
}
/**
* Represents the publishing limit information for a user.
*/
export interface PublishingLimit {
/** Current post quota usage */
quota_usage?: number;
/** Post quota configuration */
config?: QuotaConfig;
/** Current reply quota usage */
reply_quota_usage?: number;
/** Reply quota configuration */
reply_config?: QuotaConfig;
/** Current delete quota usage */
delete_quota_usage?: number;
/** Delete quota configuration */
delete_config?: QuotaConfig;
/** Current location search quota usage */
location_search_quota_usage?: number;
/** Location search quota configuration */
location_search_config?: QuotaConfig;
}
/**
* Represents a Threads media container.
*/
export interface ThreadsContainer {
/** Unique identifier for the container */
id: string;
/** Permanent link to the container */
permalink?: string;
/** Status of the container */
status?: "EXPIRED" | "ERROR" | "FINISHED" | "IN_PROGRESS" | "PUBLISHED";
/** Error message if the container failed */
error_message?: string;
}
/**
* Represents a Threads user profile (own profile via GET /me or GET /{user-id}).
*/
export interface ThreadsProfile {
/** Unique identifier for the user */
id: string;
/** Username of the account */
username?: string;
/** Display name of the user */
name?: string;
/** URL of the user's profile picture */
threads_profile_picture_url?: string;
/** Biography text of the user */
threads_biography?: string;
/** Whether the user is verified */
is_verified?: boolean;
/** Recently searched keywords */
recently_searched_keywords?: Array<{ query: string; timestamp: number }>;
/** Whether the user is eligible for geo-gating */
is_eligible_for_geo_gating?: boolean;
}
/**
* Represents a public profile as returned by profile_lookup.
*/
export interface PublicProfile {
/** Unique identifier */
id: string;
/** Username */
username?: string;
/** Display name */
name?: string;
/** Profile picture URL */
profile_picture_url?: string;
/** Biography */
biography?: string;
/** Whether the user is verified */
is_verified?: boolean;
/** Follower count */
follower_count?: number;
/** Likes count (past 7 days) */
likes_count?: number;
/** Quotes count (past 7 days) */
quotes_count?: number;
/** Replies count (past 7 days) */
replies_count?: number;
/** Reposts count (past 7 days) */
reposts_count?: number;
/** Views count (past 7 days) */
views_count?: number;
}
// ─── Insights Types ──────────────────────────────────────────────────────────
/**
* A single insight metric value.
*/
export interface InsightValue {
/** The metric value */
value: number | Record<string, number>;
/** End time for the period (ISO 8601) */
end_time?: string;
}
/**
* Media insight metric.
*/
export interface MediaInsight {
/** Metric name */
name: string;
/** Time period (e.g., "lifetime") */
period: string;
/** Metric values */
values: InsightValue[];
/** Human-readable title */
title: string;
/** Description of the metric */
description: string;
/** Unique identifier */
id: string;
}
/**
* Response from media insights endpoint.
*/
export interface MediaInsightsResponse {
/** Array of insight metrics */
data: MediaInsight[];
}
/**
* User insight metric (supports both time series and total value formats).
*/
export interface UserInsight {
/** Metric name */
name: string;
/** Time period (e.g., "day", "lifetime") */
period: string;
/** Metric values (time series) */
values: InsightValue[];
/** Human-readable title */
title: string;
/** Description of the metric */
description: string;
/** Unique identifier */
id: string;
/** Total value (for lifetime metrics) */
total_value?: { value: number | Record<string, number> };
/** Link total values (for clicks metric) */
link_total_values?: Array<{ value: number; link_url: string }>;
}
/**
* Response from user insights endpoint.
*/
export interface UserInsightsResponse {
/** Array of insight metrics */
data: UserInsight[];
}
/**
* Options for user insights request.
*/
export interface UserInsightsOptions {
/** Start of the time range (Unix timestamp, required for time series) */
since?: number;
/** End of the time range (Unix timestamp, required for time series) */
until?: number;
/** Breakdown dimension */
breakdown?: "country" | "city" | "age" | "gender";
}
// ─── Search Types ────────────────────────────────────────────────────────────
/**
* Options for keyword/tag search.
* Properties use snake_case to match the Threads API payload format directly.
*/
export interface KeywordSearchOptions extends PaginationOptions {
/** The search query string (required) */
q: string;
/** Search behavior: TOP (popular) or RECENT (chronological) */
search_type?: "TOP" | "RECENT";
/** Search mode: KEYWORD (default) or TAG (topic tag search) */
search_mode?: "KEYWORD" | "TAG";
/** Filter by media type */
media_type?: "TEXT" | "IMAGE" | "VIDEO";
/** Filter by author username */
author_username?: string;
}
/**
* Options for location search.
*/
export interface LocationSearchOptions {
/** Search query for location name */
query?: string;
/** Latitude for proximity search (must be used with longitude) */
latitude?: number;
/** Longitude for proximity search (must be used with latitude) */
longitude?: number;
}
// ─── Token Types ─────────────────────────────────────────────────────────────
/**
* Response from exchanging an OAuth authorization code for a short-lived token.
*/
export interface AuthCodeResponse {
/** The short-lived access token */
access_token: string;
/** The user ID of the authenticated user */
user_id: string;
}
/**
* Response from token exchange/refresh endpoints.
*/
export interface TokenResponse {
/** The access token */
access_token: string;
/** Token type (e.g., "bearer") */
token_type: string;
/** Expiration time in seconds */
expires_in?: number;
}
/**
* Response from debug_token endpoint.
*/
export interface DebugTokenInfo {
/** Token data */
data: {
/** Token type (e.g., "USER") */
type?: string;
/** Application name */
application?: string;
/** Data access expiration timestamp */
data_access_expires_at?: number;
/** Token expiration timestamp */
expires_at?: number;
/** Whether the token is valid */
is_valid?: boolean;
/** Issued timestamp */
issued_at?: number;
/** Granted scopes */
scopes?: string[];
/** User ID */
user_id?: string;
};
}
// ─── oEmbed Types ────────────────────────────────────────────────────────────
/**
* Response from the oEmbed endpoint.
*/
export interface OEmbedResponse {
/** HTML embed code */
html: string;
/** Provider name (e.g., "Threads") */
provider_name?: string;
/** Provider URL */
provider_url?: string;
/** oEmbed type (e.g., "rich") */
type?: string;
/** oEmbed version */
version?: string;
/** Width of the embed */
width?: number;
}
// ─── Webhook Types ───────────────────────────────────────────────────────────
/**
* Base webhook payload structure.
*/
export interface WebhookPayload {
/** The Threads App ID */
app_id: string;
/** Webhook topic ("moderate" or "interaction") */
topic: "moderate" | "interaction";
/** Target ID (media ID or mentioned user ID) */
target_id: string;
/** Timestamp when the notification was sent */
time: number;
/** Subscription ID */
subscription_id: string;
/** Whether the payload has a UID field */
has_uid_field: boolean;
/** Webhook values */
values: {
/** The webhook data */
value: Record<string, unknown>;
/** The subscribed field name */
field: "replies" | "mentions" | "delete" | "publish";
};
}
/**
* Reply webhook payload values.
*/
export interface WebhookReplyValue {
/** Reply media ID */
id: string;
/** Username of the replier */
username: string;
/** Reply text content */
text?: string;
/** Media type */
media_type: string;
/** Permalink to the reply */
permalink: string;
/** The post being replied to */
replied_to?: { id: string };
/** The root post of the conversation */
root_post?: {
id: string;
owner_id?: string;
username?: string;
};
/** Short code */
shortcode: string;
/** Timestamp (ISO 8601) */
timestamp: string;
/** Whether the user is verified */
is_verified?: boolean;
/** Profile picture URL */
profile_picture_url?: string;
}
/**
* Mention webhook payload values.
*/
export interface WebhookMentionValue {
/** Media ID */
id: string;
/** Alt text */
alt_text?: string;
/** GIF URL */
gif_url?: string;
/** Whether the post has replies */
has_replies?: boolean;
/** Whether it's a quote post */
is_quote_post?: boolean;
/** Whether it's a reply */
is_reply?: boolean;
/** Media product type */
media_product_type?: string;
/** Media type */
media_type: string;
/** Permalink */
permalink: string;
/** Short code */
shortcode?: string;
/** Text content */
text?: string;
/** Timestamp (ISO 8601) */
timestamp: string;
/** Username */
username: string;
/** Whether the user is verified */
is_verified?: boolean;
/** Profile picture URL */
profile_picture_url?: string;
}
/**
* Delete webhook payload values.
*/
export interface WebhookDeleteValue {
/** Deleted media ID */
id: string;
/** Owner information */
owner?: { owner_id: string };
/** When the post was deleted (ISO 8601) */
deleted_at: string;
/** When the post was originally published (ISO 8601) */
timestamp: string;
/** Username */
username: string;
}
/**
* Publish webhook payload values.
*/
export interface WebhookPublishValue {
/** Published media ID */
id: string;
/** Media type */
media_type: string;
/** Permalink */
permalink: string;
/** Timestamp (ISO 8601) */
timestamp: string;
/** Username */
username: string;
}
// ─── Mock API Interface ──────────────────────────────────────────────────────
/**
* Represents the mock API for Threads operations.
*/
export interface MockThreadsAPI {
// Existing methods
createThreadsContainer(
request: ThreadsPostRequest,
): Promise<string>;
publishThreadsContainer(
userId: string,
accessToken: string,
containerId: string,
getPermalink?: boolean,
): Promise<string | { id: string; permalink: string }>;
createCarouselItem(
request: Omit<ThreadsPostRequest, "mediaType"> & {
mediaType: "IMAGE" | "VIDEO";
},
): Promise<string>;
getPublishingLimit(
userId: string,
accessToken: string,
fields?: string[],
): Promise<PublishingLimit>;
getThreadsList(
userId: string,
accessToken: string,
options?: PaginationOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
getSingleThread(
mediaId: string,
accessToken: string,
fields?: string[],
): Promise<ThreadsPost>;
// New methods
repost(
mediaId: string,
accessToken: string,
): Promise<{ id: string }>;
deleteThread(
mediaId: string,
accessToken: string,
): Promise<{ success: boolean; deleted_id?: string }>;
getProfile(
userId: string,
accessToken: string,
fields?: string[],
): Promise<ThreadsProfile>;
lookupProfile(
accessToken: string,
username: string,
fields?: string[],
): Promise<PublicProfile>;
getProfilePosts(
accessToken: string,
username: string,
options?: PaginationOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
getGhostPosts(
userId: string,
accessToken: string,
options?: PaginationOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
getUserReplies(
userId: string,
accessToken: string,
options?: PaginationOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
getReplies(
mediaId: string,
accessToken: string,
options?: CursorPaginationOptions,
fields?: string[],
reverse?: boolean,
): Promise<ThreadsListResponse>;
getConversation(
mediaId: string,
accessToken: string,
options?: CursorPaginationOptions,
fields?: string[],
reverse?: boolean,
): Promise<ThreadsListResponse>;
manageReply(
replyId: string,
accessToken: string,
hide: boolean,
): Promise<{ success: boolean }>;
getMentions(
userId: string,
accessToken: string,
options?: PaginationOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
getMediaInsights(
mediaId: string,
accessToken: string,
metrics: string[],
): Promise<MediaInsightsResponse>;
getUserInsights(
userId: string,
accessToken: string,
metrics: string[],
options?: UserInsightsOptions,
): Promise<UserInsightsResponse>;
searchKeyword(
accessToken: string,
options: KeywordSearchOptions,
fields?: string[],
): Promise<ThreadsListResponse>;
searchLocations(
accessToken: string,
options: LocationSearchOptions,
fields?: string[],
): Promise<{ data: ThreadsLocation[] }>;
getLocation(
locationId: string,
accessToken: string,
fields?: string[],
): Promise<ThreadsLocation>;
exchangeCodeForToken(
clientId: string,
clientSecret: string,
code: string,
redirectUri: string,
): Promise<AuthCodeResponse>;
getAppAccessToken(
clientId: string,
clientSecret: string,
): Promise<TokenResponse>;
exchangeToken(
clientSecret: string,
accessToken: string,
): Promise<TokenResponse>;
refreshToken(
accessToken: string,
): Promise<TokenResponse>;
debugToken(
accessToken: string,
inputToken: string,
): Promise<DebugTokenInfo>;
getOEmbed(
accessToken: string,
url: string,
maxWidth?: number,
): Promise<OEmbedResponse>;
}