Alex Xu 8 tháng trước cách đây
mục cha
commit
9328a47625

+ 77 - 0
src/controllers/artist/index.ts

@@ -0,0 +1,77 @@
+import { Request, RequestHandler, NextFunction, Router } from 'express'
+import { Controller } from '../types'
+import NotFoundException from '../../exceptions/NotFoundException'
+import jsonResponseMiddleware, {
+  JsonResponse,
+} from '../../middleware/jsonResponse.middleware'
+import { PaginationConnection } from 'sequelize-cursor-pagination'
+import { MODIFY_TOKEN } from '../../constants'
+import artistService, { GenerateArtResult } from '../../services/artistService'
+import { ArtData } from '../../db/models/Art'
+// import apiKeyMiddleware from '../../middleware/apikey.middleware'
+
+interface ImportPayload {
+  token: string
+  urls: string[]
+}
+
+export default class ArtistController implements Controller {
+  public path = '/api/v1/artist'
+  public router = Router()
+
+  constructor() {
+    this.initializeRoutes()
+  }
+
+  private initializeRoutes(): void {
+    this.router.get(
+      '/list',
+      // apiKeyMiddleware(),
+      jsonResponseMiddleware,
+      this.list as RequestHandler
+    )
+    this.router.post(
+      '/import',
+      // apiKeyMiddleware(),
+      jsonResponseMiddleware,
+      this.import as RequestHandler
+    )
+  }
+
+  private list(
+    request: Request<any, any, any, { after?: string }>,
+    response: JsonResponse<PaginationConnection<ArtData>>,
+    next: NextFunction
+  ): void {
+    const after = request.query.after
+    artistService
+      .paginateArtByOrder(after)
+      .then((art) => {
+        response.jsonSuccess(art)
+      })
+      .catch((e) => {
+        response.status(500).jsonError('Server Error', 1010)
+      })
+  }
+
+  private import(
+    request: Request<any, any, ImportPayload>,
+    response: JsonResponse<GenerateArtResult[]>,
+    next: NextFunction
+  ): void {
+    const { urls, token } = request.body
+    if (token !== MODIFY_TOKEN) {
+      response.status(401).jsonError('Unauthorized', 1012)
+      return
+    }
+
+    artistService
+      .bulkImportShot(urls)
+      .then((tweets) => {
+        response.jsonSuccess(tweets)
+      })
+      .catch((e) => {
+        response.status(500).jsonError('Server Error', 1011)
+      })
+  }
+}

+ 8 - 1
src/controllers/index.ts

@@ -2,6 +2,13 @@ import PingpongController from './pingpong'
 import XController from './x'
 import TweetController from './tweet'
 import ScoreController from './score'
+import ArtistController from './artist'
 export * from './types'
 
-export { PingpongController, XController, TweetController, ScoreController }
+export {
+  PingpongController,
+  XController,
+  TweetController,
+  ScoreController,
+  ArtistController,
+}

+ 3 - 3
src/controllers/tweet/index.ts

@@ -5,7 +5,7 @@ import jsonResponseMiddleware, {
   JsonResponse,
 } from '../../middleware/jsonResponse.middleware'
 import twitterService, {
-  GenerateResult,
+  GenerateTweetResult,
   ShotData,
 } from '../../services/twitterService'
 import { PaginationConnection } from 'sequelize-cursor-pagination'
