# Sticker Lab

`시그널_스티커다운로더.py`(Colab 노트북)를 브라우저에서 그대로 돌아가는 웹 페이지로 옮긴 것입니다.
디자인은 [`design.md`](design.md)의 Discord 디자인 언어를 따릅니다.

| | 원본 파이썬 | 이 저장소 |
|---|---|---|
| Signal 스티커팩 | `sigstickers` CLI 호출 | 브라우저에서 직접 복호화 |
| 디시콘 | `requests` 세션 | 중계 서버 + `fetch` |
| 압축 | `shutil.make_archive` | 내장 ZIP 작성기 |
| 실행 환경 | Colab 런타임 | 정적 파일 몇 개 |

## 실행

정적 파일이라 아무 웹 서버에나 올리면 됩니다. GitHub Pages도 그대로 동작합니다.

```bash
python3 -m http.server 8000
# http://127.0.0.1:8000
```

`file://`로 직접 열어도 대부분 동작하지만, 일부 브라우저가 그 상태에서 Web Crypto를 막기 때문에
`http://localhost` 또는 `https://`로 접속하는 편이 안전합니다.

## Signal 스티커팩

중계 서버 없이 바로 됩니다. Signal CDN이 `Access-Control-Allow-Origin: *`을 주기 때문에
브라우저가 직접 붙을 수 있습니다.

1. Signal 앱에서 스티커팩 → 공유로 링크를 복사합니다.
   (`https://signal.art/addstickers/#pack_id=…&pack_key=…`)
2. 입력창에 붙여넣고 **내려받기**.
3. 미리보기를 확인하고 **ZIP 내려받기**.

페이지 주소 뒤에 공유 링크의 `#pack_id=…` 부분을 그대로 붙여도 입력창이 자동으로 채워집니다.

복호화는 Signal이 쓰는 방식 그대로이며 전부 Web Crypto로 처리됩니다.

```
derived   = HKDF-SHA256(ikm = pack_key, salt = 0x00 * 32, info = "Sticker Pack", len = 64)
aesKey    = derived[0:32]        hmacKey = derived[32:64]
파일 구조  = iv(16) ‖ ciphertext ‖ HMAC-SHA256(iv ‖ ciphertext)(32)
평문      = AES-256-CBC(ciphertext, aesKey, iv)
```

manifest는 protobuf(`Pack { title=1, author=2, cover=3, stickers=4 }`)라서
`assets/js/signal.js`에 최소 파서가 들어 있습니다. 받은 바이트는 재인코딩 없이
그대로 ZIP에 담기므로 애니메이션 WebP도 움직임을 잃지 않습니다.

## 디시콘

**중계 서버가 필요합니다.** 두 가지 이유가 겹칩니다.

- dcinside는 CORS 헤더를 주지 않아 브라우저가 응답 본문을 읽지 못합니다.
- `package_detail` API가 요구하는 `ci_t` 값은 `Set-Cookie` 헤더로 오는데,
  이 헤더는 교차 출처에서 자바스크립트가 볼 수 없도록 규격상 막혀 있습니다.

`proxy/`에 바로 쓸 수 있는 중계 서버가 두 개 들어 있습니다. 둘 다 허용 호스트 목록이 박혀 있어
공개 오픈 프록시로 쓰이지 않습니다.

### 로컬 실행

```bash
python3 proxy/server.py --serve-site
# site  : http://127.0.0.1:8787/
# relay : http://127.0.0.1:8787/relay
```

페이지의 **고급 설정 → 중계 서버 주소**에 `http://127.0.0.1:8787/relay`를 넣으면 됩니다.
표준 라이브러리만 씁니다.

### Cloudflare Workers

```bash
npm create cloudflare@latest sticker-relay -- --type hello-world
# src/index.js 를 proxy/worker.js 내용으로 교체
npx wrangler deploy
```

배포된 `https://….workers.dev` 주소를 중계 서버 칸에 넣습니다.

### 중계 규약

직접 만들어 쓰고 싶다면 이 계약만 지키면 됩니다.

```
POST <relay>
{ "url": "...", "method": "GET|POST", "headers": {...}, "body": "..." }

200 OK
{ "status": 200, "statusText": "OK",
  "headers": { "set-cookie": ["ci_c=…"], ... },   // 반복 헤더는 배열
  "bodyBase64": "..." }
```

이 칸이 채워져 있으면 Signal 요청도 중계 서버를 거칩니다. CDN이 막힌 망에서만 쓰고,
평소에는 비워두는 쪽이 빠릅니다.

## 구조

```
index.html              페이지
assets/css/style.css    design.md의 토큰을 CSS 변수로
assets/js/zip.js        ZIP 작성기 (STORE 방식, 의존성 없음)
assets/js/net.js        직접 fetch / 중계 서버 전환
assets/js/signal.js     protobuf 파서 + HKDF·HMAC·AES 복호화
assets/js/dccon.js      download_dccon() 이식
assets/js/app.js        UI 배선
proxy/server.py         중계 서버 (파이썬 표준 라이브러리)
proxy/worker.js         중계 서버 (Cloudflare Workers)
```

외부 자바스크립트 의존성은 없습니다. 네트워크에서 받아오는 것은 웹폰트 두 개뿐이고,
막혀 있어도 시스템 폰트로 떨어질 뿐 기능에는 영향이 없습니다.

## 원본 파이썬과 다른 점

- `sigstickers` 패키지 대신 복호화를 직접 구현했습니다. 설치가 필요 없고, 팩 키가
  브라우저 밖으로 나가지 않습니다.
- 디시콘 쪽 파일 이름 규칙(`001_11_이름.png`), `guess_ext()`, `clean_filename()`,
  `extract_package_idx()`는 파이썬 동작을 그대로 옮겼습니다.
- Signal 파일 이름은 `순번_스티커ID_이모지.webp` 형태이고, 커버는 `000_cover_…`로 들어갑니다.
- 실패한 항목은 전체를 중단시키지 않고 건너뛴 뒤 로그에 남깁니다.

## 주의

내려받은 스티커의 저작권은 각 제작자에게 있습니다. 개인 소장 용도로만 사용하세요.
