import { Constant, Tweet } from '../../db/models' import axios from 'axios' import { getAuthClient, getTwitterClient } from './clients' import { makePaginate, PaginateOptions, PaginationConnection, } from 'sequelize-cursor-pagination' import { TweetData } from '../../db/models/Tweet' const STATE = 'banana' async function generateAuthURL(): Promise { const authClient = await getAuthClient() const authUrl = authClient.generateAuthURL({ state: STATE, code_challenge_method: 's256', }) return authUrl } async function saveAccessToken(token: any): Promise { await Constant.set('xBotToken', token) } async function requestAccessToken(code: string, state: string): Promise { if (state !== STATE) { throw new Error("State isn't matching") } const authClient = await getAuthClient() const token = await authClient.requestAccessToken(code) await saveAccessToken(token.token) } async function refreshAccessToken(): Promise { const authClient = await getAuthClient() const token = await authClient.refreshAccessToken() await saveAccessToken(token.token) } const tweetUrlReg = /^https:\/\/x\.com\/[a-zA-Z0-9_]+\/status\/([0-9]+)$/ const removeScriptReg = /)<[^<]*)*<\/script>/gi async function genenrateTweetByPublishAPI(urlRaw: string): Promise { 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 [tweet, created] = await Tweet.findOrCreate({ where: { statusId, }, defaults: { statusId, username: data.author_name, url, body: data, cleanHTML, }, }) if (!created) { tweet.setDataValue('username', data.author_name) tweet.setDataValue('url', url) tweet.setDataValue('body', data) tweet.setDataValue('cleanHTML', cleanHTML) tweet.setDataValue('disabled', false) await tweet.save() } return tweet } catch (error: any) { if (error.response?.data?.error) { throw new Error(error.response.data.error) } throw error } } async function updateTweet(statusId: string): Promise { const tweet = await Tweet.findOne({ where: { statusId } }) if (!tweet) { throw new Error('Tweet not found') } return await genenrateTweetByPublishAPI(tweet.url) } // console.log(Tweet.rawAttributes) let paginate: ( this: unknown, queryOptions: PaginateOptions ) => Promise> async function paginateTweetByOrder( after?: string, limit = 24 ): Promise> { if (!paginate) { paginate = makePaginate(Tweet) } const secondResult = await paginate({ order: [ ['order', 'DESC'], ['id', 'DESC'], ], limit, after, where: { disabled: false, }, }) const ret: PaginationConnection = { ...secondResult, edges: secondResult.edges.map((edge) => ({ cursor: edge.node.id, node: edge.node.getData(), })), } return ret } export interface GenerateResult { url: string success: boolean error?: string } async function bulkGenerateTweetsByPublishAPI( urls: string[] ): Promise { const results: GenerateResult[] = [] for (const url of urls) { try { const tweet = await genenrateTweetByPublishAPI(url) results.push({ url, success: true }) } catch (e) { results.push({ url, success: false, error: (e as Error).message ?? 'unknown error', }) } } return results } async function makeTweetTop(statusId: string, top = true): Promise { const tweet = await Tweet.findOne({ where: { statusId } }) if (!tweet) { throw new Error('Tweet not found') } if (top) { const maxOrder: number = await Tweet.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 { const tweet = await Tweet.findOne({ where: { statusId } }) if (!tweet) { throw new Error('Tweet not found') } tweet.setDataValue('disabled', disabled) await tweet.save() } export interface ShotData { url: string shot: string } async function importShot(data: ShotData): Promise { const { shot: shotRaw, url: urlRaw } = data try { return await genenrateTweetByPublishAPI(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 Tweet.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) 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 { const results: GenerateResult[] = [] for (const data of datas) { try { const tweet = 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 twitterService = { getTwitterClient, generateAuthURL, STATE, genenrateTweetByPublishAPI, requestAccessToken, refreshAccessToken, paginateTweetByOrder, updateTweet, bulkGenerateTweetsByPublishAPI, makeTweetTop, makeTweetDisabled, importShot, bulkImportShot, } export default twitterService