diff --git a/README.md b/README.md index ac5d2b3..0d21c4e 100644 --- a/README.md +++ b/README.md @@ -27,193 +27,15 @@ thread.js is a Node.js library that allows you to interact with the Threads API ``` npm install @threadsjs/threads.js ``` -## Usage +## Example usage ```js const { Client } = require('@threadsjs/threads.js'); (async () => { - const client = new Client(); - // You can also specify a token: const client = new Client({ token: 'token' }); - await client.login('username', 'password'); + const client = new Client({ token: 'token' }); - await client.users.fetch(25025320).then(user => { + await client.users.get().then(user => { console.log(user); }); })(); -``` - -## Methods -### client.users.fetch -In the parameters, pass the user id (supported as string and number) of the user whose information you want to get. -```js -await client.users.fetch(1) -``` -### client.users.search -Pass the query as the first parameter, and the number of objects in the response as the second parameter (by default - 30). The minimum is 30. -```js -await client.users.search("zuck", 10) -``` - -
- -### client.restrictions.restrict -In the parameters, pass the user id (supported as string and number) of the user you want to restrict. -```js -await client.users.restrict(1) -``` -### client.restrictions.unrestrict -In the parameters, pass the user id (supported as string and number) of the user you want to unrestrict. -```js -await client.users.unrestrict(1) -``` -
- -### client.friendships.show -In the parameters, pass the user id (supported as string and number) of the user whose friendship status information you want to get. -```js -await client.friendships.show(1) -``` -### client.friendships.follow -Pass the user id (supported as string and number) of the user you want to subscribe to in the parameters -```js -await client.friendships.follow(1) -``` -### client.friendships.unfollow -Pass the user id (supported as string and number) of the user you want to unsubscribe from in the parameters -```js -await client.friendships.unfollow(1) -``` -### client.friendships.followers -In the parameters, pass the user id (supported as string and number) of the user whose followers you want to get. -```js -await client.friendships.followers(1) -``` -### client.friendships.following -In the parameters, pass the user id (supported as string and number) of the user whose followings you want to get. -```js -await client.friendships.following(1) -``` -### client.friendships.mute -In the parameters, pass the user id (supported as string and number) of the user you want to mute. -```js -await client.friendships.mute(1) -``` -### client.friendships.unmute -In the parameters, pass the user id (supported as string and number) of the user you want to unmute. -```js -await client.friendships.unmute(1) -``` -### client.friendships.block -In the parameters, pass the user id (supported as string and number) of the user you want to block. -```js -await client.friendships.block(1) -``` -### client.friendships.unblock -In the parameters, pass the user id (supported as string and number) of the user you want to unblock. -```js -await client.friendships.unblock(1) -``` - -
- -### client.feeds.fetch -Gets the default feed. In the parameters, pass the optional max_id of the previous response's next_max_id. -```js -await client.feeds.fetch() -await client.feeds.fetch("aAaAAAaaa") -``` -### client.feeds.fetchThreads -In the parameters, pass the user id (supported as string and number) of the user whose threads you want to get, and an optional max_id of the previous response's next_max_id. -```js -await client.feeds.fetchThreads(1), -await client.feeds.fetchThreads(1, "aAaAAAaaa") -``` -### client.feeds.fetchReplies -In the parameters, pass the user id (supported as string and number) of the user whose replies you want to get, and an optional max_id of the previous response's next_max_id. -```js -await client.feeds.fetchReplies(1) -await client.feeds.fetchReplies(1, "aAaAAAaaa") -``` -### client.feeds.recommended -Getting a list of recommendations. In the parameters, pass the optional paging_token of the previous response. -```js -await client.feeds.recommended() -await client.feeds.recommended(15) -``` -### client.feeds.notifications -Getting a list of recommendations. In the parameters, pass an optional filter type and an optional pagination object with max_id and pagination_first_record_timestamp from the previous response. -Valid filter types: -- text_post_app_replies -- text_post_app_mentions -- verified -```js -let pagination = { - max_id: "1688921943.766884", - pagination_first_record_timestamp: "1689094189.845912" -} - -await client.feeds.notifications() -await client.feeds.notifications(null, pagination) - -await client.feeds.notifications("text_post_app_replies") -await client.feeds.notifications("text_post_app_replies", pagination) -``` -### client.feeds.notificationseen -Clears all notifications. You might want to do this **after** client.feeds.notifications() and checking new_stories for what wasn't seen. -```js -await client.feeds.notificationseen() -``` - -
- -### client.posts.fetch -In the parameters pass the id of the post you want to get information about, and an optional pagination token from the previous request. -```js -await client.posts.fetch("aAaAAAaaa") -await client.posts.fetch("aAaAAAaaa", "aAaAAAaaa") -``` -### client.posts.likers -In the parameters pass the id of the post whose likes you want to get -```js -await client.posts.likers("aAaAAAaaa") -``` -### client.posts.create -The method is used to create a thread. Pass the text of the thread as the first parameter, and the user id (supported as string and number) as the second -```js -await client.posts.create(1, { contents: "Hello World!" }) -``` -### client.posts.reply -The method is used to create reply to a thread. Pass the text of the reply as the first parameter, the user id (supported as string and number) as the second, and post id as the third -```js -await client.posts.reply(1, { contents: "Hello World!", post: "aAaAAAaaa" }) -``` -### client.posts.quote -The method is used to create a quote thread. Pass the text of the quote comment as the first parameter, the user id as the second, and post id as the third -```js -await client.posts.quote(1, { contents: "Hello World!", post: "aAaAAAaaa" }) -``` -### client.posts.delete -The method is used to delete a thread. Pass the post id as the first parameter, and the user id (supported as string and number) as the second -```js -await client.posts.delete("aAaAAAaaa", 1) -``` -### client.posts.like -The method is used to like a thread. Pass the post id as the first parameter, and the user id (supported as string and number) as the second -```js -await client.posts.like("aAaAAAaaa", 1) -``` -### client.posts.unlike -The method is used to unlike a thread. Pass the post id as the first parameter, and the user id (supported as string and number) as the second -```js -await client.posts.unlike("aAaAAAaaa", 1) -``` -### client.posts.repost -The method is used to repost a thread. Pass the post id as the only parameter -```js -await client.posts.repost("aAaAAAaaa") -``` -### client.posts.unrepost -The method is used to un-repost a thread. Pass the post id as the only parameter -```js -await client.posts.unrepost("aAaAAAaaa") -``` +``` \ No newline at end of file diff --git a/src/index.js b/src/index.js index 59d79d0..8bbe57c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,209 +1,23 @@ const { fetch } = require("undici"); -const FeedManager = require("./managers/FeedManager"); -const FriendshipManager = require("./managers/FriendshipManager"); -const GraphQLManager = require("./managers/GraphQLManager"); -const PostManager = require("./managers/PostManager"); const RESTManager = require("./managers/RESTManager"); -const RestrictionManager = require("./managers/RestrictionManager"); const UserManager = require("./managers/UserManager"); -const { parseBloksResponse } = require("./util/Bloks.js"); -const crypto = require('crypto'); -const { v4: uuidv4 } = require('uuid'); - -const androidId = (Math.random() * 1e24).toString(36); +const PostManager = require("./managers/PostManager"); class Client { constructor(options) { this.options = {} this.options.token = options ? options.token : null; - this.options.userAgent = options ? options.userAgent : "Barcelona 289.0.0.77.109 Android"; - this.options.appId = options ? options.appId : "3419628305025917"; - this.options.androidId = options ? options.androidId : androidId; - this.options.userId = null; - this.options.base = options ? options.base : "https://i.instagram.com"; + this.options.base = options ? options.base : "https://graph.instagram.com"; this.rest = new RESTManager(this); this.users = new UserManager(this); this.posts = new PostManager(this); - - this.feeds = new FeedManager(this); - - this.friendships = new FriendshipManager(this); - - this.restrictions = new RestrictionManager(this); - - this.graphql = new GraphQLManager(this); } - async _qeSync() { - const uuid = uuidv4(); - const params = { - id: uuid - } - - return await fetch(this.options.base + '/api/v1/qe/sync/', { - method: 'POST', - headers: { - "User-Agent": "Barcelona 289.0.0.77.109 Android", - "Sec-Fetch-Site": "same-origin", - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - 'X-DEVICE-ID': uuid - }, - body: `params=${encodeURIComponent(params)}` - }).then(res => { - return res - }) - } - - async _encryptPassword(password) { - // https://github.com/dilame/instagram-private-api/blob/master/src/repositories/account.repository.ts#L79-L103 - const key = crypto.randomBytes(32); - const iv = crypto.randomBytes(12); - let keyId; - let pubKey; - await this._qeSync().then(async res => { - const headers = res.headers; - keyId = headers.get('ig-set-password-encryption-key-id'); - pubKey = headers.get('ig-set-password-encryption-pub-key'); - }); - const rsaEncrypted = crypto.publicEncrypt({ - key: Buffer.from(pubKey, 'base64').toString(), - padding: crypto.constants.RSA_PKCS1_PADDING, - }, key); - const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); - const time = Math.floor(Date.now() / 1000).toString(); - cipher.setAAD(Buffer.from(time)); - const aesEncrypted = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]); - const sizeBuffer = Buffer.alloc(2, 0); - sizeBuffer.writeInt16LE(rsaEncrypted.byteLength, 0); - const authTag = cipher.getAuthTag(); - return { - time, - password: Buffer.concat([ - Buffer.from([1, keyId]), - iv, - sizeBuffer, - rsaEncrypted, authTag, aesEncrypted]) - .toString('base64'), - }; - } - - async login(username, password) { - const loginUrl = "/api/v1/bloks/apps/com.bloks.www.bloks.caa.login.async.send_login_request/"; - const encryptedPassword = await this._encryptPassword(password); - - const params = { - client_input_params: { - password: `#PWD_INSTAGRAM:4:${encryptedPassword.time}:${encryptedPassword.password}`, - contact_point: username, - device_id: `android-${androidId}`, - }, - server_params: { - credential_type: "password", - device_id: `android-${androidId}`, - }, - }; - - const bkClientContext = { - bloks_version: - "5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73", - styles_id: "instagram", - }; - - const requestBody = { - params: JSON.stringify(params), - bk_client_context: JSON.stringify(bkClientContext), - bloks_versioning_id: - "5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73", - }; - - const requestOptions = { - method: "POST", - headers: { - "User-Agent": "Barcelona 289.0.0.77.109 Android", - "Sec-Fetch-Site": "same-origin", - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: `params=${encodeURIComponent( - requestBody.params, - )}&bk_client_context=${encodeURIComponent( - requestBody.bk_client_context, - )}&bloks_versioning_id=${requestBody.bloks_versioning_id}`, - }; - - const response = await fetch(this.options.base + loginUrl, requestOptions); - const text = await response.text(); - const bloks = parseBloksResponse(text); - - if ('error' in bloks && bloks.error.error_user_msg === "challenge_required") { - throw new Error('Your Instagram account is facing a checkpoint.') - } - - if (bloks.two_factor_required) { - const { - two_factor_identifier, - trusted_notification_polling_nonce - } = bloks.two_factor_info - - console.log('Please approve the login request on your Instagram') - - const self = this; - const token = await new Promise((resolve) => { - const statusUrl = "/api/v1/two_factor/check_trusted_notification_status/"; - const verifyUrl = "/api/v1/accounts/two_factor_login/"; - - const interval = setInterval(async function () { - requestOptions.body = new URLSearchParams({ - two_factor_identifier: two_factor_identifier, - username, - device_id: `android-${androidId}`, - trusted_notification_polling_nonces: - JSON.stringify([trusted_notification_polling_nonce]), - }).toString(); - - const response = await fetch(self.options.base + statusUrl, requestOptions); - const json = await response.json(); - - if (json.review_status === 1) { - requestOptions.body = new URLSearchParams({ - signed_body: 'SIGNATURE.' + JSON.stringify({ - verification_code: '', - two_factor_identifier, - username, - device_id: `android-${androidId}`, - trusted_notification_polling_nonces: - JSON.stringify([trusted_notification_polling_nonce]), - verification_method: '4' - }) - }).toString() - - const response = await fetch(self.options.base + verifyUrl, requestOptions); - const json = await response.json(); - const header = response.headers.get('Ig-Set-Authorization'); - - if (json.logged_in_user.pk_id) { - self.options.userId = json.logged_in_user.pk_id; - } - - clearInterval(interval); - const token = header.replace("Bearer IGT:2:", "") || null - - resolve(token); - } - }, 2_500); - }); - - this.options.token = token; - return; - } - - if (bloks.login_response.logged_in_user.pk_id) { - this.userId = bloks.login_response.logged_in_user.pk_id; - } - - this.options.token = bloks.headers?.["IG-Set-Authorization"].replace("Bearer IGT:2:", ""); + async login(token) { + // TODO: implement Instagram API login } } diff --git a/src/managers/FeedManager.js b/src/managers/FeedManager.js deleted file mode 100644 index 016d278..0000000 --- a/src/managers/FeedManager.js +++ /dev/null @@ -1,52 +0,0 @@ -const RESTManager = require('./RESTManager'); - -class FeedManager extends RESTManager { - async fetch(max_id) { - return await this.request('/api/v1/feed/text_post_app_timeline/', { - method: 'POST', - body: 'pagination_source=text_post_feed_threads' + (max_id ? '&max_id=' + encodeURIComponent(max_id) : ''), - }) - } - - async fetchThreads(user, max_id) { - return await this.request(`/api/v1/text_feed/${String(user)}/profile/` + (max_id ? '?max_id=' + encodeURIComponent(max_id) : '')); - } - - async fetchReplies(user, max_id) { - return await this.request(`/api/v1/text_feed/${String(user)}/profile/replies/` + (max_id ? '?max_id=' + encodeURIComponent(max_id) : '')); - } - - async recommended(paging_token) { - return await this.request('/api/v1/text_feed/recommended_users/' + (paging_token ? '?paging_token=' + paging_token : '')); - } - - async notifications(filter, pagination) { - let params = { - feed_type: 'all', - mark_as_seen: false, - timezone_offset: -25200, - timezone_name: "America%2FLos_Angeles" - } - - if (filter) { - params.selected_filters = filter; - } - - if (pagination) { - params.max_id = pagination.max_id; - params.pagination_first_record_timestamp = pagination.pagination_first_record_timestamp; - } - - const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&'); - - return await this.request('/api/v1/text_feed/text_app_notifications/?' + queryString); - } - - async notificationseen() { - return await this.request('/api/v1/text_feed/text_app_inbox_seen/', { - method: 'POST', - }) - } -} - -module.exports = FeedManager; \ No newline at end of file diff --git a/src/managers/FriendshipManager.js b/src/managers/FriendshipManager.js deleted file mode 100644 index e399e5b..0000000 --- a/src/managers/FriendshipManager.js +++ /dev/null @@ -1,74 +0,0 @@ -const RESTManager = require('./RESTManager'); - -class UserManager extends RESTManager { - async show(user) { - return await this.request(`/api/v1/friendships/show/${String(user)}/`); - } - - async follow(user) { - return await this.request(`/api/v1/friendships/create/${String(user)}/`, { - method: 'POST', - }); - } - - async unfollow(user) { - return await this.request(`/api/v1/friendships/destroy/${String(user)}/`, { - method: 'POST', - }); - } - - async followers(user) { - return await this.request(`/api/v1/friendships/${String(user)}/followers/`); - } - - async following(user) { - return await this.request(`/api/v1/friendships/${String(user)}/following/`); - } - - async mute(user) { - const requestBody = { - target_posts_author_id: user, - container_module: "ig_text_feed_timeline" - }; - return await this.request(`/api/v1/friendships/mute_posts_or_story_from_follow/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } - - async unmute(user) { - const requestBody = { - target_posts_author_id: user, - container_module: "ig_text_feed_timeline" - }; - return await this.request(`/api/v1/friendships/unmute_posts_or_story_from_follow/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } - - async block(user) { - const requestBody = { - surface: "ig_text_feed_timeline", - is_auto_block_enabled: "true", - user_id: user, - }; - return await this.request(`/api/v1/friendships/block/${user}/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } - - async unblock(user) { - const requestBody = { - user_id: user, - container_module: "ig_text_feed_profile" - }; - return await this.request(`/api/v1/friendships/unblock/${user}/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } -} - -module.exports = UserManager; \ No newline at end of file diff --git a/src/managers/GraphQLManager.js b/src/managers/GraphQLManager.js deleted file mode 100644 index f63b8e4..0000000 --- a/src/managers/GraphQLManager.js +++ /dev/null @@ -1,60 +0,0 @@ -const { fetch } = require('undici'); - -const docIDs = { - BarcelonaProfileRootQuery: '23996318473300828', - BarcelonaPostPageQuery: '6821609764538244', - BarcelonaProfileThreadsTabQuery: '6549913525047487', - BarcelonaProfileRepliesTabQuery: '6480022495409040', -} - -class GraphQLManager { - async getLsd() { - return await fetch('https://www.threads.net/@instagram').then(async res => { - const text = await res.text(); - const matches = text.match(/\["LSD",\[\],{"token":"([^"]*)"}/); - return matches[1]; - }) - } - - async request(docId, variables) { - if (this.lsd === undefined) { - this.lsd = await this.getLsd(); - } - - let request = {}; - request.headers = { - 'User-Agent': 'threads-client', - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-IG-App-ID': '238260118697367', - 'X-FB-LSD': this.lsd, - 'Sec-Fetch-Site': 'same-origin', - }; - request.body = `lsd=${this.lsd}&doc_id=${docId}&variables=${encodeURIComponent(JSON.stringify(variables))}`; - request.credentials = 'omit'; - request.method = 'POST'; - const res = await fetch('https://www.threads.net/api/graphql', { ... request }); - const contentType = res.headers.get('content-type'); - if (contentType.includes('text/javascript')) { - return res.json(); - } - return res.text(); - } - - async getUser(userId) { - return await this.request(docIDs.BarcelonaProfileRootQuery, {userID: String(userId)}); - } - - async getUserPosts(userId) { - return await this.request(docIDs.BarcelonaProfileThreadsTabQuery, {userID: String(userId)}); - } - - async getUserReplies(userId) { - return await this.request(docIDs.BarcelonaProfileRepliesTabQuery, {userID: String(userId)}); - } - - async getPost(postId) { - return await this.request(docIDs.BarcelonaPostPageQuery, {postID: String(postId)}); - } -} - -module.exports = GraphQLManager; diff --git a/src/managers/PostManager.js b/src/managers/PostManager.js index 2acd40e..c78e040 100644 --- a/src/managers/PostManager.js +++ b/src/managers/PostManager.js @@ -1,153 +1,35 @@ -const RESTManager = require("./RESTManager"); - -class PostManager extends RESTManager { - async fetch(post, paging_token) { - return await this.request(`/api/v1/text_feed/${post}/replies/` + (paging_token ? `?paging_token=${encodeURIComponent(paging_token)}` : "")); - } - - async likers(post, user) { - return await this.request(`/api/v1/media/${post}_${user}/likers/`); - } - - async create( - user, - options = { - contents: "", - data: null, - } - ) { - const requestBody = { - publish_mode: "text_post", - text_post_app_info: - '{"reply_control":0}' + options.data !== null ? options.data : "", - timezone_offset: "-25200", - source_type: "4", - _uid: String(user), - device_id: `android-${this.client.androidId}`, - caption: options.contents, - upload_id: new Date().getTime(), - device: { - manufacturer: "OnePlus", - model: "ONEPLUS+A3010", - android_version: 25, - android_release: "7.1.1", - }, - }; - return await this.request(`/api/v1/media/configure_text_only_post/`, { - method: "POST", - body: `signed_body=SIGNATURE.${encodeURIComponent( - JSON.stringify(requestBody) - )}`, - }); - } - - async reply( - user, - options = { - contents: "", - post: "", - } - ) { - let text_post_app_info = JSON.stringify({ - reply_id: options.post, - reply_control: 0, - }); - const requestBody = { - publish_mode: "text_post", - text_post_app_info, - timezone_offset: "-25200", - source_type: "4", - _uid: String(user), - device_id: `android-${this.client.androidId}`, - caption: options.contents, - upload_id: new Date().getTime(), - device: { - manufacturer: "OnePlus", - model: "ONEPLUS+A3010", - android_version: 25, - android_release: "7.1.1", - }, - }; - - return await this.request(`/api/v1/media/configure_text_only_post/`, { - method: "POST", - body: `signed_body=SIGNATURE.${encodeURIComponent( - JSON.stringify(requestBody) - )}`, - }); - } - - async quote( - user, - options = { - contents: "", - post: "", - } - ) { - let text_post_app_info = JSON.stringify({"quoted_post_id": options.post, "reply_control": 0}); - - const requestBody = { - publish_mode: "text_post", - text_post_app_info, - timezone_offset: "-25200", - source_type: "4", - _uid: user, - device_id: `android-${this.client.androidId}`, - caption: options.contents, - upload_id: new Date().getTime(), - device: { - manufacturer: "OnePlus", - model: "ONEPLUS+A3010", - android_version: 25, - android_release: "7.1.1", - }, - }; - return await this.request(`/api/v1/media/configure_text_only_post/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } - - async delete(post, user) { - return await this.request( - `/api/v1/media/${post}_${String(user)}/delete/?media_type=TEXT_POST`, - { - method: "POST", - } - ); - } - - async like(post, user) { - return await this.request(`/api/v1/media/${post}_${String(user)}/like/`, { - method: "POST", - }); - } - - async unlike(post, user) { - return await this.request(`/api/v1/media/${post}_${String(user)}/unlike/`, { - method: "POST", - }); - } - - async repost(post) { - return await this.request(`/api/v1/repost/create_repost/`, { - method: 'POST', - body: 'media_id=' + post - }); - } - - async unrepost(post) { - return await this.request(`/api/v1/repost/delete_text_app_repost/`, { - method: 'POST', - body: 'original_media_id=' + post - }); - } - - async embed(url) { - return await this.request( - `/api/v1/text_feed/link_preview/?url=${encodeURIComponent(url)}` - ); - } -} - -module.exports = PostManager; +const RESTManager = require('./RESTManager'); + +class PostManager extends RESTManager { + async post(options) { + let container; + if (typeof(options) === 'string') { + await this.request(`/v19.0/me/threads?media_type=IMAGE&text=${options}`, { + method: 'POST' + }).then(res => { + containerId = res.id; + }); + } else { + // TODO: not this + let mediaType = options.mediaType === 'video' ? 'video' : 'image' + if (options.carouselItem) { + await this.request(`/v19.0/me/threads?media_type=${mediaType.toUpperCase()}&${mediaType}_url=${options.url}`, { + method: 'POST' + }).then(res => { + container = res.id; + }); + } else { + await this.request(`/v19.0/me/threads?media_type=CAROUSEL&children=${options.url}&is_carousel_item=true`, { + method: 'POST' + }).then(res => { + container = res.id; + }); + } + } + return await this.request(`/v19.0/me/threads_publish?creation_id=${container}`, { + method: 'POST' + }) + } +} + +module.exports = PostManager; \ No newline at end of file diff --git a/src/managers/RESTManager.js b/src/managers/RESTManager.js index cc2f17d..96d92ea 100644 --- a/src/managers/RESTManager.js +++ b/src/managers/RESTManager.js @@ -10,18 +10,7 @@ class RESTManager { options = {}; }; options.method = options?.method ?? 'GET'; - options.headers = { - 'Authorization': `Bearer IGT:2:${this.client.options.token}`, - 'User-Agent': this.client.options.userAgent, - 'Sec-Fetch-Site': 'same-origin', - }; - if (options.method === 'POST') { - options.headers = { - ...options.headers, - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }; - }; - const res = await fetch(this.client.options.base + url, { ... options }); + const res = await fetch(`${this.client.options.base}${url}&access-token=${this.client.options.token}`, { ... options }); const contentType = res.headers.get('content-type'); if (contentType.includes('application/json')) { return res.json(); diff --git a/src/managers/RestrictionManager.js b/src/managers/RestrictionManager.js deleted file mode 100644 index 13d283e..0000000 --- a/src/managers/RestrictionManager.js +++ /dev/null @@ -1,23 +0,0 @@ -const RESTManager = require('./RESTManager'); - -class RestrictionManager extends RESTManager { - async restrict(user) { - const requestBody = { - user_ids: user, - container_module: "ig_text_feed_timeline" - }; - return await this.request(`/api/v1/restrict_action/restrict_many/`, { - method: 'POST', - body: `signed_body=SIGNATURE.${encodeURIComponent(JSON.stringify(requestBody))}`, - }); - } - - async unrestrict(user) { - return await this.request(`/api/v1/restrict_action/unrestrict/`, { - method: 'POST', - body: `target_user_id=${user}&container_module=ig_text_feed_timeline`, - }); - } -} - -module.exports = RestrictionManager; \ No newline at end of file diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js index ad29214..6db8aee 100644 --- a/src/managers/UserManager.js +++ b/src/managers/UserManager.js @@ -1,12 +1,12 @@ const RESTManager = require('./RESTManager'); class UserManager extends RESTManager { - async fetch(user) { - return await this.request(`/api/v1/users/${String(user)}/info`); + async get(id) { + return await this.request(`/${id ? id : 'me'}?fields=id,username,threads_profile_picture_url,threads_biography`); } - async search(query, count) { - return await this.request(`/api/v1/users/search/?q=${query}&count=${count ?? 30}`); + async limit(id) { + return await this.request(`/${id ? id : 'me'}/threads_publishing_limit?fields=quota_usage,config`); } } diff --git a/src/util/Bloks.js b/src/util/Bloks.js deleted file mode 100644 index ce7f378..0000000 --- a/src/util/Bloks.js +++ /dev/null @@ -1,36 +0,0 @@ -function parseNestedJson(json) { - var parsedJson = {}; - - for (var key in json) { - if (json.hasOwnProperty(key)) { - var value = json[key]; - - if (typeof value === 'string') { - try { - parsedJson[key] = JSON.parse(value); - } catch (error) { - // assign the original string value - parsedJson[key] = value; - } - } else if (typeof value === 'object') { - // recursively call for JSON within object value - parsedJson[key] = parseNestedJson(value); - } else { - // assign non-string and non-object values - parsedJson[key] = value; - } - } - } - - return parsedJson; -} - -function parseBloksResponse(text) { - const { tree } = JSON.parse(text).layout.bloks_payload; - const sanitized = JSON.parse(JSON.parse(tree['㐟']['#'].match(/\"\{.*\}\"/)[0])); - const json = parseNestedJson(sanitized); - - return json; -} - -module.exports = { parseBloksResponse } diff --git a/typings/index.d.ts b/typings/index.d.ts index fb06580..c39eb06 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -6,463 +6,3 @@ * floating point numbers, and as such cannot accurately represent integers greater than 2^53. This is * a limitation of JavaScript, not threads.js. */ - -export interface Base { - status: 'ok' | 'fail'; -} - -export interface BiographyWithEntities { - raw_text: string; - entities: any[]; -} - -export interface BioLink { - link_id: number; - url: string; - lynx_url: string; - link_type: string; - title: string; - open_external_url_with_in_app_browser: string; -} - -export interface Caption { - pk: string; - user_id: number; - text: string; - type: number; - created_at: number; - created_at_utc: number; - content_type: string; - status: string; - bit_flags: number; - did_report_as_spam: boolean; - share_enabled: boolean; - user: any; - is_covered: boolean; - is_ranked_comment: boolean; - media_id: number; - private_reply_status: number; -} - -export class Client { - public constructor(options: ClientOptions); - private _qeSync(): Promise; - private _encryptPassword(): Promise; - - public feeds: FeedManager; - public friendships: FriendshipManager; - public graphql: GraphQLManager; - public login(username: string, password: string): Promise; - public options: ClientOptions; - public posts: PostManager; - public rest: RESTManager; - public restrictions: RestrictionManager; - public users: UserManager; -} - -export interface ClientOptions { - token?: string | null; - userAgent?: string; - appId?: string; - androidId?: string; - userId?: string | null; - base?: string; -} - -export interface CreateOptions { - contents: string; - data?: null | Object; -} - -export interface CreatorShoppingInfo { - linked_merchant_accounts: any[]; -} - -export interface EncryptedPasswordResponse { - time: string; - password: string; -} - -export interface FanClub { - fan_club_id: number | null; - fan_club_name: string | null; - is_fan_club_referral_eligible: boolean | null; - fan_consideration_page_revamp_eligiblity: boolean | null; - is_fan_club_gifting_eligible: boolean | null; - subscriber_count: number | null; - connected_member_count: number | null; - autosave_to_exclusive_highlight: boolean | null; - has_enough_subscribers_for_ssc: boolean | null; -} - -export class FeedManager extends RESTManager { - fetch(max_id?: string): Promise; - fetchThreads(user: string | number, max_id?: string): Promise; - fetchReplies(user: string | number, max_id?: string): Promise; - recommended(paging_token?: number): Promise; - notifications(filter?: NotificationFilter, pagination?: NotificationPagination): Promise; - notificationseen(): Promise; -} - -export class FriendshipManager extends RESTManager { - show(user: string | number): Promise; - follow(user: string | number): Promise; - unfollow(user: string | number): Promise; - followers(user: string | number): Promise; - following(user: string | number): Promise; - mute(user: string | number): Promise; - unmute(user: string | number): Promise; - block(user: string | number): Promise; - unblock(user: string | number): Promise; -} - -export interface FriendshipStatus { - following: boolean; - followed_by: boolean; - blocking: boolean; - muting: boolean; - is_private: boolean; - incoming_request: boolean; - outgoing_request: boolean; - text_post_app_pre_following: boolean; - is_bestie: boolean; - is_restricted: boolean; - is_feed_favorite: boolean; - is_eligible_to_subscribe: boolean; -} - -export class GraphQLManager { - getLsd(): Promise; - request(docId: string, variables: string): Promise; - getUser(userId: string | number): Promise; - getUserPosts(userId: string | number): Promise; - getUserReplies(userId: string | number): Promise; - getPost(postId: string): Promise; -} - -export interface Interests { - interests: any[]; -} - -export type NotificationFilter = - 'text_post_app_replies' | - 'text_post_app_mentions' | - 'verified'; - -export interface NotificationPagination { - max_id: string; - pagination_first_record_timestamp: string; -} - -export interface PagingTokens { - downwards: string; -} - -export interface PinnedChannels { - pinned_channel_list: any[]; - has_public_channels: boolean; -} - -export interface Post { - pk: number; - id: string; - taken_at: number; - device_timestamp: number; - client_cache_key: string; - filter_type: number; - like_and_view_counts_disabled: boolean; - integrity_review_decision: string; - text_post_app_info: TextPostAppInfo; - caption: any; - media_type: number; - code: string; - product_type: string; - organic_tracking_token: string; - image_versions2: any; - original_width: number; - original_height: number; - is_dash_eligible?: number; - video_dash_manifest?: string; - video_codec?: string; - has_audio?: boolean; - video_duration?: number; - video_versions: any[]; - like_count: number; - has_liked: boolean; - can_viewer_reshare: boolean; - top_likers: any[]; - user: PostUser; -} - -export class PostManager extends RESTManager { - fetch(post: string, paging_token?: string) : Promise; - likers(post: string, user: string | number) : Promise; - create(contents: string, options: CreateOptions) : Promise; - reply(user: string | number, options: ReplyOptions): Promise; - quote(user: string | number, options: ReplyOptions): Promise; - delete(post: string, user: string | number): Promise; - like(post: string, user: string | number) : Promise; - unlike(post: string, user: string | number) : Promise; - repost(post: string) : Promise; - unrepost(post: string) : Promise; - embed(url: string) : Promise; -} - -export interface PostUser { - pk: number; - pk_id: string; - username: string; - full_name: string; - is_private: boolean; - is_verified: boolean; - profile_pic_id: string; - profile_pic_url: string; - friendship_status: FriendshipStatus; - has_anonymous_profile_picture: boolean; - has_onboarded_to_text_post_app: boolean; - account_badges: any[]; -} - -export interface ProfilePicture { - url: string; - width: number; - height: number; -} - -export interface ReplyOptions { - contents: string; - post: string; -} - -export class RESTManager { - public constructor(client: Client); - request(url: string, options?: object): Promise; -} - -export class RestrictionManager extends RESTManager { - restrict(user: string | number): Promise; - unrestrict(user: string | number): Promise; -} - -export interface TextPostAppInfo { - is_post_unavailable: boolean; - is_reply: boolean; - reply_to_author: any | null; - direct_reply_count: number; - self_thread_count: number; - reply_facepile_users: any[]; - link_preview_attachment: any | null; - can_reply: boolean; - reply_control: string; - hush_info: any | null; - share_info: any; -} - -export interface Thread extends Base { - containing_thread: ThreadObject; - reply_threads: ThreadObject[]; - sibling_threads: any[]; - paging_tokens: PagingTokens; - downwards_thread_will_continue: boolean; - target_post_reply_placeholder: string; -} - -export interface ThreadItem { - post: Post; - line_type: string; - view_replies_cta_string: string; - reply_facepile_users: any[]; - can_inline_expand_below: boolean; -} - -export interface ThreadObject { - thread_items: ThreadItem[]; - thread_type: string; - show_create_reply_cta: boolean; - id: number; - posts: Post[]; -} - -export interface User extends Base { - user: UserObject; -} - -export class UserManager extends RESTManager { - fetch(user: string | number): Promise; - search(query: string, count?: number | string): Promise; -} - -export interface UserObject { - has_anonymous_profile_picture: boolean; - is_supervision_features_enabled: boolean; - is_new_to_instagram: boolean; - follower_count: number; - media_count: number; - following_count: number; - following_tag_count: number; - can_use_affiliate_partnership_messaging_as_creator: boolean; - can_use_affiliate_partnership_messaging_as_brand: boolean; - has_collab_collections: boolean; - has_private_collections: boolean; - bio_interests: Interests; - has_music_on_profile: boolean; - is_potential_business: boolean; - page_id: number; - page_name: string; - ads_page_id: number; - ads_page_name: string; - can_use_branded_content_discovery_as_creator: boolean; - can_use_branded_content_discovery_as_brand: boolean; - fan_club_info: FanClub; - fbid_v2: string; - pronouns: any[]; - is_eligible_for_diverse_owned_business_info: boolean; - is_eligible_to_display_diverse_owned_business_info: boolean; - is_whatsapp_linked: boolean; - transparency_product_enabled: boolean; - account_category: string; - interop_messaging_user_fbid: number; - bio_links: BioLink[]; - can_add_fb_group_link_on_profile: boolean; - external_url: string; - show_shoppable_feed: boolean; - merchant_checkout_style: string; - seller_shoppable_feed_type: string; - creator_shopping_info: CreatorShoppingInfo; - has_guides: boolean; - has_highlight_reels: boolean; - hd_profile_pic_url_info: ProfilePicture; - hd_profile_pic_versions: ProfilePicture[]; - is_interest_account: boolean; - is_favorite: boolean; - is_favorite_for_stories: boolean; - is_favorite_for_igtv: boolean; - is_favorite_for_clips: boolean; - is_favorite_for_highlights: boolean; - live_subscription_status: string; - is_bestie: boolean; - usertags_count: number; - total_ar_effects: number; - total_clips_count: number; - has_videos: boolean; - total_igtv_videos: number; - has_igtv_series: boolean; - biography: string; - include_direct_blacklist_status: boolean; - biography_with_entities: BiographyWithEntities; - show_fb_link_on_profile: boolean; - primary_profile_link_type: number; - can_hide_category: boolean; - can_hide_public_contacts: boolean; - should_show_category: boolean; - category_id: number; - is_category_tappable: boolean; - should_show_public_contacts: boolean; - is_eligible_for_smb_support_flow: boolean; - is_eligible_for_lead_center: boolean; - is_experienced_advertiser: boolean; - lead_details_app_id: string; - is_business: boolean; - professional_conversion_suggested_account_type: number; - account_type: number; - direct_messaging: string; - instagram_location_id: number; - address_street: string; - business_contact_method: string; - city_id: number; - city_name: string; - contact_phone_number: string; - is_profile_audio_call_enabled: boolean; - latitude: number; - longitude: number; - public_email: string; - public_phone_country_code: string; - public_phone_number: string; - zip: string; - mutual_followers_count: number; - has_onboarded_to_text_post_app: boolean; - show_text_post_app_badge: boolean; - show_ig_app_switcher_badge: boolean; - show_text_post_app_switcher_badge: boolean; - profile_context: string; - profile_context_links_with_user_ids: any[]; - profile_context_facepile_users: any[]; - has_chaining: boolean; - pk: string; - pk_id: string; - username: string; - full_name: string; - is_private: boolean; - follow_friction_type: number; - is_verified: boolean; - profile_pic_id: string; - profile_pic_url: string; - current_catalog_id: any | null; - mini_shop_seller_onboarding_status: any | null; - shopping_post_onboard_nux_type: any | null; - ads_incentive_expiration_date: any | null; - displayed_action_button_partner: any | null; - smb_delivery_partner: any | null; - smb_support_delivery_partner: any | null; - displayed_action_button_type: string; - smb_support_partner: any | null; - is_call_to_action_enabled: boolean; - num_of_admined_pages: any | null; - category: string; - account_badges: any[]; - highlight_reshare_disabled: boolean; - auto_expand_chaining: any | null; - feed_post_reshare_disabled: boolean; - robi_feedback_source: any | null; - is_memorialized: boolean; - open_external_url_with_in_app_browser: boolean; - has_exclusive_feed_content: boolean; - has_fan_club_subscriptions: boolean; - pinned_channels_info: PinnedChannels; - nametag: any | null; - remove_message_entrypoint: boolean; - show_account_transparency_details: boolean; - existing_user_age_collection_enabled: boolean; - show_post_insights_entry_point: boolean; - has_public_tab_threads: boolean; - third_party_downloads_enabled: number; - is_regulated_c18: boolean; - is_in_canada: boolean; - profile_type: number; - is_profile_broadcast_sharing_enabled: boolean; -} - -export interface UserSearch extends Base { - num_result: number; - users: UserSearchObject[]; - has_more: boolean; - rank_token: string; -} - -export interface UserSearchObject { - has_anonymous_profile_picture: boolean; - follower_count: number; - media_count: number; - following_count: number; - following_tag_count: number; - fbid_v2: string; - has_onboarded_to_text_post_app: boolean; - show_text_post_app_badge: boolean; - text_post_app_joiner_number: number; - show_ig_app_switcher_badge: boolean; - pk: string; - pk_id: string; - username: string; - full_name: string; - is_private: boolean; - is_verified: boolean; - profile_pic_id: string; - profile_pic_url: string; - has_opt_eligible_shop: boolean; - account_badges: any[]; - third_party_downloads_enabled: number; - unseen_count: number; - friendship_status: FriendshipStatus; - latest_reel_media: number; - should_show_category: boolean; -} \ No newline at end of file