diff --git a/.gitignore b/.gitignore index 8992d45..f17da50 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ /dvenv copy.txt user_location.db -/utils/.env +/bot/.env diff --git a/utils/__pycache__/database.cpython-313.pyc b/bot/__pycache__/database.cpython-313.pyc similarity index 85% rename from utils/__pycache__/database.cpython-313.pyc rename to bot/__pycache__/database.cpython-313.pyc index ad10040..d088d3e 100644 Binary files a/utils/__pycache__/database.cpython-313.pyc and b/bot/__pycache__/database.cpython-313.pyc differ diff --git a/bot/__pycache__/task.cpython-313.pyc b/bot/__pycache__/task.cpython-313.pyc new file mode 100644 index 0000000..7bd93ad Binary files /dev/null and b/bot/__pycache__/task.cpython-313.pyc differ diff --git a/utils/__pycache__/weather.cpython-313.pyc b/bot/__pycache__/weather.cpython-313.pyc similarity index 92% rename from utils/__pycache__/weather.cpython-313.pyc rename to bot/__pycache__/weather.cpython-313.pyc index f46dd28..b74f21d 100644 Binary files a/utils/__pycache__/weather.cpython-313.pyc and b/bot/__pycache__/weather.cpython-313.pyc differ diff --git a/utils/database.py b/bot/database.py similarity index 100% rename from utils/database.py rename to bot/database.py diff --git a/bot/task.py b/bot/task.py new file mode 100644 index 0000000..e63ce8d --- /dev/null +++ b/bot/task.py @@ -0,0 +1,40 @@ +import asyncio, json, datetime, discord +from discord.ui import View, Select + +class TaskSelect(Select): + def __init__(self, options): + super().__init__(placeholder="Choose a task to complete...", + min_values=1, max_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + + payload = json.loads(self.values[0]) + tasklist_id = payload["tasklist_id"] + task_id = payload["task_id"] + + await interaction.response.defer(thinking=True) + try: + service = build_tasks_service() + task = await asyncio.to_thread(lambda: + service.tasks().get(tasklist=tasklist_id, task=task_id).execute()) + if not task: + await interaction.followup.send("Task not found.", ephemeral=True) + return + + now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() + body = dict(task) + body["status"] = "completed" + body["completed"] = now + + updated = await asyncio.to_thread(lambda: service.tasks().update(tasklist=tasklist_id, task=task_id, body=body).execute()) + + if updated.get("status") == "completed": + await interaction.followup.send("Failed to mark completed.", ephemeral=True) + + except Exception as e: + await interaction.followup.send(f"Error: {e}", ephemeral=True) + +class TasksView(View): + def __init__(self, options, timeout=120): + super().__init__(timeout=timeout) + self.add_item(TaskSelect(options)) \ No newline at end of file diff --git a/utils/weather.py b/bot/weather.py similarity index 100% rename from utils/weather.py rename to bot/weather.py diff --git a/main.py b/main.py index f515483..53a4ebb 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,15 @@ -import os -import discord -import datetime +import discord, asyncio, json, datetime, os from google_auth_oauthlib.flow import InstalledAppFlow -from utils.weather import OpenWeatherMapAPIClient +from bot.weather import OpenWeatherMapAPIClient from googleapiclient.discovery import build from google.oauth2.credentials import Credentials from discord.ext import commands from dotenv import load_dotenv -from utils.database import UserLocation +from bot.database import UserLocation from asyncio import to_thread +from discord import app_commands +from discord.ui import View, Select +from bot.task import TaskSelect, TasksView load_dotenv() DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") @@ -61,14 +62,16 @@ async def authenticate(): with open(TOKEN_FILE, 'w') as f: f.write(credentials.to_json()) print(f"Saved credentials to {TOKEN_FILE} for instant access") - - -@bot.tree.command(name="daily_tasks", description="Check today's saved tasks") + + +@bot.tree.command(name="daily_tasks", description="Check today's saved tasks and complete them") async def today(interaction: discord.Interaction): await interaction.response.defer(thinking=True) + + print(f'Recieved daily_tasks command by {interaction.user.display_name}') try: service = build_tasks_service() - tl_res = service.tasklists().list(maxResults=100).execute() + tl_res = await asyncio.to_thread(lambda: service.tasklists().list(maxResults=100).execute()) lists = tl_res.get("items", []) or [] today = datetime.datetime.now(tz=TZ).date() @@ -79,15 +82,16 @@ async def today(interaction: discord.Interaction): timestamp=datetime.datetime.now(tz=TZ) ) + options = [] added = 0 + for tl in lists: tl_id = tl.get("id") tl_title = tl.get("title") or "" - tasks_res = service.tasks().list( - tasklist=tl_id, showCompleted=True, showHidden=True, maxResults=250).execute() or {} + tasks_res = await asyncio.to_thread(lambda: service.tasks().list( + tasklist=tl_id, showCompleted=True, showHidden=True, maxResults=250).execute() or {}) items = tasks_res.get("items", []) or [] - lines = [] for t in items: if not isinstance(t, dict): @@ -96,35 +100,26 @@ async def today(interaction: discord.Interaction): due_date = parse_rfc3339_to_local_date(t.get("due")) completed_date = parse_rfc3339_to_local_date(t.get("completed")) - print("title:", t.get("title"), "due:", t.get("due"), "completed:", t.get("completed"), "status:", t.get("status")) - if due_date != today and completed_date != today: continue status = "✅" if t.get("status") == "completed" else "🔲" + t_id = t.get("id") title = t.get("title") or "(no title)" - - if completed_date == today and t.get("completed"): - completed_short = t["completed"].replace("T", " ").split(".")[0] - lines.append(f"{status} {title} - completed {completed_short}") - else: - if due_date: - lines.append(f"{status} {title} (due {due_date.isoformat()})") - else: - lines.append(f"{status}{title}") - if not lines: - continue + embed.add_field(name=f"{tl_title} - {status} {title}", value=f"ID: {t.get('id')}", inline=False) - value = "\n".join(lines)[:1024] - embed.add_field(name=tl_title, value=value, inline=False) + value = json.dumps({"tasklist_id": tl_id, "task_id": t.get("id")}) + options.append(discord.SelectOption(label=(title[:90] or "(no title)"), description=tl_title[:50], value=value)) added += 1 if added == 0: embed.description = "No tasks due today." - - await interaction.followup.send(embed=embed) + return + options = options[:25] + view = TasksView(options, interaction.user.id) + await interaction.followup.send(embed=embed, view=view) except Exception as e: await interaction.followup.send(f"An error occured:{e}") @@ -134,6 +129,7 @@ async def today(interaction: discord.Interaction): async def events(interaction: discord.Interaction): await interaction.response.defer(thinking=True) + print(f'Recieved weekly_events command by {interaction.user.display_name}') try: service = build_calendar_service() except FileNotFoundError: @@ -246,13 +242,21 @@ async def on_ready(): print(f'Logged in as {bot.user}') await authenticate() await bot.tree.sync() - #Liste commandes enregistré commands = await bot.tree.fetch_commands() print("Registered Commands:") for command in commands: print(f"- {command.name}") print("- !set_location") + + channel = bot.get_channel(1480922508566990879) + if channel: + embed = discord.Embed( + description="Bot is now online and ready to serve! Type / to check available commands" + ) + await channel.send(embed=embed) + print("your bot is online and ready to serve !") + bot.run(DISCORD_TOKEN)