From 3aab9eace24a676733e570f4d534265e373d3c28 Mon Sep 17 00:00:00 2001 From: tenzi Date: Tue, 24 Mar 2026 22:49:57 +0100 Subject: [PATCH] tasks pas fini --- task_bot.py | 203 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 47 deletions(-) diff --git a/task_bot.py b/task_bot.py index 3a8f72c..6e4536d 100644 --- a/task_bot.py +++ b/task_bot.py @@ -1,43 +1,138 @@ import os import discord import datetime +import pytz import google_auth_oauthlib.flow from weather import OpenWeatherMapAPIClient from googleapiclient.discovery import build +from google.oauth2.credentials import Credentials from discord.ext import commands from dotenv import load_dotenv - -global service -service = None - -SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] +from database import UserLocation +from asyncio import to_thread load_dotenv() DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") WEATHER_TOKEN = os.getenv("WEATHER_TOKEN") +CREDENTIALS_FILE = './calendar/credentials.json' +TOKEN_FILE = './calendar/token.json' +SCOPES = ['https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/tasks.readonly'] + weather_client = OpenWeatherMapAPIClient(WEATHER_TOKEN, "MyDiscordWeatherBot") intents = discord.Intents(messages=True, guilds=True) intents.message_content = True bot = commands.Bot(command_prefix='!', intents=intents) +user_location_db = UserLocation() +TZ = pytz.timezone("Europe/Paris") +def build_calendar_service(): + creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES) + return build('calendar', 'v3', credentials=creds) +def build_tasks_service(): + creds = creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES) + return build('tasks', 'v1', credentials=creds) + +def parse_google_time(s: str) -> datetime.datetime: + dt = datetime.datetime.fromisoformat(s) + if 'T' not in s: + return dt.replace(tzinfo=datetime.timezone.utc) + + if s.endswith('Z'): + return datetime.datetime.fromisoformat(s.replace('Z', '+00:00')) + + if dt.tzinfo is None: + return dt.replace(tzinfo=datetime.timezone.utc) + return dt + +def is_due_today(due_iso: str, tz: pytz.timezone = TZ) -> bool: + if not due_iso: + return False + try: + dt = datetime.datetime.fromisoformat(due_iso.replace("Z", "+00:00")) + except Exception: + return False + return dt.astimezone(tz).date() == datetime.datetime.now(tz).date() + +def due_time_str(due_iso: str, tz: pytz.timezone = TZ) -> str: + if not due_iso: + return "" + try: + dt = datetime.datetime.fromisoformat(due_iso.replace("Z", "+00:00")).astimezone(tz) + return dt.strftime("%H:%M") + except Exception: + return "" + #Calendar authentification async def authenticate(): - - print("Authenticating..") - global service - flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file('./calendar/credentials.json', SCOPES) - credentials = flow.run_local_server(port=0) - service = build('calendar', 'v3', credentials=credentials) + if os.path.exists(TOKEN_FILE): + print("Token file exists; skipping interactive authentication.") + return + print("No token found: running interactive OAuth flow(will open browser).") + flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES) + credentials = flow.run_local_server(port=0) -@bot.tree.command(name='weekly_events') + 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") +async def today(interaction: discord.Interaction): + await interaction.response.defer() + try: + service = build_tasks_service() + tl_res = service.tasklists().list(maxResults=100).execute() + lists = tl_res.get("items", []) + embed = discord.Embed( + title="Today's tasks", + color=0x2ecc71, + timestamp=datetime.datetime.now(tz=TZ) + ) + any_tasks = False + + for tl in lists: + tl_id = tl["id"] + tl_title = tl.get("title", "Untitled") + tasks_res = service.tasks().list(tasklist=tl_id, showCompleted=True, maxResults=200).execute() + items = tasks_res.get("items", []) + day_tasks = [t for t in items if is_due_today(t.get("due"))] + if not day_tasks: + continue + any_tasks = True + lines = [] + for t in sorted(day_tasks, key=lambda x: x.get("due") or ""): + status = "✅done" if t.get("status") == "completed" else "🔲" + time = due_time_str(t.get("due")) + title = t.get("title", "(no title)") + tid = t.get("id", "") + lines.append(f"{status} {title}{(' - ' + time) if time else ''} (id:{tid})") + value = "\n".join(lines)[:1024] + embed.add_field(name=tl_title, value=value, inline=False) + + if not any_tasks: + embed.description = "No tasks due today." + await interaction.followup.send(embed=embed) + except Exception as e: + await interaction.followup.send(f"An error occured:{e}") + + +@bot.tree.command(name='weekly_events', description="Check this weeks saved events") async def events(interaction: discord.Interaction): await interaction.response.defer(thinking=True) - now = datetime.datetime.utcnow() + try: + service = build_calendar_service() + except FileNotFoundError: + await interaction.followup.send("Calendar credentials not found.") + return + except Exception as e: + await interaction.followup.send(f"Failed to build service: {e}") + return + + now = datetime.datetime.now(datetime.timezone.utc) current_weekday = now.weekday() start_of_week = now - datetime.timedelta(days=current_weekday) @@ -46,22 +141,34 @@ async def events(interaction: discord.Interaction): end_of_week = start_of_week + datetime.timedelta(days=6) end_of_week= end_of_week.replace(hour=23, minute=59, second=59, microsecond=999999) + print(f"Start of week: {start_of_week.isoformat()}, End of week: {end_of_week.isoformat()}") + + try: events_result = service.events().list( calendarId='primary', - timeMin=start_of_week.isoformat() + 'Z', - timeMax=end_of_week.isoformat() + 'Z', + timeMin=start_of_week.isoformat(), + timeMax=end_of_week.isoformat(), singleEvents=True, orderBy='startTime' ).execute() - events = events_result.get('items',[]) + print(f"API Response: {events_result}") + events = events_result.get('items',[]) + + if not events: - await interaction.response.send_message('No events this week !') + embed = discord.Embed( + title="No events this week!", + description="Go outside maybe" + ) + await interaction.followup.send(embed=embed) + return else: embed = discord.Embed( title="📅 Weekly Events Summary", + description="This week's events", color=discord.Color.blue() ) @@ -73,7 +180,9 @@ async def events(interaction: discord.Interaction): event_time = datetime.datetime.fromisoformat(event_time_str) event_time_local = event_time.strftime("%A, %B %d, %Y, %H:%M") - task = f"**{event['summary']}**\n⏰ **At: {event_time_local}**" + + event_description = event.get('description') + task = f"**{event['summary']}**\n⏰ {event_time_local}\n Description: {event_description}" embed.add_field(name="\u200b", value=task, inline=False) await interaction.followup.send(embed=embed) @@ -83,8 +192,21 @@ async def events(interaction: discord.Interaction): #Weather commad tree -@bot.tree.command(name="weather") -async def current_weather(interaction: discord.Interaction, location: str): +@bot.command(name="set_location") +async def set_location(ctx, *, location: str): + user_id = ctx.author.id + user_location_db.set_user_location(user_id, location) + await ctx.send(f"Location set to: {location}") + +@bot.tree.command(name="weather", description="Check the weather!") +async def current_weather(interaction: discord.Interaction, location: str = None): + user_id = interaction.user.id + + if location is None: + location = user_location_db.get_user_location(user_id) + if not location: + await interaction.response.send_message("Please provide a location or set one using '!set_location '.\n(Your location will be stored in a database for ease of access)") + return print(f"Received weather command from {interaction.user.display_name}") # Log intéraction @@ -93,36 +215,22 @@ async def current_weather(interaction: discord.Interaction, location: str): print(f"JSON API {current_weather}") # Check that current_weather is a dictionary and contains necessary keys - if not isinstance(current_weather, dict) or 'main' not in current_weather or 'weather' not in current_weather: - await interaction.response.send_message("Could not retrieve weather data. Please check the location.") - return - - weather_list = current_weather['weather'] - if len(weather_list) == 0: - await interaction.response.send_message("Weather information is not available.") - return + if isinstance(current_weather, dict) and 'main' in current_weather and 'weather' in current_weather: + weather_condition = current_weather['weather'][0]['main'] + temp = current_weather['main']['temp'] + icon = current_weather['weather'][0]['icon'] - weather_condition = weather_list[0]['main'] + embed = discord.Embed( + title=f"Current weather in {location}", + description=f"Temperature: {temp}°C\nWeather condition: {weather_condition}", + color=discord.Color.yellow() + ) + embed.set_thumbnail(url=f"https://openweathermap.org/img/wn/{icon}.png") - temp = current_weather['main']['temp'] - - icon = current_weather['weather']['icon'] - embed = discord.Embed( - title=f"Current weather in {location}", - description=f"Temperature: {temp}°C\nSky: {weather_condition}", - ) - embed.set_thumbnail(url=f"https://openweathermap.org/img/wn/{icon}.png") - - await interaction.response.send_message(embed=embed) - + await interaction.response.send_message(embed=embed) -@bot.tree.command(name='hello') -async def hello_command(interaction: discord.Interaction): - - print(f"Received hello command from {interaction.user.display_name}") # Log intéraction - - user_nick = interaction.user.display_name - await interaction.response.send_message(f'Hello {user_nick}!') + else: + await interaction.response.send_message("Could not retrieve weather data. Please check the location or try again. ") @bot.event async def on_ready(): @@ -135,5 +243,6 @@ async def on_ready(): print("Registered Commands:") for command in commands: print(f"- {command.name}") + print("your bot is online and ready to serve !") bot.run(DISCORD_TOKEN)