From 8fd77ea2b222e125aba2aef6ccb79e4ccc455a49 Mon Sep 17 00:00:00 2001 From: TobyG74 Date: Fri, 29 Mar 2024 20:07:18 +0700 Subject: [PATCH] fix: response --- .../downloader/downloader_musicaldown.ts | 18 +- src/utils/downloader/downloader_ssstik.ts | 18 +- src/utils/downloader/downloader_tiktokApi.ts | 184 +++++++--------- src/utils/search/tiktok_stalker.ts | 196 ++++++++++++++++++ src/utils/stalker/tiktok_stalker.ts | 138 ------------ 5 files changed, 298 insertions(+), 256 deletions(-) create mode 100644 src/utils/search/tiktok_stalker.ts delete mode 100644 src/utils/stalker/tiktok_stalker.ts diff --git a/src/utils/downloader/downloader_musicaldown.ts b/src/utils/downloader/downloader_musicaldown.ts index 2dee709..49ed943 100644 --- a/src/utils/downloader/downloader_musicaldown.ts +++ b/src/utils/downloader/downloader_musicaldown.ts @@ -1,15 +1,23 @@ import Axios from "axios" import { load } from "cheerio" -import { MusicalDownResponse, getMusic, getRequest } from "../../types/musicaldown" -import { _musicaldownapi, _musicaldownmusicapi, _musicaldownurl } from "../../api" +import { MusicalDownResponse, getMusic, getRequest } from "../../types/downloader/musicaldown" +import { _musicaldownapi, _musicaldownmusicapi, _musicaldownurl } from "../../constants/api" /** * Using API from Website: * BASE URL : https://ssstik.io */ +const TiktokURLregex = /(?:http[s]?:\/\/)?(?:www\.|m\.)?(?:tiktok\.com\/(?:@[\w.-]+\/video\/|@[\w.-]+\/video\/))?(\d+)/ + const getRequest = (url: string) => new Promise((resolve, reject) => { + if (!TiktokURLregex.test(url)) { + return resolve({ + status: "error", + message: "Invalid Tiktok URL. Make sure your url is correct!" + }) + } Axios.get(_musicaldownurl, { headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0" @@ -46,6 +54,12 @@ const getMusic = (cookie: string) => .catch((e) => resolve({ status: "error" })) }) +/** + * Tiktok MusicalDown Downloader + * @param {string} url - Tiktok URL + * @returns {Promise} + */ + export const MusicalDown = (url: string) => new Promise(async (resolve, reject) => { const request: getRequest = await getRequest(url) diff --git a/src/utils/downloader/downloader_ssstik.ts b/src/utils/downloader/downloader_ssstik.ts index a15fce8..fbde18d 100644 --- a/src/utils/downloader/downloader_ssstik.ts +++ b/src/utils/downloader/downloader_ssstik.ts @@ -1,13 +1,15 @@ import Axios from "axios" import { load } from "cheerio" -import { Author, Statistics, SSSTikFetchTT, SSSTikResponse } from "../../types/ssstik" -import { _ssstikapi, _ssstikurl } from "../../api" +import { Author, Statistics, SSSTikFetchTT, SSSTikResponse } from "../../types/downloader/ssstik" +import { _ssstikapi, _ssstikurl } from "../../constants/api" /** * Using API from Website: * BASE URL : https://ssstik.io */ +const TiktokURLregex = /(?:http[s]?:\/\/)?(?:www\.|m\.)?(?:tiktok\.com\/(?:@[\w.-]+\/video\/|@[\w.-]+\/video\/))?(\d+)/ + const fetchTT = () => new Promise(async (resolve, reject) => { Axios.get(_ssstikurl, { @@ -28,8 +30,20 @@ const fetchTT = () => .catch((e) => resolve({ status: "error", message: e.message })) }) +/** + * Tiktok SSSTik Downloader + * @param {string} url - Tiktok URL + * @returns {Promise} + */ + export const SSSTik = (url: string) => new Promise(async (resolve, reject) => { + if (!TiktokURLregex.test(url)) { + return resolve({ + status: "error", + message: "Invalid Tiktok URL. Make sure your url is correct!" + }) + } const tt: SSSTikFetchTT = await fetchTT() if (tt.status !== "success") return resolve({ status: "error", message: tt.message }) Axios(_ssstikapi, { diff --git a/src/utils/downloader/downloader_tiktokApi.ts b/src/utils/downloader/downloader_tiktokApi.ts index f1030db..cdff051 100644 --- a/src/utils/downloader/downloader_tiktokApi.ts +++ b/src/utils/downloader/downloader_tiktokApi.ts @@ -1,13 +1,26 @@ -import axios from "axios" -import asyncRetry from "async-retry" -import { _tiktokapi, _tiktokurl } from "../../api" -import { Author, TiktokAPIResponse, Statistics, Music, responseParser } from "../../types/tiktokApi" +import Axios from "axios" +import { _tiktokapi, _tiktokurl } from "../../constants/api" +import { _tiktokApiParams } from "../../constants/params" +import { Author, TiktokAPIResponse, Statistics, Music, responseParser, Video } from "../../types/downloader/tiktokApi" + +const TiktokURLregex = /(?:http[s]?:\/\/)?(?:www\.|m\.)?(?:tiktok\.com\/(?:@[\w.-]+\/video\/|@[\w.-]+\/video\/))?(\d+)/ + +/** + * Tiktok API Downloader + * @param {string} url - Tiktok URL + * @returns {Promise} + */ export const TiktokAPI = (url: string) => new Promise((resolve, reject) => { + if (!TiktokURLregex.test(url)) { + return resolve({ + status: "error", + message: "Invalid Tiktok URL. Make sure your url is correct!" + }) + } url = url.replace("https://vm", "https://vt") - axios - .head(url) + Axios.head(url) .then(async ({ request }) => { const { responseUrl } = request.res let ID = responseUrl.match(/\d{17,21}/g) @@ -20,7 +33,7 @@ export const TiktokAPI = (url: string) => let data2 = await fetchTiktokData(ID) - if (!data2.content) { + if (!data2?.content) { return resolve({ status: "error", message: "Failed to fetch tiktok data. Make sure your tiktok url is correct!" @@ -40,6 +53,7 @@ export const TiktokAPI = (url: string) => createTime: content.create_time, description: content.desc, hashtag: content.text_extra.filter((x) => x.hashtag_name !== undefined).map((v) => v.hashtag_name), + isADS: content.is_ads, author, statistics, images: content.image_post_info.images.map((v) => v.display_image.url_list[0]), @@ -48,6 +62,16 @@ export const TiktokAPI = (url: string) => }) } else { // Video Result + const video: Video = { + ratio: content.video.ratio, + duration: content.video.duration, + playAddr: content.video.play_addr.url_list, + downloadAddr: content.video.download_addr.url_list, + cover: content.video.cover.url_list, + dynamicCover: content.video.dynamic_cover.url_list, + originCover: content.video.origin_cover.url_list + } + resolve({ status: "success", result: { @@ -56,13 +80,10 @@ export const TiktokAPI = (url: string) => createTime: content.create_time, description: content.desc, hashtag: content.text_extra.filter((x) => x.hashtag_name !== undefined).map((v) => v.hashtag_name), - duration: toMinute(content.duration), + isADS: content.is_ads, author, statistics, - video: content.video.play_addr.url_list, - cover: content.video.cover.url_list, - dynamicCover: content.video.dynamic_cover.url_list, - originCover: content.video.origin_cover.url_list, + video, music } }) @@ -71,62 +92,54 @@ export const TiktokAPI = (url: string) => .catch((e) => resolve({ status: "error", message: e.message })) }) -const fetchTiktokData = async (ID: string) => { - let data2: responseParser - await asyncRetry( - async () => { - const res = await fetch( - _tiktokapi( - new URLSearchParams( - withParams({ - aweme_id: ID - }) - ).toString() - ), - { - method: "GET", - headers: { - "User-Agent": "com.ss.android.ugc.trill/494+Mozilla/5.0+(Linux;+Android+12;+2112123G+Build/SKQ1.211006.001;+wv)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Version/4.0+Chrome/107.0.5304.105+Mobile+Safari/537.36" - } - } - ) - - if (res.headers.get("content-length") !== "0") { - const data = await res.json() - - if (data) { - data2 = parseTiktokData(data) - return - } +const fetchTiktokData = async (ID: string): Promise | null => { + const res = await fetch( + _tiktokapi( + new URLSearchParams( + _tiktokApiParams({ + aweme_id: ID + }) + ).toString() + ), + { + 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" } - - throw new Error("Data is empty!") - }, - { forever: true, minTimeout: 0, maxTimeout: 0 } + } ) - return data2 + if (res.headers.get("content-length") !== "0") { + const data = await res.json() + + if (data) { + return parseTiktokData(ID, data) + } + } + + return null } -const parseTiktokData = (data: any): responseParser => { +const parseTiktokData = (ID: string, data: any): responseParser => { let content = data?.aweme_list if (!content) return { content: null } - content = content[0] + content = content.find((v: any) => v.aweme_id === ID) // Statistics Result const statistics: Statistics = { - playCount: content.statistics.play_count, - downloadCount: content.statistics.download_count, - shareCount: content.statistics.share_count, commentCount: content.statistics.comment_count, - likeCount: content.statistics.digg_count, - favoriteCount: content.statistics.collect_count, + diggCount: content.statistics.digg_count, + downloadCount: content.statistics.download_count, + playCount: content.statistics.play_count, + shareCount: content.statistics.share_count, forwardCount: content.statistics.forward_count, - whatsappShareCount: content.statistics.whatsapp_share_count, loseCount: content.statistics.lose_count, - loseCommentCount: content.statistics.lose_comment_count + loseCommentCount: content.statistics.lose_comment_count, + whatsappShareCount: content.statistics.whatsapp_share_count, + collectCount: content.statistics.collect_count, + repostCount: content.statistics.repost_count } // Author Result @@ -151,68 +164,11 @@ const parseTiktokData = (data: any): responseParser => { coverLarge: content.music.cover_large.url_list, coverMedium: content.music.cover_medium.url_list, coverThumb: content.music.cover_thumb.url_list, - duration: content.music.duration + duration: content.music.duration, + isCommerceMusic: content.music.is_commerce_music, + isOriginalSound: content.music.is_original_sound, + isAuthorArtist: content.music.is_author_artist } return { content, statistics, author, music } } - -const withParams = (args) => { - return { - ...args, - version_name: "1.1.9", - version_code: "2018111632", - build_number: "1.1.9", - manifest_version_code: "2018111632", - update_version_code: "2018111632", - openudid: randomChar("0123456789abcdef", 16), - uuid: randomChar("1234567890", 16), - _rticket: Date.now() * 1000, - ts: Date.now(), - device_brand: "Google", - device_type: "Pixel 4", - device_platform: "android", - resolution: "1080*1920", - dpi: 420, - os_version: "10", - os_api: "29", - carrier_region: "US", - sys_region: "US", - region: "US", - app_name: "trill", - app_language: "en", - language: "en", - timezone_name: "America/New_York", - timezone_offset: "-14400", - channel: "googleplay", - ac: "wifi", - mcc_mnc: "310260", - is_my_cn: 0, - aid: 1180, - ssmix: "a", - as: "a1qwert123", - cp: "cbfhckdckkde1" - } -} - -const toMinute = (duration: number) => { - const mins = ~~((duration % 3600) / 60) - const secs = ~~duration % 60 - - let ret = "" - - ret += "" + mins + ":" + (secs < 10 ? "0" : "") - ret += "" + secs - - return ret -} - -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/utils/search/tiktok_stalker.ts b/src/utils/search/tiktok_stalker.ts new file mode 100644 index 0000000..926655e --- /dev/null +++ b/src/utils/search/tiktok_stalker.ts @@ -0,0 +1,196 @@ +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/stalker/tiktok_stalker.ts b/src/utils/stalker/tiktok_stalker.ts deleted file mode 100644 index c65595a..0000000 --- a/src/utils/stalker/tiktok_stalker.ts +++ /dev/null @@ -1,138 +0,0 @@ -import axios from "axios" -import { load } from "cheerio" -import { _tiktokurl } from "../../api" -import { StalkResult, Stats, Users } from "../../types/stalker" - -const getCookie = () => - new Promise((resolve, reject) => { - axios - .get("https://pastebin.com/raw/ELJjcbZT") - .then(({ data: cookie }) => { - resolve(cookie) - }) - .catch((e) => resolve({ status: "error", message: "Failed to fetch cookie." })) - }) - -export const TiktokStalk = (username: string, options?: { cookie: string }): 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: (options?.cookie ? options.cookie : await getCookie()) as string - } - }) - .then(({ data }) => { - const $ = load(data) - const result = JSON.parse($("script#__UNIVERSAL_DATA_FOR_REHYDRATION__").text()) - if (!result?.__DEFAULT_SCOPE__?.["webapp.user-detail"]) { - return resolve({ - status: "error", - message: "User not found!" - }) - } - const dataUser = result.__DEFAULT_SCOPE__["webapp.user-detail"].userInfo - - // User Info Result - const users: Users = { - 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: itemKeys.length - } - - // Posts Result - /** - const posts: Posts[] = [] - itemKeys.forEach((key) => { - const post = result.ItemModule[key] - let media - if (post.imagePost) { - // Images or Slide Posts Result - media = { - images: post.imagePost.images.map((v: any) => v.imageURL.urlList[0]) - } - } else { - // Video Posts Result - media = { - video: { - id: post.video.id, - duration: post.video.duration, - ratio: post.video.ratio, - cover: post.video.cover, - originCover: post.video.originCover, - dynamicCover: post.video.dynamicCover, - playAddr: post.video.playAddr, - downloadAddr: post.video.downloadAddr, - format: post.video.format, - bitrate: post.video.bitrate - } as Video - } - } - - // Music Posts Result - const music: Music = { - id: post.music.id, - title: post.music.title, - authorName: post.music.authorName, - album: post.music.album, - coverLarge: post.music.coverLarge, - coverMedium: post.music.coverMedium, - coverThumb: post.music.coverThumb, - playUrl: post.music.playUrl, - duration: post.music.duration - } - - // Statistics Posts Result - const statistics: Statistics = { - likeCount: post.stats.diggCount, - shareCount: post.stats.shareCount, - commentCount: post.stats.commentCount, - playCount: post.stats.playCount, - favoriteCount: post.stats.collectCount - } - - posts.push({ - id: post.id, - desc: post.desc, - createTime: post.createTime, - author: post.author, - locationCreated: post.locationCreated, - hashtags: post.challenges.map((v: any) => v.title), - statistics, - music, - ...media - }) - }) - */ - resolve({ - status: "success", - result: { - users, - stats - // posts - } - }) - }) - .catch((e) => resolve({ status: "error", message: e.message })) - })