├── .gitignore ├── Dockerfile ├── bun.lockb ├── package.json ├── public └── favicon.ico ├── readme.md ├── src ├── api │ └── index.ts ├── lib │ ├── consts.ts │ ├── env.ts │ ├── fetch.ts │ ├── index.ts │ └── map.ts └── types │ └── threads-api.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:0.6.13 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY bun.lockb bun.lockb 5 | RUN bun install 6 | COPY . . 7 | EXPOSE 3000 8 | ENTRYPOINT ["bun", "run", "src/api/index.ts"] 9 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/threads-api/32afc6fcbd84792451a1af8a8014efa648f48928/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.103", 3 | "name": "threads-api", 4 | "devDependencies": { 5 | "bun-types": "latest" 6 | }, 7 | "dependencies": { 8 | "hono": "^3.3.0" 9 | }, 10 | "scripts": { 11 | "start:api": "bun run src/api/index.ts", 12 | "watch:api": "bun run --watch src/api/index.ts" 13 | }, 14 | "module": "src/lib/index.js" 15 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/threads-api/32afc6fcbd84792451a1af8a8014efa648f48928/public/favicon.ico -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Threads API no oficial 2 | 3 |
4 | Para fines educativos 5 |
6 | 7 | ## Primeras pruebas con Curl 8 | 9 | ```sh 10 | curl 'https://www.threads.net/api/graphql' \ 11 | -H 'content-type: application/x-www-form-urlencoded' \ 12 | -H 'sec-fetch-site: same-origin' \ 13 | -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' \ 14 | -H 'x-ig-app-id: 238260118697367' \ 15 | --data 'variables={ "userID": "8242141302" }' \ 16 | --data doc_id=23996318473300828 17 | ``` -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { fetchThreadReplies, fetchUserProfile, fetchUserProfileThreads } from '../lib/fetch'; 3 | 4 | const port = +(Bun.env.PORT ?? 3000); 5 | 6 | console.log('Initializing API server on port', port); 7 | 8 | const app = new Hono(); 9 | 10 | 11 | 12 | // Endpoint to get user profiles based on userName 13 | app.get('/api/users', async (context) => { 14 | try { 15 | // Extract the userName query parameter from the request 16 | const userName = context.req.query('userName'); 17 | 18 | // If the userName is missing, return a "Missing userName" error response with status code 400 19 | if (!userName) return context.text('Missing userName', 400); 20 | 21 | // Fetch the user profile using the provided userName 22 | const data = await fetchUserProfile({ userName }); 23 | 24 | // Return the fetched data as a JSON response 25 | return context.json(data); 26 | } catch (error) { 27 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message 28 | return context.text('Internal Server Error', 500); 29 | } 30 | }); 31 | 32 | // Endpoint to get a specific user profile based on userId 33 | app.get('/api/users/:userId', async (context) => { 34 | try { 35 | // Extract the userId from the request parameters 36 | const userId = context.req.param('userId'); 37 | 38 | // If the userName is missing, return a "Missing userId" error response with status code 400 39 | if (!userId) return context.text('Missing userId', 400); 40 | 41 | // Fetch the user profile using the provided userId 42 | const data = await fetchUserProfile({ userId }); 43 | 44 | // Return the fetched data as a JSON response 45 | return context.json(data); 46 | } catch (error) { 47 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message 48 | return context.text('Internal Server Error', 500); 49 | } 50 | }); 51 | 52 | // Endpoint to get replies from a specific thread 53 | app.get('/api/threads/:threadId/replies', async (context) => { 54 | try { 55 | // Extract the threadId from the request parameters 56 | const threadId = context.req.param('threadId'); 57 | 58 | // If the userName is missing, return a "Missing threadId" error response with status code 400 59 | if (!threadId) return context.text('Missing threadId', 400); 60 | 61 | // Fetch the thread replies using the provided threadId 62 | const data = await fetchThreadReplies({ threadId }); 63 | 64 | // Return the fetched data as a JSON response 65 | return context.json(data); 66 | } catch (error) { 67 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message 68 | return context.text('Internal Server Error', 500); 69 | } 70 | }); 71 | 72 | 73 | // Endpoint to get user profile threads 74 | app.get('/api/users/:userId/threads', async (context) => { 75 | try { 76 | // Extract the userId from the request parameters 77 | const userId = context.req.param('userId'); 78 | 79 | // If the userName is missing, return a "Missing userId" error response with status code 400 80 | if (!userId) return context.text('Missing userId', 400); 81 | 82 | // Fetch the user profile threads using the provided userId 83 | const data = await fetchUserProfileThreads({ userId }); 84 | 85 | // Return the fetched data as a JSON response 86 | return context.json(data); 87 | } catch (error) { 88 | // If an error occurs, respond with a 500 status code and an "Internal Server Error" message 89 | return context.text('Internal Server Error', 500); 90 | } 91 | }); 92 | 93 | 94 | 95 | 96 | 97 | app.use('*', async (c) => { 98 | c.notFound(); 99 | }); 100 | 101 | export default { 102 | port, 103 | fetch: app.fetch, 104 | }; 105 | -------------------------------------------------------------------------------- /src/lib/consts.ts: -------------------------------------------------------------------------------- 1 | export const THREADS_APP_ID = "238260118697367" 2 | export const GRAPHQL_ENDPOINT = 'https://www.threads.net/api/graphql' 3 | 4 | export const ENDPOINTS_DOCUMENT_ID = { 5 | 6 | USER_PROFILE: 23996318473300828, 7 | USER_PROFILE_THREADS: 6451898791498605, 8 | USER_PROFILE_THREADS_REPLIES: 6529829603744567, 9 | USER_REPLIES: 6684830921547925, 10 | THREADS_REPLIES: 6960296570650501, 11 | 12 | } 13 | 14 | export const getMidudev = () => `pheralb` 15 | -------------------------------------------------------------------------------- /src/lib/env.ts: -------------------------------------------------------------------------------- 1 | export const IS_DEBUG = Boolean(Bun.env.DEBUG) -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ENDPOINTS_DOCUMENT_ID, 3 | THREADS_APP_ID, 4 | GRAPHQL_ENDPOINT, 5 | } from './consts'; 6 | import { IS_DEBUG } from './env'; 7 | import { ThreadsUserProfileResponse } from '../types/threads-api'; 8 | import { mapUserProfile } from './map'; 9 | 10 | const fetchBase = ({ documentId, variables }) => { 11 | return fetch(GRAPHQL_ENDPOINT, { 12 | method: 'POST', 13 | headers: { 14 | 'content-type': 'application/x-www-form-urlencoded', 15 | 'user-agent': 'Threads API midu client', 16 | 'x-ig-app-id': THREADS_APP_ID, 17 | 'x-fb-lsd': 'jdFoLBsUcm9h-j90PeanuC', 18 | }, 19 | body: `lsd=jdFoLBsUcm9h-j90PeanuC&jazoest=21926&variables=${JSON.stringify( 20 | variables 21 | )}&doc_id=${documentId}`, 22 | }).then((response) => response.json()); 23 | }; 24 | 25 | export const fetchUserIdByName = ({ userName }) => { 26 | if (IS_DEBUG) console.info(`https://www.threads.net/@${userName}`); 27 | 28 | return fetch(`https://www.threads.net/@${userName}`, { 29 | headers: { 'sec-fetch-site': 'same-site' }, 30 | }) 31 | .then((res) => res.text()) 32 | .then((html) => { 33 | const userId = html.match(/"user_id":"(\d+)"/)?.[1]; 34 | return userId; 35 | }); 36 | }; 37 | 38 | export const fetchUserProfile = async ({ 39 | userId, 40 | userName, 41 | }: { 42 | userId?: string; 43 | userName?: string; 44 | }) => { 45 | if (userName && !userId) { 46 | userId = await fetchUserIdByName({ userName }); 47 | } 48 | 49 | const variables = { userID: userId }; 50 | const data = (await fetchBase({ 51 | variables, 52 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE, 53 | })) as ThreadsUserProfileResponse; 54 | 55 | return mapUserProfile(data); 56 | }; 57 | 58 | export const fetchUserProfileThreads = async ({ 59 | userId, 60 | userName, 61 | }: { 62 | userId?: string; 63 | userName?: string; 64 | }) => { 65 | if (userName && !userId) { 66 | userId = await fetchUserIdByName({ userName }); 67 | } 68 | 69 | const variables = { userID: userId }; 70 | return fetchBase({ 71 | variables, 72 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE_THREADS, 73 | }); 74 | }; 75 | 76 | export const fetchUserReplies = async ({ 77 | userId, 78 | userName, 79 | }: { 80 | userId?: string; 81 | userName?: string; 82 | }) => { 83 | if (userName && !userId) { 84 | userId = await fetchUserIdByName({ userName }); 85 | } 86 | 87 | const variables = { userID: userId }; 88 | return fetchBase({ 89 | variables, 90 | documentId: ENDPOINTS_DOCUMENT_ID.USER_REPLIES, 91 | }); 92 | }; 93 | 94 | export const fetchThreadReplies = ({ threadId }) => { 95 | const variables = { postID: threadId }; 96 | return fetchBase({ 97 | variables, 98 | documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE_THREADS_REPLIES, 99 | }); 100 | }; 101 | 102 | export const fetchPostReplies = ({ threadId }) => { 103 | const variables = { postID: threadId }; 104 | return fetchBase({ 105 | variables, 106 | documentId: ENDPOINTS_DOCUMENT_ID.THREADS_REPLIES, 107 | }); 108 | }; 109 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetch'; -------------------------------------------------------------------------------- /src/lib/map.ts: -------------------------------------------------------------------------------- 1 | import { ThreadsUserProfileResponse} from '../types/threads-api' 2 | 3 | /* 4 | {"data":{"userData":{"user":{"is_private":false,"profile_pic_url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s150x150&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfDaktW3vHUeFvaE14qoy7LmddGuAqWUh2uirC7ulm_TsQ&oe=64B34341&_nc_sid=10d13b","username":"midu.dev","hd_profile_pic_versions":[{"height":320,"url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s320x320&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfBUgVik0k-VaqXmyuuJUp6bEmAyDHIkkB3ssbnHYwGg_A&oe=64B34341&_nc_sid=10d13b","width":320},{"height":640,"url":"https://scontent.cdninstagram.com/v/t51.2885-19/358174537_954616899107816_8099109910283809308_n.jpg?stp=dst-jpg_s640x640&_nc_ht=scontent.cdninstagram.com&_nc_cat=108&_nc_ohc=s5qTOIc_KREAX8qfpDD&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfCG0VVjm58zezRMrUgG_HlTuOL0MlMMsUpGRDgn4CrMiA&oe=64B34341&_nc_sid=10d13b","width":640}],"is_verified":false,"biography":"👨‍💻 Ingeniero de Software + JavaScript\n⌨️ Aprende Programación conmigo\n🏆 Google Expert + GitHub Star\n🙌 Comparto recursos y tutoriales","biography_with_entities":null,"follower_count":34756,"profile_context_facepile_users":null,"bio_links":[{"url":"https://twitch.tv/midudev"}],"pk":"8242141302","full_name":"midudev • Programación y Desarrollo JavaScript","id":null}}},"extensions":{"is_final":true}} 5 | */ 6 | 7 | export const mapUserProfile = (rawResponse: ThreadsUserProfileResponse) => { 8 | const userApiResponse = rawResponse?.data?.userData?.user 9 | if (!userApiResponse) return null 10 | 11 | const { username, is_verified, biography, follower_count, bio_links, pk: id, full_name, hd_profile_pic_versions, profile_pic_url } = userApiResponse 12 | 13 | const profile_pics = [{ 14 | height: 150, 15 | width: 150, 16 | url: profile_pic_url 17 | }, ...hd_profile_pic_versions] 18 | 19 | return { 20 | id, 21 | username, 22 | is_verified, 23 | biography, 24 | follower_count, 25 | bio_links, 26 | full_name, 27 | profile_pics 28 | } 29 | } -------------------------------------------------------------------------------- /src/types/threads-api.ts: -------------------------------------------------------------------------------- 1 | export type ThreadsUserProfileResponse = { 2 | data: Data; 3 | extensions: Extensions; 4 | } 5 | 6 | export type Data = { 7 | userData: UserData; 8 | } 9 | 10 | export type UserData = { 11 | user: User; 12 | } 13 | 14 | export type User = { 15 | is_private: boolean; 16 | profile_pic_url: string; 17 | username: string; 18 | hd_profile_pic_versions: HDProfilePicVersion[]; 19 | is_verified: boolean; 20 | biography: string; 21 | biography_with_entities: null; 22 | follower_count: number; 23 | profile_context_facepile_users: null; 24 | bio_links: BioLink[]; 25 | pk: string; 26 | full_name: string; 27 | id: null; 28 | } 29 | 30 | export type BioLink = { 31 | url: string; 32 | } 33 | 34 | export type HDProfilePicVersion = { 35 | height: number; 36 | url: string; 37 | width: number; 38 | } 39 | 40 | export type Extensions = { 41 | is_final: boolean; 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | // "bun-types" is the important part 8 | "types": ["bun-types"] 9 | } 10 | } --------------------------------------------------------------------------------