diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..04a763b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,60 @@ +# Copilot instructions for this repo (DiscordBot) + +This Python Discord bot is modular (Cogs) and Windows-oriented. Use these notes to be productive fast. + +## Architecture at a glance + +- Entry: `bot.py` + - Creates `DISCORD_CLIENT` (discord.py), loads every module in `cogs/` via `load_extension`. + - Global state on client: `USER_MESSAGES` (per-guild per-user message timeline), Spring AI flags (mode/style/convoIds), `SETTING_DATA` path, `PARTY_LIST`. + - Event flow in `on_message`: store message -> `check_youtube_link()` (YouTube summary prompt UI) -> `find1557()` (pattern/count + OCR) -> `spring_ai()` (if AI mode). + - Startup syncs slash commands globally: `DISCORD_CLIENT.tree.sync()`. +- Background loops: `cogs/loop.py` + - Presence updates, midnight reset + holidays/special-days notice, weekly 1557 report, optional YouTube live checker. + - Uses `settingData.json` and `special_days.json`. +- Features by Cog (examples): + - `cogs/summarize.py` and `util/get_recent_messages.py` format today’s chat for GPT. + - `cogs/translation.py` shows dropdown-then-edit flow with optional image input to GPT. + - `cogs/spring_ai.py` toggles Spring AI mode/style; responses handled by `func/spring_ai.py` (remote API, per-style convoId persistence). + - `cogs/YoutubeCheckerCog.py` toggles live-check loop by mutating `settingData.json`. +- External wrappers / utils: `api/chatGPT.py` (OpenAI Responses API), `api/riot.py`, `func/youtube_summary.py` (yt-dlp/FFmpeg/Google API, comments + transcript + Whisper fallback), `func/find1557.py` (pattern/OCR with GPT). + +## Run, env, and dependencies + +- Environment (.env) required: + - `DISCORD_TOKEN`, `MY_CHANNEL_ID`, `TEST_CHANNEL_ID`, `GUILD_ID` + - `OPENAI_KEY`, `GOOGLE_API_KEY`, `RIOT_KEY` +- Install deps (note: requirements.txt not present; use this file): `pip_install.txt`. +- Windows launch scripts: + - Dev: `._launchBot.ps1` or `._launchBot.bat` (activates `.venv`, runs `python bot.py`). + - Auto-update runner: `._scheduler.ps1` -> runs `_autoPullAndLaunch.py` (git stash+pull; restarts launch script if changes). +- Media tools: put `bin/ffmpeg.exe` to prefer local FFmpeg; optional `cookies.txt` improves yt-dlp reliability. + +## Patterns and conventions + +- Cogs auto-load if placed in `cogs/` and expose `async def setup(bot): await bot.add_cog(...)`. +- Slash commands use `discord.app_commands` (see `cogs/custom_help.py`, `cogs/translation.py`). Startup does a global sync. +- Message history is “today-only”: `load_recent_messages()` scans each text channel (limit=100) for today and normalizes to `USER_MESSAGES`. Daily reset occurs at midnight in `LoopTasks.new_day_clear`. +- OpenAI usage: call `api.chatGPT.custom_prompt_model(prompt={id, version, variables}, image_content=...)`. The code expects remote prompt IDs; follow the existing call sites to add variables. +- YouTube summary flow (`func/youtube_summary.py`): + +1. Button prompt on link -> 2) try transcript (ko→en→auto) with yt-dlp → 3) fallback MP3+Whisper → 4) GPT summary → 5) append comments summary via YouTube Data API. + Live/Upcoming videos are skipped. + +## Extending the bot + +- New feature = new Cog in `cogs/`. + - For scheduled jobs, use `discord.ext.tasks` like `LoopTasks`; guard with config flags in `settingData.json` if user-togglable. + - To use chat context, import `util.get_recent_messages` or read `DISCORD_CLIENT.USER_MESSAGES[guild_id]`. +- External APIs: + - Riot: use `api.riot.get_rank_data(game_name, tag_line, rank_type)`. + - Spring AI: `func.spring_ai.spring_ai(DISCORD_CLIENT, message)` is auto-called in `on_message` when AI mode is on; just manage toggles in your Cog. + +## Gotchas + +- Don’t add requirements to README; install from `pip_install.txt` (current source of truth). +- Slash command not visible? Wait for global sync or restart bot. +- YouTube/Whisper are CPU/network heavy; tests under `test/` are ad-hoc scripts, not pytest suites. +- Summaries/translation operate on today’s messages only unless you expand `load_recent_messages()`. + +If any of these are unclear (e.g., prompt IDs for GPT, expected variables, or how to add a new loop with config), tell me what you’re building and I’ll refine these rules. diff --git a/.gitignore b/.gitignore index bfee25c..0ef6b66 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ mp3_to_text/voice.txt 1557Counter.json bin/ffmpeg.exe 1557Counter.json +gambling_balance.json diff --git a/README.md b/README.md index ae9b79f..1c11397 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,13 @@ AI, 음악, 게임, 유틸리티 기능을 통합하여 서버 운영과 사용 - `/솔랭`, `/자랭`, `/일일랭크`, `/일일랭크변경` - **1557 챌린지** - 채팅/이미지에서 "1557" 조합 감지 → 자동 카운트 및 주간 통계 보고 +- **도박 시스템** + - `/돈줘` → 매일 1회 10,000원 지급 + - `/잔액` → 보유 금액 확인 + - `/송금 [유저] [금액]` → 다른 유저에게 송금 + - `/가위바위보 [선택] [배팅금액]` → 승리 시 2배, 무승부 시 절반, 패배 시 전액 잃음 + - `/도박 [배팅금액]` → 30%~70% 랜덤 확률, 당첨 시 2배 + - `/즉석복권` → 300원으로 복권 구매 (만원 1%, 삼천원 1.7%, 천원 5.6%, 삼백원 11.7%) --- diff --git a/cogs/custom_help.py b/cogs/custom_help.py index 0b8485c..86c1d60 100644 --- a/cogs/custom_help.py +++ b/cogs/custom_help.py @@ -103,6 +103,14 @@ async def custom_help(self, interaction: discord.Interaction): ("`/일시정지`", "재생 중인 음악을 일시정지합니다."), ("`/다시재생`", "일시정지된 음악을 다시 재생합니다."), ], + "도박": [ + ("`/돈줘`", "매일 1번 10,000원을 받을 수 있습니다."), + ("`/잔액`", "보유한 돈을 확인합니다."), + ("`/송금 [유저] [금액]`", "다른 사용자에게 돈을 송금합니다."), + ("`/가위바위보 [선택] [금액]`", "가위바위보 배팅 (승: 2배, 무: 절반, 패: 전액 잃음)"), + ("`/도박 [금액]`", "30~70% 확률의 도박 (당첨: 2배, 실패: 전액 잃음)"), + ("`/즉석복권`", "즉석복권 구매 (300원, 최대 만원 당첨)"), + ], } # 2) 첫 번째 임베드 diff --git a/cogs/gambling.py b/cogs/gambling.py new file mode 100644 index 0000000..816ef0b --- /dev/null +++ b/cogs/gambling.py @@ -0,0 +1,407 @@ +import json +import os +import random +from datetime import datetime, timezone, timedelta + +import discord +from discord import app_commands +from discord.ext import commands + +# 서울 시간대 설정 (UTC+9) +SEOUL_TZ = timezone(timedelta(hours=9)) +BALANCE_FILE = "gambling_balance.json" + + +class GamblingCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + print("Gambling Cog : init 로드 완료!") + + @commands.Cog.listener() + async def on_ready(self): + """봇이 준비되었을 때 호출됩니다.""" + print("DISCORD_CLIENT -> Gambling Cog : on ready!") + + def load_balance_data(self): + """길드별 유저 잔액 데이터 로드""" + if not os.path.isfile(BALANCE_FILE): + return {} + with open(BALANCE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + + def save_balance_data(self, data): + """길드별 유저 잔액 데이터 저장""" + with open(BALANCE_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_user_balance(self, guild_id: str, user_id: str) -> int: + """특정 유저의 잔액 조회""" + data = self.load_balance_data() + return data.get(guild_id, {}).get(user_id, {}).get("balance", 0) + + def set_user_balance(self, guild_id: str, user_id: str, amount: int): + """특정 유저의 잔액 설정""" + data = self.load_balance_data() + if guild_id not in data: + data[guild_id] = {} + if user_id not in data[guild_id]: + data[guild_id][user_id] = {"balance": 0, "last_daily": None} + data[guild_id][user_id]["balance"] = amount + self.save_balance_data(data) + + def get_last_daily(self, guild_id: str, user_id: str) -> str: + """마지막 /돈줘 사용 일자 조회""" + data = self.load_balance_data() + return data.get(guild_id, {}).get(user_id, {}).get("last_daily") + + def set_last_daily(self, guild_id: str, user_id: str, date_str: str): + """마지막 /돈줘 사용 일자 설정""" + data = self.load_balance_data() + if guild_id not in data: + data[guild_id] = {} + if user_id not in data[guild_id]: + data[guild_id][user_id] = {"balance": 0, "last_daily": None} + data[guild_id][user_id]["last_daily"] = date_str + self.save_balance_data(data) + + def can_use_daily(self, guild_id: str, user_id: str) -> bool: + """오늘 /돈줘를 사용할 수 있는지 확인""" + last_daily = self.get_last_daily(guild_id, user_id) + if last_daily is None: + return True + + # 서울 시간대 기준으로 오늘 날짜 확인 + today = datetime.now(SEOUL_TZ).date().isoformat() + return last_daily != today + + @app_commands.command(name="돈줘", description="매일 1번 10,000원을 받을 수 있습니다.") + async def daily_money(self, interaction: discord.Interaction): + """매일 1번 10,000원 지급""" + guild_id = str(interaction.guild_id) + user_id = str(interaction.user.id) + + if not self.can_use_daily(guild_id, user_id): + await interaction.response.send_message( + "❌ 오늘은 이미 돈을 받았습니다. 내일 다시 시도해주세요!", + ephemeral=True + ) + return + + current_balance = self.get_user_balance(guild_id, user_id) + new_balance = current_balance + 10000 + self.set_user_balance(guild_id, user_id, new_balance) + + today = datetime.now(SEOUL_TZ).date().isoformat() + self.set_last_daily(guild_id, user_id, today) + + embed = discord.Embed( + title="💰 일일 보상", + description=f"{interaction.user.mention}님이 10,000원을 받았습니다!", + color=0x00FF00 + ) + embed.add_field(name="현재 잔액", value=f"{new_balance:,}원", inline=False) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="잔액", description="보유한 돈을 확인합니다.") + async def check_balance(self, interaction: discord.Interaction): + """현재 잔액 확인""" + guild_id = str(interaction.guild_id) + user_id = str(interaction.user.id) + + balance = self.get_user_balance(guild_id, user_id) + + embed = discord.Embed( + title="💵 잔액 조회", + description=f"{interaction.user.mention}님의 현재 잔액", + color=0x3498DB + ) + embed.add_field(name="보유 금액", value=f"{balance:,}원", inline=False) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="송금", description="다른 사용자에게 돈을 송금합니다.") + @app_commands.describe( + 대상="송금할 대상 유저를 선택하세요.", + 금액="송금할 금액을 입력하세요." + ) + async def transfer_money( + self, + interaction: discord.Interaction, + 대상: discord.Member, + 금액: int + ): + """다른 유저에게 송금""" + guild_id = str(interaction.guild_id) + sender_id = str(interaction.user.id) + receiver_id = str(대상.id) + + # 자신에게 송금 방지 + if sender_id == receiver_id: + await interaction.response.send_message( + "❌ 자신에게는 송금할 수 없습니다.", + ephemeral=True + ) + return + + # 봇에게 송금 방지 + if 대상.bot: + await interaction.response.send_message( + "❌ 봇에게는 송금할 수 없습니다.", + ephemeral=True + ) + return + + # 금액 유효성 검사 + if 금액 <= 0: + await interaction.response.send_message( + "❌ 송금 금액은 0보다 커야 합니다.", + ephemeral=True + ) + return + + # 잔액 확인 + sender_balance = self.get_user_balance(guild_id, sender_id) + if sender_balance < 금액: + await interaction.response.send_message( + f"❌ 잔액이 부족합니다. (현재 잔액: {sender_balance:,}원)", + ephemeral=True + ) + return + + # 송금 처리 + new_sender_balance = sender_balance - 금액 + receiver_balance = self.get_user_balance(guild_id, receiver_id) + new_receiver_balance = receiver_balance + 금액 + + self.set_user_balance(guild_id, sender_id, new_sender_balance) + self.set_user_balance(guild_id, receiver_id, new_receiver_balance) + + embed = discord.Embed( + title="💸 송금 완료", + description=f"{interaction.user.mention} → {대상.mention}", + color=0x9B59B6 + ) + embed.add_field(name="송금 금액", value=f"{금액:,}원", inline=False) + embed.add_field(name="보낸 사람 잔액", value=f"{new_sender_balance:,}원", inline=True) + embed.add_field(name="받은 사람 잔액", value=f"{new_receiver_balance:,}원", inline=True) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="가위바위보", description="가위바위보 배팅 게임 (승리: 2배, 무승부: 절반, 패배: 전액 잃음)") + @app_commands.describe( + 선택="가위, 바위, 보 중 하나를 선택하세요.", + 배팅금액="배팅할 금액을 입력하세요." + ) + @app_commands.choices(선택=[ + app_commands.Choice(name="가위", value="가위"), + app_commands.Choice(name="바위", value="바위"), + app_commands.Choice(name="보", value="보") + ]) + async def rock_paper_scissors( + self, + interaction: discord.Interaction, + 선택: app_commands.Choice[str], + 배팅금액: int + ): + """가위바위보 게임""" + guild_id = str(interaction.guild_id) + user_id = str(interaction.user.id) + + # 배팅금액 유효성 검사 + if 배팅금액 <= 0: + await interaction.response.send_message( + "❌ 배팅 금액은 0보다 커야 합니다.", + ephemeral=True + ) + return + + # 잔액 확인 + current_balance = self.get_user_balance(guild_id, user_id) + if current_balance < 배팅금액: + await interaction.response.send_message( + f"❌ 잔액이 부족합니다. (현재 잔액: {current_balance:,}원)", + ephemeral=True + ) + return + + # 배팅금액 차감 + new_balance = current_balance - 배팅금액 + self.set_user_balance(guild_id, user_id, new_balance) + + # 봇의 선택 + choices = ["가위", "바위", "보"] + bot_choice = random.choice(choices) + user_choice = 선택.value + + # 승부 판정 + result = "" + prize = 0 + + if user_choice == bot_choice: + # 무승부 + result = "무승부" + prize = 배팅금액 // 2 + color = 0xF39C12 + elif ( + (user_choice == "가위" and bot_choice == "보") or + (user_choice == "바위" and bot_choice == "가위") or + (user_choice == "보" and bot_choice == "바위") + ): + # 승리 + result = "승리" + prize = 배팅금액 * 2 + color = 0x00FF00 + else: + # 패배 + result = "패배" + prize = 0 + color = 0xFF0000 + + # 상금 지급 + final_balance = new_balance + prize + self.set_user_balance(guild_id, user_id, final_balance) + + embed = discord.Embed( + title="✊✋✌️ 가위바위보", + color=color + ) + embed.add_field(name="당신의 선택", value=user_choice, inline=True) + embed.add_field(name="봇의 선택", value=bot_choice, inline=True) + embed.add_field(name="결과", value=result, inline=False) + embed.add_field(name="배팅 금액", value=f"{배팅금액:,}원", inline=True) + embed.add_field(name="획득 금액", value=f"{prize:,}원", inline=True) + embed.add_field(name="최종 잔액", value=f"{final_balance:,}원", inline=False) + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="도박", description="30%~70% 확률의 도박 (당첨: 2배, 실패: 전액 잃음)") + @app_commands.describe(배팅금액="배팅할 금액을 입력하세요.") + async def gamble(self, interaction: discord.Interaction, 배팅금액: int): + """랜덤 확률 도박""" + guild_id = str(interaction.guild_id) + user_id = str(interaction.user.id) + + # 배팅금액 유효성 검사 + if 배팅금액 <= 0: + await interaction.response.send_message( + "❌ 배팅 금액은 0보다 커야 합니다.", + ephemeral=True + ) + return + + # 잔액 확인 + current_balance = self.get_user_balance(guild_id, user_id) + if current_balance < 배팅금액: + await interaction.response.send_message( + f"❌ 잔액이 부족합니다. (현재 잔액: {current_balance:,}원)", + ephemeral=True + ) + return + + # 배팅금액 차감 + new_balance = current_balance - 배팅금액 + self.set_user_balance(guild_id, user_id, new_balance) + + # 당첨 확률 결정 (30% ~ 70%) + win_chance = random.randint(30, 70) + roll = random.randint(1, 100) + + is_win = roll <= win_chance + + if is_win: + # 당첨 + prize = 배팅금액 * 2 + final_balance = new_balance + prize + result = "🎉 당첨!" + color = 0x00FF00 + else: + # 낙첨 + prize = 0 + final_balance = new_balance + result = "💥 실패..." + color = 0xFF0000 + + self.set_user_balance(guild_id, user_id, final_balance) + + embed = discord.Embed( + title="🎰 도박", + description=result, + color=color + ) + embed.add_field(name="당첨 확률", value=f"{win_chance}%", inline=True) + embed.add_field(name="결과 값", value=f"{roll}/100", inline=True) + embed.add_field(name="배팅 금액", value=f"{배팅금액:,}원", inline=True) + embed.add_field(name="획득 금액", value=f"{prize:,}원", inline=True) + embed.add_field(name="최종 잔액", value=f"{final_balance:,}원", inline=False) + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="즉석복권", description="즉석복권 구매 (300원)") + async def instant_lottery(self, interaction: discord.Interaction): + """즉석복권""" + guild_id = str(interaction.guild_id) + user_id = str(interaction.user.id) + + ticket_price = 300 + + # 잔액 확인 + current_balance = self.get_user_balance(guild_id, user_id) + if current_balance < ticket_price: + await interaction.response.send_message( + f"❌ 잔액이 부족합니다. (현재 잔액: {current_balance:,}원, 필요 금액: {ticket_price}원)", + ephemeral=True + ) + return + + # 복권 구매 (차감) + new_balance = current_balance - ticket_price + self.set_user_balance(guild_id, user_id, new_balance) + + # 당첨 확률 및 금액 설정 + # 만원: 1%, 삼천원: 1.7%, 천원: 5.6%, 삼백원: 11.7%, 꽝: 나머지 + roll = random.uniform(0, 100) + + if roll < 1.0: + # 만원 당첨 + prize = 10000 + result = "🎊 대박! 만원 당첨!" + color = 0xFFD700 + elif roll < 2.7: # 1.0 + 1.7 + # 삼천원 당첨 + prize = 3000 + result = "🎉 삼천원 당첨!" + color = 0xC0C0C0 + elif roll < 8.3: # 2.7 + 5.6 + # 천원 당첨 + prize = 1000 + result = "🎈 천원 당첨!" + color = 0xCD7F32 + elif roll < 20.0: # 8.3 + 11.7 + # 삼백원 당첨 (본전) + prize = 300 + result = "😊 삼백원 당첨! (본전)" + color = 0x3498DB + else: + # 꽝 + prize = 0 + result = "😢 꽝..." + color = 0x95A5A6 + + # 상금 지급 + final_balance = new_balance + prize + self.set_user_balance(guild_id, user_id, final_balance) + + embed = discord.Embed( + title="🎫 즉석복권", + description=result, + color=color + ) + embed.add_field(name="구매 금액", value=f"{ticket_price}원", inline=True) + embed.add_field(name="당첨 금액", value=f"{prize:,}원", inline=True) + embed.add_field(name="최종 잔액", value=f"{final_balance:,}원", inline=False) + + await interaction.response.send_message(embed=embed) + + +async def setup(bot): + """Cog를 봇에 추가합니다.""" + await bot.add_cog(GamblingCommands(bot)) + print("Gambling Cog : setup 완료!")