From c1be62ea4a1fc5fa2f3383b39e8d4c988f3b2600 Mon Sep 17 00:00:00 2001 From: TobyG74 Date: Sat, 20 Apr 2024 22:12:43 +0700 Subject: [PATCH] feat: add postLimit & fix missing posts response --- src/constants/api.ts | 11 +- src/constants/params.ts | 19 ++ src/index.ts | 38 +++- ...wnloader_musicaldown.ts => musicalDown.ts} | 0 .../{downloader_ssstik.ts => ssstik.ts} | 0 .../{downloader_tiktokApi.ts => tiktokApi.ts} | 0 src/utils/search/stalker.ts | 196 ++++++++++++++++++ src/utils/search/tiktok_stalker.ts | 196 ------------------ .../{tiktok_user_search.ts => userSearch.ts} | 2 +- 9 files changed, 253 insertions(+), 209 deletions(-) rename src/utils/downloader/{downloader_musicaldown.ts => musicalDown.ts} (100%) rename src/utils/downloader/{downloader_ssstik.ts => ssstik.ts} (100%) rename src/utils/downloader/{downloader_tiktokApi.ts => tiktokApi.ts} (100%) create mode 100644 src/utils/search/stalker.ts delete mode 100644 src/utils/search/tiktok_stalker.ts rename src/utils/search/{tiktok_user_search.ts => userSearch.ts} (94%) diff --git a/src/constants/api.ts b/src/constants/api.ts index 916965f..e60c844 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,14 +1,15 @@ /** Tiktok */ export const _tiktokurl: string = "https://www.tiktok.com" export const _tiktokapi = (params: any): string => `https://api.tiktokv.com/aweme/v1/feed/?${params}` -export const _tiktokSearchUserFull = (params: any): string => _tiktokurl + `/api/search/user/full/?${params}` -export const _tiktokSearchVideoFull = (params: any): string => _tiktokurl + `/api/search/item/full/?${params}` +export const _tiktokSearchUserFull = (params: any): string => `${_tiktokurl}/api/search/user/full/?${params}` +export const _tiktokSearchVideoFull = (params: any): string => `${_tiktokurl}/api/search/item/full/?${params}` +export const _tiktokGetPosts = (params: any) => `${_tiktokurl}/api/post/item_list/?${params}` /** SSSTik */ -export const _ssstikapi: string = "https://ssstik.io/abc?url=dl" export const _ssstikurl: string = "https://ssstik.io" +export const _ssstikapi: string = `${_ssstikurl}/abc?url=dl` /** Musicaldown */ -export const _musicaldownapi: string = "https://musicaldown.com/download" export const _musicaldownurl: string = "https://musicaldown.com" -export const _musicaldownmusicapi: string = "https://musicaldown.com/mp3/download" +export const _musicaldownapi: string = `${_musicaldownurl}/download` +export const _musicaldownmusicapi: string = `${_musicaldownurl}/mp3/download` diff --git a/src/constants/params.ts b/src/constants/params.ts index fef4ea9..876e89e 100644 --- a/src/constants/params.ts +++ b/src/constants/params.ts @@ -111,6 +111,25 @@ export const _tiktokApiParams = (args: any) => { } } +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 2cdb20e..7957655 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ /** Downloader */ -import { MusicalDown } from "./utils/downloader/downloader_musicaldown" -import { SSSTik } from "./utils/downloader/downloader_ssstik" -import { TiktokAPI } from "./utils/downloader/downloader_tiktokApi" +import { MusicalDown } from "./utils/downloader/musicalDown" +import { SSSTik } from "./utils/downloader/ssstik" +import { TiktokAPI } from "./utils/downloader/tiktokApi" /** Search */ -import { StalkUser } from "./utils/search/tiktok_stalker" -import { SearchUser } from "./utils/search/tiktok_user_search" +import { StalkUser } from "./utils/search/stalker" +import { SearchUser } from "./utils/search/userSearch" /** Types */ import { MusicalDownResponse } from "./types/downloader/musicaldown" @@ -18,6 +18,14 @@ type TiktokDownloaderResponse = T extends "v1" ? T type TiktokSearchResponse = T extends "user" ? TiktokUserSearchResponse : T extends "video" ? any : TiktokUserSearchResponse export = { + /** + * Tiktok Downloader + * @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 + * @returns {Promise} + */ + Downloader: async (url: string, options?: { version: T }): Promise> => { switch (options?.version) { case "v1": { @@ -38,6 +46,15 @@ export = { } } }, + /** + * Tiktok Search + * @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 {number} options.page - The page of search (optional) + * @returns {Promise} + */ Search: async (query: string, options: { type: T; cookie?: string; page?: number }): Promise> => { switch (options?.type) { case "user": { @@ -54,8 +71,15 @@ export = { } } }, - StalkUser: async (username: string, options?: { cookie?: string }): Promise => { - const response = await StalkUser(username, options?.cookie) + /** + * 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) + * @returns {Promise} + */ + StalkUser: async (username: string, options?: { cookie?: string; postLimit?: number }): Promise => { + const response = await StalkUser(username, options?.cookie, options?.postLimit) return response } } diff --git a/src/utils/downloader/downloader_musicaldown.ts b/src/utils/downloader/musicalDown.ts similarity index 100% rename from src/utils/downloader/downloader_musicaldown.ts rename to src/utils/downloader/musicalDown.ts diff --git a/src/utils/downloader/downloader_ssstik.ts b/src/utils/downloader/ssstik.ts similarity index 100% rename from src/utils/downloader/downloader_ssstik.ts rename to src/utils/downloader/ssstik.ts diff --git a/src/utils/downloader/downloader_tiktokApi.ts b/src/utils/downloader/tiktokApi.ts similarity index 100% rename from src/utils/downloader/downloader_tiktokApi.ts rename to src/utils/downloader/tiktokApi.ts diff --git a/src/utils/search/stalker.ts b/src/utils/search/stalker.ts new file mode 100644 index 0000000..a7db979 --- /dev/null +++ b/src/utils/search/stalker.ts @@ -0,0 +1,196 @@ +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" + +/** + * Tiktok Stalk User + * @param {string} username - The username you want to stalk + * @param {object|string} cookie - Your Tiktok Cookie (optional) + * @returns {Promise} + */ + +export const StalkUser = (username: string, cookie?: any, postLimit?: number): Promise => + new Promise(async (resolve, reject) => { + 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 + } + }) + .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) + 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) => { + 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)) + } + }) + + 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): Promise => { + // Posts Result + let hasMore = true + let cursor: number | null = null + const posts: Posts[] = [] + while (hasMore) { + let result2: any | null = null + + // Prevent missing response posts + for (let i = 0; i < 30; i++) { + result2 = await request(dataUser.user.secUid, cursor, 30) + 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 + }) + } + }) + + hasMore = result2.hasMore + cursor = hasMore ? result2.cursor : null + } + + 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/tiktok_stalker.ts b/src/utils/search/tiktok_stalker.ts deleted file mode 100644 index 926655e..0000000 --- a/src/utils/search/tiktok_stalker.ts +++ /dev/null @@ -1,196 +0,0 @@ -import Axios from "axios" -import qs from "qs" -import { load } from "cheerio" -import { _tiktokurl } from "../../constants/api" -import { AuthorPost, Posts, StalkResult, Stats, Users } from "../../types/search/stalker" -import { _userPostsParams } from "../../constants/params" -import { createCipheriv } from "crypto" - -/** - * Tiktok Stalk User - * @param {string} username - The username you want to stalk - * @param {object|string} cookie - Your Tiktok Cookie (optional) - * @returns {Promise} - */ - -export const StalkUser = (username: string, cookie?: any): Promise => - new Promise(async (resolve, reject) => { - 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 - } - }) - .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"] - - // Posts Result - let hasMore = true - let cursor - const posts: Posts[] = [] - - while (hasMore) { - const result2 = await request(dataUser.user.secUid, cursor, 30) - - if (result2 === "") hasMore = false - - result2?.itemList?.forEach((v) => { - 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) => 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 - }) - } - }) - - hasMore = result2.hasMore - cursor = hasMore ? result2.cursor : null - } - - // 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 - } - - 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) => { - const { data } = await Axios.get(`https://www.tiktok.com/api/post/item_list/?${_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( - 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 - }) - ) - } - }) - - return data -} - -const xttparams = (params) => { - 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/tiktok_user_search.ts b/src/utils/search/userSearch.ts similarity index 94% rename from src/utils/search/tiktok_user_search.ts rename to src/utils/search/userSearch.ts index 898f863..526e518 100644 --- a/src/utils/search/tiktok_user_search.ts +++ b/src/utils/search/userSearch.ts @@ -17,7 +17,7 @@ export const SearchUser = (username: string, cookie?: any, page?: number): Promi method: "GET", headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", - cookie: typeof cookie === "object" ? cookie.map((v) => `${v.name}=${v.value}`).join("; ") : cookie + cookie: typeof cookie === "object" ? cookie.map((v: any) => `${v.name}=${v.value}`).join("; ") : cookie } }) .then(({ data }) => {