123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- 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<string> {
- const authClient = await getAuthClient()
- const authUrl = authClient.generateAuthURL({
- state: STATE,
- code_challenge_method: 's256',
- })
- return authUrl
- }
- async function saveAccessToken(token: any): Promise<void> {
- await Constant.set('xBotToken', token)
- }
- async function requestAccessToken(code: string, state: string): Promise<void> {
- 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<void> {
- 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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
- async function genenrateTweetByPublishAPI(urlRaw: string): Promise<Tweet> {
- 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<Tweet> {
- 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<Tweet>
- ) => Promise<PaginationConnection<Tweet>>
- async function paginateTweetByOrder(
- after?: string,
- limit = 24
- ): Promise<PaginationConnection<TweetData>> {
- if (!paginate) {
- paginate = makePaginate(Tweet)
- }
- const secondResult = await paginate({
- order: [
- ['order', 'DESC'],
- ['id', 'DESC'],
- ],
- limit,
- after,
- where: {
- disabled: false,
- },
- })
- const ret: PaginationConnection<TweetData> = {
- ...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<GenerateResult[]> {
- 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<void> {
- 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<void> {
- 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<Tweet> {
- 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<GenerateResult[]> {
- 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
|