From 1bae63d2e82f8d80c8b307e189209e488a76c266 Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Sat, 7 Dec 2024 23:54:36 +0700 Subject: [PATCH] feat: add page & totalResults object --- src/types/search/liveSearch.ts | 4 +- src/types/search/stalker.ts | 135 -------------- src/types/search/userSearch.ts | 2 + src/utils/downloader/musicalDown.ts | 11 +- src/utils/downloader/ssstik.ts | 8 +- src/utils/get/getProfile.ts | 269 ++++++++++++++++++++++++++++ src/utils/search/liveSearch.ts | 28 ++- src/utils/search/userSearch.ts | 17 +- 8 files changed, 321 insertions(+), 153 deletions(-) delete mode 100644 src/types/search/stalker.ts create mode 100644 src/utils/get/getProfile.ts diff --git a/src/types/search/liveSearch.ts b/src/types/search/liveSearch.ts index 0101ba1..749c1c9 100644 --- a/src/types/search/liveSearch.ts +++ b/src/types/search/liveSearch.ts @@ -2,6 +2,8 @@ export type TiktokLiveSearchResponse = { status: "success" | "error" message?: string result?: Result[] + page?: number + totalResults?: number } export type Result = { @@ -34,7 +36,7 @@ export type Stats = { } export type Owner = { - id: string + uid: string nickname: string username: string signature: string diff --git a/src/types/search/stalker.ts b/src/types/search/stalker.ts deleted file mode 100644 index fc22dbb..0000000 --- a/src/types/search/stalker.ts +++ /dev/null @@ -1,135 +0,0 @@ -export type StalkResult = { - status: "success" | "error" - message?: string - result?: { - users: Users - stats: Stats - posts: Posts[] - } -} - -export type Users = { - id: string - username: string - nickname: string - avatarLarger: string - avatarThumb: string - avatarMedium: string - signature: string - verified: boolean - privateAccount: boolean - region: string - commerceUser: boolean - usernameModifyTime: number - nicknameModifyTime: number -} - -export type Stats = { - followerCount: number - followingCount: number - heartCount: number - videoCount: number - likeCount: number - friendCount: number - postCount: number -} - -export type Statistics = { - likeCount: number - shareCount: number - commentCount: number - playCount: number - favoriteCount: number -} - -export type Video = { - id: string - duration: string - ratio: string - cover: string - originCover: string - dynamicCover: string - playAddr: string - downloadAddr: string - format: string - bitrate: number -} - -export type Music = { - id: string - title: string - album: string - playUrl: string - coverLarge: string - coverMedium: string - coverThumb: string - authorName: string - duration: string -} - -export type Posts = { - id: string - desc: string - createTime: number - digged: number - duetEnabled: number - forFriend: number - officalItem: number - originalItem: number - privateItem: number - shareEnabled: number - stitchEnabled: number - stats: StatsPost - author: AuthorPost - video?: VideoPost - music: MusicPost - images?: string[] -} - -export type StatsPost = { - collectCount: number - commentCount: number - diggCount: number - playCount: number - shareCount: number -} - -export type AuthorPost = { - id: string - username: string - nickname: string - avatarLarger: string - avatarThumb: string - avatarMedium: string - signature: string - verified: boolean - openFavorite: boolean - privateAccount: boolean - isADVirtual: boolean - isEmbedBanned: boolean -} - -export type VideoPost = { - id: string - duration: number - ratio: string - cover: string - originCover: string - dynamicCover: string - playAddr: string - downloadAddr: string - format: string - bitrate: number -} - -export type MusicPost = { - authorName: string - coverLarge: string - coverMedium: string - coverThumb: string - duration: number - id: string - title: string - playUrl: string - original: boolean -} diff --git a/src/types/search/userSearch.ts b/src/types/search/userSearch.ts index bd68bbf..cbe98c5 100644 --- a/src/types/search/userSearch.ts +++ b/src/types/search/userSearch.ts @@ -2,6 +2,8 @@ export type TiktokUserSearchResponse = { status: "success" | "error" message?: string result?: Result[] + page?: number + totalResults?: number } export type Result = { diff --git a/src/utils/downloader/musicalDown.ts b/src/utils/downloader/musicalDown.ts index 62bf2b0..34e9abf 100644 --- a/src/utils/downloader/musicalDown.ts +++ b/src/utils/downloader/musicalDown.ts @@ -21,8 +21,8 @@ import { SocksProxyAgent } from "socks-proxy-agent" const TiktokURLregex = /https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ -const getRequest = (url: string, proxy?: string) => - new Promise((resolve) => { +const getRequest = (url: string, proxy?: string): Promise => + new Promise((resolve) => { if (!TiktokURLregex.test(url)) { return resolve({ status: "error", @@ -98,8 +98,11 @@ const getRequest = (url: string, proxy?: string) => * @returns {Promise} */ -export const MusicalDown = (url: string, proxy?: string) => - new Promise(async (resolve) => { +export const MusicalDown = ( + url: string, + proxy?: string +): Promise => + new Promise(async (resolve) => { const request: getRequest = await getRequest(url) if (request.status !== "success") return resolve({ status: "error", message: request.message }) diff --git a/src/utils/downloader/ssstik.ts b/src/utils/downloader/ssstik.ts index 3be8088..e8173e8 100644 --- a/src/utils/downloader/ssstik.ts +++ b/src/utils/downloader/ssstik.ts @@ -19,8 +19,8 @@ import { SocksProxyAgent } from "socks-proxy-agent" const TiktokURLregex = /https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/ -const fetchTT = (proxy?: string) => - new Promise(async (resolve) => { +const fetchTT = (proxy?: string): Promise => + new Promise(async (resolve) => { Axios(_ssstikurl, { method: "GET", headers: { @@ -59,8 +59,8 @@ const fetchTT = (proxy?: string) => * @returns {Promise} */ -export const SSSTik = (url: string, proxy?: string) => - new Promise(async (resolve) => { +export const SSSTik = (url: string, proxy?: string): Promise => + new Promise(async (resolve) => { try { if (!TiktokURLregex.test(url)) { return resolve({ diff --git a/src/utils/get/getProfile.ts b/src/utils/get/getProfile.ts new file mode 100644 index 0000000..dc7b521 --- /dev/null +++ b/src/utils/get/getProfile.ts @@ -0,0 +1,269 @@ +import Axios from "axios" +import { load } from "cheerio" +import { _tiktokGetPosts, _tiktokurl } from "../../constants/api" +import { + AuthorPost, + Posts, + StalkResult, + Stats, + Users +} from "../../types/get/getProfile" +import { _getUserPostsParams, _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(`${_tiktokurl}/@${username}`, { + 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", + cookie: + typeof cookie === "object" + ? cookie.map((v: any) => `${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) + + let response: StalkResult = { + status: "success", + result: { + users, + stats, + posts + }, + totalPosts: posts.length + } + + resolve(response) + }) + .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 getUserPosts = async ( + secUid: string, + cursor = 0, + count = 30, + proxy?: string +) => { + const { data } = await Axios.get( + `${_tiktokGetPosts(_getUserPostsParams())}`, + { + 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 = { + uid: 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 result: any | null = null + let counter = 0 + + // Prevent missing response posts + for (let i = 0; i < 30; i++) { + result = await getUserPosts(dataUser.user.secUid, cursor, 30, proxy) + if (result !== "") break + } + + // Validate + if (result === "") hasMore = false // No More Post + + result?.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) hasMore = false + } + + hasMore = result.hasMore + cursor = hasMore ? result.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" + ) +} diff --git a/src/utils/search/liveSearch.ts b/src/utils/search/liveSearch.ts index 24afb6e..684a593 100644 --- a/src/utils/search/liveSearch.ts +++ b/src/utils/search/liveSearch.ts @@ -1,14 +1,19 @@ import Axios from "axios" import { _tiktokSearchLiveFull } from "../../constants/api" import { _liveSearchParams } from "../../constants/params" -import { LiveInfo, Owner, OwnerStats } from "../../types/search/liveSearch" +import { + LiveInfo, + Owner, + OwnerStats, + TiktokLiveSearchResponse +} from "../../types/search/liveSearch" import { SocksProxyAgent } from "socks-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent" /** * Tiktok Search Live * @param {string} keyword - The keyword you want to search - * @param {object|string} cookie - Your Tiktok cookie (optional) + * @param {string | any[]} cookie - Your Tiktok cookie (optional) * @param {number} page - The page you want to search (optional) * @param {string} proxy - Your Proxy (optional) * @returns {Promise} @@ -16,11 +21,17 @@ import { HttpsProxyAgent } from "https-proxy-agent" export const SearchLive = async ( keyword: string, - cookie?: any, + cookie: string | any[], page: number = 1, proxy?: string -) => +): Promise => new Promise(async (resolve) => { + if (!cookie) { + return resolve({ + status: "error", + message: "Cookie is required!" + }) + } Axios(_tiktokSearchLiveFull(_liveSearchParams(keyword, page)), { method: "GET", headers: { @@ -75,7 +86,7 @@ export const SearchLive = async ( likeCount: content.like_count }, owner: { - id: content.owner.id, + uid: content.owner.id, nickname: content.owner.nickname, username: content.owner.display_id, signature: content.owner.bio_description, @@ -101,7 +112,12 @@ export const SearchLive = async ( result.push({ roomInfo, liveInfo }) }) - resolve({ status: "success", result }) + resolve({ + status: "success", + result, + page, + totalResults: data.result.length + }) }) .catch((e) => { resolve({ status: "error", message: e.message }) diff --git a/src/utils/search/userSearch.ts b/src/utils/search/userSearch.ts index da46ea4..f1261d8 100644 --- a/src/utils/search/userSearch.ts +++ b/src/utils/search/userSearch.ts @@ -31,7 +31,7 @@ export const generateURLXbogus = (username: string, page: number) => { /** * Tiktok Search User * @param {string} username - The username you want to search - * @param {object|string} cookie - Your Tiktok cookie (optional) + * @param {string | any[]} cookie - Your Tiktok cookie (optional) * @param {number} page - The page you want to search (optional) * @param {string} proxy - Your Proxy (optional) * @returns {Promise} @@ -39,11 +39,17 @@ export const generateURLXbogus = (username: string, page: number) => { export const SearchUser = ( username: string, - cookie?: any, + cookie: string | any[], page: number = 1, proxy?: string ): Promise => new Promise(async (resolve) => { + if (!cookie) { + return resolve({ + status: "error", + message: "Cookie is required!" + }) + } Axios(generateURLXbogus(username, page), { method: "GET", headers: { @@ -93,7 +99,12 @@ export const SearchUser = ( }) } - resolve({ status: "success", result }) + resolve({ + status: "success", + result, + page, + totalResults: data.result.length + }) }) .catch((e) => { resolve({ status: "error", message: e.message })