{"id":31663970,"url":"https://github.com/2skydev/toonation-donation-list-cron","last_synced_at":"2026-05-04T08:42:07.161Z","repository":{"id":314823508,"uuid":"1055198374","full_name":"2skydev/toonation-donation-list-cron","owner":"2skydev","description":"투네이션의 후원 목록을 크롤링 후 webhook 처리","archived":false,"fork":false,"pushed_at":"2025-09-15T02:45:05.000Z","size":12,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-15T03:18:42.187Z","etag":null,"topics":["deno","playwright","toonation","webhook"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/2skydev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-11T23:21:09.000Z","updated_at":"2025-09-15T02:45:09.000Z","dependencies_parsed_at":"2025-09-15T03:18:48.137Z","dependency_job_id":"98fce367-5e95-4909-8709-fead5e871dba","html_url":"https://github.com/2skydev/toonation-donation-list-cron","commit_stats":null,"previous_names":["2skydev/toonation-donation-list-cron"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/2skydev/toonation-donation-list-cron","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2skydev%2Ftoonation-donation-list-cron","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2skydev%2Ftoonation-donation-list-cron/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2skydev%2Ftoonation-donation-list-cron/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2skydev%2Ftoonation-donation-list-cron/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/2skydev","download_url":"https://codeload.github.com/2skydev/toonation-donation-list-cron/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2skydev%2Ftoonation-donation-list-cron/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278846318,"owners_count":26056090,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deno","playwright","toonation","webhook"],"created_at":"2025-10-07T20:52:41.644Z","updated_at":"2026-05-04T08:42:07.154Z","avatar_url":"https://github.com/2skydev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 투네이션 후원 목록 자동 조회 크론 (playwright 크롤링)\n\n투네이션 스트리머 계정의 후원 내역을 수집해 지정한 Webhook으로 전달하는 Deno 스크립트입니다. 크론 환경에서 주기적으로 실행하도록 설계되었습니다.\n\n\u003cimg width=\"1338\" height=\"800\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ee4df2ce-5a95-47c1-a615-43a963b1b972\" /\u003e\n\n## 주요 기능\n- **자동 로그인**: Playwright를 사용해 `toon.at` 스트리머 대시보드에 로그인합니다.\n- **후원 목록 수집**: 투네이션 내부 API(`dapi/streamer/donation_list`)를 호출하여 지정 기간(기본 2016-01-01부터 오늘까지)의 후원 내역을 페이지네이션하며 수집합니다.\n- **Webhook 전송**: 누적된 후원 아이템 배열을 Webhook URL로 POST 전송합니다.\n- **페이지 탐색 제어**: Webhook이 404 응답으로 `not-found-last-donation`을 반환하면 다음 페이지를 추가로 탐색합니다.\n\n## 동작 개요\n1. Playwright Chromium 컨텍스트를 한국어 로케일(`ko-KR`)로 생성합니다.\n2. 로그인 페이지에서 아이디/비밀번호를 입력해 대시보드로 이동합니다.\n3. 페이지 컨텍스트에서 `fetch(https://toon.at/dapi/streamer/donation_list?from\u0026to\u0026page)`로 데이터를 조회합니다.\n4. 응답 리스트를 아래 스키마로 변환합니다.\n5. Webhook으로 POST 요청을 전송합니다. 404 + `not-found-last-donation`이면 다음 페이지를 재귀적으로 조회합니다.\n\n## 요구 사항\n- Deno 설치\n- 네트워크로 `toon.at` 및 Webhook 도메인 접근 가능\n- 첫 실행 시 Playwright가 브라우저를 다운로드할 수 있는 환경\n\n## 설치 및 실행\n1) 저장소 클론 후 `.env` 파일을 준비합니다.\n\n```bash\ncp .env.example .env # 예시 파일이 없으면 아래 예시를 참고해 직접 생성\n```\n\n2) 스크립트 실행\n\n```bash\ndeno task start\n# 또는 파일 변경 감지 모드\ndeno task dev\n```\n\n`deno.json`에 설정된 태스크는 `.env`를 자동으로 로드합니다.\n\n## 환경 변수\n- **TOONATION_ID**: 투네이션 스트리머 계정 아이디\n- **TOONATION_PASSWORD**: 투네이션 스트리머 계정 비밀번호\n- **WEBHOOK_URL**: 후원 데이터 배열을 수신할 Webhook 엔드포인트 URL\n- **WEBHOOK_SECRET**: 웹훅 서명(HMAC-SHA256) 생성/검증에 사용할 비밀값\n\n`.env` 예시:\n\n```bash\nTOONATION_ID=your_toonation_id\nTOONATION_PASSWORD=your_toonation_password\nWEBHOOK_URL=https://your.service.example.com/toonation/webhook\nWEBHOOK_SECRET=your_webhook_secret\n```\n\n## Webhook\n- **HTTP 메서드**: `POST`\n- **헤더**:\n  - `Content-Type: application/json`\n  - `X-Signature-Timestamp: \u003cunix timestamp seconds\u003e`\n  - `X-Signature-Sha256: \u003chex hmac sha256\u003e`\n- **본문(payload)**: `ToonationDonationItem[]` 배열\n\n`ToonationDonationItem` 타입:\n\n```typescript\ninterface ToonationDonationItem {\n  account: string;\n  nickname: string;\n  amount: number;\n  message: string;\n  createdAt: string;\n}\n```\n\n예시 페이로드:\n\n```json\n[\n  {\n    \"account\": \"acc123\",\n    \"nickname\": \"닉네임\",\n    \"amount\": 5000,\n    \"message\": \"응원합니다!\",\n    \"createdAt\": \"2024-05-10T09:15:27.123Z\"\n  }\n]\n```\n\n응답 요구사항:\n- 정상 처리 시 2xx를 반환하십시오.\n- 추가 페이지 탐색이 필요하면 `404` 상태 코드와 본문 텍스트로 정확히 `not-found-last-donation`을 반환하십시오. 그러면 스크립트가 다음 페이지를 조회합니다.\n\n## Webhook 수신 서버 서명 검증 가이드\n수신 서버는 아래 순서대로 서명을 검증하면 됩니다.\n\n1. 요청의 raw body 문자열을 그대로 읽습니다.\n2. 헤더 `X-Signature-Timestamp`, `X-Signature-Sha256`가 모두 존재하는지 확인합니다.\n3. `X-Signature-Timestamp`를 정수로 파싱하고, 현재 시각과의 차이가 허용 범위(예: 300초) 이내인지 확인합니다.\n4. `expected = HMAC_SHA256_HEX(WEBHOOK_SECRET, timestamp + rawBody)`를 계산합니다.\n5. 수신한 `X-Signature-Sha256`와 constant-time 비교로 일치 여부를 확인합니다.\n6. 통과 후에만 raw body를 JSON으로 파싱해 비즈니스 로직을 수행합니다.\n\n### Node.js(Express) 예시\n```typescript\nimport crypto from 'node:crypto';\nimport express from 'express';\n\nconst app = express();\n\n// 반드시 raw body가 필요합니다.\napp.use('/toonation/webhook', express.text({ type: 'application/json' }));\n\nconst SKEW_SECONDS = 300;\nconst SECRET = process.env.WEBHOOK_SECRET!;\n\napp.post('/toonation/webhook', (req, res) =\u003e {\n  const rawBody = req.body as string;\n  const timestamp = req.header('X-Signature-Timestamp');\n  const signature = req.header('X-Signature-Sha256');\n\n  if (!timestamp || !signature) {\n    return res.status(401).send('missing-signature-headers');\n  }\n\n  const ts = Number.parseInt(timestamp, 10);\n  if (!Number.isFinite(ts)) {\n    return res.status(401).send('invalid-timestamp');\n  }\n\n  const nowSec = Math.floor(Date.now() / 1000);\n  if (Math.abs(nowSec - ts) \u003e SKEW_SECONDS) {\n    return res.status(401).send('timestamp-out-of-range');\n  }\n\n  const expectedHex = crypto\n    .createHmac('sha256', SECRET)\n    .update(`${ts}${rawBody}`, 'utf8')\n    .digest('hex');\n\n  const expected = Buffer.from(expectedHex, 'hex');\n  const received = Buffer.from(signature, 'hex');\n\n  if (\n    expected.length !== received.length ||\n    !crypto.timingSafeEqual(expected, received)\n  ) {\n    return res.status(401).send('invalid-signature');\n  }\n\n  const payload = JSON.parse(rawBody);\n  // TODO: payload 처리\n  return res.status(200).send('ok');\n});\n```\n\n참고:\n- JSON 파서를 먼저 붙이면 raw body가 바뀌어 서명 검증에 실패할 수 있습니다. 반드시 raw body 기준으로 검증하세요.\n- `WEBHOOK_SECRET`은 GitHub Actions의 비밀 변수와 수신 서버가 동일해야 합니다.\n\n## 스케줄 실행(GitHub Actions)\n이 프로젝트는 GitHub Actions 워크플로우로 스케줄 실행됩니다. 설정은 `.github/workflows/cron.yml` 에 정의되어 있습니다.\n\n- 스케줄:\n  - `0 0-14 * * *` (KST 09:00 ~ 23:00, 매 시 정각)\n  - `0 15-18 * * *` (KST 00:00 ~ 03:00, 매 시 정각)\n- 수동 실행: `workflow_dispatch` 지원\n\n필수 리포지토리 시크릿:\n- `TOONATION_ID`\n- `TOONATION_PASSWORD`\n- `WEBHOOK_URL`\n- `WEBHOOK_SECRET`\n\n워크플로우 요약:\n\n```yaml\nname: Cron\non:\n  schedule:\n    - cron: '0 0-14 * * *' # KST 09:00 ~ 23:00\n    - cron: '0 15-18 * * *' # KST 00:00 ~ 03:00\n  workflow_dispatch:\n\njobs:\n  cron:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Deno\n        uses: denoland/setup-deno@v2\n        with:\n          deno-version: v2.x\n      \n      - name: Install Playwright\n        run: deno run -A playwright install chromium --only-shell\n      \n      - name: Run main.ts\n        run: deno -A main.ts\n        env:\n          TOONATION_ID: ${{ secrets.TOONATION_ID }}\n          TOONATION_PASSWORD: ${{ secrets.TOONATION_PASSWORD }}\n          WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}\n          WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}\n```\n\n## 프로젝트 구조\n\n```text\n./\n  ├── main.ts          # 진입점: 로그인, 수집, Webhook 전송\n  ├── toonation.ts     # 목록 조회 로직(page.evaluate, from/to/page)\n  ├── utils.ts         # .NET ticks → ISO 문자열 변환 및 매핑\n  ├── types.ts         # 타입 정의(ToonationDonationItem 등)\n  ├── config.ts        # 환경 변수 로드 및 검증\n  ├── deno.json        # 태스크/포맷/린트/임포트 매핑\n  └── deno.lock        # 종속성 잠금 파일\n```\n\n## 포맷/린트\n- `deno fmt` 설정: `singleQuote: true`\n- `deno lint` 규칙 일부 비활성화: `no-explicit-any`\n\n## 트러블슈팅\n- **로그인 실패**: `TOONATION_ID`, `TOONATION_PASSWORD`를 확인하세요. 캡차/2단계 인증이 필요한 계정은 지원하지 않을 수 있습니다.\n- **브라우저 다운로드 실패**: 네트워크/권한 문제로 Playwright가 Chromium을 받지 못할 수 있습니다. 재시도하거나 네트워크 정책을 확인하세요.\n- **Webhook 401/403**: `X-Signature-*` 검증 실패일 수 있습니다. `WEBHOOK_SECRET`, raw body 사용 여부, 타임스탬프 허용 범위를 확인하세요.\n- **404 반환 후 스크립트 종료**: Webhook이 본문으로 `not-found-last-donation` 외 다른 문자열을 반환하면 스크립트가 에러로 간주합니다. 처리 로직을 점검하세요.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2skydev%2Ftoonation-donation-list-cron","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F2skydev%2Ftoonation-donation-list-cron","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2skydev%2Ftoonation-donation-list-cron/lists"}