@codybrom/denim 1.3.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +17 -7
  2. package/.vscode/settings.json +34 -9
  3. package/CHANGELOG.md +128 -0
  4. package/deno.json +22 -8
  5. package/deno.lock +17 -59
  6. package/examples/edge-function.ts +171 -177
  7. package/mod.ts +138 -650
  8. package/mod_test.ts +1287 -380
  9. package/package.json +22 -22
  10. package/readme.md +155 -191
  11. package/src/api/createCarouselItem.ts +86 -0
  12. package/src/api/createThreadsContainer.ts +122 -0
  13. package/src/api/debugToken.ts +35 -0
  14. package/src/api/deleteThread.ts +36 -0
  15. package/src/api/exchangeCodeForToken.ts +50 -0
  16. package/src/api/exchangeToken.ts +36 -0
  17. package/src/api/getAppAccessToken.ts +35 -0
  18. package/src/api/getConversation.ts +51 -0
  19. package/src/api/getGhostPosts.ts +50 -0
  20. package/src/api/getLocation.ts +38 -0
  21. package/src/api/getMediaInsights.ts +39 -0
  22. package/src/api/getMentions.ts +57 -0
  23. package/src/api/getOEmbed.ts +41 -0
  24. package/src/api/getProfile.ts +46 -0
  25. package/src/api/getProfilePosts.ts +53 -0
  26. package/src/api/getPublishingLimit.ts +59 -0
  27. package/src/api/getReplies.ts +51 -0
  28. package/src/api/getSingleThread.ts +37 -0
  29. package/src/api/getThreadsList.ts +49 -0
  30. package/src/api/getUserInsights.ts +54 -0
  31. package/src/api/getUserReplies.ts +54 -0
  32. package/src/api/lookupProfile.ts +53 -0
  33. package/src/api/manageReply.ts +41 -0
  34. package/src/api/publishThreadsContainer.ts +107 -0
  35. package/src/api/refreshToken.ts +33 -0
  36. package/src/api/repost.ts +38 -0
  37. package/src/api/searchKeyword.ts +86 -0
  38. package/src/api/searchLocations.ts +46 -0
  39. package/src/constants.ts +80 -0
  40. package/src/types.ts +925 -0
  41. package/src/utils/checkContainerStatus.ts +39 -0
  42. package/src/utils/getAPI.ts +13 -0
  43. package/src/utils/mock_threads_api.ts +582 -0
  44. package/src/utils/validateRequest.ts +166 -0
  45. package/mock_threads_api.ts +0 -174
  46. package/types.ts +0 -235
