mcpeasy 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.claudeignore +0 -3
  3. data/.mcp.json +19 -1
  4. data/CHANGELOG.md +59 -0
  5. data/CLAUDE.md +19 -5
  6. data/README.md +19 -3
  7. data/lib/mcpeasy/cli.rb +62 -10
  8. data/lib/mcpeasy/config.rb +22 -1
  9. data/lib/mcpeasy/setup.rb +1 -0
  10. data/lib/mcpeasy/version.rb +1 -1
  11. data/lib/utilities/gcal/README.md +11 -3
  12. data/lib/utilities/gcal/cli.rb +110 -108
  13. data/lib/utilities/gcal/mcp.rb +463 -308
  14. data/lib/utilities/gcal/service.rb +312 -0
  15. data/lib/utilities/gdrive/README.md +3 -3
  16. data/lib/utilities/gdrive/cli.rb +98 -96
  17. data/lib/utilities/gdrive/mcp.rb +290 -288
  18. data/lib/utilities/gdrive/service.rb +293 -0
  19. data/lib/utilities/gmail/README.md +278 -0
  20. data/lib/utilities/gmail/cli.rb +264 -0
  21. data/lib/utilities/gmail/mcp.rb +846 -0
  22. data/lib/utilities/gmail/service.rb +547 -0
  23. data/lib/utilities/gmeet/cli.rb +131 -129
  24. data/lib/utilities/gmeet/mcp.rb +374 -372
  25. data/lib/utilities/gmeet/service.rb +411 -0
  26. data/lib/utilities/notion/README.md +287 -0
  27. data/lib/utilities/notion/cli.rb +245 -0
  28. data/lib/utilities/notion/mcp.rb +607 -0
  29. data/lib/utilities/notion/service.rb +327 -0
  30. data/lib/utilities/slack/README.md +3 -3
  31. data/lib/utilities/slack/cli.rb +69 -54
  32. data/lib/utilities/slack/mcp.rb +277 -226
  33. data/lib/utilities/slack/service.rb +134 -0
  34. data/mcpeasy.gemspec +6 -1
  35. metadata +87 -10
  36. data/env.template +0 -11
  37. data/lib/utilities/gcal/gcal_tool.rb +0 -308
  38. data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
  39. data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
  40. data/lib/utilities/slack/slack_tool.rb +0 -119
  41. data/logs/.keep +0 -0
