import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import axios from 'axios'; import * as crypto from 'crypto'; import { WosUser } from '../entities/wos-user.entity'; import { CouponLog } from '../entities/coupon-log.entity'; const SECRET = 'tB87#kPtkxqOS2'; const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api'; const COMMON_HEADERS = { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Origin': 'https://wos-giftcode.centurygame.com', 'Referer': 'https://wos-giftcode.centurygame.com/', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8', }; const ERR_MESSAGES: Record = { 20000: '쿠폰 코드가 존재하지 않습니다.', 20001: '이미 사용된 쿠폰입니다.', 20002: '만료된 쿠폰입니다.', 20003: '캡차 인증에 실패했습니다.', 40001: '존재하지 않는 플레이어 ID입니다.', 40004: '캡차 입력이 올바르지 않습니다.', 40014: '캡차 세션이 만료되었습니다. 다시 시도해주세요.', }; @Injectable() export class WosService { private sessionStore: Map = new Map(); constructor( @InjectRepository(WosUser) private wosUserRepo: Repository, @InjectRepository(CouponLog) private couponLogRepo: Repository, ) {} private makeSign(fid: string, timestamp: number): string { const raw = `fid=${fid}&time=${timestamp}${SECRET}`; return crypto.createHash('md5').update(raw).digest('hex'); } async getPlayer(fid: string) { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/player`, new URLSearchParams({ fid, time: String(timestamp), sign }), { headers: { ...COMMON_HEADERS } }, ); const data = response.data; if (data.code !== 0) { throw new HttpException( ERR_MESSAGES[data.err_code] || `오류가 발생했습니다. (${data.err_code})`, HttpStatus.BAD_REQUEST, ); } const playerInfo = data.data; await this.wosUserRepo.upsert( { fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image }, ['fid'], ); return playerInfo; } private async acquireSession(fid: string): Promise { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/captcha`, new URLSearchParams({ fid, time: String(timestamp), sign }), { headers: { ...COMMON_HEADERS }, withCredentials: true }, ); const setCookie = response.headers['set-cookie']; let sessionValue = ''; if (setCookie) { const found = setCookie.find((c: string) => c.toLowerCase().includes('session')); if (found) sessionValue = found.split(';')[0]; } if (sessionValue) this.sessionStore.set(fid, sessionValue); return sessionValue; } private async redeemOne(fid: string, couponCode: string, captcha: string, session: string): Promise { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/gift_code`, new URLSearchParams({ fid, time: String(timestamp), sign, cdk: couponCode, validate: captcha }), { headers: { ...COMMON_HEADERS, Cookie: session } }, ); const data = response.data; if (data.code === 0) return '성공'; return ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`; } async redeemAuto(fid: string, codes: string[]) { const results: { code: string; status: string; message: string }[] = []; let session = await this.acquireSession(fid); for (const rawCode of codes) { const code = rawCode.trim(); if (!code) continue; let message = ''; let status = 'error'; try { message = await this.redeemOne(fid, code, '1', session); if (message.includes('캡차') || message.includes('세션')) { session = await this.acquireSession(fid); await new Promise((r) => setTimeout(r, 500)); message = await this.redeemOne(fid, code, '1', session); } status = message === '성공' ? 'success' : 'error'; } catch (err: any) { message = err.response?.data?.message || err.message || '오류 발생'; status = 'error'; } await this.couponLogRepo.save({ fid, coupon_code: code, result_msg: message }); results.push({ code, status, message }); await new Promise((r) => setTimeout(r, 1000)); } return results; } async getHistory(fid: string) { return this.couponLogRepo.find({ where: { fid }, order: { executed_at: 'DESC' }, take: 100, }); } }