Skip to content

Commit

Permalink
v1.3.5
Browse files Browse the repository at this point in the history
  • Loading branch information
codybrom committed Sep 23, 2024
1 parent 1dcb73a commit f76e53e
Show file tree
Hide file tree
Showing 8 changed files with 1,171 additions and 637 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codybrom/denim",
"version": "1.3.4",
"version": "1.3.5",
"description": "A Deno function for posting to Threads.",
"entry": "./mod.ts",
"exports": {
Expand Down
41 changes: 40 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 56 additions & 20 deletions examples/edge-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
publishThreadsContainer,
createCarouselItem,
getPublishingLimit,
} from "jsr:@codybrom/denim@1.3.0";
} from "jsr:@codybrom/denim@1.3.5";

async function postToThreads(request: ThreadsPostRequest): Promise<string> {
async function postToThreads(
request: ThreadsPostRequest
): Promise<{ id: string; permalink: string }> {
try {
// Check rate limit
const rateLimit = await getPublishingLimit(
Expand All @@ -23,17 +25,40 @@ async function postToThreads(request: ThreadsPostRequest): Promise<string> {
delete request.imageUrl;
}

const containerId = await createThreadsContainer(request);
console.log(`Container created with ID: ${containerId}`);
const containerResult = await createThreadsContainer(request);
console.log(
`Container created with ID: ${
typeof containerResult === "string"
? containerResult
: containerResult.id
}`
);

const publishedId = await publishThreadsContainer(
const publishedResult = await publishThreadsContainer(
request.userId,
request.accessToken,
containerId
typeof containerResult === "string"
? containerResult
: containerResult.id,
true // Get permalink
);

console.log(
`Post published with ID: ${
typeof publishedResult === "string"
? publishedResult
: publishedResult.id
}`
);
console.log(`Post published with ID: ${publishedId}`);

return publishedId;
if (typeof publishedResult === "string") {
return { id: publishedResult, permalink: "" };
}

return {
id: publishedResult.id,
permalink: publishedResult.permalink,
};
} catch (error) {
console.error("Error posting to Threads:", error);
throw error;
Expand Down Expand Up @@ -102,7 +127,6 @@ Deno.serve(async (req: Request) => {
videoUrl: body.videoUrl,
altText: body.altText,
linkAttachment: body.linkAttachment,
allowlistedCountryCodes: body.allowlistedCountryCodes,
replyControl: body.replyControl,
children: body.children,
};
Expand All @@ -117,14 +141,23 @@ Deno.serve(async (req: Request) => {
videoUrl: item.videoUrl,
altText: item.altText,
});
postRequest.children.push(itemId);
postRequest.children.push(
typeof itemId === "string" ? itemId : itemId.id
);
}
}
const publishedId = await postToThreads(postRequest);
return new Response(JSON.stringify({ success: true, publishedId }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
const publishedResult = await postToThreads(postRequest);
return new Response(
JSON.stringify({
success: true,
id: publishedResult.id,
permalink: publishedResult.permalink,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (error) {
console.error("Error processing request:", error);
return new Response(
Expand Down Expand Up @@ -169,29 +202,32 @@ Deno.serve(async (req: Request) => {
"userId": "YOUR_USER_ID",
"accessToken": "YOUR_ACCESS_TOKEN",
"mediaType": "TEXT",
"text": "Hello from Denim!"
"text": "Hello from Denim!",
"linkAttachment": "https://example.com"
}'
# Post an image Thread
# Post an image Thread with alt text
curl -X POST <YOUR_FUNCTION_URI>/post \
-H "Content-Type: application/json" \
-d '{
"userId": "YOUR_USER_ID",
"accessToken": "YOUR_ACCESS_TOKEN",
"mediaType": "IMAGE",
"text": "Check out this image I posted with Denim!",
"imageUrl": "https://example.com/image.jpg"
"imageUrl": "https://example.com/image.jpg",
"altText": "A beautiful sunset over the ocean"
}'
# Post a video Thread
# Post a video Thread with reply control
curl -X POST <YOUR_FUNCTION_URI>/post \
-H "Content-Type: application/json" \
-d '{
"userId": "YOUR_USER_ID",
"accessToken": "YOUR_ACCESS_TOKEN",
"mediaType": "VIDEO",
"text": "Watch this video I posted with Denim!",
"videoUrl": "https://example.com/video.mp4"
"videoUrl": "https://example.com/video.mp4",
"replyControl": "mentioned_only"
}'
# Post a carousel Thread
Expand Down
174 changes: 174 additions & 0 deletions mock_threads_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// mock_threads_api.ts

import type {
ThreadsContainer,
ThreadsPost,
ThreadsProfile,
PublishingLimit,
ThreadsPostRequest,
ThreadsListResponse,
} from "./types.ts";

export class MockThreadsAPI implements MockThreadsAPI {
private containers: Map<string, ThreadsContainer> = new Map();
private posts: Map<string, ThreadsPost> = new Map();
private users: Map<string, ThreadsProfile> = new Map();
private publishingLimits: Map<string, PublishingLimit> = new Map();
private errorMode = false;

constructor() {
// Initialize with some sample data
this.users.set("12345", {
id: "12345",
username: "testuser",
name: "Test User",
threadsProfilePictureUrl: "https://example.com/profile.jpg",
threadsBiography: "This is a test user",
});

this.publishingLimits.set("12345", {
quota_usage: 10,
config: {
quota_total: 250,
quota_duration: 86400,
},
});
}

setErrorMode(mode: boolean) {
this.errorMode = mode;
}

createThreadsContainer(
request: ThreadsPostRequest
): Promise<string | { id: string; permalink: string }> {
if (this.errorMode) {
return Promise.reject(new Error("Failed to create Threads container"));
}
const containerId = `container_${Math.random().toString(36).substring(7)}`;
const permalink = `https://www.threads.net/@${request.userId}/post/${containerId}`;
const container: ThreadsContainer = {
id: containerId,
permalink,
status: "FINISHED",
};
this.containers.set(containerId, container);

// Create a post immediately when creating a container
const postId = `post_${Math.random().toString(36).substring(7)}`;
const post: ThreadsPost = {
id: postId,
media_product_type: "THREADS",
media_type: request.mediaType,
permalink,
owner: { id: request.userId },
username: "testuser",
text: request.text || "",
timestamp: new Date().toISOString(),
shortcode: postId,
is_quote_post: false,
hasReplies: false,
isReply: false,
isReplyOwnedByMe: false,
};
this.posts.set(postId, post);

// Always return an object with both id and permalink
return Promise.resolve({ id: containerId, permalink });
}

publishThreadsContainer(
_userId: string,
_accessToken: string,
containerId: string,
getPermalink: boolean = false
): Promise<string | { id: string; permalink: string }> {
if (this.errorMode) {
return Promise.reject(new Error("Failed to publish Threads container"));
}
const container = this.containers.get(containerId);
if (!container) {
return Promise.reject(new Error("Container not found"));
}

// Find the post associated with this container
const existingPost = Array.from(this.posts.values()).find(
(post) => post.permalink === container.permalink
);

if (!existingPost) {
return Promise.reject(
new Error("Post not found for the given container")
);
}

return Promise.resolve(
getPermalink
? { id: existingPost.id, permalink: existingPost.permalink || "" }
: existingPost.id
);
}

createCarouselItem(
request: Omit<ThreadsPostRequest, "mediaType"> & {
mediaType: "IMAGE" | "VIDEO";
}
): Promise<string | { id: string }> {
const itemId = `item_${Math.random().toString(36).substring(7)}`;
const container: ThreadsContainer = {
id: itemId,
permalink: `https://www.threads.net/@${request.userId}/post/${itemId}`,
status: "FINISHED",
};
this.containers.set(itemId, container);
return Promise.resolve({ id: itemId });
}

getPublishingLimit(
userId: string,
_accessToken: string
): Promise<PublishingLimit> {
if (this.errorMode) {
return Promise.reject(new Error("Failed to get publishing limit"));
}
const limit = this.publishingLimits.get(userId);
if (!limit) {
return Promise.reject(new Error("Publishing limit not found"));
}
return Promise.resolve(limit);
}

getThreadsList(
userId: string,
_accessToken: string,
options?: {
since?: string;
until?: string;
limit?: number;
after?: string;
before?: string;
}
): Promise<ThreadsListResponse> {
const threads = Array.from(this.posts.values())
.filter((post) => post.owner.id === userId)
.slice(0, options?.limit || 25);

return Promise.resolve({
data: threads as ThreadsPost[],
paging: {
cursors: {
before: "BEFORE_CURSOR",
after: "AFTER_CURSOR",
},
},
});
}

getSingleThread(mediaId: string, _accessToken: string): Promise<ThreadsPost> {
const post = this.posts.get(mediaId);
if (!post) {
return Promise.reject(new Error("Thread not found"));
}
return Promise.resolve(post as ThreadsPost);
}
}
Loading

0 comments on commit f76e53e

Please sign in to comment.