@@ -1,291 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "google/apis/drive_v3"
5
- require "googleauth"
6
- require "signet/oauth_2/client"
7
- require "fileutils"
8
- require "json"
9
- require "time"
10
- require_relative "../_google/auth_server"
11
- require_relative "../../mcpeasy/config"
12
-
13
- class GdriveTool
14
- SCOPES = [
15
- "https://www.googleapis.com/auth/calendar.readonly",
16
- "https://www.googleapis.com/auth/drive.readonly"
17
- ]
18
- SCOPE = SCOPES.join(" ")
19
-
20
- # MIME type mappings for Google Workspace documents
21
- EXPORT_FORMATS = {
22
- "application/vnd.google-apps.document" => {
23
- format: "text/markdown",
24
- extension: ".md"
25
- },
26
- "application/vnd.google-apps.spreadsheet" => {
27
- format: "text/csv",
28
- extension: ".csv"
29
- },
30
- "application/vnd.google-apps.presentation" => {
31
- format: "text/plain",
32
- extension: ".txt"
33
- },
34
- "application/vnd.google-apps.drawing" => {
35
- format: "image/png",
36
- extension: ".png"
37
- }
38
- }.freeze
39
-
40
- def initialize(skip_auth: false)
41
- ensure_env!
42
- @service = Google::Apis::DriveV3::DriveService.new
43
- @service.authorization = authorize unless skip_auth
44
- end
45
-
46
- def search_files(query, max_results: 10)
47
- results = @service.list_files(
48
- q: "fullText contains '#{query.gsub("'", "\\'")}' and trashed=false",
49
- page_size: max_results,
50
- fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
51
- )
52
-
53
- files = results.files.map do |file|
54
- {
55
- id: file.id,
56
- name: file.name,
57
- mime_type: file.mime_type,
58
- size: file.size&.to_i,
59
- modified_time: file.modified_time,
60
- web_view_link: file.web_view_link
61
- }
62
- end
63
-
64
- {files: files, count: files.length}
65
- rescue Google::Apis::Error => e
66
- raise "Google Drive API Error: #{e.message}"
67
- rescue => e
68
- log_error("search_files", e)
69
- raise e
70
- end
71
-
72
- def get_file_content(file_id)
73
- # First get file metadata
74
- file = @service.get_file(file_id, fields: "id,name,mimeType,size")
75
-
76
- content = if EXPORT_FORMATS.key?(file.mime_type)
77
- # Export Google Workspace document
78
- export_format = EXPORT_FORMATS[file.mime_type][:format]
79
- @service.export_file(file_id, export_format)
80
- else
81
- # Download regular file
82
- @service.get_file(file_id, download_dest: StringIO.new)
83
- end
84
-
85
- {
86
- id: file.id,
87
- name: file.name,
88
- mime_type: file.mime_type,
89
- size: file.size&.to_i,
90
- content: content.is_a?(StringIO) ? content.string : content
91
- }
92
- rescue Google::Apis::Error => e
93
- raise "Google Drive API Error: #{e.message}"
94
- rescue => e
95
- log_error("get_file_content", e)
96
- raise e
97
- end
98
-
99
- def list_files(max_results: 20)
100
- results = @service.list_files(
101
- q: "trashed=false",
102
- page_size: max_results,
103
- order_by: "modifiedTime desc",
104
- fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
105
- )
106
-
107
- files = results.files.map do |file|
108
- {
109
- id: file.id,
110
- name: file.name,
111
- mime_type: file.mime_type,
112
- size: file.size&.to_i,
113
- modified_time: file.modified_time,
114
- web_view_link: file.web_view_link
115
- }
116
- end
117
-
118
- {files: files, count: files.length}
119
- rescue Google::Apis::Error => e
120
- raise "Google Drive API Error: #{e.message}"
121
- rescue => e
122
- log_error("list_files", e)
123
- raise e
124
- end
125
-
126
- def test_connection
127
- about = @service.get_about(fields: "user,storageQuota")
128
- {
129
- ok: true,
130
- user: about.user.display_name,
131
- email: about.user.email_address,
132
- storage_used: about.storage_quota&.usage,
133
- storage_limit: about.storage_quota&.limit
134
- }
135
- rescue Google::Apis::Error => e
136
- raise "Google Drive API Error: #{e.message}"
137
- rescue => e
138
- log_error("test_connection", e)
139
- raise e
140
- end
141
-
142
- def authenticate
143
- perform_auth_flow
144
- {success: true}
145
- rescue => e
146
- {success: false, error: e.message}
147
- end
148
-
149
- def perform_auth_flow
150
- client_id = Mcpeasy::Config.google_client_id
151
- client_secret = Mcpeasy::Config.google_client_secret
152
-
153
- unless client_id && client_secret
154
- raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
155
- end
156
-
157
- # Create credentials using OAuth2 flow with localhost redirect
158
- redirect_uri = "http://localhost:8080"
159
- client = Signet::OAuth2::Client.new(
160
- client_id: client_id,
161
- client_secret: client_secret,
162
- scope: SCOPE,
163
- redirect_uri: redirect_uri,
164
- authorization_uri: "https://accounts.google.com/o/oauth2/auth",
165
- token_credential_uri: "https://oauth2.googleapis.com/token"
166
- )
167
-
168
- # Generate authorization URL
169
- url = client.authorization_uri.to_s
170
-
171
- puts "DEBUG: Client ID: #{client_id[0..20]}..."
172
- puts "DEBUG: Scope: #{SCOPE}"
173
- puts "DEBUG: Redirect URI: #{redirect_uri}"
174
- puts
175
-
176
- # Start callback server to capture OAuth code
177
- puts "Starting temporary web server to capture OAuth callback..."
178
- puts "Opening authorization URL in your default browser..."
179
- puts url
180
- puts
181
-
182
- # Automatically open URL in default browser on macOS/Unix
183
- if system("which open > /dev/null 2>&1")
184
- system("open", url)
185
- else
186
- puts "Could not automatically open browser. Please copy the URL above manually."
187
- end
188
- puts
189
- puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
190
-
191
- # Wait for the authorization code with timeout
192
- code = GoogleAuthServer.capture_auth_code
193
-
194
- unless code
195
- raise "Failed to receive authorization code. Please try again."
196
- end
197
-
198
- puts "✅ Authorization code received!"
199
- client.code = code
200
- client.fetch_access_token!
201
-
202
- # Save credentials to config
203
- credentials_data = {
204
- client_id: client.client_id,
205
- client_secret: client.client_secret,
206
- scope: client.scope,
207
- refresh_token: client.refresh_token,
208
- access_token: client.access_token,
209
- expires_at: client.expires_at
210
- }
211
-
212
- Mcpeasy::Config.save_google_token(credentials_data)
213
- puts "✅ Authentication successful! Token saved to config"
214
-
215
- client
216
- rescue => e
217
- log_error("perform_auth_flow", e)
218
- raise "Authentication flow failed: #{e.message}"
219
- end
220
-
221
- private
222
-
223
- def authorize
224
- credentials_data = Mcpeasy::Config.google_token
225
- unless credentials_data
226
- raise <<~ERROR
227
- Google Drive authentication required!
228
- Run the auth command first:
229
- mcpz gdrive auth
230
- ERROR
231
- end
232
-
233
- client = Signet::OAuth2::Client.new(
234
- client_id: credentials_data.client_id,
235
- client_secret: credentials_data.client_secret,
236
- scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
237
- refresh_token: credentials_data.refresh_token,
238
- access_token: credentials_data.access_token,
239
- token_credential_uri: "https://oauth2.googleapis.com/token"
240
- )
241
-
242
- # Check if token needs refresh
243
- if credentials_data.expires_at
244
- expires_at = if credentials_data.expires_at.is_a?(String)
245
- Time.parse(credentials_data.expires_at)
246
- else
247
- Time.at(credentials_data.expires_at)
248
- end
249
-
250
- if Time.now >= expires_at
251
- client.refresh!
252
- # Update saved credentials with new access token
253
- updated_data = {
254
- client_id: credentials_data.client_id,
255
- client_secret: credentials_data.client_secret,
256
- scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
257
- refresh_token: credentials_data.refresh_token,
258
- access_token: client.access_token,
259
- expires_at: client.expires_at
260
- }
261
- Mcpeasy::Config.save_google_token(updated_data)
262
- end
263
- end
264
-
265
- client
266
- rescue JSON::ParserError
267
- raise "Invalid token data. Please re-run: mcpz gdrive auth"
268
- rescue => e
269
- log_error("authorize", e)
270
- raise "Authentication failed: #{e.message}"
271
- end
272
-
273
- def ensure_env!
274
- unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
275
- raise <<~ERROR
276
- Google API credentials not configured!
277
- Please save your Google credentials.json file using:
278
- mcpz config set_google_credentials <path_to_credentials.json>
279
- ERROR
280
- end
281
- end
282
-
283
- def log_error(method, error)
284
- Mcpeasy::Config.ensure_config_dirs
285
- File.write(
286
- Mcpeasy::Config.log_file_path("gdrive", "error"),
287
- "#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
288
- mode: "a"
289
- )
290
- end
291
- end
@@ -1,407 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
- require "google/apis/calendar_v3"
5
- require "googleauth"
6
- require "signet/oauth_2/client"
7
- require "fileutils"
8
- require "json"
9
- require "time"
10
- require_relative "../_google/auth_server"
11
- require_relative "../../mcpeasy/config"
12
-
13
- class GmeetTool
14
- SCOPE = "https://www.googleapis.com/auth/calendar.readonly"
15
-
16
- def initialize(skip_auth: false)
17
- ensure_env!
18
- @service = Google::Apis::CalendarV3::CalendarService.new
19
- @service.authorization = authorize unless skip_auth
20
- end
21
-
22
- def list_meetings(start_date: nil, end_date: nil, max_results: 20, calendar_id: "primary")
23
- # Default to today if no start date provided
24
- start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
25
- # Default to 7 days from start if no end date provided
26
- end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 7 * 24 * 60 * 60
27
-
28
- results = @service.list_events(
29
- calendar_id,
30
- max_results: max_results,
31
- single_events: true,
32
- order_by: "startTime",
33
- time_min: start_time.utc.iso8601,
34
- time_max: end_time.utc.iso8601
35
- )
36
-
37
- # Filter for events that have Google Meet links
38
- meetings = results.items.filter_map do |event|
39
- meet_link = extract_meet_link(event)
40
- next unless meet_link
41
-
42
- {
43
- id: event.id,
44
- summary: event.summary,
45
- description: event.description,
46
- location: event.location,
47
- start: format_event_time(event.start),
48
- end: format_event_time(event.end),
49
- attendees: format_attendees(event.attendees),
50
- html_link: event.html_link,
51
- meet_link: meet_link,
52
- calendar_id: calendar_id
53
- }
54
- end
55
-
56
- {meetings: meetings, count: meetings.length}
57
- rescue Google::Apis::Error => e
58
- raise "Google Calendar API Error: #{e.message}"
59
- rescue => e
60
- log_error("list_meetings", e)
61
- raise e
62
- end
63
-
64
- def upcoming_meetings(max_results: 10, calendar_id: "primary")
65
- # Get meetings starting from now
66
- start_time = Time.now
67
- # Look ahead 24 hours by default
68
- end_time = start_time + 24 * 60 * 60
69
-
70
- results = @service.list_events(
71
- calendar_id,
72
- max_results: max_results,
73
- single_events: true,
74
- order_by: "startTime",
75
- time_min: start_time.utc.iso8601,
76
- time_max: end_time.utc.iso8601
77
- )
78
-
79
- # Filter for events that have Google Meet links and are upcoming
80
- meetings = results.items.filter_map do |event|
81
- meet_link = extract_meet_link(event)
82
- next unless meet_link
83
-
84
- event_start_time = event.start.date_time&.to_time || (event.start.date ? Time.parse(event.start.date) : nil)
85
- next unless event_start_time && event_start_time >= start_time
86
-
87
- {
88
- id: event.id,
89
- summary: event.summary,
90
- description: event.description,
91
- location: event.location,
92
- start: format_event_time(event.start),
93
- end: format_event_time(event.end),
94
- attendees: format_attendees(event.attendees),
95
- html_link: event.html_link,
96
- meet_link: meet_link,
97
- calendar_id: calendar_id,
98
- time_until_start: time_until_event(event_start_time)
99
- }
100
- end
101
-
102
- {meetings: meetings, count: meetings.length}
103
- rescue Google::Apis::Error => e
104
- raise "Google Calendar API Error: #{e.message}"
105
- rescue => e
106
- log_error("upcoming_meetings", e)
107
- raise e
108
- end
109
-
110
- def search_meetings(query, start_date: nil, end_date: nil, max_results: 10)
111
- # Default to today if no start date provided
112
- start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
113
- # Default to 30 days from start if no end date provided
114
- end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 30 * 24 * 60 * 60
115
-
116
- results = @service.list_events(
117
- "primary",
118
- q: query,
119
- max_results: max_results,
120
- single_events: true,
121
- order_by: "startTime",
122
- time_min: start_time.utc.iso8601,
123
- time_max: end_time.utc.iso8601
124
- )
125
-
126
- # Filter for events that have Google Meet links
127
- meetings = results.items.filter_map do |event|
128
- meet_link = extract_meet_link(event)
129
- next unless meet_link
130
-
131
- {
132
- id: event.id,
133
- summary: event.summary,
134
- description: event.description,
135
- location: event.location,
136
- start: format_event_time(event.start),
137
- end: format_event_time(event.end),
138
- attendees: format_attendees(event.attendees),
139
- html_link: event.html_link,
140
- meet_link: meet_link,
141
- calendar_id: "primary"
142
- }
143
- end
144
-
145
- {meetings: meetings, count: meetings.length}
146
- rescue Google::Apis::Error => e
147
- raise "Google Calendar API Error: #{e.message}"
148
- rescue => e
149
- log_error("search_meetings", e)
150
- raise e
151
- end
152
-
153
- def get_meeting_url(event_id, calendar_id: "primary")
154
- event = @service.get_event(calendar_id, event_id)
155
- meet_link = extract_meet_link(event)
156
-
157
- unless meet_link
158
- raise "No Google Meet link found for this event"
159
- end
160
-
161
- {
162
- event_id: event_id,
163
- summary: event.summary,
164
- meet_link: meet_link,
165
- start: format_event_time(event.start),
166
- end: format_event_time(event.end)
167
- }
168
- rescue Google::Apis::Error => e
169
- raise "Google Calendar API Error: #{e.message}"
170
- rescue => e
171
- log_error("get_meeting_url", e)
172
- raise e
173
- end
174
-
175
- def test_connection
176
- calendar = @service.get_calendar("primary")
177
- {
178
- ok: true,
179
- user: calendar.summary,
180
- email: calendar.id
181
- }
182
- rescue Google::Apis::Error => e
183
- raise "Google Calendar API Error: #{e.message}"
184
- rescue => e
185
- log_error("test_connection", e)
186
- raise e
187
- end
188
-
189
- def authenticate
190
- perform_auth_flow
191
- {success: true}
192
- rescue => e
193
- {success: false, error: e.message}
194
- end
195
-
196
- def perform_auth_flow
197
- client_id = Mcpeasy::Config.google_client_id
198
- client_secret = Mcpeasy::Config.google_client_secret
199
-
200
- unless client_id && client_secret
201
- raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
202
- end
203
-
204
- # Create credentials using OAuth2 flow with localhost redirect
205
- redirect_uri = "http://localhost:8080"
206
- client = Signet::OAuth2::Client.new(
207
- client_id: client_id,
208
- client_secret: client_secret,
209
- scope: SCOPE,
210
- redirect_uri: redirect_uri,
211
- authorization_uri: "https://accounts.google.com/o/oauth2/auth",
212
- token_credential_uri: "https://oauth2.googleapis.com/token"
213
- )
214
-
215
- # Generate authorization URL
216
- url = client.authorization_uri.to_s
217
-
218
- puts "DEBUG: Client ID: #{client_id[0..20]}..."
219
- puts "DEBUG: Scope: #{SCOPE}"
220
- puts "DEBUG: Redirect URI: #{redirect_uri}"
221
- puts
222
-
223
- # Start callback server to capture OAuth code
224
- puts "Starting temporary web server to capture OAuth callback..."
225
- puts "Opening authorization URL in your default browser..."
226
- puts url
227
- puts
228
-
229
- # Automatically open URL in default browser on macOS/Unix
230
- if system("which open > /dev/null 2>&1")
231
- system("open", url)
232
- else
233
- puts "Could not automatically open browser. Please copy the URL above manually."
234
- end
235
- puts
236
- puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
237
-
238
- # Wait for the authorization code with timeout
239
- code = GoogleAuthServer.capture_auth_code
240
-
241
- unless code
242
- raise "Failed to receive authorization code. Please try again."
243
- end
244
-
245
- puts "✅ Authorization code received!"
246
- client.code = code
247
- client.fetch_access_token!
248
-
249
- # Save credentials to config
250
- credentials_data = {
251
- client_id: client.client_id,
252
- client_secret: client.client_secret,
253
- scope: client.scope,
254
- refresh_token: client.refresh_token,
255
- access_token: client.access_token,
256
- expires_at: client.expires_at
257
- }
258
-
259
- Mcpeasy::Config.save_google_token(credentials_data)
260
- puts "✅ Authentication successful! Token saved to config"
261
-
262
- client
263
- rescue => e
264
- log_error("perform_auth_flow", e)
265
- raise "Authentication flow failed: #{e.message}"
266
- end
267
-
268
- private
269
-
270
- def authorize
271
- credentials_data = Mcpeasy::Config.google_token
272
- unless credentials_data
273
- raise <<~ERROR
274
- Google Calendar authentication required!
275
- Run the auth command first:
276
- mcpz gmeet auth
277
- ERROR
278
- end
279
-
280
- client = Signet::OAuth2::Client.new(
281
- client_id: credentials_data.client_id,
282
- client_secret: credentials_data.client_secret,
283
- scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
284
- refresh_token: credentials_data.refresh_token,
285
- access_token: credentials_data.access_token,
286
- token_credential_uri: "https://oauth2.googleapis.com/token"
287
- )
288
-
289
- # Check if token needs refresh
290
- if credentials_data.expires_at
291
- expires_at = if credentials_data.expires_at.is_a?(String)
292
- Time.parse(credentials_data.expires_at)
293
- else
294
- Time.at(credentials_data.expires_at)
295
- end
296
-
297
- if Time.now >= expires_at
298
- client.refresh!
299
- # Update saved credentials with new access token
300
- updated_data = {
301
- client_id: credentials_data.client_id,
302
- client_secret: credentials_data.client_secret,
303
- scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
304
- refresh_token: credentials_data.refresh_token,
305
- access_token: client.access_token,
306
- expires_at: client.expires_at
307
- }
308
- Mcpeasy::Config.save_google_token(updated_data)
309
- end
310
- end
311
-
312
- client
313
- rescue JSON::ParserError
314
- raise "Invalid token data. Please re-run: mcpz gmeet auth"
315
- rescue => e
316
- log_error("authorize", e)
317
- raise "Authentication failed: #{e.message}"
318
- end
319
-
320
- def ensure_env!
321
- unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
322
- raise <<~ERROR
323
- Google API credentials not configured!
324
- Please save your Google credentials.json file using:
325
- mcpz config set_google_credentials <path_to_credentials.json>
326
- ERROR
327
- end
328
- end
329
-
330
- def extract_meet_link(event)
331
- # Check various places where Google Meet links might be stored
332
-
333
- # 1. Check conference data (most reliable)
334
- if event.conference_data&.conference_solution&.name == "Google Meet"
335
- return event.conference_data.entry_points&.find { |ep| ep.entry_point_type == "video" }&.uri
336
- end
337
-
338
- # 2. Check hangout link (legacy)
339
- return event.hangout_link if event.hangout_link
340
-
341
- # 3. Check description for meet.google.com links
342
- if event.description
343
- meet_match = event.description.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
344
- return meet_match[0] if meet_match
345
- end
346
-
347
- # 4. Check location field
348
- if event.location
349
- meet_match = event.location.match(/https:\/\/meet\.google\.com\/[a-z-]+/)
350
- return meet_match[0] if meet_match
351
- end
352
-
353
- nil
354
- end
355
-
356
- def format_event_time(event_time)
357
- return nil unless event_time
358
-
359
- if event_time.date
360
- # All-day event
361
- {date: event_time.date}
362
- elsif event_time.date_time
363
- # Specific time event
364
- {date_time: event_time.date_time.iso8601}
365
- else
366
- nil
367
- end
368
- end
369
-
370
- def format_attendees(attendees)
371
- return [] unless attendees
372
-
373
- attendees.map do |attendee|
374
- attendee.email
375
- end.compact
376
- end
377
-
378
- def time_until_event(event_time)
379
- now = Time.now
380
- event_time = event_time.is_a?(String) ? Time.parse(event_time) : event_time
381
-
382
- diff_seconds = (event_time - now).to_i
383
- return "started" if diff_seconds < 0
384
-
385
- if diff_seconds < 60
386
- "#{diff_seconds} seconds"
387
- elsif diff_seconds < 3600
388
- "#{diff_seconds / 60} minutes"
389
- elsif diff_seconds < 86400
390
- hours = diff_seconds / 3600
391
- minutes = (diff_seconds % 3600) / 60
392
- "#{hours}h #{minutes}m"
393
- else
394
- days = diff_seconds / 86400
395
- "#{days} days"
396
- end
397
- end
398
-
399
- def log_error(method, error)
400
- Mcpeasy::Config.ensure_config_dirs
401
- File.write(
402
- Mcpeasy::Config.log_file_path("gmeet", "error"),
403
- "#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
404
- mode: "a"
405
- )
406
- end
407
- end