initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { Controller, Post, HttpCode } from '@nestjs/common';
|
||||
import { CrawlerService } from './crawler.service';
|
||||
|
||||
@Controller('api/crawler')
|
||||
export class CrawlerController {
|
||||
constructor(private readonly crawlerService: CrawlerService) {}
|
||||
|
||||
@Post('trigger')
|
||||
@HttpCode(200)
|
||||
async triggerCrawl() {
|
||||
return this.crawlerService.crawlWiki();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CrawlerController } from './crawler.controller';
|
||||
import { CrawlerService } from './crawler.service';
|
||||
import { WosCoupon } from '../entities/wos-coupon.entity';
|
||||
import { WosModule } from '../wos/wos.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([WosCoupon]), WosModule],
|
||||
controllers: [CrawlerController],
|
||||
providers: [CrawlerService],
|
||||
})
|
||||
export class CrawlerModule {}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import axios from 'axios';
|
||||
import { WosCoupon } from '../entities/wos-coupon.entity';
|
||||
import { WosService } from '../wos/wos.service';
|
||||
|
||||
const WIKI_URL = 'https://www.whiteoutsurvival.wiki/giftcodes/';
|
||||
|
||||
@Injectable()
|
||||
export class CrawlerService {
|
||||
private readonly logger = new Logger(CrawlerService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WosCoupon)
|
||||
private wosCouponRepo: Repository<WosCoupon>,
|
||||
private readonly wosService: WosService,
|
||||
) {}
|
||||
|
||||
private extractCodesFromHtml(html: string): string[] {
|
||||
const found: string[] = [];
|
||||
const pattern = /<span[^>]*class="code"[^>]*>([^<]+)<\/span>/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(html)) !== null) {
|
||||
const code = match[1].trim();
|
||||
if (code.length > 0) found.push(code);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
async crawlWiki(): Promise<{ found: number; added: string[] }> {
|
||||
this.logger.log('Wiki 쿠폰 크롤링 시작...');
|
||||
|
||||
let html: string;
|
||||
try {
|
||||
const resp = await axios.get(WIKI_URL, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': 'text/html',
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
html = resp.data as string;
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Wiki 요청 실패: ${err.message}`);
|
||||
return { found: 0, added: [] };
|
||||
}
|
||||
|
||||
const wikiCodes = this.extractCodesFromHtml(html);
|
||||
this.logger.log(`Wiki에서 ${wikiCodes.length}개 코드 발견`);
|
||||
|
||||
if (wikiCodes.length === 0) return { found: 0, added: [] };
|
||||
|
||||
const existingCoupons = await this.wosCouponRepo.find();
|
||||
const existingCodes = new Set(existingCoupons.map((c) => c.code));
|
||||
|
||||
const addedCodes: string[] = [];
|
||||
|
||||
for (const code of wikiCodes) {
|
||||
if (existingCodes.has(code)) continue;
|
||||
|
||||
this.logger.log(`새 쿠폰 발견: ${code}`);
|
||||
try {
|
||||
await this.wosService.addCoupon(`Wiki: ${code}`, code);
|
||||
existingCodes.add(code);
|
||||
addedCodes.push(code);
|
||||
this.logger.log(`쿠폰 추가 완료: ${code}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`쿠폰 추가 실패 [${code}]: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCodes.length > 0) {
|
||||
await this.sendDiscordNotification(addedCodes);
|
||||
}
|
||||
|
||||
this.logger.log(`Wiki 크롤링 완료 - 신규 쿠폰: ${addedCodes.length}개`);
|
||||
return { found: wikiCodes.length, added: addedCodes };
|
||||
}
|
||||
|
||||
private async sendDiscordNotification(codes: string[]): Promise<void> {
|
||||
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
|
||||
if (!webhookUrl) return;
|
||||
|
||||
const codeList = codes.map((c) => `\`${c}\``).join(', ');
|
||||
const payload = {
|
||||
username: 'WOS 쿠폰 봇',
|
||||
avatar_url: 'https://wos-giftcode.centurygame.com/favicon.ico',
|
||||
embeds: [
|
||||
{
|
||||
title: '🎁 새 WOS 쿠폰 발견!',
|
||||
description: `Wiki에서 새로운 쿠폰 **${codes.length}개**를 찾았어요!\n\n${codeList}`,
|
||||
color: 0x00b0f4,
|
||||
footer: { text: 'WOS 쿠폰 자동 크롤러 | whiteoutsurvival.wiki' },
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.post(webhookUrl, payload, { timeout: 5000 });
|
||||
this.logger.log(`Discord 알림 전송 완료 (${codes.length}개 쿠폰)`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Discord 알림 실패: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron('0 0 * * * *')
|
||||
async scheduledCrawl() {
|
||||
await this.crawlWiki();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user