feat: add tiktok get comments

This commit is contained in:
Tobi Saputra 2024-12-07 23:50:21 +07:00
parent 46acd692e3
commit 3bcbecbb6e
6 changed files with 333 additions and 301 deletions

View File

@ -8,6 +8,8 @@ export const _tiktokSearchLiveFull = (params: any): string =>
`${_tiktokurl}/api/search/live/full/?${params}`
export const _tiktokGetPosts = (params: any): string =>
`${_tiktokurl}/api/post/item_list/?${params}`
export const _tiktokGetComments = (params: any): string =>
`${_tiktokurl}/api/comment/list/?${params}`
/** Tiktokv */
export const _tiktokvApi: string = `https://api16-normal-useast5.tiktokv.us`

View File

@ -1,6 +1,7 @@
import qs from "qs"
export const _userPostsParams = () => {
/** Get Params */
export const _getUserPostsParams = () => {
return (
qs.stringify({
aid: 1988,
@ -37,14 +38,70 @@ export const _userPostsParams = () => {
)
}
export const _xttParams = (secUid: string, cursor: number, count: number) => {
return qs.stringify({
aid: "1988",
cookie_enabled: true,
screen_width: 0,
screen_height: 0,
browser_language: "",
browser_platform: "",
browser_name: "",
browser_version: "",
browser_online: "",
timezone_name: "Europe/London",
secUid,
cursor,
count,
is_encryption: 1
})
}
export const _getCommentsParams = (id: string, count: number) => {
let cursor = 0
// 50 comments per page
if (count > 50) {
for (let i = 1; i < count; i++) {
cursor += 50
}
}
return qs.stringify({
aid: "1988",
app_language: "ja-JP",
app_name: "tiktok_web",
aweme_id: id,
browser_language: "en-US",
browser_name: "Mozilla",
browser_online: true,
browser_platform: "Linux x86_64",
browser_version: "5.0 (X11)",
channel: "tiktok_web",
cookie_enabled: true,
count: 50,
cursor: cursor,
device_id: "7445428925624813064",
os: "linux",
region: "ID",
screen_height: 768,
screen_width: 1366
})
}
/** Search */
export const _userSearchParams = (
keyword: string,
page: number = 1,
page: number,
xbogus?: any
) => {
let cursor = 0
for (let i = 1; i < page; i++) {
cursor += 10
// 10 users per page
if (page > 1) {
for (let i = 1; i < page; i++) {
cursor += 10
}
}
const params = {
@ -98,10 +155,14 @@ export const _userSearchParams = (
return qs.stringify(params)
}
export const _liveSearchParams = (keyword: string, page: number = 1) => {
export const _liveSearchParams = (keyword: string, page: number) => {
let cursor = 0
for (let i = 1; i < page; i++) {
cursor += 12
// 12 cursor for 20 lives per page
if (page > 1) {
for (let i = 1; i < page; i++) {
cursor += 12
}
}
let offset = `${cursor}`
@ -142,6 +203,7 @@ export const _liveSearchParams = (keyword: string, page: number = 1) => {
})
}
/** Downloader Params */
export const _tiktokApiParams = (args: any) => {
return new URLSearchParams({
...args,
@ -178,25 +240,6 @@ export const _tiktokApiParams = (args: any) => {
}).toString()
}
export const _xttParams = (secUid: string, cursor: number, count: number) => {
return qs.stringify({
aid: "1988",
cookie_enabled: true,
screen_width: 0,
screen_height: 0,
browser_language: "",
browser_platform: "",
browser_name: "",
browser_version: "",
browser_online: "",
timezone_name: "Europe/London",
secUid,
cursor,
count,
is_encryption: 1
})
}
const randomChar = (char: string, range: number) => {
let chars = ""

View File

@ -3,18 +3,22 @@ import { MusicalDown } from "./utils/downloader/musicalDown"
import { SSSTik } from "./utils/downloader/ssstik"
import { TiktokAPI } from "./utils/downloader/tiktokApi"
/** Get */
import { StalkUser } from "./utils/get/getProfile"
/** Search */
import { StalkUser } from "./utils/search/stalker"
import { SearchUser } from "./utils/search/userSearch"
import { SearchLive } from "./utils/search/liveSearch"
/** Types */
import { MusicalDownResponse } from "./types/downloader/musicaldown"
import { SSSTikResponse } from "./types/downloader/ssstik"
import { TiktokAPIResponse } from "./types/downloader/tiktokApi"
import { TiktokUserSearchResponse } from "./types/search/userSearch"
import { StalkResult } from "./types/search/stalker"
import { SearchLive } from "./utils/search/liveSearch"
import { StalkResult } from "./types/get/getProfile"
import { TiktokLiveSearchResponse } from "./types/search/liveSearch"
import { CommentsResult } from "./types/get/getComments"
import { getComments } from "./utils/get/getComments"
type TiktokDownloaderResponse<T extends "v1" | "v2" | "v3"> = T extends "v1"
? TiktokAPIResponse
@ -35,28 +39,38 @@ export = {
* @param {string} url - The Tiktok URL you want to download
* @param {object} options - The options for downloader
* @param {string} options.version - The version of downloader
* @param {string} options.proxy - Your Proxy (optional)
* @param {boolean} options.showOriginalResponse - Show Original Response (optional) & Only for v1
* @returns {Promise<TiktokDownloaderResponse>}
*/
Downloader: async <T extends "v1" | "v2" | "v3">(
url: string,
options?: { version: T }
options?: { version: T; proxy?: string; showOriginalResponse?: boolean }
): Promise<TiktokDownloaderResponse<T>> => {
switch (options?.version.toLowerCase()) {
case "v1": {
const response = await TiktokAPI(url)
const response = await TiktokAPI(
url,
options?.proxy,
options?.showOriginalResponse
)
return response as TiktokDownloaderResponse<T>
}
case "v2": {
const response = await SSSTik(url)
const response = await SSSTik(url, options?.proxy)
return response as TiktokDownloaderResponse<T>
}
case "v3": {
const response = await MusicalDown(url)
const response = await MusicalDown(url, options?.proxy)
return response as TiktokDownloaderResponse<T>
}
default: {
const response = await TiktokAPI(url)
const response = await TiktokAPI(
url,
options?.proxy,
options?.showOriginalResponse
)
return response as TiktokDownloaderResponse<T>
}
}
@ -66,13 +80,19 @@ export = {
* @param {string} query - The query you want to search
* @param {object} options - The options for search
* @param {string} options.type - The type of search
* @param {string} options.cookie - Your Tiktok Cookie (optional)
* @param {string | any[]} options.cookie - Your Tiktok Cookie (optional)
* @param {number} options.page - The page of search (optional)
* @param {string} options.proxy - Your Proxy (optional)
* @returns {Promise<TiktokSearchResponse>}
*/
Search: async <T extends "user" | "live">(
query: string,
options: { type: T; cookie?: string; page?: number; proxy?: string }
options: {
type: T
cookie?: string | any[]
page?: number
proxy?: string
}
): Promise<TiktokSearchResponse<T>> => {
switch (options?.type.toLowerCase()) {
case "user": {
@ -108,12 +128,18 @@ export = {
* Tiktok Stalk User
* @param {string} username - The username you want to stalk
* @param {object} options - The options for stalk
* @param {string} options.cookie - Your Tiktok Cookie (optional)
* @param {string | any[]} options.cookie - Your Tiktok Cookie (optional)
* @param {number} options.postLimit - The limit of post you want to get (optional)
* @param {string} options.proxy - Your Proxy (optional)
* @returns {Promise<StalkResult>}
*/
StalkUser: async (
username: string,
options?: { cookie?: string; postLimit?: number; proxy?: string }
options?: {
cookie?: string | any[]
postLimit?: number
proxy?: string
}
): Promise<StalkResult> => {
const response = await StalkUser(
username,
@ -122,5 +148,25 @@ export = {
options?.proxy
)
return response
},
/**
* Tiktok Get Comments
* @param {string} url - The Tiktok URL you want to get comments
* @param {object} options - The options for get comments
* @param {string} options.proxy - Your Proxy (optional)
* @param {number} options.page - The page you want to get (optional)
* @returns {Promise<CommentsResult>}
*/
GetComments: async (
url: string,
options?: { commentLimit?: number; proxy?: string }
): Promise<CommentsResult> => {
const response = await getComments(
url,
options?.proxy,
options?.commentLimit
)
return response
}
}

View File

@ -0,0 +1,28 @@
export type CommentsResult = {
status: "success" | "error"
message?: string
result?: Comments[]
totalComments?: number
}
export type Comments = {
cid: string
text: string
commentLanguage: string
createTime: number
likeCount: number
isAuthorLiked: boolean
isCommentTranslatable: boolean
replyCommentTotal: number
replyComment: Comments[] | null
user: User
url: string
}
export type User = {
uid: string
avatarThumb: string[]
nickname: string
username: string
isVerified: boolean
}

View File

@ -0,0 +1,176 @@
import Axios from "axios"
import { _tiktokGetComments } from "../../constants/api"
import { _getCommentsParams } from "../../constants/params"
import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent"
import { Comments, CommentsResult, User } from "../../types/get/getComments"
const TiktokURLregex =
/https:\/\/(?:m|www|vm|vt|lite)?\.?tiktok\.com\/((?:.*\b(?:(?:usr|v|embed|user|video|photo)\/|\?shareId=|\&item_id=)(\d+))|\w+)/
/**
* Tiktok Get Comments
* @param {string} url - Tiktok URL
* @param {string} proxy - Your Proxy (optional)
* @param {number} commentLimit - Comment Limit (optional)
* @returns {Promise<CommentsResult>}
*/
export const getComments = async (
url: string,
proxy?: string,
commentLimit?: number
): Promise<CommentsResult> =>
new Promise(async (resolve) => {
if (!TiktokURLregex.test(url)) {
return resolve({
status: "error",
message: "Invalid Tiktok URL. Make sure your url is correct!"
})
}
url = url.replace("https://vm", "https://vt")
Axios(url, {
method: "HEAD",
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
.then(async ({ request }) => {
const { responseUrl } = request.res
let ID = responseUrl.match(/\d{17,21}/g)
if (ID === null)
return resolve({
status: "error",
message:
"Failed to fetch tiktok url. Make sure your tiktok url is correct!"
})
ID = ID[0]
const resultComments = await parseComments(ID, commentLimit, proxy)
return resolve({
status: "success",
result: resultComments.comments,
totalComments: resultComments.total
})
})
.catch((e) => resolve({ status: "error", message: e.message }))
})
const requestComments = async (
id: string,
commentLimit: number,
proxy?: string
) => {
const { data } = await Axios(
_tiktokGetComments(_getCommentsParams(id, commentLimit)),
{
method: "GET",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
}
)
return data
}
const parseComments = async (
id: string,
commentLimit?: number,
proxy?: string
) => {
const comments: Comments[] = []
let cursor: number = 0
let counter: number = 0
let count: number = 50
let total: number = 0
let hasMore: boolean = true
while (hasMore) {
for (let i = 0; i < count; i++) {}
const result = await requestComments(id, cursor, proxy)
// Check if the result has more comments
if (result.has_more === 0) hasMore = false
result.comments?.forEach((v: any) => {
const comment = {
cid: v.cid,
text: v.text,
commentLanguage: v.comment_language,
createTime: v.create_time,
likeCount: v.digg_count,
isAuthorLiked: v.is_author_digged,
isCommentTranslatable: v.is_comment_translatable,
replyCommentTotal: v.reply_comment_total,
user: {
uid: v.user.uid,
avatarThumb: v.user.avatar_thumb.url_list,
nickname: v.user.nickname,
username: v.user.unique_id,
isVerified: v.user.custom_verify !== ""
} as User,
url: v.share_info?.url || "",
replyComment: []
}
if (v.reply_comment !== null) {
v.reply_comment.forEach((v: any) => {
comment.replyComment.push({
cid: v.cid,
text: v.text,
commentLanguage: v.comment_language,
createTime: v.create_time,
likeCount: v.digg_count,
isAuthorLiked: v.is_author_digged,
isCommentTranslatable: v.is_comment_translatable,
replyCommentTotal: v.reply_comment_total,
user: {
uid: v.user.uid,
avatarThumb: v.user.avatar_thumb.url_list,
nickname: v.user.nickname,
username: v.user.unique_id,
isVerified: v.user.custom_verify !== ""
} as User,
url: v.share_info?.url || "",
replyComment: []
})
total++
})
}
total++
comments.push(comment)
})
// Check if the comments length is equal to the comment limit
if (commentLimit) {
let loopCount = Math.floor(commentLimit / 50)
if (counter >= loopCount) hasMore = false
}
hasMore = result.has_more === 1
cursor = result.has_more === 1 ? result.cursor : 0
counter++
}
return {
total: total,
comments: commentLimit ? comments.slice(0, commentLimit) : comments
}
}

View File

@ -1,263 +0,0 @@
import Axios from "axios"
import qs from "qs"
import { load } from "cheerio"
import { _tiktokGetPosts, _tiktokurl } from "../../constants/api"
import {
AuthorPost,
Posts,
StalkResult,
Stats,
Users
} from "../../types/search/stalker"
import { _userPostsParams, _xttParams } from "../../constants/params"
import { createCipheriv } from "crypto"
import { HttpsProxyAgent } from "https-proxy-agent"
import { SocksProxyAgent } from "socks-proxy-agent"
/**
* Tiktok Stalk User
* @param {string} username - The username you want to stalk
* @param {object|string} cookie - Your Tiktok Cookie (optional)
* @param {number} postLimit - The limit of post you want to get (optional)
* @param {string} proxy - Your Proxy (optional)
* @returns {Promise<StalkResult>}
*/
export const StalkUser = (
username: string,
cookie?: any,
postLimit?: number,
proxy?: string
): Promise<StalkResult> =>
new Promise(async (resolve) => {
username = username.replace("@", "")
Axios.get(`${_tiktokurl}/@${username}`, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
cookie:
typeof cookie === "object"
? cookie.map((v) => `${v.name}=${v.value}`).join("; ")
: cookie
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
.then(async ({ data }) => {
const $ = load(data)
const result = JSON.parse(
$("script#__UNIVERSAL_DATA_FOR_REHYDRATION__").text()
)
if (
!result["__DEFAULT_SCOPE__"] &&
!result["__DEFAULT_SCOPE__"]["webapp.user-detail"]
) {
return resolve({
status: "error",
message: "User not found!"
})
}
const dataUser =
result["__DEFAULT_SCOPE__"]["webapp.user-detail"]["userInfo"]
const posts: Posts[] = await parsePosts(dataUser, postLimit, proxy)
const { users, stats } = parseDataUser(dataUser, posts)
resolve({
status: "success",
result: {
users,
stats,
posts
}
})
})
.catch((e) => resolve({ status: "error", message: e.message }))
})
/**
* Thanks to:
* https://github.com/atharahmed/tiktok-private-api/blob/020ede2eaa6021bcd363282d8cef1aacaff2f88c/src/repositories/user.repository.ts#L148
*/
const request = async (
secUid: string,
cursor = 0,
count = 30,
proxy?: string
) => {
const { data } = await Axios.get(`${_tiktokGetPosts(_userPostsParams())}`, {
headers: {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35",
"X-tt-params": xttparams(_xttParams(secUid, cursor, count))
},
httpsAgent:
(proxy &&
(proxy.startsWith("http") || proxy.startsWith("https")
? new HttpsProxyAgent(proxy)
: proxy.startsWith("socks")
? new SocksProxyAgent(proxy)
: undefined)) ||
undefined
})
return data
}
const parseDataUser = (dataUser: any, posts: Posts[]) => {
// User Info Result
const users: Users = {
id: dataUser.user.id,
username: dataUser.user.uniqueId,
nickname: dataUser.user.nickname,
avatarLarger: dataUser.user.avatarLarger,
avatarThumb: dataUser.user.avatarThumb,
avatarMedium: dataUser.user.avatarMedium,
signature: dataUser.user.signature,
verified: dataUser.user.verified,
privateAccount: dataUser.user.privateAccount,
region: dataUser.user.region,
commerceUser: dataUser.user.commerceUserInfo.commerceUser,
usernameModifyTime: dataUser.user.uniqueIdModifyTime,
nicknameModifyTime: dataUser.user.nickNameModifyTime
}
// Statistics Result
const stats: Stats = {
followerCount: dataUser.stats.followerCount,
followingCount: dataUser.stats.followingCount,
heartCount: dataUser.stats.heartCount,
videoCount: dataUser.stats.videoCount,
likeCount: dataUser.stats.diggCount,
friendCount: dataUser.stats.friendCount,
postCount: posts.length
}
return { users, stats }
}
const parsePosts = async (
dataUser: any,
postLimit?: number,
proxy?: string
): Promise<Posts[]> => {
// Posts Result
let hasMore = true
let cursor: number | null = null
const posts: Posts[] = []
while (hasMore) {
let result2: any | null = null
let counter = 0
// Prevent missing response posts
for (let i = 0; i < 30; i++) {
result2 = await request(dataUser.user.secUid, cursor, 30, proxy)
if (result2 !== "") break
}
// Validate
if (result2 === "") hasMore = false // No More Post
result2?.itemList?.forEach((v: any) => {
const author: AuthorPost = {
id: v.author.id,
username: v.author.uniqueId,
nickname: v.author.nickname,
avatarLarger: v.author.avatarLarger,
avatarThumb: v.author.avatarThumb,
avatarMedium: v.author.avatarMedium,
signature: v.author.signature,
verified: v.author.verified,
openFavorite: v.author.openFavorite,
privateAccount: v.author.privateAccount,
isADVirtual: v.author.isADVirtual,
isEmbedBanned: v.author.isEmbedBanned
}
if (v.imagePost) {
const images: string[] = v.imagePost.images.map(
(img: any) => img.imageURL.urlList[0]
)
posts.push({
id: v.id,
desc: v.desc,
createTime: v.createTime,
digged: v.digged,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend,
officalItem: v.officalItem,
originalItem: v.originalItem,
privateItem: v.privateItem,
shareEnabled: v.shareEnabled,
stitchEnabled: v.stitchEnabled,
stats: v.stats,
music: v.music,
author,
images
})
} else {
const video = {
id: v.video.id,
duration: v.video.duration,
format: v.video.format,
bitrate: v.video.bitrate,
ratio: v.video.ratio,
playAddr: v.video.playAddr,
cover: v.video.cover,
originCover: v.video.originCover,
dynamicCover: v.video.dynamicCover,
downloadAddr: v.video.downloadAddr
}
posts.push({
id: v.id,
desc: v.desc,
createTime: v.createTime,
digged: v.digged,
duetEnabled: v.duetEnabled,
forFriend: v.forFriend,
officalItem: v.officalItem,
originalItem: v.originalItem,
privateItem: v.privateItem,
shareEnabled: v.shareEnabled,
stitchEnabled: v.stitchEnabled,
stats: v.stats,
music: v.music,
author,
video
})
}
})
// Restrict too many data requests
if (postLimit !== 0) {
let loopCount = Math.floor(postLimit / 30)
if (counter >= loopCount) break
}
hasMore = result2.hasMore
cursor = hasMore ? result2.cursor : null
counter++
}
return postLimit ? posts.slice(0, postLimit) : posts
}
const xttparams = (params: any) => {
const cipher = createCipheriv(
"aes-128-cbc",
"webapp1.0+202106",
"webapp1.0+202106"
)
return Buffer.concat([cipher.update(params), cipher.final()]).toString(
"base64"
)
}