import discord, asyncio, json, datetime, os from google_auth_oauthlib.flow import InstalledAppFlow 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 bot.database import UserLocation, TaskCompleted from asyncio import to_thread from discord import app_commands from discord.ui import View, Select load_dotenv() DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") WEATHER_TOKEN = os.getenv("WEATHER_TOKEN") CREDENTIALS_FILE = './secret/credentials.json' TOKEN_FILE = './secret/token.json' SCOPES = ['https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/tasks.readonly', 'https://www.googleapis.com/auth/tasks'] 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() task_completed_db = TaskCompleted() TZ = datetime.datetime.now().astimezone().tzinfo def build_calendar_service(): creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES) return build('calendar', 'v3', credentials=creds) def build_tasks_service(): creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES) service = build('tasks', 'v1', credentials=creds) return service def parse_rfc3339_to_local_date(ts): if not ts: return None try: s = ts.replace("Z", "+00:00") if ts.endswith("Z") else ts if "T" in ts: dt = datetime.datetime.fromisoformat(s) if dt.tzinfo is None: dt = dt.replace(tzinfo=datetime.timezone.utc) return dt.astimezone(TZ).date() return datetime.date.fromisoformat(s) except Exception: return None #Calendar authentification async def authenticate(): 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 = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES) credentials = flow.run_local_server(port=0) with open(TOKEN_FILE, 'w') as f: f.write(credentials.to_json()) print(f"Saved credentials to {TOKEN_FILE} for instant access") #Marl task as completed 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 if task.get("status") == "completed": await interaction.followup.send("Task already completed.", ephemeral=True) return now = datetime.datetime.now(TZ).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(f"User {interaction.user.display_name} marked task '{updated.get('title')}' as completed.", ephemeral=True) print(f"User {interaction.user.display_name} just completed task {updated.get('title')}") username = interaction.user.display_name user_id = interaction.user.id task = updated.get('title') time = now task_completed_db.set_info(user_id, username, task, time) 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)) @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 = await asyncio.to_thread(lambda: service.tasklists().list(maxResults=100).execute()) lists = tl_res.get("items", []) or [] today = datetime.datetime.now(tz=TZ).date() embed = discord.Embed( title="Today's tasks", color=0x2ecc71, timestamp=datetime.datetime.now(tz=TZ) ) options = [] total = 0 for tl in lists: tl_id = tl.get("id") tl_title = tl.get("title") 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 [] for t in items: if not isinstance(t, dict): continue due_date = parse_rfc3339_to_local_date(t.get("due")) completed_date = parse_rfc3339_to_local_date(t.get("completed")) if due_date != today and completed_date != today: continue print(f"API RESPONSE: {t}") total += 1 status = "✅" if t.get("status") == "completed" else "🔲" t_id = t.get("id") title = t.get("title") or "(no title)" embed.add_field(name=f"{tl_title} - {status} {title}", value=f"ID: {t_id}", inline=False) value = json.dumps({"tasklist_id": tl_id, "task_id": t_id}) options.append(discord.SelectOption(label=(title[:90] or "(no title)"), description=tl_title[:50], value=value)) if total == 0: await interaction.followup.send("No tasks due today.") 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}") @bot.tree.command(name='weekly_events', description="Check this weeks saved events") 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: 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) start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) 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(), timeMax=end_of_week.isoformat(), singleEvents=True, orderBy='startTime' ).execute() print(f"API RESPONSE: {events_result}") events = events_result.get('items',[]) if not events: await interaction.followup.send("No events this week!") return else: embed = discord.Embed( title="📅 This week's events", color=discord.Color.blue(), timestamp=datetime.datetime.now(tz=TZ) ) for event in events: event_description = event.get('description', '') if 'tasks.google.com' in event_description: continue event_time_str = event['start'].get('dateTime', event['start'].get('date')) if 'Z' in event_time_str: event_time = datetime.datetime.fromisoformat(event_time_str.replace('Z', '+00:00')) else: 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⏰ {event_time_local}\n Description: {event_description}" embed.add_field(name="\u200b", value=task, inline=False) await interaction.followup.send(embed=embed) except Exception as e: await interaction.followup.send(f'An error occurred: {e}') #Weather commad tree @bot.command(name="set_location") async def set_location(ctx, *, location: str): user_id = ctx.author.id username = ctx.author.display_name user_location_db.set_user_location(username, user_id, location) await ctx.send(f"Location set to: {location}") print(f"{username} just set location 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 current_weather = weather_client.get_current_weather(location) print(f"API RESPONSE {current_weather}") # Check that current_weather is a dictionary and contains necessary keys 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'] embed = discord.Embed( title=f"Current weather in {location}", description=f"Temperature: {temp}°C\nWeather condition: {weather_condition}", color=discord.Color.yellow(), timestamp=datetime.datetime.now(tz=TZ) ) embed.set_thumbnail(url=f"https://openweathermap.org/img/wn/{icon}.png") await interaction.response.send_message(embed=embed) else: await interaction.response.send_message("Could not retrieve weather data. Please check the location or try again. ") @bot.event 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)