import discord from discord import app_commands from discord.ext import commands, tasks import datetime import os import asyncio from typing import Optional, List, Dict, Any from database import Database from dotenv import load_dotenv load_dotenv() # --- Views & Modals --- class TaskModal(discord.ui.Modal, title="新規タスク登録"): task_title = discord.ui.TextInput(label="タイトル", placeholder="タスク名", required=True, max_length=100) priority = discord.ui.TextInput(label="優先順位 (SSS-D)", placeholder="S", default="A", min_length=1, max_length=3) deadline = discord.ui.TextInput(label="締め切り (YYYY-MM-DD HH:MM)", placeholder="2024-12-31 23:59", required=True) details = discord.ui.TextInput(label="詳細", style=discord.TextStyle.long, required=False, max_length=1000) def __init__(self, db: Database): super().__init__() self.db = db async def on_submit(self, interaction: discord.Interaction): prio = self.priority.value.upper() if prio not in ["SSS", "SS", "S", "A", "B", "C", "D"]: await interaction.response.send_message(f"❌ 優先順位が無効です。", ephemeral=True) return try: deadline_dt = datetime.datetime.strptime(self.deadline.value, "%Y-%m-%d %H:%M") except ValueError: await interaction.response.send_message("❌ 日時の形式が正しくありません。", ephemeral=True) return tid = await self.db.add_task( user_id=interaction.user.id, user_name=interaction.user.display_name, guild_id=interaction.guild_id, channel_id=interaction.channel_id, title=self.task_title.value, description=self.details.value, priority=prio, deadline=deadline_dt.isoformat() ) await interaction.response.send_message(f"✅ タスク『{self.task_title.value}』(ID: {tid}) を登録しました!", ephemeral=True) class EditTaskModal(discord.ui.Modal): def __init__(self, db: Database, task: dict): super().__init__(title=f"タスク編集 (ID: {task['id']})") self.db = db self.task_id = task['id'] deadline_dt = datetime.datetime.fromisoformat(task['deadline']) self.task_title = discord.ui.TextInput(label="タイトル", default=task['title'], required=True) self.priority = discord.ui.TextInput(label="優先順位 (SSS-D)", default=task['priority'], min_length=1, max_length=3) self.deadline = discord.ui.TextInput(label="締め切り (YYYY-MM-DD HH:MM)", default=deadline_dt.strftime("%Y-%m-%d %H:%M"), required=True) self.details = discord.ui.TextInput(label="詳細", default=task['description'] or "", style=discord.TextStyle.long, required=False) self.add_item(self.task_title) self.add_item(self.priority) self.add_item(self.deadline) self.add_item(self.details) async def on_submit(self, interaction: discord.Interaction): prio = self.priority.value.upper() if prio not in ["SSS", "SS", "S", "A", "B", "C", "D"]: await interaction.response.send_message("❌ 優先順位が無効です。", ephemeral=True) return try: deadline_dt = datetime.datetime.strptime(self.deadline.value, "%Y-%m-%d %H:%M") except ValueError: await interaction.response.send_message("❌ 日時の形式が正しくありません。", ephemeral=True) return await self.db.update_task(self.task_id, self.task_title.value, self.details.value, prio, deadline_dt.isoformat()) await interaction.response.send_message(f"✨ タスクを更新しました!", ephemeral=True) class TaskActionView(discord.ui.View): """通知やリストに表示する「完了」ボタン""" def __init__(self, db: Database, task_id: str): super().__init__(timeout=None) self.db = db self.task_id = task_id @discord.ui.button(label="✅ 完了にする", style=discord.ButtonStyle.success) async def complete(self, interaction: discord.Interaction, button: discord.ui.Button): await self.db.complete_task(self.task_id) # メッセージを「完了済み」に更新 embed = interaction.message.embeds[0] embed.title = f"🏁 【完了】 {embed.title}" embed.color = discord.Color.light_grey() await interaction.response.edit_message(content="お疲れ様でした!", embed=embed, view=None) class ControlPanel(discord.ui.View): def __init__(self, db: Database): super().__init__(timeout=None) self.db = db @discord.ui.button(label="➕ タスク追加", style=discord.ButtonStyle.success, custom_id="panel_add") async def add_btn(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.send_modal(TaskModal(self.db)) @discord.ui.button(label="📋 タスク一覧", style=discord.ButtonStyle.primary, custom_id="panel_list") async def list_btn(self, interaction: discord.Interaction, button: discord.ui.Button): tasks_list = await self.db.get_user_tasks(interaction.user.id, interaction.channel_id) if not tasks_list: await interaction.response.send_message("📋 未完了のタスクはありません。", ephemeral=True) return await interaction.response.defer(ephemeral=True) for t in tasks_list: deadline_fmt = datetime.datetime.fromisoformat(t['deadline']).strftime("%m/%d %H:%M") embed = discord.Embed(title=t['title'], color=discord.Color.green()) embed.add_field(name="優先度", value=t['priority'], inline=True) embed.add_field(name="締切", value=deadline_fmt, inline=True) if t['description']: embed.description = t['description'] await interaction.followup.send(embed=embed, view=TaskActionView(self.db, t['id']), ephemeral=True) # --- Bot Core --- class TaskBot(commands.Bot): def __init__(self, db: Database): intents = discord.Intents.default() intents.message_content = True intents.members = True super().__init__(command_prefix="!", intents=intents) self.db = db async def setup_hook(self): await self.db.initialize() self.daily_notification.start() self.deadline_monitor.start() await self.add_cog(TaskCommands(self)) print("Setup hook completed.") async def on_ready(self): print(f"Logged in as {self.user} (ID: {self.user.id})") async def on_message(self, message: discord.Message): if message.author.bot: return if self.user.mentioned_in(message): await message.reply("📋 **タスク管理パネル**", view=ControlPanel(self.db)) await self.process_commands(message) @tasks.loop(time=datetime.time(hour=7, minute=0)) async def daily_notification(self): print("Running daily notification...") tasks_list = await self.db.get_all_active_tasks() now = datetime.datetime.now() for t in tasks_list: deadline = datetime.datetime.fromisoformat(t["deadline"]) diff = deadline - now new_prio = t["priority"] if diff.days <= 1: new_prio = "SSS" elif diff.days <= 3: new_prio = "SS" elif diff.days <= 7: new_prio = "S" if new_prio != t["priority"]: await self.db.update_priority(t["id"], new_prio) # チャンネルごとの通知 channel_tasks = {} for t in tasks_list: cid = t["channel_id"] if cid not in channel_tasks: channel_tasks[cid] = {} uid = t["user_id"] if uid not in channel_tasks[cid]: channel_tasks[cid][uid] = [] channel_tasks[cid][uid].append(t) for cid, u_dict in channel_tasks.items(): channel = self.get_channel(cid) if not channel: continue for uid, t_list in u_dict.items(): embed = discord.Embed(title="☀️ 本日のタスクリマインド", color=discord.Color.gold()) desc = "\n".join([f"• **[{t['priority']}] {t['title']}** ({datetime.datetime.fromisoformat(t['deadline']).strftime('%m/%d %H:%M')})" for t in t_list]) embed.description = desc await channel.send(content=f"<@{uid}> さん、今日の予定です!", embed=embed) @tasks.loop(minutes=1) async def deadline_monitor(self): now = datetime.datetime.now() tasks_list = await self.db.get_all_active_tasks() for t in tasks_list: deadline = datetime.datetime.fromisoformat(t["deadline"]) diff = deadline - now seconds_left = diff.total_seconds() channel = self.get_channel(t["channel_id"]) if not channel: continue # 1時間前 if 0 < seconds_left <= 3600 and t['last_reminded_type'] != '1h': embed = discord.Embed(title=f"⏳ 締切1時間前: {t['title']}", color=discord.Color.orange()) await channel.send(content=f"<@{t['user_id']}> 準備はいいですか?あと1時間です!", embed=embed, view=TaskActionView(self.db, t['id'])) await self.db.update_reminded_type(t['id'], '1h') # 締切 elif seconds_left <= 0 and t['last_reminded_type'] != '0h': embed = discord.Embed(title=f"⏰ 締切時刻です!: {t['title']}", color=discord.Color.red()) await channel.send(content=f"<@{t['user_id']}> 締切時間になりました!完了しましたか?", embed=embed, view=TaskActionView(self.db, t['id'])) await self.db.update_reminded_type(t['id'], '0h') class TaskCommands(commands.Cog): def __init__(self, bot: TaskBot): self.bot = bot self.db = bot.db async def task_autocomplete(self, interaction: discord.Interaction, current: str): tasks = await self.db.get_user_tasks(interaction.user.id, interaction.channel_id) return [app_commands.Choice(name=f"[{t['priority']}] {t['title']}", value=t['id']) for t in tasks if current.lower() in t['title'].lower()][:25] @commands.command(name="sync") @commands.is_owner() async def sync_commands(self, ctx: commands.Context, guild_only: bool = True): try: if guild_only: self.bot.tree.copy_global_to(guild=ctx.guild) await self.bot.tree.sync(guild=ctx.guild) await ctx.send("✅ 同期完了(サーバー)") else: await self.bot.tree.sync() await ctx.send("✅ 同期完了(グローバル)") except Exception as e: await ctx.send(f"❌ エラー: {e}") @app_commands.command(name="add") async def add_slash(self, interaction: discord.Interaction): await interaction.response.send_modal(TaskModal(self.db)) @app_commands.command(name="list") async def list_tasks_slash(self, interaction: discord.Interaction): tasks_list = await self.db.get_user_tasks(interaction.user.id, interaction.channel_id) if not tasks_list: await interaction.response.send_message("📋 未完了のタスクはありません。", ephemeral=True) return await interaction.response.defer(ephemeral=True) for t in tasks_list: deadline_fmt = datetime.datetime.fromisoformat(t['deadline']).strftime("%m/%d %H:%M") embed = discord.Embed(title=f"[{t['priority']}] {t['title']}", color=discord.Color.green()) embed.description = f"📅 締切: {deadline_fmt}\n{t['description'] or ''}" await interaction.followup.send(embed=embed, view=TaskActionView(self.db, t['id']), ephemeral=True) @app_commands.command(name="done") @app_commands.autocomplete(task_id=task_autocomplete) async def done_slash(self, interaction: discord.Interaction, task_id: str): await self.db.complete_task(task_id) await interaction.response.send_message(f"✅ 完了にしました!", ephemeral=True) @app_commands.command(name="edit") @app_commands.autocomplete(task_id=task_autocomplete) async def edit_slash(self, interaction: discord.Interaction, task_id: str): task = await self.db.get_task_by_id(task_id) if task: await interaction.response.send_modal(EditTaskModal(self.db, task)) else: await interaction.response.send_message("❌ 見つかりません。", ephemeral=True)