@@ -93,7 +93,7 @@ export default class TweetController implements Controller {
 
   private import(
     request: Request<any, any, ImportPayload>,
-    response: JsonResponse<GenerateResult[]>,
+    response: JsonResponse<GenerateTweetResult[]>,
     next: NextFunction
   ): void {
     const { urls, token } = request.body
@@ -114,7 +114,7 @@ export default class TweetController implements Controller {
 
   private importShot(
     request: Request<any, any, ImportShotPayload>,
-    response: JsonResponse<GenerateResult[]>,
+    response: JsonResponse<GenerateTweetResult[]>,
     next: NextFunction
   ): void {
     const { shots, token } = request.body

+ 2 - 2
src/db/index.ts

@@ -4,7 +4,7 @@ import { RPC } from '../utils/EVMHelper/dbModels'
 import { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS } from '../constants'
 // import { promisify } from 'util'
 
-const { Constant, Application, Tweet, Score } = Models
+const { Constant, Application, Tweet, Score, Art } = Models
 
 const sequelize = new Sequelize({
   host: DB_HOST,
@@ -21,7 +21,7 @@ const sequelize = new Sequelize({
   pool: {
     max: 25,
   },
-  models: [Constant, RPC, Application, Tweet, Score],
+  models: [Constant, RPC, Application, Tweet, Score, Art],
 })
 
 export default sequelize

+ 52 - 0
src/db/models/Art.ts

@@ -0,0 +1,52 @@
+import {
+  Table,
+  Column,
+  AllowNull,
+  DataType,
+  Model,
+  Default,
+} from 'sequelize-typescript'
+import crypto from 'crypto'
+
+export interface ArtData {
+  id: number
+  url: string
+  urlId: string
+}
+
+@Table({
+  modelName: 'art',
+  indexes: [
+    {
+      fields: ['urlId'],
+      unique: true,
+    },
+  ],
+})
+export default class Art extends Model {
+  @AllowNull(false)
+  @Column(DataType.CHAR(64))
+  get urlId(): string {
+    return this.getDataValue('scoreId')
+  }
+
+  static getUrlId(url: string): string {
+    const cleanurl = url.trim().toLowerCase().replaceAll('\n', '')
+    const hash = crypto.createHash('md5').update(cleanurl).digest('hex')
+    return hash
+  }
+
+  @AllowNull(false)
+  @Column(DataType.TEXT)
+  get url(): string {
+    return this.getDataValue('url')
+  }
+
+  getData(): ArtData {
+    return {
+      id: this.id,
+      url: this.url,
+      urlId: this.urlId,
+    }
+  }
+}

+ 2 - 1
src/db/models/index.ts

@@ -2,5 +2,6 @@ import Constant from './Constant'
 import Application from './Application'
 import Tweet from './Tweet'
 import Score from './Score'
+import Art from './Art'
 
-export { Constant, Application, Tweet, Score }
+export { Constant, Application, Tweet, Score, Art }

+ 102 - 0
src/services/artistService/index.ts

@@ -0,0 +1,102 @@
+import { Art } from '../../db/models'
+import {
+  makePaginate,
+  PaginateOptions,
+  PaginationConnection,
+} from 'sequelize-cursor-pagination'
+import { ArtData } from '../../db/models/Art'
+
+let paginate: (
+  this: unknown,
+  queryOptions: PaginateOptions<Art>
+) => Promise<PaginationConnection<Art>>
+
+async function paginateArtByOrder(
+  after?: string,
+  limit = 10
+): Promise<PaginationConnection<ArtData>> {
+  if (!paginate) {
+    paginate = makePaginate(Art)
+  }
+
+  const secondResult = await paginate({
+    order: [['id', 'DESC']],
+    limit,
+    after,
+  })
+
+  const ret: PaginationConnection<ArtData> = {
+    ...secondResult,
+    edges: secondResult.edges.map((edge) => ({
+      cursor: edge.node.id,
+      node: edge.node.getData(),
+    })),
+  }
+
+  return ret
+}
+
+export interface GenerateArtResult {
+  url: string
+  success: boolean
+  error?: string
+}
+
+async function importShot(urlRaw: string): Promise<Art> {
+  const url = urlRaw.trim()
+
+  if (!url?.startsWith('https://')) {
+    throw new Error('Invalid shot image URL')
+  }
+
+  try {
+    const urlId = Art.getUrlId(url)
+
+    const [art, created] = await Art.findOrCreate({
+      where: {
+        urlId,
+      },
+      defaults: {
+        urlId,
+        url,
+      },
+    })
+
+    if (!created) {
+      art.setDataValue('url', url)
+      await art.save()
+    }
+
+    return art
+  } catch (error: any) {
+    if (error.response?.data?.error) {
+      throw new Error(error.response.data.error)
+    }
+    throw error
+  }
+}
+
+async function bulkImportShot(urls: string[]): Promise<GenerateArtResult[]> {
+  const results: GenerateArtResult[] = []
+  for (const url of urls) {
+    try {
+      const art = await importShot(url)
+      results.push({ url, success: true })
+    } catch (e) {
+      results.push({
+        url,
+        success: false,
+        error: (e as Error).message ?? 'unknown error',
+      })
+    }
+  }
+  return results
+}
+
+const artistService = {
+  importShot,
+  bulkImportShot,
+  paginateArtByOrder,
+}
+
+export default artistService

+ 5 - 5
src/services/twitterService/index.ts

@@ -115,7 +115,7 @@ async function paginateTweetByOrder(
   return ret
 }
 
-export interface GenerateResult {
+export interface GenerateTweetResult {
   url: string
   success: boolean
   error?: string
@@ -123,8 +123,8 @@ export interface GenerateResult {
 
 async function bulkGenerateTweetsByPublishAPI(
   urls: string[]
-): Promise<GenerateResult[]> {
-  const results: GenerateResult[] = []
+): Promise<GenerateTweetResult[]> {
+  const results: GenerateTweetResult[] = []
   for (const url of urls) {
     try {
       const tweet = await genenrateTweetByPublishAPI(url)
@@ -226,8 +226,8 @@ async function importShot(data: ShotData): Promise<Tweet> {
   }
 }
 
-async function bulkImportShot(datas: ShotData[]): Promise<GenerateResult[]> {
-  const results: GenerateResult[] = []
+async function bulkImportShot(datas: ShotData[]): Promise<GenerateTweetResult[]> {
+  const results: GenerateTweetResult[] = []
   for (const data of datas) {
     try {
       const tweet = await importShot(data)

+ 2 - 0
src/start.ts

@@ -5,6 +5,7 @@ import {
   XController,
   TweetController,
   ScoreController,
+  ArtistController,
 } from './controllers'
 import { PORT, TG_BOT_TOKEN } from './constants'
 
@@ -17,6 +18,7 @@ const app = new APP({
     new XController(),
     new TweetController(),
     new ScoreController(),
+    new ArtistController(),
   ],
   tgBotToken: TG_BOT_TOKEN,
 })