google_calendar 0.4.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile.lock +25 -55
- data/Guardfile +7 -7
- data/README.rdoc +3 -1
- data/VERSION +1 -1
- data/google_calendar.gemspec +7 -7
- data/lib/google/calendar.rb +26 -17
- data/lib/google/calendar_list.rb +43 -0
- data/lib/google/calendar_list_entry.rb +40 -0
- data/lib/google/connection.rb +26 -22
- data/lib/google/event.rb +142 -19
- data/lib/google/freebusy.rb +76 -0
- data/lib/google_calendar.rb +3 -0
- data/readme_code.rb +1 -0
- data/test/helper.rb +2 -2
- data/test/mocks/find_calendar_list.json +69 -0
- data/test/mocks/freebusy_query.json +22 -0
- data/test/mocks/query_events.json +2 -1
- data/test/test_google_calendar.rb +191 -13
- metadata +40 -73
data/lib/google/event.rb
CHANGED
@@ -8,13 +8,14 @@ module Google
|
|
8
8
|
#
|
9
9
|
# === Attributes
|
10
10
|
#
|
11
|
-
# * +id+ - The google assigned id of the event (nil until saved). Read
|
11
|
+
# * +id+ - The google assigned id of the event (nil until saved). Read Write.
|
12
12
|
# * +status+ - The status of the event (confirmed, tentative or cancelled). Read only.
|
13
13
|
# * +title+ - The title of the event. Read Write.
|
14
14
|
# * +description+ - The content of the event. Read Write.
|
15
15
|
# * +location+ - The location of the event. Read Write.
|
16
16
|
# * +start_time+ - The start time of the event (Time object, defaults to now). Read Write.
|
17
17
|
# * +end_time+ - The end time of the event (Time object, defaults to one hour from now). Read Write.
|
18
|
+
# * +recurrence+ - A hash containing recurrence info for repeating events. Read write.
|
18
19
|
# * +calendar+ - What calendar the event belongs to. Read Write.
|
19
20
|
# * +all_day + - Does the event run all day. Read Write.
|
20
21
|
# * +quickadd+ - A string that Google parses when setting up a new event. If set and then saved it will take priority over any attributes you have set. Read Write.
|
@@ -24,10 +25,11 @@ module Google
|
|
24
25
|
# * +duration+ - The duration of the event in seconds. Read only.
|
25
26
|
# * +html_link+ - An absolute link to this event in the Google Calendar Web UI. Read only.
|
26
27
|
# * +raw+ - The full google json representation of the event. Read only.
|
28
|
+
# * +visibility+ - The visibility of the event (*'default'*, 'public', 'private', 'confidential'). Read Write.
|
27
29
|
#
|
28
30
|
class Event
|
29
|
-
attr_reader :
|
30
|
-
attr_accessor :title, :location, :calendar, :quickadd, :transparency, :attendees, :description, :reminders
|
31
|
+
attr_reader :raw, :html_link, :status
|
32
|
+
attr_accessor :id, :title, :location, :calendar, :quickadd, :transparency, :attendees, :description, :reminders, :recurrence, :visibility, :creator_name, :color_id
|
31
33
|
|
32
34
|
#
|
33
35
|
# Create a new event, and optionally set it's attributes.
|
@@ -36,25 +38,37 @@ module Google
|
|
36
38
|
#
|
37
39
|
# event = Google::Event.new
|
38
40
|
# event.calendar = AnInstanceOfGoogleCalendaer
|
41
|
+
# event.id = "0123456789abcdefghijklmopqrstuv"
|
39
42
|
# event.start_time = Time.now
|
40
43
|
# event.end_time = Time.now + (60 * 60)
|
44
|
+
# event.recurrence = {'freq' => 'monthly'}
|
41
45
|
# event.title = "Go Swimming"
|
42
46
|
# event.description = "The polar bear plunge"
|
43
47
|
# event.location = "In the arctic ocean"
|
44
48
|
# event.transparency = "opaque"
|
45
|
-
# event.
|
49
|
+
# event.visibility = "public"
|
50
|
+
# event.reminders = {'useDefault' => false, 'overrides' => ['minutes' => 10, 'method' => "popup"]}
|
46
51
|
# event.attendees = [
|
47
52
|
# {'email' => 'some.a.one@gmail.com', 'displayName' => 'Some A One', 'responseStatus' => 'tentative'},
|
48
53
|
# {'email' => 'some.b.one@gmail.com', 'displayName' => 'Some B One', 'responseStatus' => 'tentative'}
|
49
54
|
# ]
|
50
55
|
#
|
51
56
|
def initialize(params = {})
|
52
|
-
[:id, :status, :raw, :html_link, :title, :location, :calendar, :quickadd, :attendees, :description, :reminders, :start_time, :end_time,
|
57
|
+
[:id, :status, :raw, :html_link, :title, :location, :calendar, :quickadd, :attendees, :description, :reminders, :recurrence, :start_time, :end_time, :color_id].each do |attribute|
|
53
58
|
instance_variable_set("@#{attribute}", params[attribute])
|
54
59
|
end
|
55
60
|
|
61
|
+
self.visibility = params[:visibility]
|
56
62
|
self.transparency = params[:transparency]
|
57
63
|
self.all_day = params[:all_day] if params[:all_day]
|
64
|
+
self.creator_name = params[:creator]['displayName'] if params[:creator]
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Sets the id of the Event.
|
69
|
+
#
|
70
|
+
def id=(id)
|
71
|
+
@id = Event.parse_id(id) unless id.nil?
|
58
72
|
end
|
59
73
|
|
60
74
|
#
|
@@ -124,21 +138,50 @@ module Google
|
|
124
138
|
# Stores reminders for this event. Multiple reminders are allowed.
|
125
139
|
#
|
126
140
|
# Examples
|
127
|
-
#
|
141
|
+
#
|
128
142
|
# event = cal.create_event do |e|
|
129
143
|
# e.title = 'Some Event'
|
130
144
|
# e.start_time = Time.now + (60 * 10)
|
131
145
|
# e.end_time = Time.now + (60 * 60) # seconds * min
|
132
|
-
# e.reminders = { 'useDefault' => false, 'overrides' => [{method: 'email', minutes: 4}, {method: 'popup', minutes: 60}, {method: 'sms', minutes: 30}]}
|
146
|
+
# e.reminders = { 'useDefault' => false, 'overrides' => [{method: 'email', minutes: 4}, {method: 'popup', minutes: 60}, {method: 'sms', minutes: 30}]}
|
133
147
|
# end
|
134
|
-
#
|
148
|
+
#
|
135
149
|
# event = Event.new :start_time => "2012-03-31", :end_time => "2012-04-03", :reminders => { 'useDefault' => false, 'overrides' => [{'minutes' => 10, 'method' => "popup"}]}
|
136
150
|
#
|
137
151
|
def reminders
|
138
152
|
@reminders ||= {}
|
139
153
|
end
|
140
154
|
|
141
|
-
#
|
155
|
+
#
|
156
|
+
# Stores recurrence rules for repeating events.
|
157
|
+
#
|
158
|
+
# Allowed contents:
|
159
|
+
# :freq => frequence information ("daily", "weekly", "monthly", "yearly") REQUIRED
|
160
|
+
# :count => how many times the repeating event should occur OPTIONAL
|
161
|
+
# :until => Time class, until when the event should occur OPTIONAL
|
162
|
+
# :interval => how often should the event occur (every "2" weeks, ...) OPTIONAL
|
163
|
+
# :byday => if frequence is "weekly", contains ordered (starting with OPTIONAL
|
164
|
+
# Sunday)comma separated abbreviations of days the event
|
165
|
+
# should occur on ("su,mo,th")
|
166
|
+
# if frequence is "monthly", can specify which day of month
|
167
|
+
# the event should occur on ("2mo" - second Monday, "-1th" - last Thursday,
|
168
|
+
# allowed indices are 1,2,3,4,-1)
|
169
|
+
#
|
170
|
+
# Note: The hash should not contain :count and :until keys simultaneously.
|
171
|
+
#
|
172
|
+
# ===== Example
|
173
|
+
# event = cal.create_event do |e|
|
174
|
+
# e.title = 'Work-day Event'
|
175
|
+
# e.start_time = Time.now
|
176
|
+
# e.end_time = Time.now + (60 * 60) # seconds * min
|
177
|
+
# e.recurrence = {freq: "weekly", byday: "mo,tu,we,th,fr"}
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
def recurrence
|
181
|
+
@recurrence ||= {}
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
142
185
|
# Utility method that simplifies setting the transparency of an event.
|
143
186
|
# You can pass true or false. Defaults to transparent.
|
144
187
|
#
|
@@ -161,11 +204,22 @@ module Google
|
|
161
204
|
#
|
162
205
|
# Returns true if the event is opaque otherwise returns false.
|
163
206
|
# Opaque events block time on a calendar.
|
164
|
-
#
|
207
|
+
#
|
165
208
|
def opaque?
|
166
209
|
@transparency == "opaque"
|
167
210
|
end
|
168
211
|
|
212
|
+
#
|
213
|
+
# Sets the visibility of the Event.
|
214
|
+
#
|
215
|
+
def visibility=(val)
|
216
|
+
if val
|
217
|
+
@visibility = Event.parse_visibility(val)
|
218
|
+
else
|
219
|
+
@visibility = "default"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
169
223
|
#
|
170
224
|
# Convenience method used to build an array of events from a Google feed.
|
171
225
|
#
|
@@ -180,21 +234,30 @@ module Google
|
|
180
234
|
def to_json
|
181
235
|
"{
|
182
236
|
\"summary\": \"#{title}\",
|
183
|
-
\"
|
184
|
-
\"
|
237
|
+
\"visibility\": \"#{visibility}\",
|
238
|
+
\"description\": \"#{description}\",
|
239
|
+
\"location\": \"#{location}\",
|
185
240
|
\"start\": {
|
186
241
|
\"dateTime\": \"#{start_time}\"
|
242
|
+
#{timezone_needed? ? local_timezone_json : ''}
|
187
243
|
},
|
188
244
|
\"end\": {
|
189
245
|
\"dateTime\": \"#{end_time}\"
|
246
|
+
#{timezone_needed? ? local_timezone_json : ''}
|
190
247
|
},
|
248
|
+
#{recurrence_json}
|
249
|
+
#{color_json}
|
191
250
|
#{attendees_json}
|
192
251
|
\"reminders\": {
|
193
252
|
#{reminders_json}
|
194
253
|
}
|
195
254
|
}"
|
196
255
|
end
|
197
|
-
|
256
|
+
|
257
|
+
def color_json
|
258
|
+
return unless color_id
|
259
|
+
"\"colorId\": \"#{color_id}\","
|
260
|
+
end
|
198
261
|
#
|
199
262
|
# JSON representation of attendees
|
200
263
|
#
|
@@ -203,7 +266,7 @@ module Google
|
|
203
266
|
|
204
267
|
attendees = @attendees.map do |attendee|
|
205
268
|
"{
|
206
|
-
\"displayName\": \"#{attendee['displayName']}\",
|
269
|
+
\"displayName\": \"#{attendee['displayName']}\",
|
207
270
|
\"email\": \"#{attendee['email']}\",
|
208
271
|
\"responseStatus\": \"#{attendee['responseStatus']}\"
|
209
272
|
}"
|
@@ -216,10 +279,10 @@ module Google
|
|
216
279
|
# JSON representation of a reminder
|
217
280
|
#
|
218
281
|
def reminders_json
|
219
|
-
if reminders && reminders.is_a?(Hash) && reminders['overrides']
|
282
|
+
if reminders && reminders.is_a?(Hash) && reminders['overrides']
|
220
283
|
overrides = reminders['overrides'].map do |reminder|
|
221
284
|
"{
|
222
|
-
\"method\": \"#{reminder['method']}\",
|
285
|
+
\"method\": \"#{reminder['method']}\",
|
223
286
|
\"minutes\": #{reminder['minutes']}
|
224
287
|
}"
|
225
288
|
end.join(",\n")
|
@@ -229,11 +292,38 @@ module Google
|
|
229
292
|
end
|
230
293
|
end
|
231
294
|
|
295
|
+
#
|
296
|
+
# Timezone info is needed only at recurring events
|
297
|
+
#
|
298
|
+
def timezone_needed?
|
299
|
+
@recurrence && @recurrence[:freq]
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# JSON representation of local timezone
|
304
|
+
#
|
305
|
+
def local_timezone_json
|
306
|
+
",\"timeZone\" : \"#{Time.now.getlocal.zone}\""
|
307
|
+
end
|
308
|
+
|
309
|
+
#
|
310
|
+
# JSON representation of recurrence rules for repeating events
|
311
|
+
#
|
312
|
+
def recurrence_json
|
313
|
+
return unless @recurrence && @recurrence[:freq]
|
314
|
+
|
315
|
+
@recurrence[:until] = @recurrence[:until].strftime('%Y%m%dT%H%M%SZ') if @recurrence[:until]
|
316
|
+
rrule = "RRULE:" + @recurrence.collect { |k,v| "#{k}=#{v}" }.join(';').upcase
|
317
|
+
@recurrence[:until] = Time.parse(@recurrence[:until]) if @recurrence[:until]
|
318
|
+
|
319
|
+
"\"recurrence\": [\n\"#{rrule}\"],"
|
320
|
+
end
|
321
|
+
|
232
322
|
#
|
233
323
|
# String representation of an event object.
|
234
324
|
#
|
235
325
|
def to_s
|
236
|
-
"Event Id '#{self.id}'\n\tStatus: #{status}\n\tTitle: #{title}\n\tStarts: #{start_time}\n\tEnds: #{end_time}\n\tLocation: #{location}\n\tDescription: #{description}\n\n"
|
326
|
+
"Event Id '#{self.id}'\n\tStatus: #{status}\n\tTitle: #{title}\n\tStarts: #{start_time}\n\tEnds: #{end_time}\n\tLocation: #{location}\n\tDescription: #{description}\n\tColor: #{color_id}\n\n"
|
237
327
|
end
|
238
328
|
|
239
329
|
#
|
@@ -281,16 +371,34 @@ module Google
|
|
281
371
|
:title => e['summary'],
|
282
372
|
:description => e['description'],
|
283
373
|
:location => e['location'],
|
374
|
+
:creator => e['creator'],
|
284
375
|
:start_time => Event.parse_json_time(e['start']),
|
285
376
|
:end_time => Event.parse_json_time(e['end']),
|
286
377
|
:transparency => e['transparency'],
|
287
378
|
:html_link => e['htmlLink'],
|
288
379
|
:updated => e['updated'],
|
289
380
|
:reminders => e['reminders'],
|
290
|
-
:attendees => e['attendees']
|
381
|
+
:attendees => e['attendees'],
|
382
|
+
:recurrence => Event.parse_recurrence_rule(e['recurrence']),
|
383
|
+
:visibility => e['visibility'],
|
384
|
+
:color_id => e['colorId'])
|
291
385
|
|
292
386
|
end
|
293
387
|
|
388
|
+
#
|
389
|
+
# Parse recurrence rule
|
390
|
+
# Returns hash with recurrence info
|
391
|
+
#
|
392
|
+
def self.parse_recurrence_rule(recurrence_entry)
|
393
|
+
return {} unless recurrence_entry && recurrence_entry != []
|
394
|
+
|
395
|
+
rrule = recurrence_entry[0].sub('RRULE:', '')
|
396
|
+
rhash = Hash[*rrule.downcase.split(/[=;]/)]
|
397
|
+
|
398
|
+
rhash[:until] = Time.parse(rhash[:until]) if rhash[:until]
|
399
|
+
rhash
|
400
|
+
end
|
401
|
+
|
294
402
|
#
|
295
403
|
# Set the ID after google assigns it (only necessary when we are creating a new event)
|
296
404
|
#
|
@@ -308,7 +416,7 @@ module Google
|
|
308
416
|
Time.parse(time_hash['dateTime']).utc
|
309
417
|
else
|
310
418
|
Time.now.utc
|
311
|
-
end
|
419
|
+
end
|
312
420
|
end
|
313
421
|
|
314
422
|
#
|
@@ -319,5 +427,20 @@ module Google
|
|
319
427
|
(time.is_a? String) ? Time.parse(time) : time.dup.utc
|
320
428
|
end
|
321
429
|
|
430
|
+
#
|
431
|
+
# Validates id format
|
432
|
+
#
|
433
|
+
def self.parse_id(id)
|
434
|
+
raise ArgumentError, "Event ID is invalid. Please check Google documentation: https://developers.google.com/google-apps/calendar/v3/reference/events/insert" unless id.gsub(/(^[a-v0-9]{5,1024}$)/o)
|
435
|
+
end
|
436
|
+
|
437
|
+
#
|
438
|
+
# Validates visibility value
|
439
|
+
#
|
440
|
+
def self.parse_visibility(visibility)
|
441
|
+
raise ArgumentError, "Event visibility must be 'default', 'public', 'private' or 'confidential'." unless ['default', 'public', 'private', 'confidential'].include?(visibility)
|
442
|
+
return visibility
|
443
|
+
end
|
444
|
+
|
322
445
|
end
|
323
446
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Google
|
5
|
+
|
6
|
+
#
|
7
|
+
# Freebusy returns free/busy information for a set of calendars
|
8
|
+
#
|
9
|
+
class Freebusy
|
10
|
+
|
11
|
+
attr_reader :connection
|
12
|
+
|
13
|
+
#
|
14
|
+
# Setup and query the free/busy status of a collection of calendars.
|
15
|
+
#
|
16
|
+
# The +params+ parameter accepts
|
17
|
+
# * :client_id => the client ID that you received from Google after registering your application with them (https://console.developers.google.com/). REQUIRED
|
18
|
+
# * :client_secret => the client secret you received from Google after registering your application with them. REQUIRED
|
19
|
+
# * :redirect_url => the url where your users will be redirected to after they have successfully permitted access to their calendars. Use 'urn:ietf:wg:oauth:2.0:oob' if you are using an 'application'" REQUIRED
|
20
|
+
# * :refresh_token => if a user has already given you access to their calendars, you can specify their refresh token here and you will be 'logged on' automatically (i.e. they don't need to authorize access again). OPTIONAL
|
21
|
+
#
|
22
|
+
# See Readme.rdoc or readme_code.rb for an explication on the OAuth2 authorization process.
|
23
|
+
#
|
24
|
+
def initialize(params={}, connection=nil)
|
25
|
+
@connection = connection || Connection.new(
|
26
|
+
:client_id => params[:client_id],
|
27
|
+
:client_secret => params[:client_secret],
|
28
|
+
:refresh_token => params[:refresh_token],
|
29
|
+
:redirect_url => params[:redirect_url]
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Find the busy times of the supplied calendar IDs, within the boundaries
|
35
|
+
# of the supplied start_time and end_time
|
36
|
+
#
|
37
|
+
# The arguments supplied are
|
38
|
+
# * calendar_ids => array of Google calendar IDs as strings
|
39
|
+
# * start_time => a Time object, the start of the interval for the query.
|
40
|
+
# * end_time => a Time object, the end of the interval for the query.
|
41
|
+
#
|
42
|
+
def query(calendar_ids, start_time, end_time)
|
43
|
+
query_content = json_for_query(calendar_ids, start_time, end_time)
|
44
|
+
response = @connection.send("/freeBusy", :post, query_content)
|
45
|
+
|
46
|
+
return nil if response.status != 200 || response.body.empty?
|
47
|
+
|
48
|
+
parse_freebusy_response(response.body)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
#
|
54
|
+
# Prepare the JSON
|
55
|
+
#
|
56
|
+
def json_for_query(calendar_ids, start_time, end_time)
|
57
|
+
{}.tap{ |obj|
|
58
|
+
obj[:items] = calendar_ids.map {|id| Hash[:id, id] }
|
59
|
+
obj[:timeMin] = start_time.utc.iso8601
|
60
|
+
obj[:timeMax] = end_time.utc.iso8601
|
61
|
+
}.to_json
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_freebusy_response(response_body)
|
65
|
+
query_result = JSON.parse(response_body)
|
66
|
+
|
67
|
+
return nil unless query_result['calendars'].is_a? Hash
|
68
|
+
|
69
|
+
query_result['calendars'].each_with_object({}) do |(calendar_id, value), result|
|
70
|
+
result[calendar_id] = value['busy'] || []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/google_calendar.rb
CHANGED
data/readme_code.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -14,7 +14,7 @@ rescue Bundler::BundlerError => e
|
|
14
14
|
end
|
15
15
|
|
16
16
|
require "minitest/autorun"
|
17
|
-
require 'minitest/reporters'
|
17
|
+
require 'minitest/reporters'
|
18
18
|
require 'shoulda/context'
|
19
19
|
require 'mocha/setup'
|
20
20
|
require 'faraday'
|
@@ -27,4 +27,4 @@ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
|
27
27
|
|
28
28
|
class Minitest::Test
|
29
29
|
@@mock_path = File.expand_path(File.join(File.dirname(__FILE__), 'mocks'))
|
30
|
-
end
|
30
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
{
|
2
|
+
"kind": "calendar#calendarList",
|
3
|
+
"etag": "\"1420633154361000\"",
|
4
|
+
"nextSyncToken": "00001420633154361000",
|
5
|
+
"items": [
|
6
|
+
{
|
7
|
+
"kind": "calendar#calendarListEntry",
|
8
|
+
"etag": "\"1420564622795000\"",
|
9
|
+
"id": "initech.com_ed493d0a9b46ea46c3a0d48611ce@resource.calendar.google.com",
|
10
|
+
"summary": "Small cubicle",
|
11
|
+
"timeZone": "America/Los_Angeles",
|
12
|
+
"colorId": "2",
|
13
|
+
"backgroundColor": "#d06b64",
|
14
|
+
"foregroundColor": "#000000",
|
15
|
+
"accessRole": "owner",
|
16
|
+
"defaultReminders": []
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"kind": "calendar#calendarListEntry",
|
20
|
+
"etag": "\"1420564132697000\"",
|
21
|
+
"id": "initech.com_db18a4e59c230a5cc5d2b069a30f@resource.calendar.google.com",
|
22
|
+
"summary": "Large cubicle",
|
23
|
+
"timeZone": "America/Los_Angeles",
|
24
|
+
"colorId": "3",
|
25
|
+
"backgroundColor": "#f83a22",
|
26
|
+
"foregroundColor": "#000000",
|
27
|
+
"accessRole": "reader",
|
28
|
+
"defaultReminders": []
|
29
|
+
},
|
30
|
+
{
|
31
|
+
"kind": "calendar#calendarListEntry",
|
32
|
+
"etag": "\"1420564622222000\"",
|
33
|
+
"id": "bob@initech.com",
|
34
|
+
"summary": "Bob's Calendar",
|
35
|
+
"timeZone": "Europe/London",
|
36
|
+
"colorId": "17",
|
37
|
+
"backgroundColor": "#9a9cff",
|
38
|
+
"foregroundColor": "#000000",
|
39
|
+
"accessRole": "owner",
|
40
|
+
"defaultReminders": [
|
41
|
+
{
|
42
|
+
"method": "popup",
|
43
|
+
"minutes": 10
|
44
|
+
}
|
45
|
+
],
|
46
|
+
"notificationSettings": {
|
47
|
+
"notifications": [
|
48
|
+
{
|
49
|
+
"type": "eventCreation",
|
50
|
+
"method": "email"
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"type": "eventChange",
|
54
|
+
"method": "email"
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"type": "eventCancellation",
|
58
|
+
"method": "email"
|
59
|
+
},
|
60
|
+
{
|
61
|
+
"type": "eventResponse",
|
62
|
+
"method": "email"
|
63
|
+
}
|
64
|
+
]
|
65
|
+
},
|
66
|
+
"primary": true
|
67
|
+
}
|
68
|
+
]
|
69
|
+
}
|