@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.
- package/.github/workflows/publish.yml +17 -7
- package/.vscode/settings.json +34 -9
- package/CHANGELOG.md +128 -0
- package/deno.json +22 -8
- package/deno.lock +17 -59
- package/examples/edge-function.ts +171 -177
- package/mod.ts +138 -650
- package/mod_test.ts +1287 -380
- package/package.json +22 -22
- package/readme.md +155 -191
- package/src/api/createCarouselItem.ts +86 -0
- package/src/api/createThreadsContainer.ts +122 -0
- package/src/api/debugToken.ts +35 -0
- package/src/api/deleteThread.ts +36 -0
- package/src/api/exchangeCodeForToken.ts +50 -0
- package/src/api/exchangeToken.ts +36 -0
- package/src/api/getAppAccessToken.ts +35 -0
- package/src/api/getConversation.ts +51 -0
- package/src/api/getGhostPosts.ts +50 -0
- package/src/api/getLocation.ts +38 -0
- package/src/api/getMediaInsights.ts +39 -0
- package/src/api/getMentions.ts +57 -0
- package/src/api/getOEmbed.ts +41 -0
- package/src/api/getProfile.ts +46 -0
- package/src/api/getProfilePosts.ts +53 -0
- package/src/api/getPublishingLimit.ts +59 -0
- package/src/api/getReplies.ts +51 -0
- package/src/api/getSingleThread.ts +37 -0
- package/src/api/getThreadsList.ts +49 -0
- package/src/api/getUserInsights.ts +54 -0
- package/src/api/getUserReplies.ts +54 -0
- package/src/api/lookupProfile.ts +53 -0
- package/src/api/manageReply.ts +41 -0
- package/src/api/publishThreadsContainer.ts +107 -0
- package/src/api/refreshToken.ts +33 -0
- package/src/api/repost.ts +38 -0
- package/src/api/searchKeyword.ts +86 -0
- package/src/api/searchLocations.ts +46 -0
- package/src/constants.ts +80 -0
- package/src/types.ts +925 -0
- package/src/utils/checkContainerStatus.ts +39 -0
- package/src/utils/getAPI.ts +13 -0
- package/src/utils/mock_threads_api.ts +582 -0
- package/src/utils/validateRequest.ts +166 -0
- package/mock_threads_api.ts +0 -174
- 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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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;
|