|
@@ -0,0 +1,321 @@
|
|
|
+import { Tiktok } from '../../db/models'
|
|
|
+import axios from 'axios'
|
|
|
+import {
|
|
|
+ makePaginate,
|
|
|
+ PaginateOptions,
|
|
|
+ PaginationConnection,
|
|
|
+} from 'sequelize-cursor-pagination'
|
|
|
+import { TweetData } from '../../db/models/Tweet'
|
|
|
+import { TiktokData } from '../../db/models/Tiktok'
|
|
|
+
|
|
|
+const STATE = 'banana'
|
|
|
+
|
|
|
+const tweetUrlReg = /^https:\/\/x\.com\/[a-zA-Z0-9_]+\/status\/([0-9]+)$/
|
|
|
+const tiktokUrlReg = /^https:\/\/www\.tiktok\.com\/@[a-zA-Z0-9_\-]+\/video\/([0-9]+)$/
|
|
|
+
|
|
|
+const removeScriptReg = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
|
|
|
+
|
|
|
+async function generateTweetByPublishAPI(urlRaw: string): Promise<Tiktok> {
|
|
|
+ const url = urlRaw.trim().split('?')[0]
|
|
|
+ const match = url.match(tweetUrlReg)
|
|
|
+ if (!match) {
|
|
|
+ throw new Error('Invalid tweet URL')
|
|
|
+ }
|
|
|
+
|
|
|
+ const statusId = match[1]
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await axios.get('https://publish.twitter.com/oembed', {
|
|
|
+ params: {
|
|
|
+ url,
|
|
|
+ partner: '',
|
|
|
+ hideConversation: 'on',
|
|
|
+ hide_thread: 'true',
|
|
|
+ omit_script: 'false',
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const data = resp.data
|
|
|
+ if (!data.html || !data.author_name) {
|
|
|
+ throw new Error('Failed to fetch tweet HTML')
|
|
|
+ }
|
|
|
+ const cleanHTML = data.html.replace(removeScriptReg, '').trim()
|
|
|
+
|
|
|
+ const [tiktok, created] = await Tiktok.findOrCreate({
|
|
|
+ where: {
|
|
|
+ statusId,
|
|
|
+ },
|
|
|
+ defaults: {
|
|
|
+ statusId,
|
|
|
+ username: data.author_name,
|
|
|
+ url,
|
|
|
+ body: data,
|
|
|
+ cleanHTML,
|
|
|
+ type: 0,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!created) {
|
|
|
+ tiktok.setDataValue('username', data.author_name)
|
|
|
+ tiktok.setDataValue('url', url)
|
|
|
+ tiktok.setDataValue('body', data)
|
|
|
+ tiktok.setDataValue('cleanHTML', cleanHTML)
|
|
|
+ tiktok.setDataValue('disabled', false)
|
|
|
+ tiktok.setDataValue('type', 0)
|
|
|
+ await tiktok.save()
|
|
|
+ }
|
|
|
+
|
|
|
+ return tiktok
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error.response?.data?.error) {
|
|
|
+ throw new Error(error.response.data.error)
|
|
|
+ }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+async function generateTiktokByPublishAPI(urlRaw: string): Promise<Tiktok> {
|
|
|
+ const url = urlRaw.trim().split('?')[0]
|
|
|
+ const match = url.match(tiktokUrlReg)
|
|
|
+ if (!match) {
|
|
|
+ throw new Error('Invalid tiktok URL')
|
|
|
+ }
|
|
|
+
|
|
|
+ const statusId = match[1]
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await axios.get('https://www.tiktok.com/oembed', {
|
|
|
+ params: {
|
|
|
+ url,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const data = resp.data
|
|
|
+ if (!data.html || !data.author_name) {
|
|
|
+ throw new Error('Failed to fetch tiktok HTML')
|
|
|
+ }
|
|
|
+ const cleanHTML = data.html.replace(removeScriptReg, '').trim()
|
|
|
+
|
|
|
+ const [tiktok, created] = await Tiktok.findOrCreate({
|
|
|
+ where: {
|
|
|
+ statusId,
|
|
|
+ },
|
|
|
+ defaults: {
|
|
|
+ statusId,
|
|
|
+ username: data.author_name,
|
|
|
+ url,
|
|
|
+ body: data,
|
|
|
+ cleanHTML,
|
|
|
+ type: 1,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!created) {
|
|
|
+ tiktok.setDataValue('username', data.author_name)
|
|
|
+ tiktok.setDataValue('url', url)
|
|
|
+ tiktok.setDataValue('body', data)
|
|
|
+ tiktok.setDataValue('cleanHTML', cleanHTML)
|
|
|
+ tiktok.setDataValue('disabled', false)
|
|
|
+ tiktok.setDataValue('type', 1)
|
|
|
+ await tiktok.save()
|
|
|
+ }
|
|
|
+
|
|
|
+ return tiktok
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error.response?.data?.error) {
|
|
|
+ throw new Error(error.response.data.error)
|
|
|
+ }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// console.log(Tiktok.rawAttributes)
|
|
|
+let paginate: (
|
|
|
+ this: unknown,
|
|
|
+ queryOptions: PaginateOptions<Tiktok>
|
|
|
+) => Promise<PaginationConnection<Tiktok>>
|
|
|
+
|
|
|
+async function paginateTweetByOrder(
|
|
|
+ after?: string,
|
|
|
+ limit = 24
|
|
|
+): Promise<PaginationConnection<TiktokData>> {
|
|
|
+ if (!paginate) {
|
|
|
+ paginate = makePaginate(Tiktok)
|
|
|
+ }
|
|
|
+
|
|
|
+ const secondResult = await paginate({
|
|
|
+ order: [
|
|
|
+ ['order', 'DESC'],
|
|
|
+ ['id', 'DESC'],
|
|
|
+ ],
|
|
|
+ limit,
|
|
|
+ after,
|
|
|
+ where: {
|
|
|
+ disabled: false,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const ret: PaginationConnection<TiktokData> = {
|
|
|
+ ...secondResult,
|
|
|
+ edges: secondResult.edges.map((edge) => ({
|
|
|
+ cursor: edge.node.id,
|
|
|
+ node: edge.node.getData(),
|
|
|
+ })),
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret
|
|
|
+}
|
|
|
+
|
|
|
+export interface GenerateTiktokResult {
|
|
|
+ url: string
|
|
|
+ success: boolean
|
|
|
+ error?: string
|
|
|
+}
|
|
|
+
|
|
|
+async function bulkGenerateTiktokByPublishAPI(
|
|
|
+ urls: string[]
|
|
|
+): Promise<GenerateTiktokResult[]> {
|
|
|
+ const results: GenerateTiktokResult[] = []
|
|
|
+ for (const url of urls) {
|
|
|
+ try {
|
|
|
+ if (url.includes('tiktok')) {
|
|
|
+ await generateTiktokByPublishAPI(url)
|
|
|
+ results.push({ url, success: true })
|
|
|
+ } else if (url.includes('x.com')) {
|
|
|
+ await generateTweetByPublishAPI(url)
|
|
|
+ results.push({ url, success: true })
|
|
|
+ } else {
|
|
|
+ results.push({
|
|
|
+ url,
|
|
|
+ success: false,
|
|
|
+ error: 'Invalid URL',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ results.push({
|
|
|
+ url,
|
|
|
+ success: false,
|
|
|
+ error: (e as Error).message ?? 'unknown error',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return results
|
|
|
+}
|
|
|
+
|
|
|
+async function makeTweetTop(statusId: string, top = true): Promise<void> {
|
|
|
+ const tweet = await Tiktok.findOne({ where: { statusId } })
|
|
|
+ if (!tweet) {
|
|
|
+ throw new Error('Tiktok not found')
|
|
|
+ }
|
|
|
+ if (top) {
|
|
|
+ const maxOrder: number = await Tiktok.max('order')
|
|
|
+ tweet.setDataValue('order', maxOrder ? maxOrder + 1 : 1)
|
|
|
+ } else {
|
|
|
+ tweet.setDataValue('order', 0)
|
|
|
+ }
|
|
|
+ await tweet.save()
|
|
|
+}
|
|
|
+
|
|
|
+async function makeTweetDisabled(
|
|
|
+ statusId: string,
|
|
|
+ disabled = true
|
|
|
+): Promise<void> {
|
|
|
+ const tweet = await Tiktok.findOne({ where: { statusId } })
|
|
|
+ if (!tweet) {
|
|
|
+ throw new Error('Tiktok not found')
|
|
|
+ }
|
|
|
+ tweet.setDataValue('disabled', disabled)
|
|
|
+ await tweet.save()
|
|
|
+}
|
|
|
+
|
|
|
+export interface ShotData {
|
|
|
+ url: string
|
|
|
+ shot: string
|
|
|
+}
|
|
|
+
|
|
|
+async function importShot(data: ShotData): Promise<Tiktok> {
|
|
|
+ const { shot: shotRaw, url: urlRaw } = data
|
|
|
+
|
|
|
+ try {
|
|
|
+ return await generateTweetByPublishAPI(urlRaw)
|
|
|
+ } catch (error) {
|
|
|
+ // if error, use shot
|
|
|
+ }
|
|
|
+
|
|
|
+ const shot = shotRaw.trim()
|
|
|
+
|
|
|
+ if (!shot?.startsWith('https://')) {
|
|
|
+ throw new Error('Invalid shot image URL')
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = urlRaw.trim().split('?')[0]
|
|
|
+ const match = url.match(tweetUrlReg)
|
|
|
+ if (!match) {
|
|
|
+ throw new Error('Invalid tweet URL')
|
|
|
+ }
|
|
|
+
|
|
|
+ const statusId = match[1]
|
|
|
+
|
|
|
+ try {
|
|
|
+ const [tweet, created] = await Tiktok.findOrCreate({
|
|
|
+ where: {
|
|
|
+ statusId,
|
|
|
+ },
|
|
|
+ defaults: {
|
|
|
+ statusId,
|
|
|
+ username: '',
|
|
|
+ url,
|
|
|
+ body: data,
|
|
|
+ cleanHTML: '',
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!created) {
|
|
|
+ tweet.setDataValue('username', '')
|
|
|
+ tweet.setDataValue('url', url)
|
|
|
+ tweet.setDataValue('body', data)
|
|
|
+ tweet.setDataValue('cleanHTML', '')
|
|
|
+ tweet.setDataValue('disabled', false)
|
|
|
+ tweet.setDataValue('type', 0)
|
|
|
+ await tweet.save()
|
|
|
+ }
|
|
|
+
|
|
|
+ return tweet
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error.response?.data?.error) {
|
|
|
+ throw new Error(error.response.data.error)
|
|
|
+ }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function bulkImportShot(
|
|
|
+ datas: ShotData[]
|
|
|
+): Promise<GenerateTiktokResult[]> {
|
|
|
+ const results: GenerateTiktokResult[] = []
|
|
|
+ for (const data of datas) {
|
|
|
+ try {
|
|
|
+ await importShot(data)
|
|
|
+ results.push({ url: data.url, success: true })
|
|
|
+ } catch (e) {
|
|
|
+ results.push({
|
|
|
+ url: data.url,
|
|
|
+ success: false,
|
|
|
+ error: (e as Error).message ?? 'unknown error',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return results
|
|
|
+}
|
|
|
+
|
|
|
+const tiktokService = {
|
|
|
+ STATE,
|
|
|
+ generateTweetByPublishAPI,
|
|
|
+ paginateTweetByOrder,
|
|
|
+ bulkGenerateTiktokByPublishAPI,
|
|
|
+ makeTweetTop,
|
|
|
+ makeTweetDisabled,
|
|
|
+ importShot,
|
|
|
+ bulkImportShot,
|
|
|
+}
|
|
|
+
|
|
|
+export default tiktokService
|