mingle-website/bot/discord_cleanup.py
68893236+KINDNICK@users.noreply.github.com 8384d1a1d9 Add: Contact 문의 폼 + Discord 봇 연동 + PIPA 개인정보처리방침
- 문의 폼 프론트엔드 (이름, 이메일, 전화, 문의유형, 메시지, 개인정보동의)
- api-server.py에 /api/contact 엔드포인트 추가 (Discord Bot API 2채널)
- bot/discord_cleanup.py: 7일 후 자동 삭제 봇
- 개인정보처리방침 모달 (PIPA 10개 항목, 국외이전 명시)
- 민감정보 입력 경고 문구
- 개인정보 동의 체크 시에만 제출 버튼 활성화
- API 실패 시 대체 연락 안내 오버레이 (이메일/전화/Discord)
- 4개 언어 i18n 지원 (ko/en/zh/ja)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:12:43 +09:00

196 lines
6.5 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
밍글 스튜디오 Discord 문의 자동 삭제 봇
- 지정된 채널의 메시지를 7일 후 자동 삭제
- 개인정보보호법(PIPA) 준수를 위한 데이터 보관기간 관리
"""
import os
import sys
import json
import urllib.request
import ssl
import time
from datetime import datetime, timedelta, timezone
# ─── 설정 ───────────────────────────────────────
BOT_TOKEN = ''
CHANNEL_ID = ''
DELETE_AFTER_DAYS = 7
CHECK_INTERVAL_HOURS = 6 # 삭제 체크 주기 (시간)
def load_env():
"""Load .env file from parent directory"""
global BOT_TOKEN, CHANNEL_ID, DELETE_AFTER_DAYS, CHECK_INTERVAL_HOURS
env_paths = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '.env'),
os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env'),
]
for env_path in env_paths:
if os.path.exists(env_path):
with open(env_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key == 'DISCORD_BOT_TOKEN':
BOT_TOKEN = value
elif key == 'DISCORD_INQUIRY_CHANNEL_ID':
CHANNEL_ID = value
elif key == 'DELETE_AFTER_DAYS':
DELETE_AFTER_DAYS = int(value)
elif key == 'CHECK_INTERVAL_HOURS':
CHECK_INTERVAL_HOURS = int(value)
break
def discord_api(method, endpoint, data=None):
"""Discord API 호출"""
url = f'https://discord.com/api/v10{endpoint}'
headers = {
'Authorization': f'Bot {BOT_TOKEN}',
'Content-Type': 'application/json',
}
body = json.dumps(data).encode('utf-8') if data else None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
ctx = ssl.create_default_context()
try:
with urllib.request.urlopen(req, context=ctx, timeout=15) as resp:
if resp.status == 204:
return None
return json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else ''
print(f"[ERROR] Discord API {method} {endpoint}: {e.code} {error_body}")
# Rate limit 처리
if e.code == 429:
retry_data = json.loads(error_body) if error_body else {}
retry_after = retry_data.get('retry_after', 5)
print(f"[RATE LIMIT] Waiting {retry_after}s...")
time.sleep(retry_after + 0.5)
return discord_api(method, endpoint, data)
raise
def get_old_messages():
"""지정 채널에서 만료된 메시지 조회"""
cutoff = datetime.now(timezone.utc) - timedelta(days=DELETE_AFTER_DAYS)
old_messages = []
last_id = None
while True:
params = f'?limit=100'
if last_id:
params += f'&before={last_id}'
messages = discord_api('GET', f'/channels/{CHANNEL_ID}/messages{params}')
if not messages:
break
for msg in messages:
# Discord 타임스탬프 파싱
msg_time = datetime.fromisoformat(msg['timestamp'].replace('+00:00', '+00:00'))
if msg_time.tzinfo is None:
msg_time = msg_time.replace(tzinfo=timezone.utc)
if msg_time < cutoff:
old_messages.append({
'id': msg['id'],
'timestamp': msg['timestamp'],
'content': msg.get('content', '')[:50]
})
last_id = messages[-1]['id']
# 가장 오래된 메시지도 만료 기간 내라면 더 조회할 필요 없음
oldest_time = datetime.fromisoformat(messages[-1]['timestamp'].replace('+00:00', '+00:00'))
if oldest_time.tzinfo is None:
oldest_time = oldest_time.replace(tzinfo=timezone.utc)
if oldest_time >= cutoff:
continue
else:
break
time.sleep(0.5) # Rate limit 방지
return old_messages
def delete_messages(messages):
"""메시지 삭제"""
deleted = 0
for msg in messages:
try:
discord_api('DELETE', f'/channels/{CHANNEL_ID}/messages/{msg["id"]}')
print(f" [DELETE] {msg['id']} ({msg['timestamp'][:10]})")
deleted += 1
time.sleep(0.5) # Rate limit 방지
except Exception as e:
print(f" [ERROR] Failed to delete {msg['id']}: {e}")
return deleted
def run_cleanup():
"""한 번의 정리 사이클 실행"""
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"\n[{now}] Cleanup check started (delete after {DELETE_AFTER_DAYS} days)")
try:
messages = get_old_messages()
if messages:
print(f" Found {len(messages)} expired message(s)")
deleted = delete_messages(messages)
print(f" Deleted {deleted}/{len(messages)} message(s)")
else:
print(f" No expired messages found")
except Exception as e:
print(f" [ERROR] Cleanup failed: {e}")
def main():
"""메인 실행"""
load_env()
if not BOT_TOKEN:
print("Error: DISCORD_BOT_TOKEN not set in .env")
sys.exit(1)
if not CHANNEL_ID:
print("Error: DISCORD_CHANNEL_ID not set in .env")
sys.exit(1)
print("Mingle Studio Discord Cleanup Bot")
print("=" * 40)
print(f"Channel ID: {CHANNEL_ID}")
print(f"Delete after: {DELETE_AFTER_DAYS} days")
print(f"Check interval: {CHECK_INTERVAL_HOURS} hours")
print("=" * 40)
# 봇 정보 확인
try:
bot_info = discord_api('GET', '/users/@me')
print(f"Bot: {bot_info['username']}#{bot_info.get('discriminator', '0')}")
except Exception as e:
print(f"Error: Failed to connect to Discord: {e}")
sys.exit(1)
print("Bot started. Press Ctrl+C to stop.\n")
try:
while True:
run_cleanup()
print(f" Next check in {CHECK_INTERVAL_HOURS} hours...")
time.sleep(CHECK_INTERVAL_HOURS * 3600)
except KeyboardInterrupt:
print("\nBot stopped.")
if __name__ == '__main__':
main()