Skip to content

Commit bf4798e

Browse files
committed
Add project files for API and lib Threads
1 parent 1320fc3 commit bf4798e

File tree

11 files changed

+188
-39
lines changed

11 files changed

+188
-39
lines changed

bun.lockb

1.55 KB
Binary file not shown.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
"bun-types": "latest"
66
},
77
"dependencies": {
8-
"hono": "^2.2.5"
8+
"hono": "^3.3.0"
99
},
1010
"scripts": {
11-
"start": "bun run src/index.ts"
11+
"start:api": "bun run src/api/index.ts",
12+
"watch:api": "bun run --watch src/api/index.ts"
1213
},
13-
"module": "src/index.js"
14+
"module": "src/lib/index.js"
1415
}

readme.md

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
# Hono with Bun runtime
1+
# Threads API no oficial
22

3-
## Getting Started
3+
<div align="center">
4+
<small>Para fines educativos</small>
5+
</div>
46

5-
### Cloning the repo
7+
## Primeras pruebas con Curl
68

79
```sh
8-
bun create hono ./NAME_HERE
9-
```
10-
11-
### Development
12-
13-
```
14-
bun run start
15-
```
16-
17-
Open http://localhost:3000 with your browser to see the result.
18-
19-
### For more information
20-
21-
See <https://honojs.dev/>
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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Hono } from 'hono';
2+
import { fetchUserProfile } from '../lib/fetch'
3+
4+
const port = +(Bun.env.PORT ?? 1234)
5+
6+
const app = new Hono()
7+
8+
app.get('/api/users/:userId', async (context) => {
9+
const userId = context.req.param('userId')
10+
const data = await fetchUserProfile({ userId })
11+
return context.json(data)
12+
})
13+
14+
export default {
15+
port,
16+
fetch: app.fetch,
17+
}

src/index.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/lib/consts.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
USER_THREADS: 6451898791498605,
6+
USER_PROFILE: 23996318473300828,
7+
THREADS_REPLIES: 6529829603744567,
8+
USER_REPLIES: 6684830921547925
9+
}
10+
11+
export const getMidudev = () => `pheralb`

src/lib/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const IS_DEBUG = Boolean(Bun.env.DEBUG)

src/lib/fetch.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ENDPOINTS_DOCUMENT_ID, THREADS_APP_ID, GRAPHQL_ENDPOINT } from './consts';
2+
import { IS_DEBUG } from './env';
3+
import { ThreadsUserProfileResponse } from '../types/threads-api'
4+
import { mapUserProfile } from './map'
5+
6+
const fetchBase = ({ documentId, variables }) => {
7+
return fetch(GRAPHQL_ENDPOINT, {
8+
method: 'POST',
9+
headers: {
10+
'content-type': 'application/x-www-form-urlencoded',
11+
'user-agent': 'Threads API midu client',
12+
'x-ig-app-id': THREADS_APP_ID
13+
},
14+
body: `variables=${JSON.stringify(variables)}&doc_id=${documentId}`
15+
})
16+
.then(response => response.json())
17+
}
18+
19+
export const fetchUserIdByName = ({ userName }) => {
20+
if (IS_DEBUG) console.info(`https://www.threads.net/@${userName}`)
21+
22+
return fetch(`https://www.threads.net/@${userName}`)
23+
.then(res => res.text())
24+
.then(html => {
25+
const regex = /{"user_id":"(\d+)"}/g
26+
const [[, userId]] = html.matchAll(regex) ?? []
27+
return userId
28+
})
29+
}
30+
31+
export const fetchUserProfile = async (
32+
{ userId, userName }: { userId?: string, userName?: string }) => {
33+
if (userName && !userId) {
34+
userId = await fetchUserIdByName({ userName })
35+
}
36+
37+
const variables = { userID: userId }
38+
const data = (
39+
await fetchBase({ variables, documentId: ENDPOINTS_DOCUMENT_ID.USER_PROFILE })
40+
) as ThreadsUserProfileResponse
41+
42+
return mapUserProfile(data)
43+
}
44+
45+
export const fetchUserThreads = async (
46+
{ userId, userName }: { userId?: string, userName?: string }
47+
) => {
48+
if (userName && !userId) {
49+
userId = await fetchUserIdByName({ userName })
50+
}
51+
52+
const variables = { userID: userId }
53+
return fetchBase({ variables, documentId: ENDPOINTS_DOCUMENT_ID.USER_THREADS })
54+
}
55+
56+
export const fetchUserReplies = async (
57+
{ userId, userName }: { userId?: string, userName?: string }
58+
) => {
59+
if (userName && !userId) {
60+
userId = await fetchUserIdByName({ userName })
61+
}
62+
63+
const variables = { userID: userId }
64+
return fetchBase({ variables, documentId: ENDPOINTS_DOCUMENT_ID.USER_REPLIES })
65+
}
66+
67+
export const fetchThreadReplies = ({ threadId }) => {
68+
const variables = { postID: threadId }
69+
return fetchBase({ variables, documentId: ENDPOINTS_DOCUMENT_ID.THREADS_REPLIES })
70+
}

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './fetch';

src/lib/map.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
}

0 commit comments

Comments
 (0)