@@ -0,0 +1,37 @@
1
+ import { SINGLE_THREAD_FIELDS, THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { ThreadsPost } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ /**
6
+ * Retrieves a single Threads media object.
7
+ *
8
+ * @param mediaId - The ID of the Threads media object
9
+ * @param accessToken - The access token for authentication
10
+ * @param fields - Optional array of fields to return (defaults to all available fields)
11
+ * @returns A Promise that resolves to the ThreadsPost object
12
+ * @throws Will throw an error if the API request fails
13
+ */
14
+ export async function getSingleThread(
15
+ mediaId: string,
16
+ accessToken: string,
17
+ fields?: string[],
18
+ ): Promise<ThreadsPost> {
19
+ const api = getAPI();
20
+ if (api) {
21
+ return api.getSingleThread(mediaId, accessToken, fields);
22
+ }
23
+ const fieldList = (fields ?? SINGLE_THREAD_FIELDS).join(",");
24
+ const url = new URL(`${THREADS_API_BASE_URL}/${mediaId}`);
25
+ url.searchParams.append("fields", fieldList);
26
+ url.searchParams.append("access_token", accessToken);
27
+
28
+ const response = await fetch(url.toString());
29
+ if (!response.ok) {
30
+ const errorBody = await response.text();
31
+ throw new Error(
32
+ `Failed to retrieve thread (${response.status}): ${errorBody}`,
33
+ );
34
+ }
35
+
36
+ return await response.json();
37
+ }
@@ -0,0 +1,49 @@
1
+ import { THREADS_API_BASE_URL, USER_THREADS_FIELDS } from "../constants.ts";
2
+ import type { PaginationOptions, ThreadsListResponse } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ /**
6
+ * Retrieves a list of all threads created by a user.
7
+ *
8
+ * @param userId - The user ID of the Threads account
9
+ * @param accessToken - The access token for authentication
10
+ * @param options - Optional parameters for pagination and date range
11
+ * @param fields - Optional array of fields to return (defaults to all available fields)
12
+ * @returns A Promise that resolves to the ThreadsListResponse
13
+ * @throws Will throw an error if the API request fails
14
+ */
15
+ export async function getThreadsList(
16
+ userId: string,
17
+ accessToken: string,
18
+ options?: PaginationOptions,
19
+ fields?: string[],
20
+ ): Promise<ThreadsListResponse> {
21
+ const api = getAPI();
22
+ if (api) {
23
+ return api.getThreadsList(userId, accessToken, options, fields);
24
+ }
25
+ const fieldList = (fields ?? USER_THREADS_FIELDS).join(",");
26
+ const url = new URL(`${THREADS_API_BASE_URL}/${userId}/threads`);
27
+ url.searchParams.append("fields", fieldList);
28
+ url.searchParams.append("access_token", accessToken);
29
+
30
+ if (options) {
31
+ if (options.since) url.searchParams.append("since", String(options.since));
32
+ if (options.until) url.searchParams.append("until", String(options.until));
33
+ if (options.limit) {
34
+ url.searchParams.append("limit", options.limit.toString());
35
+ }
36
+ if (options.after) url.searchParams.append("after", options.after);
37
+ if (options.before) url.searchParams.append("before", options.before);
38
+ }
39
+
40
+ const response = await fetch(url.toString());
41
+ if (!response.ok) {
42
+ const errorBody = await response.text();
43
+ throw new Error(
44
+ `Failed to retrieve threads list (${response.status}): ${errorBody}`,
45
+ );
46
+ }
47
+
48
+ return await response.json();
49
+ }
@@ -0,0 +1,54 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { UserInsightsOptions, UserInsightsResponse } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ /**
6
+ * Retrieves insight metrics for a Threads user.
7
+ *
8
+ * Available metrics: views, likes, replies, reposts, quotes, clicks,
9
+ * followers_count, follower_demographics
10
+ *
11
+ * @param userId - The user ID of the Threads account
12
+ * @param accessToken - The access token for authentication
13
+ * @param metrics - Array of metric names to retrieve
14
+ * @param options - Optional parameters for time range and breakdown
15
+ * @returns A Promise that resolves to the UserInsightsResponse
16
+ * @throws Will throw an error if the API request fails
17
+ */
18
+ export async function getUserInsights(
19
+ userId: string,
20
+ accessToken: string,
21
+ metrics: string[],
22
+ options?: UserInsightsOptions,
23
+ ): Promise<UserInsightsResponse> {
24
+ const api = getAPI();
25
+ if (api) {
26
+ return api.getUserInsights(userId, accessToken, metrics, options);
27
+ }
28
+
29
+ const url = new URL(`${THREADS_API_BASE_URL}/${userId}/threads_insights`);
30
+ url.searchParams.append("metric", metrics.join(","));
31
+ url.searchParams.append("access_token", accessToken);
32
+
33
+ if (options) {
34
+ if (options.since !== undefined) {
35
+ url.searchParams.append("since", options.since.toString());
36
+ }
37
+ if (options.until !== undefined) {
38
+ url.searchParams.append("until", options.until.toString());
39
+ }
40
+ if (options.breakdown) {
41
+ url.searchParams.append("breakdown", options.breakdown);
42
+ }
43
+ }
44
+
45
+ const response = await fetch(url.toString());
46
+ if (!response.ok) {
47
+ const errorBody = await response.text();
48
+ throw new Error(
49
+ `Failed to get user insights (${response.status}): ${errorBody}`,
50
+ );
51
+ }
52
+
53
+ return await response.json();
54
+ }
@@ -0,0 +1,54 @@
1
+ import { REPLY_FIELDS, THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { PaginationOptions, ThreadsListResponse } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ const DEFAULT_FIELDS = REPLY_FIELDS.filter(
6
+ (f) => f !== "hide_status" && f !== "topic_tag",
7
+ );
8
+
9
+ /**
10
+ * Retrieves a list of all replies created by a user.
11
+ *
12
+ * @param userId - The user ID of the Threads account
13
+ * @param accessToken - The access token for authentication
14
+ * @param options - Optional pagination parameters
15
+ * @param fields - Optional array of fields to return
16
+ * @returns A Promise that resolves to the ThreadsListResponse
17
+ * @throws Will throw an error if the API request fails
18
+ */
19
+ export async function getUserReplies(
20
+ userId: string,
21
+ accessToken: string,
22
+ options?: PaginationOptions,
23
+ fields?: string[],
24
+ ): Promise<ThreadsListResponse> {
25
+ const api = getAPI();
26
+ if (api) {
27
+ return api.getUserReplies(userId, accessToken, options, fields);
28
+ }
29
+
30
+ const fieldList = (fields ?? DEFAULT_FIELDS).join(",");
31
+ const url = new URL(`${THREADS_API_BASE_URL}/${userId}/replies`);
32
+ url.searchParams.append("fields", fieldList);
33
+ url.searchParams.append("access_token", accessToken);
34
+
35
+ if (options) {
36
+ if (options.since) url.searchParams.append("since", String(options.since));
37
+ if (options.until) url.searchParams.append("until", String(options.until));
38
+ if (options.limit) {
39
+ url.searchParams.append("limit", options.limit.toString());
40
+ }
41
+ if (options.after) url.searchParams.append("after", options.after);
42
+ if (options.before) url.searchParams.append("before", options.before);
43
+ }
44
+
45
+ const response = await fetch(url.toString());
46
+ if (!response.ok) {
47
+ const errorBody = await response.text();
48
+ throw new Error(
49
+ `Failed to get user replies (${response.status}): ${errorBody}`,
50
+ );
51
+ }
52
+
53
+ return await response.json();
54
+ }
@@ -0,0 +1,53 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { PublicProfile } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ const DEFAULT_FIELDS = [
6
+ "username",
7
+ "name",
8
+ "profile_picture_url",
9
+ "biography",
10
+ "is_verified",
11
+ "follower_count",
12
+ "likes_count",
13
+ "quotes_count",
14
+ "replies_count",
15
+ "reposts_count",
16
+ "views_count",
17
+ ];
18
+
19
+ /**
20
+ * Looks up a public Threads profile by username.
21
+ *
22
+ * @param accessToken - The access token for authentication
23
+ * @param username - The exact username to look up
24
+ * @param fields - Optional array of fields to return
25
+ * @returns A Promise that resolves to the PublicProfile object
26
+ * @throws Will throw an error if the API request fails
27
+ */
28
+ export async function lookupProfile(
29
+ accessToken: string,
30
+ username: string,
31
+ fields?: string[],
32
+ ): Promise<PublicProfile> {
33
+ const api = getAPI();
34
+ if (api) {
35
+ return api.lookupProfile(accessToken, username, fields);
36
+ }
37
+
38
+ const fieldList = (fields ?? DEFAULT_FIELDS).join(",");
39
+ const url = new URL(`${THREADS_API_BASE_URL}/profile_lookup`);
40
+ url.searchParams.append("username", username);
41
+ url.searchParams.append("fields", fieldList);
42
+ url.searchParams.append("access_token", accessToken);
43
+
44
+ const response = await fetch(url.toString());
45
+ if (!response.ok) {
46
+ const errorBody = await response.text();
47
+ throw new Error(
48
+ `Failed to look up profile (${response.status}): ${errorBody}`,
49
+ );
50
+ }
51
+
52
+ return await response.json();
53
+ }
@@ -0,0 +1,41 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import { getAPI } from "../utils/getAPI.ts";
3
+
4
+ /**
5
+ * Hides or unhides a reply on a Threads post.
6
+ *
7
+ * @param replyId - The ID of the reply to manage
8
+ * @param accessToken - The access token for authentication
9
+ * @param hide - Whether to hide (true) or unhide (false) the reply
10
+ * @returns A Promise that resolves to an object indicating success
11
+ * @throws Will throw an error if the API request fails
12
+ */
13
+ export async function manageReply(
14
+ replyId: string,
15
+ accessToken: string,
16
+ hide: boolean,
17
+ ): Promise<{ success: boolean }> {
18
+ const api = getAPI();
19
+ if (api) {
20
+ return api.manageReply(replyId, accessToken, hide);
21
+ }
22
+
23
+ const url = `${THREADS_API_BASE_URL}/${replyId}/manage_reply`;
24
+ const body = new URLSearchParams({
25
+ access_token: accessToken,
26
+ hide: String(hide),
27
+ });
28
+
29
+ const response = await fetch(url, {
30
+ method: "POST",
31
+ body: body,
32
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const responseText = await response.text();
37
+ throw new Error(`Failed to manage reply: ${responseText}`);
38
+ }
39
+
40
+ return await response.json();
41
+ }
@@ -0,0 +1,107 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import { checkContainerStatus } from "../utils/checkContainerStatus.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+ import { getSingleThread } from "./getSingleThread.ts";
5
+ /**
6
+ * Publishes a Threads media container.
7
+ *
8
+ * @param userId - The user ID of the Threads account
9
+ * @param accessToken - The access token for authentication
10
+ * @param containerId - The ID of the container to publish
11
+ * @returns A Promise that resolves to the published container ID
12
+ * @throws Will throw an error if the API request fails or if publishing times out
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const publishedId = await publishThreadsContainer("123456", "your_access_token", "container_id");
17
+ * ```
18
+ */
19
+ export async function publishThreadsContainer(
20
+ userId: string,
21
+ accessToken: string,
22
+ containerId: string,
23
+ getPermalink: boolean = false,
24
+ ): Promise<string | { id: string; permalink: string }> {
25
+ const api = getAPI();
26
+ if (api) {
27
+ return api.publishThreadsContainer(
28
+ userId,
29
+ accessToken,
30
+ containerId,
31
+ getPermalink,
32
+ );
33
+ }
34
+ try {
35
+ // Wait 30 seconds before first status check (per API docs recommendation)
36
+ await new Promise((resolve) => setTimeout(resolve, 30_000));
37
+
38
+ // Poll container status once per minute, for up to 5 minutes (per API docs)
39
+ let result = await checkContainerStatus(containerId, accessToken);
40
+ let attempts = 0;
41
+ const maxAttempts = 5;
42
+ const pollInterval = 60_000; // 1 minute
43
+
44
+ while (
45
+ result.status !== "FINISHED" &&
46
+ attempts < maxAttempts
47
+ ) {
48
+ if (result.status === "ERROR" || result.status === "EXPIRED") {
49
+ throw new Error(
50
+ `Container cannot be published. Status: ${result.status}${
51
+ result.error_message ? ` - ${result.error_message}` : ""
52
+ }`,
53
+ );
54
+ }
55
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
56
+ result = await checkContainerStatus(containerId, accessToken);
57
+ attempts++;
58
+ }
59
+
60
+ if (result.status !== "FINISHED") {
61
+ throw new Error(
62
+ `Container not ready after ${maxAttempts} attempts. Current status: ${result.status}`,
63
+ );
64
+ }
65
+
66
+ // Publish the container
67
+ const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
68
+ const publishBody = new URLSearchParams({
69
+ access_token: accessToken,
70
+ creation_id: containerId,
71
+ });
72
+
73
+ const publishResponse = await fetch(publishUrl, {
74
+ method: "POST",
75
+ body: publishBody,
76
+ headers: {
77
+ "Content-Type": "application/x-www-form-urlencoded",
78
+ },
79
+ });
80
+
81
+ if (!publishResponse.ok) {
82
+ const errorBody = await publishResponse.text();
83
+ throw new Error(
84
+ `Failed to publish Threads container (${publishResponse.status}): ${errorBody}`,
85
+ );
86
+ }
87
+
88
+ const publishData = await publishResponse.json();
89
+
90
+ if (getPermalink) {
91
+ const threadData = await getSingleThread(publishData.id, accessToken);
92
+ return {
93
+ id: publishData.id,
94
+ permalink: threadData.permalink || "",
95
+ };
96
+ }
97
+
98
+ return publishData.id;
99
+ } catch (error) {
100
+ if (error instanceof Error) {
101
+ throw error;
102
+ }
103
+ throw new Error(
104
+ `Failed to publish Threads container: ${String(error)}`,
105
+ );
106
+ }
107
+ }
@@ -0,0 +1,33 @@
1
+ import { THREADS_OAUTH_BASE_URL } from "../constants.ts";
2
+ import type { TokenResponse } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ /**
6
+ * Refreshes a long-lived access token.
7
+ *
8
+ * @param accessToken - The long-lived access token to refresh
9
+ * @returns A Promise that resolves to the TokenResponse with the refreshed token
10
+ * @throws Will throw an error if the API request fails
11
+ */
12
+ export async function refreshToken(
13
+ accessToken: string,
14
+ ): Promise<TokenResponse> {
15
+ const api = getAPI();
16
+ if (api) {
17
+ return api.refreshToken(accessToken);
18
+ }
19
+
20
+ const url = new URL(`${THREADS_OAUTH_BASE_URL}/refresh_access_token`);
21
+ url.searchParams.append("grant_type", "th_refresh_token");
22
+ url.searchParams.append("access_token", accessToken);
23
+
24
+ const response = await fetch(url.toString());
25
+ if (!response.ok) {
26
+ const errorBody = await response.text();
27
+ throw new Error(
28
+ `Failed to refresh token (${response.status}): ${errorBody}`,
29
+ );
30
+ }
31
+
32
+ return await response.json();
33
+ }
@@ -0,0 +1,38 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import { getAPI } from "../utils/getAPI.ts";
3
+
4
+ /**
5
+ * Reposts a previously published Threads post.
6
+ *
7
+ * @param mediaId - The ID of the Threads media to repost
8
+ * @param accessToken - The access token for authentication
9
+ * @returns A Promise that resolves to an object with the repost ID
10
+ * @throws Will throw an error if the API request fails
11
+ */
12
+ export async function repost(
13
+ mediaId: string,
14
+ accessToken: string,
15
+ ): Promise<{ id: string }> {
16
+ const api = getAPI();
17
+ if (api) {
18
+ return api.repost(mediaId, accessToken);
19
+ }
20
+
21
+ const url = `${THREADS_API_BASE_URL}/${mediaId}/repost`;
22
+ const body = new URLSearchParams({
23
+ access_token: accessToken,
24
+ });
25
+
26
+ const response = await fetch(url, {
27
+ method: "POST",
28
+ body: body,
29
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const responseText = await response.text();
34
+ throw new Error(`Failed to repost: ${responseText}`);
35
+ }
36
+
37
+ return await response.json();
38
+ }
@@ -0,0 +1,86 @@
1
+ import { THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { KeywordSearchOptions, ThreadsListResponse } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ const DEFAULT_FIELDS = [
6
+ "id",
7
+ "media_product_type",
8
+ "media_type",
9
+ "media_url",
10
+ "permalink",
11
+ "username",
12
+ "text",
13
+ "timestamp",
14
+ "shortcode",
15
+ "thumbnail_url",
16
+ "children",
17
+ "is_quote_post",
18
+ "alt_text",
19
+ "link_attachment_url",
20
+ "has_replies",
21
+ "is_reply",
22
+ "root_post",
23
+ "replied_to",
24
+ "reply_audience",
25
+ "quoted_post",
26
+ "reposted_post",
27
+ "gif_url",
28
+ "poll_attachment",
29
+ "topic_tag",
30
+ ];
31
+
32
+ /**
33
+ * Searches for Threads posts by keyword or topic tag.
34
+ *
35
+ * @param accessToken - The access token for authentication
36
+ * @param options - Search options including query string and filters
37
+ * @param fields - Optional array of fields to return
38
+ * @returns A Promise that resolves to the ThreadsListResponse
39
+ * @throws Will throw an error if the API request fails
40
+ */
41
+ export async function searchKeyword(
42
+ accessToken: string,
43
+ options: KeywordSearchOptions,
44
+ fields?: string[],
45
+ ): Promise<ThreadsListResponse> {
46
+ const api = getAPI();
47
+ if (api) {
48
+ return api.searchKeyword(accessToken, options, fields);
49
+ }
50
+
51
+ const fieldList = (fields ?? DEFAULT_FIELDS).join(",");
52
+ const url = new URL(`${THREADS_API_BASE_URL}/keyword_search`);
53
+ url.searchParams.append("q", options.q);
54
+ url.searchParams.append("fields", fieldList);
55
+ url.searchParams.append("access_token", accessToken);
56
+
57
+ if (options.search_type) {
58
+ url.searchParams.append("search_type", options.search_type);
59
+ }
60
+ if (options.search_mode) {
61
+ url.searchParams.append("search_mode", options.search_mode);
62
+ }
63
+ if (options.media_type) {
64
+ url.searchParams.append("media_type", options.media_type);
65
+ }
66
+ if (options.author_username) {
67
+ url.searchParams.append("author_username", options.author_username);
68
+ }
69
+ if (options.since) url.searchParams.append("since", String(options.since));
70
+ if (options.until) url.searchParams.append("until", String(options.until));
71
+ if (options.limit) {
72
+ url.searchParams.append("limit", options.limit.toString());
73
+ }
74
+ if (options.after) url.searchParams.append("after", options.after);
75
+ if (options.before) url.searchParams.append("before", options.before);
76
+
77
+ const response = await fetch(url.toString());
78
+ if (!response.ok) {
79
+ const errorBody = await response.text();
80
+ throw new Error(
81
+ `Failed to search keywords (${response.status}): ${errorBody}`,
82
+ );
83
+ }
84
+
85
+ return await response.json();
86
+ }
@@ -0,0 +1,46 @@
1
+ import { LOCATION_FIELDS, THREADS_API_BASE_URL } from "../constants.ts";
2
+ import type { LocationSearchOptions, ThreadsLocation } from "../types.ts";
3
+ import { getAPI } from "../utils/getAPI.ts";
4
+
5
+ /**
6
+ * Searches for locations on Threads.
7
+ *
8
+ * @param accessToken - The access token for authentication
9
+ * @param options - Search options including query and coordinates
10
+ * @param fields - Optional array of fields to return
11
+ * @returns A Promise that resolves to an array of locations
12
+ * @throws Will throw an error if the API request fails
13
+ */
14
+ export async function searchLocations(
15
+ accessToken: string,
16
+ options: LocationSearchOptions,
17
+ fields?: string[],
18
+ ): Promise<{ data: ThreadsLocation[] }> {
19
+ const api = getAPI();
20
+ if (api) {
21
+ return api.searchLocations(accessToken, options, fields);
22
+ }
23
+
24
+ const fieldList = (fields ?? LOCATION_FIELDS).join(",");
25
+ const url = new URL(`${THREADS_API_BASE_URL}/location_search`);
26
+ url.searchParams.append("access_token", accessToken);
27
+ url.searchParams.append("fields", fieldList);
28
+
29
+ if (options.query) url.searchParams.append("query", options.query);
30
+ if (options.latitude !== undefined) {
31
+ url.searchParams.append("latitude", options.latitude.toString());
32
+ }
33
+ if (options.longitude !== undefined) {
34
+ url.searchParams.append("longitude", options.longitude.toString());
35
+ }
36
+
37
+ const response = await fetch(url.toString());
38
+ if (!response.ok) {
39
+ const errorBody = await response.text();
40
+ throw new Error(
41
+ `Failed to search locations (${response.status}): ${errorBody}`,
42
+ );
43
+ }
44
+
45
+ return await response.json();
46
+ }
@@ -0,0 +1,80 @@
1
+ /** The base URL for the Threads API */
2
+ export const THREADS_API_BASE_URL = "https://graph.threads.net/v1.0";
3
+
4
+ /** The base URL for OAuth/token endpoints (no version prefix) */
5
+ export const THREADS_OAUTH_BASE_URL = "https://graph.threads.net";
6
+
7
+ /** Fields common to all media post endpoints */
8
+ const MEDIA_BASE_FIELDS = [
9
+ "id",
10
+ "media_product_type",
11
+ "media_type",
12
+ "media_url",
13
+ "permalink",
14
+ "username",
15
+ "text",
16
+ "timestamp",
17
+ "shortcode",
18
+ "thumbnail_url",
19
+ "children",
20
+ "is_quote_post",
21
+ "has_replies",
22
+ "is_verified",
23
+ "profile_picture_url",
24
+ ] as const;
25
+
26
+ /** Fields for own-post retrieval (GET /{user-id}/threads) */
27
+ export const USER_THREADS_FIELDS = [
28
+ ...MEDIA_BASE_FIELDS,
29
+ "owner",
30
+ "alt_text",
31
+ "link_attachment_url",
32
+ "reply_audience",
33
+ "quoted_post",
34
+ "reposted_post",
35
+ "gif_url",
36
+ "poll_attachment",
37
+ "topic_tag",
38
+ "is_spoiler_media",
39
+ "text_entities",
40
+ "text_attachment",
41
+ "ghost_post_status",
42
+ "ghost_post_expiration_timestamp",
43
+ ] as const;
44
+
45
+ /** Fields for single thread retrieval (GET /{media-id}) */
46
+ export const SINGLE_THREAD_FIELDS = [
47
+ ...USER_THREADS_FIELDS,
48
+ "is_reply",
49
+ "is_reply_owned_by_me",
50
+ "root_post",
51
+ "replied_to",
52
+ "hide_status",
53
+ ] as const;
54
+
55
+ /** Fields for reply/conversation endpoints */
56
+ export const REPLY_FIELDS = [
57
+ ...MEDIA_BASE_FIELDS,
58
+ "root_post",
59
+ "replied_to",
60
+ "is_reply",
61
+ "is_reply_owned_by_me",
62
+ "hide_status",
63
+ "reply_audience",
64
+ "quoted_post",
65
+ "reposted_post",
66
+ "gif_url",
67
+ "topic_tag",
68
+ ] as const;
69
+
70
+ /** Fields for location endpoints */
71
+ export const LOCATION_FIELDS = [
72
+ "id",
73
+ "name",
74
+ "address",
75
+ "city",
76
+ "country",
77
+ "latitude",
78
+ "longitude",
79
+ "postal_code",
80
+ ] as const;