From 973a94e51761fbd61e37bebb1644c60c15f93dfe Mon Sep 17 00:00:00 2001 From: Tobi Saputra Date: Sat, 3 May 2025 15:02:48 +0700 Subject: [PATCH] feat: add new feature (tiktok get user liked videos & user posts) & fix some types --- src/constants/api.ts | 2 + src/constants/headers.ts | 5 + src/constants/params.ts | 94 +++++++- src/index.ts | 105 +++++++-- src/{utils => services}/cookieManager.ts | 2 +- src/{utils => services}/downloadManager.ts | 2 +- src/services/tiktokService.ts | 114 ++++++++++ src/types/downloader/musicaldown.ts | 4 +- src/types/downloader/ssstik.ts | 8 +- src/types/downloader/tiktokApi.ts | 24 +-- src/types/get/getComments.ts | 2 +- src/types/get/getProfile.ts | 83 +------- src/types/get/getUserLiked.ts | 97 +++++++++ src/types/get/getUserPosts.ts | 73 +++++++ src/types/search/liveSearch.ts | 4 +- src/types/search/userSearch.ts | 2 +- src/types/search/videoSearch.ts | 67 ++++++ src/utils/downloader/musicalDown.ts | 11 +- src/utils/downloader/ssstik.ts | 12 +- src/utils/downloader/tiktokApi.ts | 22 +- src/utils/get/getComments.ts | 12 +- src/utils/get/getProfile.ts | 216 +++---------------- src/utils/get/getUserLiked.ts | 236 +++++++++++++++++++++ src/utils/get/getUserPosts.ts | 181 ++++++++++++++++ src/utils/logger.ts | 23 -- src/utils/search/liveSearch.ts | 3 +- src/utils/search/userSearch.ts | 37 +--- src/utils/search/videoSearch.ts | 154 ++++++++++++++ 28 files changed, 1209 insertions(+), 386 deletions(-) create mode 100644 src/constants/headers.ts rename src/{utils => services}/cookieManager.ts (97%) rename src/{utils => services}/downloadManager.ts (98%) create mode 100644 src/services/tiktokService.ts create mode 100644 src/types/get/getUserLiked.ts create mode 100644 src/types/get/getUserPosts.ts create mode 100644 src/types/search/videoSearch.ts create mode 100644 src/utils/get/getUserLiked.ts create mode 100644 src/utils/get/getUserPosts.ts delete mode 100644 src/utils/logger.ts create mode 100644 src/utils/search/videoSearch.ts diff --git a/src/constants/api.ts b/src/constants/api.ts index c930172..81bdde4 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -10,6 +10,8 @@ export const _tiktokGetPosts = (params: any): string => `${_tiktokurl}/api/post/item_list/?${params}` export const _tiktokGetComments = (params: any): string => `${_tiktokurl}/api/comment/list/?${params}` +export const _tiktokGetUserLiked = (params: any): string => + `${_tiktokurl}/api/favorite/item_list/?${params}` /** Tiktokv */ export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us` diff --git a/src/constants/headers.ts b/src/constants/headers.ts new file mode 100644 index 0000000..1fbe377 --- /dev/null +++ b/src/constants/headers.ts @@ -0,0 +1,5 @@ +export const userAgent = + "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" + +export const webUserAgent = + "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" diff --git a/src/constants/params.ts b/src/constants/params.ts index 2d48fc9..58ca970 100644 --- a/src/constants/params.ts +++ b/src/constants/params.ts @@ -38,6 +38,42 @@ export const _getUserPostsParams = () => { ) } +export const _getUserLikedParams = ( + id: string, + secUid: string, + count: number +) => { + let cursor = 0 + if (count > 50) { + for (let i = 1; i < count; i++) { + cursor += 50 + } + } + + return qs.stringify({ + aid: "1988", + cookie_enabled: true, + screen_width: 0, + screen_height: 0, + browser_language: "", + browser_platform: "", + browser_name: "", + browser_version: + "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0", + browser_online: "", + timezone_name: "Europe/London", + is_page_visible: true, + id, + secUid, + count, + cursor, + needPinnedItemIds: true, + odinId: "7002566096994190854", + history_len: 3, + user_is_login: true + }) +} + export const _xttParams = (secUid: string, cursor: number, count: number) => { return qs.stringify({ aid: "1988", @@ -59,8 +95,6 @@ export const _xttParams = (secUid: string, cursor: number, count: number) => { 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 @@ -89,15 +123,13 @@ export const _getCommentsParams = (id: string, count: number) => { }) } -/** Search */ +/** Search Params */ export const _userSearchParams = ( keyword: string, page: number, xbogus?: any ) => { let cursor = 0 - - // 10 users per page if (page > 1) { for (let i = 1; i < page; i++) { cursor += 10 @@ -157,8 +189,6 @@ export const _userSearchParams = ( export const _liveSearchParams = (keyword: string, page: number) => { let cursor = 0 - - // 12 cursor for 20 lives per page if (page > 1) { for (let i = 1; i < page; i++) { cursor += 12 @@ -203,6 +233,53 @@ export const _liveSearchParams = (keyword: string, page: number) => { }) } +export const _videoSearchParams = (keyword: string, page: number) => { + let cursor = 0 + if (page > 1) { + for (let i = 1; i < page; i++) { + cursor += 12 + } + } + + let offset = `${cursor}` + + return qs.stringify({ + WebIdLastTime: "1720342268", + aid: "1988", + app_language: "en", + app_name: "tiktok_web", + 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: "20", + device_id: "7388813454814086664", + device_platform: "web_pc", + device_type: "web_h264", + focus_state: "true", + from_page: "search", + history_len: "10", + is_fullscreen: "false", + is_page_visible: "true", + is_user_login: "true", + keyword, + offset, + os: "linux", + priority_region: "", + referer: "", + region: "ID", + screen_height: "768", + screen_width: "1366", + tz_name: "Asia/Jakarta", + web_search_code: + "{ tiktok: { client_params_x: { search_engine: { ies_mt_user_live_video_card_use_libra: 1, mt_search_general_user_live_card: 1 } }, search_server: {} } }", + webcast_language: "en" + }) +} + /** Downloader Params */ export const _tiktokApiParams = (args: any) => { return new URLSearchParams({ @@ -240,12 +317,11 @@ export const _tiktokApiParams = (args: any) => { }).toString() } +/** Helper Functions */ const randomChar = (char: string, range: number) => { let chars = "" - for (let i = 0; i < range; i++) { chars += char[Math.floor(Math.random() * char.length)] } - return chars } diff --git a/src/index.ts b/src/index.ts index e936e68..78cf263 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,10 +15,16 @@ 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/get/getProfile" +import { TiktokStalkUserResponse } from "./types/get/getProfile" import { TiktokLiveSearchResponse } from "./types/search/liveSearch" -import { CommentsResult } from "./types/get/getComments" +import { TiktokVideoCommentsResponse } from "./types/get/getComments" import { getComments } from "./utils/get/getComments" +import { TiktokUserPostsResponse } from "./types/get/getUserPosts" +import { getUserPosts } from "./utils/get/getUserPosts" +import { getUserLiked } from "./utils/get/getUserLiked" +import { TiktokUserFavoriteVideosResponse } from "./types/get/getUserLiked" +import { TiktokVideoSearchResponse } from "./types/search/videoSearch" +import { SearchVideo } from "./utils/search/videoSearch" type TiktokDownloaderResponse = T extends "v1" ? TiktokAPIResponse @@ -27,11 +33,12 @@ type TiktokDownloaderResponse = T extends "v1" : T extends "v3" ? MusicalDownResponse : TiktokAPIResponse -type TiktokSearchResponse = T extends "user" - ? TiktokUserSearchResponse - : T extends "live" - ? TiktokLiveSearchResponse - : any +type TiktokSearchResponse = + T extends "user" + ? TiktokUserSearchResponse + : T extends "live" + ? TiktokLiveSearchResponse + : TiktokVideoSearchResponse export = { /** @@ -85,15 +92,21 @@ export = { * @param {string} options.proxy - Your Proxy (optional) * @returns {Promise} */ - Search: async ( + Search: async ( query: string, options: { type: T - cookie?: string | any[] + cookie: string | any[] page?: number proxy?: string } ): Promise> => { + if (!options?.cookie) { + return { + status: "error", + message: "Cookie is required!" + } as TiktokSearchResponse + } switch (options?.type.toLowerCase()) { case "user": { const response = await SearchUser( @@ -113,6 +126,15 @@ export = { ) return response as TiktokSearchResponse } + case "video": { + const response = await SearchVideo( + query, + options?.cookie, + options?.page, + options?.proxy + ) + return response as TiktokSearchResponse + } default: { const response = await SearchUser( query, @@ -129,9 +151,8 @@ export = { * @param {string} username - The username you want to stalk * @param {object} options - The options for stalk * @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} + * @returns {Promise} */ StalkUser: async ( username: string, @@ -140,12 +161,8 @@ export = { postLimit?: number proxy?: string } - ): Promise => { - const response = await StalkUser( - username, - options?.postLimit, - options?.proxy - ) + ): Promise => { + const response = await StalkUser(username, options?.cookie, options?.proxy) return response }, @@ -155,17 +172,65 @@ export = { * @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} + * @returns {Promise} */ - GetComments: async ( + GetVideoComments: async ( url: string, options?: { commentLimit?: number; proxy?: string } - ): Promise => { + ): Promise => { const response = await getComments( url, options?.proxy, options?.commentLimit ) return response + }, + + /** + * Tiktok Get User Posts + * @param {string} username - The username you want to get posts from + * @param {object} options - The options for getting posts + * @param {number} options.postLimit - Limit number of posts to return (optional) + * @param {string} options.proxy - Your Proxy (optional) + * @returns {Promise} + */ + GetUserPosts: async ( + username: string, + options?: { postLimit?: number; proxy?: string } + ): Promise => { + const response = await getUserPosts( + username, + options?.proxy, + options?.postLimit + ) + return response + }, + + /** + * Tiktok Get User Liked Videos + * @param {string} username - The username you want to get liked videos from + * @param {object} options - The options for getting liked videos + * @param {string | any[]} options.cookie - Your Tiktok Cookie (optional) + * @param {number} options.postLimit - Limit number of posts to return (optional) + * @param {string} options.proxy - Your Proxy (optional) + * @returns {Promise} + */ + GetUserLiked: async ( + username: string, + options: { cookie: string | any[]; postLimit?: number; proxy?: string } + ): Promise => { + if (!options?.cookie) { + return { + status: "error", + message: "Cookie is required!" + } as TiktokUserFavoriteVideosResponse + } + const response = await getUserLiked( + username, + options?.cookie, + options?.proxy, + options?.postLimit + ) + return response } } diff --git a/src/utils/cookieManager.ts b/src/services/cookieManager.ts similarity index 97% rename from src/utils/cookieManager.ts rename to src/services/cookieManager.ts index cd7ba46..321f67f 100644 --- a/src/utils/cookieManager.ts +++ b/src/services/cookieManager.ts @@ -5,7 +5,7 @@ export class CookieManager { private cookieFile: string private cookieData: { [key: string]: string } - constructor(name: string) { + constructor() { // Create cookies directory in user's home directory const homeDir = process.env.HOME || process.env.USERPROFILE const cookieDir = path.join(homeDir!, ".tiktok-api") diff --git a/src/utils/downloadManager.ts b/src/services/downloadManager.ts similarity index 98% rename from src/utils/downloadManager.ts rename to src/services/downloadManager.ts index 20c9dda..e9eb1fe 100644 --- a/src/utils/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -2,7 +2,7 @@ import * as path from "path" import * as os from "os" import axios from "axios" import * as fs from "fs" -import { Logger } from "./logger" +import { Logger } from "../lib/logger" function getDefaultDownloadPath(): string { const homeDir = os.homedir() diff --git a/src/services/tiktokService.ts b/src/services/tiktokService.ts new file mode 100644 index 0000000..8943a16 --- /dev/null +++ b/src/services/tiktokService.ts @@ -0,0 +1,114 @@ +import { JSDOM, ResourceLoader } from "jsdom" +import { _getUserLikedParams, _userSearchParams } from "../constants/params" +import xbogus from "../../helper/xbogus" +import { userAgent, webUserAgent } from "../constants/headers" +import qs from "qs" +import fs from "fs" +import { createCipheriv } from "crypto" + +export class TiktokService { + /** + * Generate Signature parameter for TikTok API requests + * @param {string} id - User ID to generate X-Bogus for + * @param {string} secUid - User's secure ID + * @param {number} count - Number of items to request + * @returns {string} URL with X-Bogus parameter appended + */ + public generateSignature(url: URL): string { + const stringUrl = url.toString() + const jsdomOptions = this.getJsdomOptions() + + const { window } = new JSDOM(``, jsdomOptions) + let _window = window + _window.eval(this.signaturejs.toString()) + _window.byted_acrawler.init({ + aid: 24, + dfp: true + }) + _window.eval(this.webmssdk) + const signature = _window.byted_acrawler.sign({ url: stringUrl }) + return signature + } + + /** + * Generate X-Bogus parameter for TikTok API requests + * @param {string} id - User ID to generate X-Bogus for + * @param {string} secUid - User's secure ID + * @param {number} count - Number of items to request + * @returns {string} URL with X-Bogus parameter appended + */ + public generateXBogus(url: URL, signature?: string): string { + const jsdomOptions = this.getJsdomOptions() + + const { window } = new JSDOM(``, jsdomOptions) + let _window = window + _window.eval(this.signaturejs.toString()) + _window.byted_acrawler.init({ + aid: 24, + dfp: true + }) + _window.eval(this.webmssdk) + if (signature) { + url.searchParams.append("_signature", signature) + } + const xbogus = _window._0x32d649(url.searchParams.toString()) + return xbogus + } + + /** + * Generate XTTPParams + * @param {any} params - The params you want to encrypt + * @returns {string} + */ + public generateXTTParams(params: any): string { + const cipher = createCipheriv( + "aes-128-cbc", + TiktokService.AES_KEY, + TiktokService.AES_IV + ) + return Buffer.concat([cipher.update(params), cipher.final()]).toString( + "base64" + ) + } + + /** + * Generate URL with X-Bogus + * Special thanks to https://github.com/iamatef/xbogus + * @param {string} username - The username you want to search + * @param {number} page - The page you want to search + * @returns {string} + */ + public generateURLXbogus(username: string, page: number): string { + const baseUrl = `${TiktokService.BASE_URL}api/search/user/full/?` + const queryParams = _userSearchParams(username, page) + const xbogusParams = xbogus(`${baseUrl}${queryParams}`, userAgent) + + return `${baseUrl}${_userSearchParams(username, page, xbogusParams)}` + } + + /** + * Get JSDOM Options + * @returns {object} + */ + private getJsdomOptions() { + return { + url: TiktokService.BASE_URL, + referrer: TiktokService.BASE_URL, + contentType: "text/html", + includeNodeLocations: false, + runScripts: "outside-only", + pretendToBeVisual: true, + resources: new ResourceLoader({ userAgent: webUserAgent }) + } + } + + private static readonly BASE_URL = "https://www.tiktok.com/" + private static readonly AES_KEY = "webapp1.0+202106" + private static readonly AES_IV = "webapp1.0+202106" + private signaturejs = fs.readFileSync("./helper/signature.js", "utf-8") + private webmssdk = fs.readFileSync("./helper/webmssdk.js", "utf-8") + private resourceLoader = new ResourceLoader({ + userAgent: + "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" + }) +} diff --git a/src/types/downloader/musicaldown.ts b/src/types/downloader/musicaldown.ts index 976a2dc..e4a4514 100644 --- a/src/types/downloader/musicaldown.ts +++ b/src/types/downloader/musicaldown.ts @@ -1,4 +1,4 @@ -export type getRequest = { +export type GetMusicalDownReuqest = { status: "success" | "error" request?: { [key: string]: string @@ -24,7 +24,7 @@ export type MusicalDownResponse = { } } -export type getMusic = { +export type GetMusicalDownMusic = { status: "success" | "error" result?: string } diff --git a/src/types/downloader/ssstik.ts b/src/types/downloader/ssstik.ts index 09bbe22..0b8da34 100644 --- a/src/types/downloader/ssstik.ts +++ b/src/types/downloader/ssstik.ts @@ -10,8 +10,8 @@ export type SSSTikResponse = { result?: { type: "image" | "video" | "music" desc?: string - author?: Author - statistics?: Statistics + author?: AuthorSSSTik + statistics?: StatisticsSSSTik images?: string[] video?: string music?: string @@ -19,12 +19,12 @@ export type SSSTikResponse = { } } -export type Author = { +export type AuthorSSSTik = { avatar: string nickname: string } -export type Statistics = { +export type StatisticsSSSTik = { likeCount: string commentCount: string shareCount: string diff --git a/src/types/downloader/tiktokApi.ts b/src/types/downloader/tiktokApi.ts index 474d855..b48eaef 100644 --- a/src/types/downloader/tiktokApi.ts +++ b/src/types/downloader/tiktokApi.ts @@ -6,22 +6,22 @@ export type TiktokAPIResponse = { id: string createTime: number description: string - author: Author - statistics: Statistics + author: AuthorTiktokAPI + statistics: StatisticsTiktokAPI hashtag: string[] isTurnOffComment: boolean isADS: boolean cover?: string[] dynamicCover?: string[] originCover?: string[] - video?: Video + video?: VideoTiktokAPI images?: string[] - music: Music + music: MusicTiktokAPI } resultNotParsed?: any } -export type Author = { +export type AuthorTiktokAPI = { uid: number username: string nickname: string @@ -32,7 +32,7 @@ export type Author = { url: string } -export type Statistics = { +export type StatisticsTiktokAPI = { playCount: number downloadCount: number shareCount: number @@ -46,7 +46,7 @@ export type Statistics = { repostCount: number } -export type Video = { +export type VideoTiktokAPI = { ratio: string duration: number playAddr: string[] @@ -56,7 +56,7 @@ export type Video = { originCover: string[] } -export type Music = { +export type MusicTiktokAPI = { id: number title: string author: string @@ -71,9 +71,9 @@ export type Music = { isAuthorArtist: boolean } -export type responseParser = { +export type ResponseParserTiktokAPI = { content?: any - statistics?: Statistics - author?: Author - music?: Music + statistics?: StatisticsTiktokAPI + author?: AuthorTiktokAPI + music?: MusicTiktokAPI } diff --git a/src/types/get/getComments.ts b/src/types/get/getComments.ts index 974d20e..1c8608f 100644 --- a/src/types/get/getComments.ts +++ b/src/types/get/getComments.ts @@ -1,4 +1,4 @@ -export type CommentsResult = { +export type TiktokVideoCommentsResponse = { status: "success" | "error" message?: string result?: Comments[] diff --git a/src/types/get/getProfile.ts b/src/types/get/getProfile.ts index 6b2e947..49f9f54 100644 --- a/src/types/get/getProfile.ts +++ b/src/types/get/getProfile.ts @@ -1,15 +1,13 @@ -export type StalkResult = { +export type TiktokStalkUserResponse = { status: "success" | "error" message?: string result?: { - users: Users - stats: Stats - posts: Posts[] + user: UserProfile + stats: StatsUserProfile } - totalPosts?: number } -export type Users = { +export type UserProfile = { uid: string username: string nickname: string @@ -23,19 +21,19 @@ export type Users = { commerceUser: boolean usernameModifyTime: number nicknameModifyTime: number + secUid: string } -export type Stats = { +export type StatsUserProfile = { followerCount: number followingCount: number heartCount: number videoCount: number likeCount: number friendCount: number - postCount: number } -export type Statistics = { +export type StatisticsUserProfile = { likeCount: number shareCount: number commentCount: number @@ -67,70 +65,3 @@ export type Music = { 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/get/getUserLiked.ts b/src/types/get/getUserLiked.ts new file mode 100644 index 0000000..6fea724 --- /dev/null +++ b/src/types/get/getUserLiked.ts @@ -0,0 +1,97 @@ +export type TiktokUserFavoriteVideosResponse = { + status: "success" | "error" + message?: string + result?: LikedResponse[] + totalPosts?: number +} + +export type LikedResponse = { + id: string + desc: string + createTime: string + duetEnabled: boolean + digged: boolean + forFriend: boolean + isAd: boolean + originalItem: boolean + privateItem: boolean + officialItem: boolean + secret: boolean + shareEnabled: boolean + stitchEanbled: boolean + textTranslatable: boolean + author: AuthorLiked + stats: StatisticsLiked + video?: VideoLiked + imagePost?: ImagesLiked[] + music: MusicLiked +} + +export type AuthorLiked = { + id: string + username: string + nickname: string + avatarLarger: string + avatarThumb: string + avatarMedium: string + signature: string + verified: string + openFavorite: string + privateAccount: string + isADVirtual: string + isEmbedBanned: string + stats: StatisticsAuthorLiked +} + +export type StatisticsAuthorLiked = { + likeCount: string + followerCount: string + followingCount: string + friendCount: string + heartCount: string + postsCount: string +} + +export type StatisticsLiked = { + collectCount: string + commentCount: string + diggCount: string + playCount: string + repostCount: string + shareCount: string +} + +export type ImagesLiked = { + title: string + images: string[] +} + +export type VideoLiked = { + id: string + videoID: string + duration: number + ratio: string + cover: string + originCover: string + dynamicCover: string + playAddr: string + downloadAddr: string + format: string + bitrate: number + bitrateInfo: any[] +} + +export type MusicLiked = { + id: string + title: string + playUrl: string + coverThumb: string + coverMedium: string + coverLarge: string + authorName: string + original: boolean + album: string + duration: number + isCopyrighted: boolean + private: boolean +} diff --git a/src/types/get/getUserPosts.ts b/src/types/get/getUserPosts.ts new file mode 100644 index 0000000..fffc1fe --- /dev/null +++ b/src/types/get/getUserPosts.ts @@ -0,0 +1,73 @@ +export type TiktokUserPostsResponse = { + status: "success" | "error" + message?: string + result?: Posts[] + totalPosts?: number +} + +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 + imagePost?: 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/liveSearch.ts b/src/types/search/liveSearch.ts index 749c1c9..c98d40b 100644 --- a/src/types/search/liveSearch.ts +++ b/src/types/search/liveSearch.ts @@ -1,12 +1,12 @@ export type TiktokLiveSearchResponse = { status: "success" | "error" message?: string - result?: Result[] + result?: LiveSearchResult[] page?: number totalResults?: number } -export type Result = { +export type LiveSearchResult = { roomInfo: RoomInfo liveInfo: LiveInfo } diff --git a/src/types/search/userSearch.ts b/src/types/search/userSearch.ts index 224c05a..8285a04 100644 --- a/src/types/search/userSearch.ts +++ b/src/types/search/userSearch.ts @@ -16,7 +16,7 @@ export type TiktokUserSearchResponse = { totalResults?: number } -export type Result = { +export type UserSearchResult = { uid: string username: string nickname: string diff --git a/src/types/search/videoSearch.ts b/src/types/search/videoSearch.ts new file mode 100644 index 0000000..4caa3ae --- /dev/null +++ b/src/types/search/videoSearch.ts @@ -0,0 +1,67 @@ +export type TiktokVideoSearchResponse = { + status: "success" | "error" + message?: string + result?: VideoSearchResult[] + page?: number + totalResults?: number +} + +export type VideoSearchResult = { + id: string + desc: string + createTime: number + author: AuthorVideoSearch + stats: StatisticsVideoSearch + video: VideoSearch + music: MusicVideoSearch +} + +export type VideoSearch = { + id: string + ratio: string + cover: string + originCover: string + dynamicCover: string + playAddr: string + downloadAddr: string + format: string +} + +export type StatisticsVideoSearch = { + collectCount: number + commentCount: number + diggCount: number + playCount: number + shareCount: number +} + +export type AuthorVideoSearch = { + id: string + uniqueId: string + nickname: string + avatarThumb: string + avatarMedium: string + avatarLarger: string + signature: string + verified: boolean + secUid: string + openFavorite: boolean + privateAccount: boolean + isADVirtual: boolean + tiktokSeller: boolean + isEmbedBanned: boolean +} + +export type MusicVideoSearch = { + id: string + title: string + playUrl: string + coverThumb: string + coverMedium: string + coverLarge: string + authorName: string + original: boolean + album: string + duration: number + isCopyrighted: boolean +} diff --git a/src/utils/downloader/musicalDown.ts b/src/utils/downloader/musicalDown.ts index 37098ac..d9963fc 100644 --- a/src/utils/downloader/musicalDown.ts +++ b/src/utils/downloader/musicalDown.ts @@ -2,8 +2,8 @@ import Axios from "axios" import { load } from "cheerio" import { MusicalDownResponse, - getMusic, - getRequest + GetMusicalDownMusic, + GetMusicalDownReuqest } from "../../types/downloader/musicaldown" import { _musicaldownapi, @@ -21,7 +21,10 @@ 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): Promise => +const getRequest = ( + url: string, + proxy?: string +): Promise => new Promise((resolve) => { if (!TiktokURLregex.test(url)) { return resolve({ @@ -103,7 +106,7 @@ export const MusicalDown = ( proxy?: string ): Promise => new Promise(async (resolve) => { - const request: getRequest = await getRequest(url) + const request: GetMusicalDownReuqest = await getRequest(url) if (request.status !== "success") return resolve({ status: "error", message: request.message }) Axios(_musicaldownapi, { diff --git a/src/utils/downloader/ssstik.ts b/src/utils/downloader/ssstik.ts index 1d18b7e..ba1eb78 100644 --- a/src/utils/downloader/ssstik.ts +++ b/src/utils/downloader/ssstik.ts @@ -2,10 +2,10 @@ import Axios from "axios" import asyncRetry from "async-retry" import { load } from "cheerio" import { - Author, - Statistics, + AuthorSSSTik, + StatisticsSSSTik, SSSTikFetchTT, - SSSTikResponse, + SSSTikResponse } from "../../types/downloader/ssstik" import { _ssstikapi, _ssstikurl } from "../../constants/api" import { HttpsProxyAgent } from "https-proxy-agent" @@ -115,11 +115,11 @@ export const SSSTik = (url: string, proxy?: string): Promise => const $ = load(await response) // Result - const author: Author = { + const author: AuthorSSSTik = { avatar: $("img.result_author").attr("src"), nickname: $("h2").text().trim() } - const statistics: Statistics = { + const statistics: StatisticsSSSTik = { likeCount: $("#trending-actions > .justify-content-start") .text() .trim(), @@ -174,7 +174,7 @@ export const SSSTik = (url: string, proxy?: string): Promise => result = { type: "music", music, - direct: direct || "", + direct: direct || "" } } diff --git a/src/utils/downloader/tiktokApi.ts b/src/utils/downloader/tiktokApi.ts index 8529e94..d720435 100644 --- a/src/utils/downloader/tiktokApi.ts +++ b/src/utils/downloader/tiktokApi.ts @@ -3,12 +3,12 @@ import asyncRetry from "async-retry" import { _tiktokvFeed, _tiktokurl } from "../../constants/api" import { _tiktokApiParams } from "../../constants/params" import { - Author, + AuthorTiktokAPI, TiktokAPIResponse, - Statistics, - Music, - responseParser, - Video + StatisticsTiktokAPI, + MusicTiktokAPI, + ResponseParserTiktokAPI, + VideoTiktokAPI } from "../../types/downloader/tiktokApi" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" @@ -98,7 +98,7 @@ export const TiktokAPI = ( } } else { // Video Result - const video: Video = { + const video: VideoTiktokAPI = { ratio: content.video.ratio, duration: content.video.duration, playAddr: content.video?.play_addr?.url_list || [], // No Watermark Video @@ -143,7 +143,7 @@ export const TiktokAPI = ( const fetchTiktokData = async ( ID: string, proxy?: string -): Promise | null => { +): Promise | null => { try { const response = asyncRetry( async () => { @@ -191,7 +191,7 @@ const fetchTiktokData = async ( } } -const parseTiktokData = (ID: string, data: any): responseParser => { +const parseTiktokData = (ID: string, data: any): ResponseParserTiktokAPI => { let content = data?.aweme_list if (!content) return { content: null } @@ -199,7 +199,7 @@ const parseTiktokData = (ID: string, data: any): responseParser => { content = content.find((v: any) => v.aweme_id === ID) // Statistics Result - const statistics: Statistics = { + const statistics: StatisticsTiktokAPI = { commentCount: content.statistics.comment_count, diggCount: content.statistics.digg_count, downloadCount: content.statistics.download_count, @@ -214,7 +214,7 @@ const parseTiktokData = (ID: string, data: any): responseParser => { } // Author Result - const author: Author = { + const author: AuthorTiktokAPI = { uid: content.author.uid, username: content.author.unique_id, nickname: content.author.nickname, @@ -226,7 +226,7 @@ const parseTiktokData = (ID: string, data: any): responseParser => { } // Music Result - const music: Music = { + const music: MusicTiktokAPI = { id: content.music.id, title: content.music.title, author: content.music.author, diff --git a/src/utils/get/getComments.ts b/src/utils/get/getComments.ts index b68eeb9..5264fd1 100644 --- a/src/utils/get/getComments.ts +++ b/src/utils/get/getComments.ts @@ -3,7 +3,11 @@ 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" +import { + Comments, + TiktokVideoCommentsResponse, + 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+)/ @@ -13,14 +17,14 @@ const TiktokURLregex = * @param {string} url - Tiktok URL * @param {string} proxy - Your Proxy (optional) * @param {number} commentLimit - Comment Limit (optional) - * @returns {Promise} + * @returns {Promise} */ export const getComments = async ( url: string, proxy?: string, commentLimit?: number -): Promise => +): Promise => new Promise(async (resolve) => { if (!TiktokURLregex.test(url)) { return resolve({ @@ -99,7 +103,7 @@ const parseComments = async ( let hasMore: boolean = true while (hasMore) { - const result = await requestComments(id, cursor, proxy) + const result = await requestComments(id, commentLimit, proxy) // Check if the result has more comments hasMore = result.has_more === 1 diff --git a/src/utils/get/getProfile.ts b/src/utils/get/getProfile.ts index 52e5333..b8f04e9 100644 --- a/src/utils/get/getProfile.ts +++ b/src/utils/get/getProfile.ts @@ -1,36 +1,40 @@ import Axios from "axios" import { load } from "cheerio" -import { _tiktokGetPosts, _tiktokurl } from "../../constants/api" import { - AuthorPost, - Posts, - StalkResult, - Stats, - Users + _tiktokGetUserLiked, + _tiktokGetPosts, + _tiktokurl +} from "../../constants/api" +import { + TiktokStalkUserResponse, + StatsUserProfile, + UserProfile } 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 {number} postLimit - The limit of post you want to get (optional) * @param {string} proxy - Your Proxy (optional) - * @returns {Promise} + * @returns {Promise} */ export const StalkUser = ( username: string, - postLimit?: number, + cookie?: string | any[], proxy?: string -): Promise => +): Promise => new Promise(async (resolve) => { username = username.replace("@", "") Axios(`${_tiktokurl}/@${username}`, { method: "GET", headers: { + cookie: + typeof cookie === "object" + ? cookie.map((v: any) => `${v.name}=${v.value}`).join("; ") + : cookie, "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" }, @@ -60,17 +64,21 @@ export const StalkUser = ( const dataUser = result["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"] - const posts: Posts[] = await parsePosts(dataUser, postLimit, proxy) - const { users, stats } = parseDataUser(dataUser, posts) + if (!dataUser) { + return resolve({ + status: "error", + message: "User not found!" + }) + } - let response: StalkResult = { + const { user, stats } = parseDataUser(dataUser) + + let response: TiktokStalkUserResponse = { status: "success", result: { - users, - stats, - posts - }, - totalPosts: posts.length + user, + stats + } } resolve(response) @@ -78,42 +86,9 @@ export const StalkUser = ( .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[]) => { +const parseDataUser = (dataUser: any) => { // User Info Result - const users: Users = { + const user: UserProfile = { uid: dataUser.user.id, username: dataUser.user.uniqueId, nickname: dataUser.user.nickname, @@ -126,142 +101,19 @@ const parseDataUser = (dataUser: any, posts: Posts[]) => { region: dataUser.user.region, commerceUser: dataUser.user.commerceUserInfo.commerceUser, usernameModifyTime: dataUser.user.uniqueIdModifyTime, - nicknameModifyTime: dataUser.user.nickNameModifyTime + nicknameModifyTime: dataUser.user.nickNameModifyTime, + secUid: dataUser.user.secUid } // Statistics Result - const stats: Stats = { + const stats: StatsUserProfile = { 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 + friendCount: dataUser.stats.friendCount } - return { users, stats } -} - -const parsePosts = async ( - dataUser: any, - postLimit?: number, - proxy?: string -): Promise => { - // Posts Result - let hasMore = true - let cursor = 0 - const posts: Posts[] = [] - let counter = 0 - while (hasMore) { - let result: any | null = null - - // 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 - break - } - - 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 - }) - } - }) - - // Update hasMore and cursor for next iteration - hasMore = result.hasMore - cursor = hasMore ? result.cursor : null - counter++ - - // Check post limit if specified - if (postLimit && posts.length >= postLimit) { - hasMore = false - break - } - } - - 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" - ) + return { user, stats } } diff --git a/src/utils/get/getUserLiked.ts b/src/utils/get/getUserLiked.ts new file mode 100644 index 0000000..c9892e9 --- /dev/null +++ b/src/utils/get/getUserLiked.ts @@ -0,0 +1,236 @@ +import Axios from "axios" +import { _tiktokGetUserLiked, _tiktokurl } from "../../constants/api" +import { StalkUser } from "./getProfile" +import { _getUserLikedParams, _xttParams } from "../../constants/params" +import { HttpsProxyAgent } from "https-proxy-agent" +import { SocksProxyAgent } from "socks-proxy-agent" +import { TiktokService } from "../../services/tiktokService" +import { + AuthorLiked, + LikedResponse, + TiktokUserFavoriteVideosResponse, + StatisticsLiked, + MusicLiked, + VideoLiked, + StatisticsAuthorLiked, + ImagesLiked +} from "../../types/get/getUserLiked" + +export const getUserLiked = ( + username: string, + cookie: string | any[], + proxy?: string, + postLimit?: number +): Promise => + new Promise((resolve) => { + if (!cookie) { + return { + status: "error", + message: "Cookie is required!" + } as TiktokUserFavoriteVideosResponse + } + + StalkUser(username).then(async (res) => { + if (res.status === "error") { + return resolve({ + status: "error", + message: res.message + }) + } + + const id = res.result.user.uid + const secUid = res.result.user.secUid + const data = await parseUserLiked(id, secUid, cookie, postLimit, proxy) + + resolve({ + status: "success", + result: data, + totalPosts: data.length + }) + }) + }) + +const parseUserLiked = async ( + id: string, + secUid: string, + cookie: string | any[], + postLimit?: number, + proxy?: string +): Promise => { + // Liked Result + let hasMore = true + let cursor = 0 + const favorites: LikedResponse[] = [] + let counter = 0 + while (hasMore) { + let result: any | null = null + + // Prevent missing response favorites + result = await requestUserLiked(id, secUid, cookie, postLimit, proxy) + + // Validate + if (result === "") { + hasMore = false + break + } + + result?.itemList?.forEach((v: any) => { + const statsAuthor: StatisticsAuthorLiked = { + likeCount: v.authorStats.diggCount, + followerCount: v.authorStats.followerCount, + followingCount: v.authorStats.followingCount, + friendCount: v.authorStats.friendCount, + heartCount: v.authorStats.heartCount, + postsCount: v.authorStats.videoCount + } + + const author: AuthorLiked = { + 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, + stats: statsAuthor + } + + const stats: StatisticsLiked = { + collectCount: v.statsV2.collectCount, + commentCount: v.statsV2.commentCount, + diggCount: v.statsV2.diggCount, + playCount: v.statsV2.playCount, + repostCount: v.statsV2.repostCount, + shareCount: v.statsV2.shareCount + } + + const music: MusicLiked = { + id: v.music.id, + title: v.music.title, + playUrl: v.music.playUrl, + coverThumb: v.music.coverThumb, + coverMedium: v.music.coverMedium, + coverLarge: v.music.coverLarge, + authorName: v.music.authorName, + original: v.music.original, + album: v.music.album, + duration: v.music.duration, + isCopyrighted: v.music.isCopyrighted, + private: v.music.private + } + + const response = { + id: v.id, + desc: v.desc, + createTime: v.createTime, + duetEnabled: v.duetEnabled || false, + digged: v.digged || false, + forFriend: v.forFriend || false, + isAd: v.isAd || false, + originalItem: v.originalItem || false, + privateItem: v.privateItem || false, + officialItem: v.officialItem || false, + secret: v.secret || false, + shareEnabled: v.shareEnabled || false, + stitchEanbled: v.stitchEanbled || false, + textTranslatable: v.textTranslatable || false + } + + if (v.imagePost) { + const imagePost: ImagesLiked[] = [] + v.imagePost.images.forEach((image: any) => { + imagePost.push({ + title: image.title, + images: image.imageURL.urlList[0] + }) + }) + + favorites.push({ + ...response, + author, + stats, + imagePost, + music + }) + } else { + const video: VideoLiked = { + id: v.video.id, + videoID: v.video.id, + duration: v.video.duration, + ratio: v.video.ratio, + cover: v.video.cover, + originCover: v.video.originCover, + dynamicCover: v.video.dynamicCover, + playAddr: v.video.playAddr, + downloadAddr: v.video.downloadAddr, + format: v.video.format, + bitrate: v.video.bitrate, + bitrateInfo: v.video.bitrateInfo + } + + favorites.push({ + ...response, + author, + stats, + video, + music + }) + } + }) + + // Update hasMore and cursor for next iteration + hasMore = result.hasMore + cursor = hasMore ? result.cursor : null + counter++ + + // Check post limit if specified + if (postLimit && favorites.length >= postLimit) { + hasMore = false + break + } + } + return postLimit ? favorites.slice(0, postLimit) : favorites +} + +const requestUserLiked = async ( + id: string, + secUid: string, + cookie: string | any[], + postLimit: number, + proxy?: string +): Promise => { + const Tiktok = new TiktokService() + + const url = new URL( + _tiktokGetUserLiked(_getUserLikedParams(id, secUid, postLimit)) + ) + const signature = Tiktok.generateSignature(url) + url.searchParams.append("_signature", signature) + const xbogus = Tiktok.generateXBogus(url, signature) + url.searchParams.append("X-Bogus", xbogus) + const xttparams = Tiktok.generateXTTParams(url.searchParams.toString()) + + const { data } = await Axios.get(`${url.toString()}`, { + 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", + cookie, + "x-tt-params": xttparams + }, + httpsAgent: + (proxy && + (proxy.startsWith("http") || proxy.startsWith("https") + ? new HttpsProxyAgent(proxy) + : proxy.startsWith("socks") + ? new SocksProxyAgent(proxy) + : undefined)) || + undefined + }) + + return data +} diff --git a/src/utils/get/getUserPosts.ts b/src/utils/get/getUserPosts.ts new file mode 100644 index 0000000..e2aa4ca --- /dev/null +++ b/src/utils/get/getUserPosts.ts @@ -0,0 +1,181 @@ +import Axios from "axios" +import { _tiktokGetPosts, _tiktokurl } from "../../constants/api" +import { + AuthorPost, + Posts, + TiktokUserPostsResponse +} from "../../types/get/getUserPosts" +import { _getUserPostsParams, _xttParams } from "../../constants/params" +import { HttpsProxyAgent } from "https-proxy-agent" +import { SocksProxyAgent } from "socks-proxy-agent" +import { TiktokService } from "../../services/tiktokService" +import { StalkUser } from "../get/getProfile" + +export const getUserPosts = ( + username: string, + proxy?: string, + postLimit?: number +): Promise => + new Promise((resolve) => { + StalkUser(username).then(async (res) => { + if (res.status === "error") { + return resolve({ + status: "error", + message: res.message + }) + } + + const secUid = res.result.user.secUid + const data = await parseUserPosts(secUid, postLimit, proxy) + + resolve({ + status: "success", + result: data, + totalPosts: data.length + }) + }) + }) + +const parseUserPosts = async ( + secUid: string, + postLimit?: number, + proxy?: string +): Promise => { + // Posts Result + let hasMore = true + let cursor = 0 + const posts: Posts[] = [] + let counter = 0 + while (hasMore) { + let result: any | null = null + + // Prevent missing response posts + for (let i = 0; i < 30; i++) { + result = await requestUserPosts(secUid, cursor, postLimit, proxy) + if (result !== "") break + } + + // Validate + if (result === "") { + hasMore = false + break + } + + 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 imagePost: 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, + imagePost + }) + } 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 + }) + } + }) + + // Update hasMore and cursor for next iteration + hasMore = result.hasMore + cursor = hasMore ? result.cursor : null + counter++ + + // Check post limit if specified + if (postLimit && posts.length >= postLimit) { + hasMore = false + break + } + } + + return postLimit ? posts.slice(0, postLimit) : posts +} + +const requestUserPosts = async ( + secUid: string, + cursor: number = 0, + count: number = 30, + proxy?: string +): Promise => { + const Tiktok = new TiktokService() + + 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": Tiktok.generateXTTParams( + _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 +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index eeb392c..0000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,23 +0,0 @@ -import chalk from "chalk" - -export class Logger { - static success(message: string): void { - console.log(chalk.green("✓ " + message)) - } - - static error(message: string): void { - console.error(chalk.red("✗ " + message)) - } - - static info(message: string): void { - console.log(chalk.blue("ℹ " + message)) - } - - static warning(message: string): void { - console.log(chalk.yellow("⚠ " + message)) - } - - static result(message: string, color = chalk.cyan): void { - console.log(color(message)) - } -} diff --git a/src/utils/search/liveSearch.ts b/src/utils/search/liveSearch.ts index 9fb7105..d08517e 100644 --- a/src/utils/search/liveSearch.ts +++ b/src/utils/search/liveSearch.ts @@ -5,6 +5,7 @@ import { LiveInfo, Owner, OwnerStats, + LiveSearchResult, TiktokLiveSearchResponse } from "../../types/search/liveSearch" import { SocksProxyAgent } from "socks-proxy-agent" @@ -66,7 +67,7 @@ export const SearchLive = async ( if (!data.data) return resolve({ status: "error", message: "Live not found!" }) - const result = [] + const result: LiveSearchResult[] = [] data.data.forEach((v: any) => { const content = JSON.parse(v.live_info.raw_data) diff --git a/src/utils/search/userSearch.ts b/src/utils/search/userSearch.ts index 36670a2..bef6000 100644 --- a/src/utils/search/userSearch.ts +++ b/src/utils/search/userSearch.ts @@ -1,32 +1,14 @@ import Axios from "axios" import { _tiktokSearchUserFull, _tiktokurl } from "../../constants/api" -import { TiktokUserSearchResponse } from "../../types/search/userSearch" +import { + UserSearchResult, + TiktokUserSearchResponse +} from "../../types/search/userSearch" import { _userSearchParams } from "../../constants/params" import { HttpsProxyAgent } from "https-proxy-agent" import { SocksProxyAgent } from "socks-proxy-agent" -import xbogus from "../../../helper/xbogus" - -const userAgent = - "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" - -/** - * Generate URL with X-Bogus - * Special thanks to https://github.com/iamatef/xbogus - * @param {string} username - The username you want to search - * @param {number} page - The page you want to search - * @returns {string} - */ - -export const generateURLXbogus = (username: string, page: number) => { - const url = - "https://www.tiktok.com/api/search/user/full/?" + - _userSearchParams(username, page) - const xbogusParams = xbogus(url, userAgent) - const urlXbogus = - "https://www.tiktok.com/api/search/user/full/?" + - _userSearchParams(username, page, xbogusParams) - return urlXbogus -} +import { TiktokService } from "../../services/tiktokService" +import { userAgent } from "../../constants/headers" /** * Tiktok Search User @@ -50,7 +32,10 @@ export const SearchUser = ( message: "Cookie is required!" }) } - Axios(generateURLXbogus(username, page), { + + const Tiktok = new TiktokService() + + Axios(Tiktok.generateURLXbogus(username, page), { method: "GET", headers: { "User-Agent": userAgent, @@ -83,7 +68,7 @@ export const SearchUser = ( if (!data.user_list) return resolve({ status: "error", message: "User not found!" }) - const result = [] + const result: UserSearchResult[] = [] for (let i = 0; i < data.user_list.length; i++) { const user = data.user_list[i] result.push({ diff --git a/src/utils/search/videoSearch.ts b/src/utils/search/videoSearch.ts new file mode 100644 index 0000000..4996ff2 --- /dev/null +++ b/src/utils/search/videoSearch.ts @@ -0,0 +1,154 @@ +import Axios from "axios" +import { _tiktokSearchVideoFull } from "../../constants/api" +import { _liveSearchParams, _videoSearchParams } from "../../constants/params" +import { SocksProxyAgent } from "socks-proxy-agent" +import { HttpsProxyAgent } from "https-proxy-agent" +import { TiktokService } from "../../services/tiktokService" +import { + TiktokVideoSearchResponse, + AuthorVideoSearch, + MusicVideoSearch, + VideoSearch, + VideoSearchResult, + StatisticsVideoSearch +} from "../../types/search/videoSearch" + +/** + * Tiktok Search Live + * @param {string} keyword - The keyword you want to search + * @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} + */ + +export const SearchVideo = async ( + keyword: string, + cookie: string | any[], + page: number = 1, + proxy?: string +): Promise => + new Promise(async (resolve) => { + if (!cookie) { + return resolve({ + status: "error", + message: "Cookie is required!" + }) + } + + const Tiktok = new TiktokService() + const url = new URL( + _tiktokSearchVideoFull(_videoSearchParams(keyword, page)) + ) + const signature = Tiktok.generateSignature(url) + url.searchParams.append("_signature", signature) + const xbogus = Tiktok.generateXBogus(url, signature) + url.searchParams.append("X-Bogus", xbogus) + + Axios(_tiktokSearchVideoFull(_videoSearchParams(keyword, page)), { + 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: 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(({ data }) => { + // Cookie Invalid + if (data.status_code === 2483) + return resolve({ status: "error", message: "Invalid cookie!" }) + // Another Error + if (data.status_code !== 0) + return resolve({ + status: "error", + message: + data.status_msg || + "An error occurred! Please report this issue to the developer." + }) + if (!data.item_list) + return resolve({ status: "error", message: "Video not found!" }) + + const result: VideoSearchResult[] = [] + data.item_list.forEach((v: any) => { + const video: VideoSearch = { + id: v.video.id, + ratio: v.video.ratio, + cover: v.video.cover, + originCover: v.video.originCover, + dynamicCover: v.video.dynamicCover, + playAddr: v.video.playAddr, + downloadAddr: v.video.downloadAddr, + format: v.video.format + } + + const stats: StatisticsVideoSearch = { + diggCount: v.stats.diggCount, + shareCount: v.stats.shareCount, + commentCount: v.stats.commentCount, + playCount: v.stats.playCount, + collectCount: v.stats.collectCount + } + + const author: AuthorVideoSearch = { + id: v.author.id, + uniqueId: v.author.uniqueId, + nickname: v.author.nickname, + avatarThumb: v.author.avatarThumb, + avatarMedium: v.author.avatarMedium, + avatarLarger: v.author.avatarLarger, + signature: v.author.signature, + verified: v.author.verified, + secUid: v.author.secUid, + openFavorite: v.author.openFavorite, + privateAccount: v.author.privateAccount, + isADVirtual: v.author.isADVirtual, + tiktokSeller: v.author.ttSeller, + isEmbedBanned: v.author.isEmbedBanned + } + + const music: MusicVideoSearch = { + id: v.music.id, + title: v.music.title, + playUrl: v.music.playUrl, + coverThumb: v.music.coverThumb, + coverMedium: v.music.coverMedium, + coverLarge: v.music.coverLarge, + authorName: v.music.authorName, + original: v.music.original, + album: v.music.album, + duration: v.music.duration, + isCopyrighted: v.music.isCopyrighted + } + result.push({ + id: v.id, + desc: v.desc, + createTime: v.createTime, + author, + stats, + video, + music + }) + }) + + resolve({ + status: "success", + result, + page, + totalResults: result.length + }) + }) + .catch((e) => { + resolve({ status: "error", message: e.message }) + }) + })