diff --git a/src/constants/api.ts b/src/constants/api.ts index 428a293..c930172 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -8,6 +8,8 @@ export const _tiktokSearchLiveFull = (params: any): string => `${_tiktokurl}/api/search/live/full/?${params}` export const _tiktokGetPosts = (params: any): string => `${_tiktokurl}/api/post/item_list/?${params}` +export const _tiktokGetComments = (params: any): string => + `${_tiktokurl}/api/comment/list/?${params}` /** Tiktokv */ export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us` diff --git a/src/constants/params.ts b/src/constants/params.ts index 2d2f6f4..2d48fc9 100644 --- a/src/constants/params.ts +++ b/src/constants/params.ts @@ -1,6 +1,7 @@ import qs from "qs" -export const _userPostsParams = () => { +/** Get Params */ +export const _getUserPostsParams = () => { return ( qs.stringify({ aid: 1988, @@ -37,14 +38,70 @@ export const _userPostsParams = () => { ) } +export const _xttParams = (secUid: string, cursor: number, count: number) => { + return qs.stringify({ + aid: "1988", + cookie_enabled: true, + screen_width: 0, + screen_height: 0, + browser_language: "", + browser_platform: "", + browser_name: "", + browser_version: "", + browser_online: "", + timezone_name: "Europe/London", + secUid, + cursor, + count, + is_encryption: 1 + }) +} + +export const _getCommentsParams = (id: string, count: number) => { + let cursor = 0 + + // 50 comments per page + if (count > 50) { + for (let i = 1; i < count; i++) { + cursor += 50 + } + } + + return qs.stringify({ + aid: "1988", + app_language: "ja-JP", + app_name: "tiktok_web", + aweme_id: id, + browser_language: "en-US", + browser_name: "Mozilla", + browser_online: true, + browser_platform: "Linux x86_64", + browser_version: "5.0 (X11)", + channel: "tiktok_web", + cookie_enabled: true, + count: 50, + cursor: cursor, + device_id: "7445428925624813064", + os: "linux", + region: "ID", + screen_height: 768, + screen_width: 1366 + }) +} + +/** Search */ export const _userSearchParams = ( keyword: string, - page: number = 1, + page: number, xbogus?: any ) => { let cursor = 0 - for (let i = 1; i < page; i++) { - cursor += 10 + + // 10 users per page + if (page > 1) { + for (let i = 1; i < page; i++) { + cursor += 10 + } } const params = { @@ -98,10 +155,14 @@ export const _userSearchParams = ( return qs.stringify(params) } -export const _liveSearchParams = (keyword: string, page: number = 1) => { +export const _liveSearchParams = (keyword: string, page: number) => { let cursor = 0 - for (let i = 1; i < page; i++) { - cursor += 12 + + // 12 cursor for 20 lives per page + if (page > 1) { + for (let i = 1; i < page; i++) { + cursor += 12 + } } let offset = `${cursor}` @@ -142,6 +203,7 @@ export const _liveSearchParams = (keyword: string, page: number = 1) => { }) } +/** Downloader Params */ export const _tiktokApiParams = (args: any) => { return new URLSearchParams({ ...args, @@ -178,25 +240,6 @@ export const _tiktokApiParams = (args: any) => { }).toString() } -export const _xttParams = (secUid: string, cursor: number, count: number) => { - return qs.stringify({ - aid: "1988", - cookie_enabled: true, - screen_width: 0, - screen_height: 0, - browser_language: "", - browser_platform: "", - browser_name: "", - browser_version: "", - browser_online: "", - timezone_name: "Europe/London", - secUid, - cursor, - count, - is_encryption: 1 - }) -} - const randomChar = (char: string, range: number) => { let chars = "" diff --git a/src/index.ts b/src/index.ts index 051a0de..f88d339 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,18 +3,22 @@ import { MusicalDown } from "./utils/downloader/musicalDown" import { SSSTik } from "./utils/downloader/ssstik" import { TiktokAPI } from "./utils/downloader/tiktokApi" +/** Get */ +import { StalkUser } from "./utils/get/getProfile" + /** Search */ -import { StalkUser } from "./utils/search/stalker" import { SearchUser } from "./utils/search/userSearch" +import { SearchLive } from "./utils/search/liveSearch" /** Types */ import { MusicalDownResponse } from "./types/downloader/musicaldown" import { SSSTikResponse } from "./types/downloader/ssstik" import { TiktokAPIResponse } from "./types/downloader/tiktokApi" import { TiktokUserSearchResponse } from "./types/search/userSearch" -import { StalkResult } from "./types/search/stalker" -import { SearchLive } from "./utils/search/liveSearch" +import { StalkResult } from "./types/get/getProfile" import { TiktokLiveSearchResponse } from "./types/search/liveSearch" +import { CommentsResult } from "./types/get/getComments" +import { getComments } from "./utils/get/getComments" type TiktokDownloaderResponse = T extends "v1" ? TiktokAPIResponse @@ -35,28 +39,38 @@ export = { * @param {string} url - The Tiktok URL you want to download * @param {object} options - The options for downloader * @param {string} options.version - The version of downloader + * @param {string} options.proxy - Your Proxy (optional) + * @param {boolean} options.showOriginalResponse - Show Original Response (optional) & Only for v1 * @returns {Promise} */ Downloader: async ( url: string, - options?: { version: T } + options?: { version: T; proxy?: string; showOriginalResponse?: boolean } ): Promise> => { switch (options?.version.toLowerCase()) { case "v1": { - const response = await TiktokAPI(url) + const response = await TiktokAPI( + url, + options?.proxy, + options?.showOriginalResponse + ) return response as TiktokDownloaderResponse } case "v2": { - const response = await SSSTik(url) + const response = await SSSTik(url, options?.proxy) return response as TiktokDownloaderResponse } case "v3": { - const response = await MusicalDown(url) + const response = await MusicalDown(url, options?.proxy) return response as TiktokDownloaderResponse } default: { - const response = await TiktokAPI(url) + const response = await TiktokAPI( + url, + options?.proxy, + options?.showOriginalResponse + ) return response as TiktokDownloaderResponse } } @@ -66,13 +80,19 @@ export = { * @param {string} query - The query you want to search * @param {object} options - The options for search * @param {string} options.type - The type of search - * @param {string} options.cookie - Your Tiktok Cookie (optional) + * @param {string | any[]} options.cookie - Your Tiktok Cookie (optional) * @param {number} options.page - The page of search (optional) + * @param {string} options.proxy - Your Proxy (optional) * @returns {Promise} */ Search: async ( query: string, - options: { type: T; cookie?: string; page?: number; proxy?: string } + options: { + type: T + cookie?: string | any[] + page?: number + proxy?: string + } ): Promise> => { switch (options?.type.toLowerCase()) { case "user": { @@ -108,12 +128,18 @@ export = { * Tiktok Stalk User * @param {string} username - The username you want to stalk * @param {object} options - The options for stalk - * @param {string} options.cookie - Your Tiktok Cookie (optional) + * @param {string | any[]} options.cookie - Your Tiktok Cookie (optional) + * @param {number} options.postLimit - The limit of post you want to get (optional) + * @param {string} options.proxy - Your Proxy (optional) * @returns {Promise} */ StalkUser: async ( username: string, - options?: { cookie?: string; postLimit?: number; proxy?: string } + options?: { + cookie?: string | any[] + postLimit?: number + proxy?: string + } ): Promise => { const response = await StalkUser( username, @@ -122,5 +148,25 @@ export = { options?.proxy ) return response + }, + + /** + * Tiktok Get Comments + * @param {string} url - The Tiktok URL you want to get comments + * @param {object} options - The options for get comments + * @param {string} options.proxy - Your Proxy (optional) + * @param {number} options.page - The page you want to get (optional) + * @returns {Promise} + */ + GetComments: async ( + url: string, + options?: { commentLimit?: number; proxy?: string } + ): Promise => { + const response = await getComments( + url, + options?.proxy, + options?.commentLimit + ) + return response } } diff --git a/src/types/get/getComments.ts b/src/types/get/getComments.ts new file mode 100644 index 0000000..974d20e --- /dev/null +++ b/src/types/get/getComments.ts @@ -0,0 +1,28 @@ +export type CommentsResult = { + status: "success" | "error" + message?: string + result?: Comments[] + totalComments?: number +} + +export type Comments = { + cid: string + text: string + commentLanguage: string + createTime: number + likeCount: number + isAuthorLiked: boolean + isCommentTranslatable: boolean + replyCommentTotal: number + replyComment: Comments[] | null + user: User + url: string +} + +export type User = { + uid: string + avatarThumb: string[] + nickname: string + username: string + isVerified: boolean +} diff --git a/src/utils/get/getComments.ts b/src/utils/get/getComments.ts new file mode 100644 index 0000000..93b8131 --- /dev/null +++ b/src/utils/get/getComments.ts @@ -0,0 +1,176 @@ +import Axios from "axios" +import { _tiktokGetComments } from "../../constants/api" +import { _getCommentsParams } from "../../constants/params" +import { HttpsProxyAgent } from "https-proxy-agent" +import { SocksProxyAgent } from "socks-proxy-agent" +import { Comments, CommentsResult, User } from "../../types/get/getComments" + +const TiktokURLregex = + /https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ + +/** + * Tiktok Get Comments + * @param {string} url - Tiktok URL + * @param {string} proxy - Your Proxy (optional) + * @param {number} commentLimit - Comment Limit (optional) + * @returns {Promise} + */ + +export const getComments = async ( + url: string, + proxy?: string, + commentLimit?: number +): Promise => + new Promise(async (resolve) => { + if (!TiktokURLregex.test(url)) { + return resolve({ + status: "error", + message: "Invalid Tiktok URL. Make sure your url is correct!" + }) + } + url = url.replace("https://vm", "https://vt") + Axios(url, { + method: "HEAD", + httpsAgent: + (proxy && + (proxy.startsWith("http") || proxy.startsWith("https") + ? new HttpsProxyAgent(proxy) + : proxy.startsWith("socks") + ? new SocksProxyAgent(proxy) + : undefined)) || + undefined + }) + .then(async ({ request }) => { + const { responseUrl } = request.res + let ID = responseUrl.match(/\d{17,21}/g) + if (ID === null) + return resolve({ + status: "error", + message: + "Failed to fetch tiktok url. Make sure your tiktok url is correct!" + }) + ID = ID[0] + + const resultComments = await parseComments(ID, commentLimit, proxy) + return resolve({ + status: "success", + result: resultComments.comments, + totalComments: resultComments.total + }) + }) + .catch((e) => resolve({ status: "error", message: e.message })) + }) + +const requestComments = async ( + id: string, + commentLimit: number, + proxy?: string +) => { + const { data } = await Axios( + _tiktokGetComments(_getCommentsParams(id, commentLimit)), + { + method: "GET", + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" + }, + httpsAgent: + (proxy && + (proxy.startsWith("http") || proxy.startsWith("https") + ? new HttpsProxyAgent(proxy) + : proxy.startsWith("socks") + ? new SocksProxyAgent(proxy) + : undefined)) || + undefined + } + ) + + return data +} + +const parseComments = async ( + id: string, + commentLimit?: number, + proxy?: string +) => { + const comments: Comments[] = [] + let cursor: number = 0 + let counter: number = 0 + let count: number = 50 + let total: number = 0 + let hasMore: boolean = true + + while (hasMore) { + for (let i = 0; i < count; i++) {} + + const result = await requestComments(id, cursor, proxy) + + // Check if the result has more comments + if (result.has_more === 0) hasMore = false + + result.comments?.forEach((v: any) => { + const comment = { + cid: v.cid, + text: v.text, + commentLanguage: v.comment_language, + createTime: v.create_time, + likeCount: v.digg_count, + isAuthorLiked: v.is_author_digged, + isCommentTranslatable: v.is_comment_translatable, + replyCommentTotal: v.reply_comment_total, + user: { + uid: v.user.uid, + avatarThumb: v.user.avatar_thumb.url_list, + nickname: v.user.nickname, + username: v.user.unique_id, + isVerified: v.user.custom_verify !== "" + } as User, + url: v.share_info?.url || "", + replyComment: [] + } + + if (v.reply_comment !== null) { + v.reply_comment.forEach((v: any) => { + comment.replyComment.push({ + cid: v.cid, + text: v.text, + commentLanguage: v.comment_language, + createTime: v.create_time, + likeCount: v.digg_count, + isAuthorLiked: v.is_author_digged, + isCommentTranslatable: v.is_comment_translatable, + replyCommentTotal: v.reply_comment_total, + user: { + uid: v.user.uid, + avatarThumb: v.user.avatar_thumb.url_list, + nickname: v.user.nickname, + username: v.user.unique_id, + isVerified: v.user.custom_verify !== "" + } as User, + url: v.share_info?.url || "", + replyComment: [] + }) + + total++ + }) + } + total++ + comments.push(comment) + }) + + // Check if the comments length is equal to the comment limit + if (commentLimit) { + let loopCount = Math.floor(commentLimit / 50) + if (counter >= loopCount) hasMore = false + } + + hasMore = result.has_more === 1 + cursor = result.has_more === 1 ? result.cursor : 0 + counter++ + } + + return { + total: total, + comments: commentLimit ? comments.slice(0, commentLimit) : comments + } +} diff --git a/src/utils/search/stalker.ts b/src/utils/search/stalker.ts deleted file mode 100644 index bc48500..0000000 --- a/src/utils/search/stalker.ts +++ /dev/null @@ -1,263 +0,0 @@ -import Axios from "axios" -import qs from "qs" -import { load } from "cheerio" -import { _tiktokGetPosts, _tiktokurl } from "../../constants/api" -import { - AuthorPost, - Posts, - StalkResult, - Stats, - Users -} from "../../types/search/stalker" -import { _userPostsParams, _xttParams } from "../../constants/params" -import { createCipheriv } from "crypto" -import { HttpsProxyAgent } from "https-proxy-agent" -import { SocksProxyAgent } from "socks-proxy-agent" - -/** - * Tiktok Stalk User - * @param {string} username - The username you want to stalk - * @param {object|string} cookie - Your Tiktok Cookie (optional) - * @param {number} postLimit - The limit of post you want to get (optional) - * @param {string} proxy - Your Proxy (optional) - * @returns {Promise} - */ - -export const StalkUser = ( - username: string, - cookie?: any, - postLimit?: number, - proxy?: string -): Promise => - new Promise(async (resolve) => { - username = username.replace("@", "") - Axios.get(`${_tiktokurl}/@${username}`, { - headers: { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", - cookie: - typeof cookie === "object" - ? cookie.map((v) => `${v.name}=${v.value}`).join("; ") - : cookie - }, - httpsAgent: - (proxy && - (proxy.startsWith("http") || proxy.startsWith("https") - ? new HttpsProxyAgent(proxy) - : proxy.startsWith("socks") - ? new SocksProxyAgent(proxy) - : undefined)) || - undefined - }) - .then(async ({ data }) => { - const $ = load(data) - const result = JSON.parse( - $("script#__UNIVERSAL_DATA_FOR_REHYDRATION__").text() - ) - if ( - !result["__DEFAULT_SCOPE__"] && - !result["__DEFAULT_SCOPE__"]["webapp.user-detail"] - ) { - return resolve({ - status: "error", - message: "User not found!" - }) - } - const dataUser = - result["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"] - - const posts: Posts[] = await parsePosts(dataUser, postLimit, proxy) - const { users, stats } = parseDataUser(dataUser, posts) - - resolve({ - status: "success", - result: { - users, - stats, - posts - } - }) - }) - .catch((e) => resolve({ status: "error", message: e.message })) - }) - -/** - * Thanks to: - * https://github.com/atharahmed/tiktok-private-api/blob/020ede2eaa6021bcd363282d8cef1aacaff2f88c/src/repositories/user.repository.ts#L148 - */ - -const request = async ( - secUid: string, - cursor = 0, - count = 30, - proxy?: string -) => { - const { data } = await Axios.get(`${_tiktokGetPosts(_userPostsParams())}`, { - headers: { - "user-agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35", - "X-tt-params": xttparams(_xttParams(secUid, cursor, count)) - }, - httpsAgent: - (proxy && - (proxy.startsWith("http") || proxy.startsWith("https") - ? new HttpsProxyAgent(proxy) - : proxy.startsWith("socks") - ? new SocksProxyAgent(proxy) - : undefined)) || - undefined - }) - - return data -} - -const parseDataUser = (dataUser: any, posts: Posts[]) => { - // User Info Result - const users: Users = { - id: dataUser.user.id, - username: dataUser.user.uniqueId, - nickname: dataUser.user.nickname, - avatarLarger: dataUser.user.avatarLarger, - avatarThumb: dataUser.user.avatarThumb, - avatarMedium: dataUser.user.avatarMedium, - signature: dataUser.user.signature, - verified: dataUser.user.verified, - privateAccount: dataUser.user.privateAccount, - region: dataUser.user.region, - commerceUser: dataUser.user.commerceUserInfo.commerceUser, - usernameModifyTime: dataUser.user.uniqueIdModifyTime, - nicknameModifyTime: dataUser.user.nickNameModifyTime - } - - // Statistics Result - const stats: Stats = { - followerCount: dataUser.stats.followerCount, - followingCount: dataUser.stats.followingCount, - heartCount: dataUser.stats.heartCount, - videoCount: dataUser.stats.videoCount, - likeCount: dataUser.stats.diggCount, - friendCount: dataUser.stats.friendCount, - postCount: posts.length - } - - return { users, stats } -} - -const parsePosts = async ( - dataUser: any, - postLimit?: number, - proxy?: string -): Promise => { - // Posts Result - let hasMore = true - let cursor: number | null = null - const posts: Posts[] = [] - while (hasMore) { - let result2: any | null = null - let counter = 0 - - // Prevent missing response posts - for (let i = 0; i < 30; i++) { - result2 = await request(dataUser.user.secUid, cursor, 30, proxy) - if (result2 !== "") break - } - - // Validate - if (result2 === "") hasMore = false // No More Post - - result2?.itemList?.forEach((v: any) => { - const author: AuthorPost = { - id: v.author.id, - username: v.author.uniqueId, - nickname: v.author.nickname, - avatarLarger: v.author.avatarLarger, - avatarThumb: v.author.avatarThumb, - avatarMedium: v.author.avatarMedium, - signature: v.author.signature, - verified: v.author.verified, - openFavorite: v.author.openFavorite, - privateAccount: v.author.privateAccount, - isADVirtual: v.author.isADVirtual, - isEmbedBanned: v.author.isEmbedBanned - } - - if (v.imagePost) { - const images: string[] = v.imagePost.images.map( - (img: any) => img.imageURL.urlList[0] - ) - - posts.push({ - id: v.id, - desc: v.desc, - createTime: v.createTime, - digged: v.digged, - duetEnabled: v.duetEnabled, - forFriend: v.forFriend, - officalItem: v.officalItem, - originalItem: v.originalItem, - privateItem: v.privateItem, - shareEnabled: v.shareEnabled, - stitchEnabled: v.stitchEnabled, - stats: v.stats, - music: v.music, - author, - images - }) - } else { - const video = { - id: v.video.id, - duration: v.video.duration, - format: v.video.format, - bitrate: v.video.bitrate, - ratio: v.video.ratio, - playAddr: v.video.playAddr, - cover: v.video.cover, - originCover: v.video.originCover, - dynamicCover: v.video.dynamicCover, - downloadAddr: v.video.downloadAddr - } - - posts.push({ - id: v.id, - desc: v.desc, - createTime: v.createTime, - digged: v.digged, - duetEnabled: v.duetEnabled, - forFriend: v.forFriend, - officalItem: v.officalItem, - originalItem: v.originalItem, - privateItem: v.privateItem, - shareEnabled: v.shareEnabled, - stitchEnabled: v.stitchEnabled, - stats: v.stats, - music: v.music, - author, - video - }) - } - }) - - // Restrict too many data requests - if (postLimit !== 0) { - let loopCount = Math.floor(postLimit / 30) - if (counter >= loopCount) break - } - - hasMore = result2.hasMore - cursor = hasMore ? result2.cursor : null - counter++ - } - - return postLimit ? posts.slice(0, postLimit) : posts -} - -const xttparams = (params: any) => { - const cipher = createCipheriv( - "aes-128-cbc", - "webapp1.0+202106", - "webapp1.0+202106" - ) - return Buffer.concat([cipher.update(params), cipher.final()]).toString( - "base64" - ) -}