-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.py
More file actions
269 lines (208 loc) · 8.24 KB
/
Copy pathbot.py
File metadata and controls
269 lines (208 loc) · 8.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import os
import datetime
import discord
from discord.ext import commands
from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# Load environment variables
load_dotenv()
# Google Calendar API scopes
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
# Discord bot setup
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix='!', intents=intents)
def get_calendar_service():
"""
Authenticate and return a Google Calendar API service object.
Supports both interactive and headless authentication modes.
"""
creds = None
# The file token.json stores the user's access and refresh tokens
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# If there are no (valid) credentials available, let the user log in
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
# Check if running in headless mode (no display available)
headless_mode = os.getenv("HEADLESS_AUTH", "false").lower() == "true"
if headless_mode:
# Use console-based authentication for headless environments
creds = flow.run_console()
else:
# Use local server for interactive environments
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open("token.json", "w") as token:
token.write(creds.to_json())
return build("calendar", "v3", credentials=creds)
def fetch_calendar_events(max_results=10):
"""
Fetch upcoming events from Google Calendar.
Args:
max_results: Maximum number of events to fetch
Returns:
List of calendar events
"""
try:
service = get_calendar_service()
# Get events from now onwards
now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
events_result = (
service.events()
.list(
calendarId="primary",
timeMin=now,
maxResults=max_results,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
events = events_result.get("items", [])
return events
except HttpError as error:
print(f"An error occurred fetching calendar events: {error}")
return []
def parse_event_time(event):
"""
Parse event start and end times from calendar event.
Args:
event: Google Calendar event dictionary
Returns:
Tuple of (start_time, end_time) as datetime objects
"""
start = event["start"].get("dateTime", event["start"].get("date"))
end = event["end"].get("dateTime", event["end"].get("date"))
# Parse datetime strings
if "T" in start: # DateTime format
start_time = datetime.datetime.fromisoformat(start.replace("Z", "+00:00"))
end_time = datetime.datetime.fromisoformat(end.replace("Z", "+00:00"))
else: # Date only format
start_time = datetime.datetime.fromisoformat(start + "T00:00:00+00:00")
end_time = datetime.datetime.fromisoformat(end + "T23:59:59+00:00")
return start_time, end_time
async def create_discord_event(guild, event):
"""
Create a scheduled event on Discord based on a calendar event.
Args:
guild: Discord guild (server) object
event: Google Calendar event dictionary
Returns:
Created Discord scheduled event or None if failed
"""
try:
start_time, end_time = parse_event_time(event)
# Discord requires timezone-aware datetime objects
if start_time.tzinfo is None:
start_time = start_time.replace(tzinfo=datetime.timezone.utc)
if end_time.tzinfo is None:
end_time = end_time.replace(tzinfo=datetime.timezone.utc)
# Get event details
summary = event.get("summary", "Untitled Event")
description = event.get("description", "")
location = event.get("location", "")
# Combine description and location
full_description = description
if location:
full_description = f"{description}\n\nLocation: {location}" if description else f"Location: {location}"
# Truncate description if too long (Discord has a 1000 character limit)
if len(full_description) > 1000:
full_description = full_description[:997] + "..."
# Create scheduled event on Discord
discord_event = await guild.create_scheduled_event(
name=summary[:100], # Discord has a 100 character limit for event names
description=full_description,
start_time=start_time,
end_time=end_time,
entity_type=discord.EntityType.external,
location=location[:100] if location else "See description"
)
return discord_event
except Exception as e:
print(f"Error creating Discord event: {e}")
return None
@bot.event
async def on_ready():
"""
Event handler called when the bot successfully connects to Discord.
"""
print(f'{bot.user} has connected to Discord!')
print(f'Bot is in {len(bot.guilds)} guild(s)')
@bot.command(name='sync_events')
async def sync_events(ctx, count: int = 10):
"""
Sync upcoming Google Calendar events to Discord.
Usage: !sync_events [count]
Args:
count: Number of events to sync (default: 10)
"""
await ctx.send(f"Fetching {count} upcoming events from Google Calendar...")
# Fetch calendar events
events = fetch_calendar_events(max_results=count)
if not events:
await ctx.send("No upcoming events found in Google Calendar.")
return
await ctx.send(f"Found {len(events)} events. Creating Discord events...")
# Create Discord events
created_count = 0
failed_count = 0
for event in events:
summary = event.get("summary", "Untitled Event")
discord_event = await create_discord_event(ctx.guild, event)
if discord_event:
created_count += 1
print(f"Created Discord event: {summary}")
else:
failed_count += 1
print(f"Failed to create Discord event: {summary}")
# Send summary
result_message = f"✅ Successfully created {created_count} Discord event(s)"
if failed_count > 0:
result_message += f"\n⚠️ Failed to create {failed_count} event(s)"
await ctx.send(result_message)
@bot.command(name='list_events')
async def list_events(ctx, count: int = 5):
"""
List upcoming Google Calendar events.
Usage: !list_events [count]
Args:
count: Number of events to list (default: 5)
"""
events = fetch_calendar_events(max_results=count)
if not events:
await ctx.send("No upcoming events found in Google Calendar.")
return
message = "📅 **Upcoming Calendar Events:**\n\n"
for event in events:
summary = event.get("summary", "Untitled Event")
start = event["start"].get("dateTime", event["start"].get("date"))
message += f"• **{summary}**\n {start}\n\n"
await ctx.send(message)
def main():
"""
Main function to run the Discord bot.
"""
# Get Discord bot token from environment variable
token = os.getenv("DISCORD_BOT_TOKEN")
if not token:
print("Error: DISCORD_BOT_TOKEN not found in environment variables")
print("Please create a .env file with your Discord bot token")
return
# Run the bot
try:
bot.run(token)
except Exception as e:
print(f"Error running bot: {e}")
if __name__ == "__main__":
main()