UNPKG

@codybrom/denim

Version:

A Deno/TypeScript library for the Threads API

583 lines (544 loc) 14.1 kB
import type { AuthCodeResponse, CursorPaginationOptions, DebugTokenInfo, KeywordSearchOptions, LocationSearchOptions, MediaInsightsResponse, MockThreadsAPI, OEmbedResponse, PaginationOptions, PublicProfile, PublishingLimit, ResponseMediaType, ThreadsContainer, ThreadsListResponse, ThreadsLocation, ThreadsPost, ThreadsPostRequest, ThreadsProfile, TokenResponse, UserInsightsOptions, UserInsightsResponse, } from "../types.ts"; export class MockThreadsAPIImpl 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", threads_profile_picture_url: "https://example.com/profile.jpg", threads_biography: "This is a test user", is_verified: false, }); this.publishingLimits.set("12345", { quota_usage: 10, config: { quota_total: 250, quota_duration: 86400, }, reply_quota_usage: 5, reply_config: { quota_total: 1000, quota_duration: 86400, }, }); } setErrorMode(mode: boolean) { this.errorMode = mode; } createThreadsContainer( request: ThreadsPostRequest, ): Promise<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 as ResponseMediaType, permalink, owner: { id: request.userId }, username: "testuser", text: request.text || "", timestamp: new Date().toISOString(), shortcode: postId, is_quote_post: false, has_replies: false, is_reply: false, is_reply_owned_by_me: false, }; this.posts.set(postId, post); return Promise.resolve(containerId); } 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> { if (this.errorMode) { return Promise.reject(new Error("Failed to create carousel item")); } 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(itemId); } getPublishingLimit( userId: string, _accessToken: string, _fields?: 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?: PaginationOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to retrieve threads list")); } const threads = Array.from(this.posts.values()) .filter((post) => post.owner?.id === userId) .slice(0, options?.limit || 25); return Promise.resolve({ data: threads, paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR", }, }, }); } getSingleThread( mediaId: string, _accessToken: string, _fields?: string[], ): Promise<ThreadsPost> { if (this.errorMode) { return Promise.reject(new Error("Failed to retrieve thread")); } const post = this.posts.get(mediaId); if (!post) { return Promise.reject(new Error("Thread not found")); } return Promise.resolve(post); } repost(mediaId: string, _accessToken: string): Promise<{ id: string }> { if (this.errorMode) { return Promise.reject(new Error("Failed to repost")); } const repostId = `repost_${Math.random().toString(36).substring(7)}`; const originalPost = this.posts.get(mediaId); if (!originalPost) { return Promise.reject(new Error("Post not found")); } return Promise.resolve({ id: repostId }); } deleteThread( mediaId: string, _accessToken: string, ): Promise<{ success: boolean; deleted_id?: string }> { if (this.errorMode) { return Promise.reject(new Error("Failed to delete thread")); } const post = this.posts.get(mediaId); if (!post) { return Promise.reject(new Error("Thread not found")); } this.posts.delete(mediaId); return Promise.resolve({ success: true, deleted_id: mediaId }); } getProfile( userId: string, _accessToken: string, _fields?: string[], ): Promise<ThreadsProfile> { if (this.errorMode) { return Promise.reject(new Error("Failed to get profile")); } const user = this.users.get(userId); if (!user) { return Promise.reject(new Error("User not found")); } return Promise.resolve(user); } lookupProfile( _accessToken: string, username: string, _fields?: string[], ): Promise<PublicProfile> { if (this.errorMode) { return Promise.reject(new Error("Failed to look up profile")); } const user = Array.from(this.users.values()).find( (u) => u.username === username, ); if (!user) { return Promise.reject(new Error("Profile not found")); } return Promise.resolve({ id: user.id, username: user.username, name: user.name, profile_picture_url: user.threads_profile_picture_url, biography: user.threads_biography, is_verified: user.is_verified, }); } getProfilePosts( _accessToken: string, _username: string, options?: PaginationOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get profile posts")); } const posts = Array.from(this.posts.values()).slice( 0, options?.limit || 25, ); return Promise.resolve({ data: posts, paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } getGhostPosts( userId: string, _accessToken: string, options?: PaginationOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get ghost posts")); } const posts = Array.from(this.posts.values()) .filter((p) => p.owner?.id === userId) .slice(0, options?.limit || 25); return Promise.resolve({ data: posts, paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } getUserReplies( userId: string, _accessToken: string, options?: PaginationOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get user replies")); } const replies = Array.from(this.posts.values()) .filter((p) => p.owner?.id === userId && p.is_reply) .slice(0, options?.limit || 25); return Promise.resolve({ data: replies, paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } getReplies( _mediaId: string, _accessToken: string, _options?: CursorPaginationOptions, _fields?: string[], _reverse?: boolean, ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get replies")); } return Promise.resolve({ data: Array.from(this.posts.values()).slice(0, 25), paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } getConversation( _mediaId: string, _accessToken: string, _options?: CursorPaginationOptions, _fields?: string[], _reverse?: boolean, ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get conversation")); } return Promise.resolve({ data: Array.from(this.posts.values()).slice(0, 25), paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } manageReply( _replyId: string, _accessToken: string, _hide: boolean, ): Promise<{ success: boolean }> { if (this.errorMode) { return Promise.reject(new Error("Failed to manage reply")); } return Promise.resolve({ success: true }); } getMentions( _userId: string, _accessToken: string, options?: PaginationOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get mentions")); } return Promise.resolve({ data: Array.from(this.posts.values()).slice(0, options?.limit || 25), paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } getMediaInsights( _mediaId: string, _accessToken: string, metrics: string[], ): Promise<MediaInsightsResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get media insights")); } return Promise.resolve({ data: metrics.map((m) => ({ name: m, period: "lifetime", values: [{ value: 42 }], title: m, description: `${m} metric`, id: `${m}_id`, })), }); } getUserInsights( _userId: string, _accessToken: string, metrics: string[], _options?: UserInsightsOptions, ): Promise<UserInsightsResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get user insights")); } return Promise.resolve({ data: metrics.map((m) => ({ name: m, period: "day", values: [{ value: 100 }], title: m, description: `${m} metric`, id: `${m}_id`, })), }); } searchKeyword( _accessToken: string, _options: KeywordSearchOptions, _fields?: string[], ): Promise<ThreadsListResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to search keywords")); } return Promise.resolve({ data: Array.from(this.posts.values()), paging: { cursors: { before: "BEFORE_CURSOR", after: "AFTER_CURSOR" }, }, }); } searchLocations( _accessToken: string, _options: LocationSearchOptions, _fields?: string[], ): Promise<{ data: ThreadsLocation[] }> { if (this.errorMode) { return Promise.reject(new Error("Failed to search locations")); } return Promise.resolve({ data: [ { id: "loc_123", name: "Test Location", latitude: 37.7749, longitude: -122.4194, }, ], }); } getLocation( locationId: string, _accessToken: string, _fields?: string[], ): Promise<ThreadsLocation> { if (this.errorMode) { return Promise.reject(new Error("Failed to get location")); } return Promise.resolve({ id: locationId, name: "Test Location", address: "123 Test St", city: "Test City", country: "US", latitude: 37.7749, longitude: -122.4194, }); } exchangeCodeForToken( _clientId: string, _clientSecret: string, _code: string, _redirectUri: string, ): Promise<AuthCodeResponse> { if (this.errorMode) { return Promise.reject( new Error("Failed to exchange authorization code"), ); } return Promise.resolve({ access_token: "short_lived_token_abc123", user_id: "12345", }); } getAppAccessToken( _clientId: string, _clientSecret: string, ): Promise<TokenResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get app access token")); } return Promise.resolve({ access_token: "TH|1234567890|abcd1234", token_type: "bearer", }); } exchangeToken( _clientSecret: string, _accessToken: string, ): Promise<TokenResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to exchange token")); } return Promise.resolve({ access_token: "long_lived_token_abc123", token_type: "bearer", expires_in: 5184000, }); } refreshToken(_accessToken: string): Promise<TokenResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to refresh token")); } return Promise.resolve({ access_token: "refreshed_token_abc123", token_type: "bearer", expires_in: 5184000, }); } debugToken( _accessToken: string, _inputToken: string, ): Promise<DebugTokenInfo> { if (this.errorMode) { return Promise.reject(new Error("Failed to debug token")); } return Promise.resolve({ data: { type: "USER", application: "Test App", is_valid: true, scopes: ["threads_basic", "threads_content_publish"], user_id: "12345", }, }); } getOEmbed( _accessToken: string, _url: string, _maxWidth?: number, ): Promise<OEmbedResponse> { if (this.errorMode) { return Promise.reject(new Error("Failed to get oEmbed")); } return Promise.resolve({ html: "<blockquote>Embedded Threads post</blockquote>", provider_name: "Threads", type: "rich", version: "1.0", width: 550, }); } }