4
0

5 Revīzijas e7fef237f1 ... 793a5fa211

Autors SHA1 Ziņojums Datums
  Alex Xu 793a5fa211 update 7 mēneši atpakaļ
  Alex Xu d46aa61535 update 7 mēneši atpakaļ
  Alex Xu e83726945f update 8 mēneši atpakaļ
  Alex Xu be37633ef8 update 8 mēneši atpakaļ
  Alex Xu c8b73ddfde update 8 mēneši atpakaļ

+ 27 - 1
src/controllers/tiktok/index.ts

@@ -10,6 +10,7 @@ import tiktokService, {
   ShotData,
 } from '../../services/tiktokService'
 import { TiktokData } from '../../db/models/Tiktok'
+import { PaginationData } from '../../services/types'
 
 interface ImportPayload {
   token: string
@@ -47,6 +48,12 @@ export default class TiktokController implements Controller {
       jsonResponseMiddleware,
       this.list as RequestHandler
     )
+    this.router.get(
+      '/page',
+      // apiKeyMiddleware(),
+      jsonResponseMiddleware,
+      this.page as RequestHandler
+    )
     this.router.post(
       '/import',
       // apiKeyMiddleware(),
@@ -80,7 +87,26 @@ export default class TiktokController implements Controller {
   ): void {
     const after = request.query.after
     tiktokService
-      .paginateTweetByOrder(after)
+      .paginateTiktokByOrder(after)
+      .then((tiktok) => {
+        response.jsonSuccess(tiktok)
+      })
+      .catch((e) => {
+        response.status(500).jsonError('Server Error', 1010)
+      })
+  }
+
+  private page(
+    request: Request<any, any, any, { page?: string }>,
+    response: JsonResponse<PaginationData<TiktokData>>,
+    next: NextFunction
+  ): void {
+    let p = parseInt(request.query.page ?? '1')
+    if (isNaN(p)) {
+      p = 1
+    }
+    tiktokService
+      .pageTiktok({ page: p })
       .then((tiktok) => {
         response.jsonSuccess(tiktok)
       })

+ 1 - 1
src/db/index.ts

@@ -10,7 +10,7 @@ const sequelize = new Sequelize({
   host: DB_HOST,
   port: DB_PORT ? Number.parseInt(DB_PORT) : 3306,
   database: DB_NAME,
-  dialect: 'mysql',
+  dialect: 'mariadb',
   username: DB_USER,
   password: DB_PASS,
   dialectOptions: {

+ 15 - 1
src/db/models/Score.ts

@@ -10,6 +10,7 @@ import crypto from 'crypto'
 
 export interface ScoreData {
   id: number
+  address: string
   signature: string
   query: string
   scoreId: string
@@ -25,15 +26,27 @@ const scoreReg = /((-?\d+(.\d+)?)|(-?[∞π]))🍌/
   modelName: 'score',
   indexes: [
     {
-      fields: ['scoreId'],
+      fields: ['signature'],
       unique: true,
     },
+    {
+      fields: ['address'],
+    },
+    {
+      fields: ['scoreId'],
+    },
     {
       fields: ['messageId'],
     },
   ],
 })
 export default class Score extends Model {
+  @AllowNull(false)
+  @Column(DataType.CHAR(255))
+  get address(): string {
+    return this.getDataValue('address')
+  }
+
   @AllowNull(false)
   @Column(DataType.CHAR(255))
   get signature(): string {
@@ -93,6 +106,7 @@ export default class Score extends Model {
   getData(): ScoreData {
     return {
       id: this.id,
+      address: this.address,
       signature: this.signature,
       query: this.query,
       scoreId: this.scoreId,

+ 1 - 1
src/db/models/Tiktok.ts

@@ -68,7 +68,7 @@ export default class Tiktok extends Model {
   @Default(0)
   @Column(DataType.INTEGER.UNSIGNED)
   get type(): number {
-    return this.getDataValue('order')
+    return this.getDataValue('type')
   }
 
   @AllowNull(false)

+ 108 - 0
src/services/chainService/index.ts

@@ -0,0 +1,108 @@
+import {
+  JsonRpcProvider,
+  Interface,
+  FunctionFragment,
+  TransactionResponse,
+} from 'ethers6'
+import EVMHelper from '../../utils/EVMHelper'
+import { SupportedEVMHelperChains } from '../../utils/EVMHelper/types'
+import { BANANA_ADDRESS } from '../../constants'
+import Erc20ABI from '../../utils/EVMHelper/contracts/erc20ABI.json'
+import Decimal from 'decimal.js-light'
+import { tokenRealAmount } from '../../utils'
+
+const BLACKHOLE_ADDRESS = '0x000000000000000000000000000000000000dEaD'
+
+const evmHelper = new EVMHelper(SupportedEVMHelperChains.bsc)
+
+async function getRpc(): Promise<JsonRpcProvider> {
+  const provider = await evmHelper.getRpcProvider(process.env.BSC_RPC)
+  return provider
+}
+
+const interfaceBanana = new Interface(Erc20ABI)
+const transferFragment = interfaceBanana.fragments.find(
+  (f) => f.type === 'function' && (f as FunctionFragment).name === 'transfer'
+) as FunctionFragment
+
+interface CheckTxIsSendBananaToBlackholeResult {
+  tx: TransactionResponse | null
+  success: boolean
+  reason: number
+}
+
+async function checkTxIsSendBananaToBlackhole(
+  address: string,
+  txHash: string,
+  blackholeAddress = BLACKHOLE_ADDRESS
+): Promise<CheckTxIsSendBananaToBlackholeResult> {
+  const provider = await getRpc()
+  const tx = await provider.getTransaction(txHash)
+  if (!transferFragment) {
+    return {
+      tx: null,
+      success: false,
+      reason: -1,
+    }
+  }
+  if (!tx) {
+    return {
+      tx: null,
+      success: false,
+      reason: -2,
+    }
+  }
+  const { from, to, data } = tx
+  if (from?.toLowerCase() !== address.toLowerCase()) {
+    return {
+      tx,
+      success: false,
+      reason: -3,
+    }
+  }
+  if (to?.toLowerCase() !== BANANA_ADDRESS.toLowerCase()) {
+    return {
+      tx,
+      success: false,
+      reason: -3,
+    }
+  }
+  try {
+    const decodeData = interfaceBanana.decodeFunctionData(
+      transferFragment,
+      data
+    )
+    const [recipient, amount] = decodeData
+    if (recipient.toLowerCase() !== blackholeAddress.toLowerCase()) {
+      return {
+        tx,
+        success: false,
+        reason: -4,
+      }
+    }
+    if (new Decimal(amount.toString()).lt(tokenRealAmount('30.98'))) {
+      return {
+        tx,
+        success: false,
+        reason: -5,
+      }
+    }
+    return {
+      tx,
+      success: true,
+      reason: 1,
+    }
+  } catch (error) {
+    return {
+      tx,
+      success: false,
+      reason: -99,
+    }
+  }
+}
+
+const chainService = {
+  checkTxIsSendBananaToBlackhole,
+}
+
+export default chainService

+ 1 - 0
src/services/difyService/index.ts

@@ -61,6 +61,7 @@ export interface CompletionMessagesPayload {
   query: string
   onUpdate: (chunk: string) => void
   signature: string
+  address: string
 }
 
 interface DifyResult {

+ 58 - 13
src/services/scoreService/index.ts

@@ -1,6 +1,7 @@
 import { Score } from '../../db/models'
 import { ScoreData } from '../../db/models/Score'
 import { sleep } from '../../utils'
+import chainService from '../chainService'
 import difyService, {
   CompletionMessagesPayload,
   DifyRateLimitExceedError,
@@ -14,7 +15,43 @@ import {
 async function getCompletionMessages(
   payload: CompletionMessagesPayload
 ): Promise<Score> {
-  const { query, onUpdate, signature } = payload
+  const { query, onUpdate, signature, address } = payload
+
+  const signatureRowExist = await Score.findOne({ where: { signature } })
+
+  if (signatureRowExist) {
+    const { messageId } = signatureRowExist
+
+    const words = signatureRowExist.answer.split(' ').map((word) => ` ${word}`)
+    if (words[0]) {
+      words[0] = words[0].trim()
+    }
+
+    for (const word of words) {
+      const data = { event: 'message', messageId, signature, answer: word }
+
+      const message = JSON.stringify(data)
+
+      onUpdate(`data: ${message}\n\n`)
+      await sleep(20)
+    }
+
+    const data = { event: 'message_end', messageId, signature }
+
+    const message = JSON.stringify(data)
+
+    onUpdate(`data: ${message}\n\n`)
+
+    return signatureRowExist
+  }
+
+  const checkResult = await chainService.checkTxIsSendBananaToBlackhole(
+    address,
+    signature
+  )
+  if (!checkResult.success) {
+    throw new Error('Invalid transaction signature')
+  }
   const scoreId = Score.getScoreId(query)
   const exist = await Score.findOne({ where: { scoreId } })
 
@@ -41,7 +78,17 @@ async function getCompletionMessages(
 
     onUpdate(`data: ${message}\n\n`)
 
-    return exist
+    const row = await Score.create({
+      address,
+      signature,
+      scoreId: exist.scoreId,
+      query: exist.query,
+      answer: exist.answer,
+      messageId,
+      score: exist.score,
+      scoreText: exist.scoreText,
+    })
+    return row
   }
 
   try {
@@ -49,17 +96,15 @@ async function getCompletionMessages(
       await difyService.getCompletionMessagesByFetchAPI(payload)
 
     const [scoreText, score] = Score.getScore(answer)
-    const [row] = await Score.findOrCreate({
-      where: { scoreId },
-      defaults: {
-        signature,
-        scoreId,
-        query,
-        answer,
-        messageId,
-        score,
-        scoreText,
-      },
+    const row = await Score.create({
+      address,
+      signature,
+      scoreId,
+      query,
+      answer,
+      messageId,
+      score,
+      scoreText,
     })
     return row
   } catch (e) {

+ 35 - 10
src/services/tiktokService/index.ts

@@ -5,13 +5,14 @@ import {
   PaginateOptions,
   PaginationConnection,
 } from 'sequelize-cursor-pagination'
-import { TweetData } from '../../db/models/Tweet'
 import { TiktokData } from '../../db/models/Tiktok'
+import { PaginationData, PaginationOptions } from '../types'
 
 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 tiktokUrlReg =
+  /^https:\/\/www\.tiktok\.com\/@[a-zA-Z0-9_\-.]+\/video\/([0-9]+)$/
 
 const removeScriptReg = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
 
@@ -134,9 +135,9 @@ let paginate: (
   queryOptions: PaginateOptions<Tiktok>
 ) => Promise<PaginationConnection<Tiktok>>
 
-async function paginateTweetByOrder(
+async function paginateTiktokByOrder(
   after?: string,
-  limit = 24
+  limit = 20
 ): Promise<PaginationConnection<TiktokData>> {
   if (!paginate) {
     paginate = makePaginate(Tiktok)
@@ -165,6 +166,29 @@ async function paginateTweetByOrder(
   return ret
 }
 
+async function pageTiktok(
+  option: Partial<PaginationOptions> = {}
+): Promise<PaginationData<TiktokData>> {
+  const opt = { page: 1, count: 20, ...option }
+  const result = await Tiktok.findAndCountAll({
+    order: [
+      ['order', 'DESC'],
+      ['id', 'DESC'],
+    ],
+    limit: opt.count,
+    offset: (opt.page - 1) * opt.count,
+    where: {
+      disabled: false,
+    },
+  })
+
+  return {
+    data: result.rows.map((row) => row.getData()),
+    total: result.count,
+    ...opt,
+  }
+}
+
 export interface GenerateTiktokResult {
   url: string
   success: boolean
@@ -235,11 +259,11 @@ export interface ShotData {
 async function importShot(data: ShotData): Promise<Tiktok> {
   const { shot: shotRaw, url: urlRaw } = data
 
-  try {
-    return await generateTweetByPublishAPI(urlRaw)
-  } catch (error) {
-    // if error, use shot
-  }
+  // try {
+  //   return await generateTweetByPublishAPI(urlRaw)
+  // } catch (error) {
+  //   // if error, use shot
+  // }
 
   const shot = shotRaw.trim()
 
@@ -310,7 +334,8 @@ async function bulkImportShot(
 const tiktokService = {
   STATE,
   generateTweetByPublishAPI,
-  paginateTweetByOrder,
+  paginateTiktokByOrder,
+  pageTiktok,
   bulkGenerateTiktokByPublishAPI,
   makeTweetTop,
   makeTweetDisabled,

+ 16 - 0
src/services/types.ts

@@ -0,0 +1,16 @@
+export interface PaginationOptions {
+  page: number
+  count: number
+}
+
+export interface PaginationData<T> extends PaginationOptions {
+  data: T[]
+  total: number
+}
+
+export const emptyPaginationData: PaginationData<any> = {
+  data: [],
+  total: 0,
+  page: 1,
+  count: 20,
+}

+ 7 - 1
src/utils/EVMHelper/EVMHelper.ts

@@ -5,7 +5,13 @@ import {
   SupportedEVMHelperChains,
 } from './types'
 import { RPC } from './dbModels'
-import { ethers, JsonRpcProvider, Provider, TransactionReceipt, Wallet } from 'ethers6'
+import {
+  ethers,
+  JsonRpcProvider,
+  Provider,
+  TransactionReceipt,
+  Wallet,
+} from 'ethers6'
 import axios from 'axios'
 
 function isPrivateKey(pk: string): boolean {