diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index a2db8cf4..df2bf053 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -4747,6 +4747,15 @@ paths: schema: type: string nullable: true + - name: notify_existing_attendees + in: query + description: '(office365 only) set to false when the only change is adding + attendees, so that existing attendees aren''t notified by email. Warning: + while false, only the attendee changes are propagated to the provider - + any other edits in the request body are ignored' + example: "false" + schema: + type: boolean responses: 200: description: OK @@ -4983,6 +4992,15 @@ paths: schema: type: string nullable: true + - name: notify_existing_attendees + in: query + description: '(office365 only) set to false when the only change is adding + attendees, so that existing attendees aren''t notified by email. Warning: + while false, only the attendee changes are propagated to the provider - + any other edits in the request body are ignored' + example: "false" + schema: + type: boolean responses: 200: description: OK @@ -5086,6 +5104,13 @@ paths: schema: type: string nullable: true + - name: notify_existing_attendees + in: query + description: (office365 only) when true, existing attendees are also emailed + about the change, otherwise only the new attendee is notified + example: "false" + schema: + type: boolean responses: 200: description: OK @@ -12041,6 +12066,7 @@ components: type: integer format: Int64 nullable: true + required: [] String: type: string _Array_NamedTuple_asset_id__String__booking_start__Int64__booking_end__Int64_____Array_String__: @@ -12102,6 +12128,7 @@ components: type: integer format: Int64 nullable: true + required: [] PlaceCalendar__Event__Attendee: type: object properties: @@ -12531,6 +12558,7 @@ components: type: integer format: Int64 nullable: true + required: [] PlaceCalendar__Calendar: type: object properties: @@ -13018,6 +13046,7 @@ components: id: type: string nullable: true + required: [] _PlaceCalendar__Event__Attendee___PlaceOS__Model__Attendee_: anyOf: - type: object @@ -13447,6 +13476,7 @@ components: type: integer format: Int64 nullable: true + required: [] JSON__Any: type: object _PlaceCalendar__Event___Nil_: @@ -13852,6 +13882,7 @@ components: type: integer format: Int64 nullable: true + required: [] _Array_NamedTuple_system_id__String__event_start__Int64__event_end__Int64_____Array_String__: type: array PlaceCalendar__Group: @@ -14058,6 +14089,7 @@ components: postalCode: type: string nullable: true + required: [] nullable: true displayName: type: string @@ -14084,6 +14116,7 @@ components: type: number format: Float64 nullable: true + required: [] nullable: true id: type: string @@ -14177,6 +14210,7 @@ components: additionalProperties: type: object nullable: true + required: [] PlaceOS__Model__Survey: type: object properties: @@ -14239,6 +14273,7 @@ components: type: integer format: Int64 nullable: true + required: [] PlaceOS__Model__Survey__Answer: type: object properties: @@ -14268,6 +14303,7 @@ components: type: integer format: Int64 nullable: true + required: [] Array_PlaceOS__Model__Survey__Answer_: type: array items: @@ -14299,6 +14335,7 @@ components: type: integer format: Int64 nullable: true + required: [] PlaceOS__Model__Survey__Invitation: type: object properties: @@ -14327,6 +14364,7 @@ components: type: integer format: Int64 nullable: true + required: [] PlaceOS__Model__Survey__Question: type: object properties: @@ -14373,6 +14411,7 @@ components: type: integer format: Int64 nullable: true + required: [] Office365__ChatMessageList: type: object properties: @@ -14426,6 +14465,7 @@ components: contentType: type: string nullable: true + required: [] chatId: type: string nullable: true @@ -14491,6 +14531,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -14512,6 +14553,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -14533,6 +14575,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -14554,7 +14597,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -14596,6 +14641,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -14617,6 +14663,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -14638,6 +14685,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -14659,7 +14707,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -14681,6 +14731,7 @@ components: required: - id nullable: true + required: [] nullable: true id: type: string @@ -14748,6 +14799,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -14769,6 +14821,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -14790,6 +14843,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -14811,7 +14865,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -14867,6 +14923,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -14888,6 +14945,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -14909,6 +14967,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -14930,7 +14989,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -14972,6 +15033,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -14993,6 +15055,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15014,6 +15077,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15035,15 +15099,19 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true mentionText: type: string nullable: true + required: [] nullable: true messageHistory: type: array @@ -15116,6 +15184,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15137,6 +15206,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15158,6 +15228,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15179,7 +15250,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -15221,6 +15294,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15242,6 +15316,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15263,6 +15338,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15284,7 +15360,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -15326,6 +15404,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15347,6 +15426,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15368,6 +15448,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15389,12 +15470,16 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true + required: [] nullable: true required: - actions @@ -15436,6 +15521,7 @@ components: items: type: string nullable: true + required: [] nullable: true userAction: type: string @@ -15453,6 +15539,7 @@ components: - block_access - block_access_external nullable: true + required: [] nullable: true reactions: type: array @@ -15511,6 +15598,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15532,6 +15620,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15553,6 +15642,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15574,7 +15664,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -15616,6 +15708,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15637,6 +15730,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15658,6 +15752,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15679,7 +15774,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -15721,6 +15818,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15742,6 +15840,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15763,6 +15862,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15784,12 +15884,16 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true + required: [] nullable: true replyToId: type: string @@ -15849,6 +15953,7 @@ components: contentType: type: string nullable: true + required: [] chatId: type: string nullable: true @@ -15914,6 +16019,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -15935,6 +16041,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -15956,6 +16063,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -15977,7 +16085,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16019,6 +16129,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16040,6 +16151,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16061,6 +16173,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16082,7 +16195,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16104,6 +16219,7 @@ components: required: - id nullable: true + required: [] nullable: true id: type: string @@ -16171,6 +16287,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16192,6 +16309,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16213,6 +16331,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16234,7 +16353,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16290,6 +16411,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16311,6 +16433,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16332,6 +16455,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16353,7 +16477,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16395,6 +16521,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16416,6 +16543,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16437,6 +16565,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16458,15 +16587,19 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true mentionText: type: string nullable: true + required: [] nullable: true messageHistory: type: array @@ -16539,6 +16672,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16560,6 +16694,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16581,6 +16716,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16602,7 +16738,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16644,6 +16782,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16665,6 +16804,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16686,6 +16826,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16707,7 +16848,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -16749,6 +16892,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16770,6 +16914,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16791,6 +16936,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16812,12 +16958,16 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true + required: [] nullable: true required: - actions @@ -16859,6 +17009,7 @@ components: items: type: string nullable: true + required: [] nullable: true userAction: type: string @@ -16876,6 +17027,7 @@ components: - block_access - block_access_external nullable: true + required: [] nullable: true reactions: type: array @@ -16934,6 +17086,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -16955,6 +17108,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -16976,6 +17130,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -16997,7 +17152,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -17039,6 +17196,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -17060,6 +17218,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -17081,6 +17240,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -17102,7 +17262,9 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id @@ -17144,6 +17306,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true medium: type: object @@ -17165,6 +17328,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true small: type: object @@ -17186,6 +17350,7 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true source: type: object @@ -17207,12 +17372,16 @@ components: sourceItemId: type: string nullable: true + required: [] nullable: true + required: [] nullable: true required: - id nullable: true + required: [] nullable: true + required: [] nullable: true replyToId: type: string @@ -17286,6 +17455,7 @@ components: type: integer format: Int64 nullable: true + required: [] Hash_String__Int32_: type: object additionalProperties: @@ -17648,6 +17818,7 @@ components: type: integer format: Int64 nullable: true + required: [] nullable: true required: - error diff --git a/shard.lock b/shard.lock index af22476d..538cbb16 100644 --- a/shard.lock +++ b/shard.lock @@ -139,7 +139,7 @@ shards: place_calendar: git: https://github.com/placeos/calendar.git - version: 4.29.4 + version: 4.30.0 placeos: git: https://github.com/placeos/crystal-client.git @@ -167,7 +167,7 @@ shards: tasker: git: https://github.com/spider-gazelle/tasker.git - version: 3.0.1 + version: 3.1.0 timecop: git: https://github.com/crystal-community/timecop.cr.git diff --git a/spec/controllers/events_spec.cr b/spec/controllers/events_spec.cr index 0d8d8353..5ab1ca9b 100644 --- a/spec/controllers/events_spec.cr +++ b/spec/controllers/events_spec.cr @@ -524,6 +524,66 @@ describe Events, tags: ["event"] do ]).size.should eq(0) end + it "#add_attendee only emails new attendees by default and notifies everyone when notify_existing_attendees=true" do + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + captured_patch_bodies = [] of String + WebMock.stub(:patch, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return do |request| + captured_patch_bodies << (request.body.try(&.gets_to_end) || "") + HTTP::Client::Response.new(200, body: File.read("./spec/fixtures/events/o365/update.json")) + end + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendars") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + + # Stub getting the host event + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/calendarView?startDateTime=2020-08-26T14%3A00%3A00-00%3A00&endDateTime=2020-08-27T13%3A59%3A59-00%3A00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&%24top=10000") + .to_return(body: File.read("./spec/fixtures/events/o365/events_query.json")) + + req_body = EventsHelper.create_event_input(permission: PlaceOS::Model::EventMetadata::Permission::PUBLIC) + + event = JSON.parse(client.post(EVENTS_BASE, headers: headers, body: req_body).body).as_h + event_id = event["id"].to_s + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/calendarView?startDateTime=2020-08-26T14:00:00-00:00&endDateTime=2020-08-27T13:59:59-00:00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&$top=10000") + .to_return(EventsHelper.event_query_response(event_id)) + + system_id = "sys-rJQQlR4Cn7" + EventsHelper.stub_permissions_check(system_id) + + # Default: existing attendees are not notified, so only the attendees + # property is sent upstream. + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: Mock::Headers.office365_no_auth, body: { + name: "User Two", + email: "user-two@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(200) + + captured_patch_bodies.size.should eq(1) + JSON.parse(captured_patch_bodies[0]).as_h.keys.should eq(["attendees"]) + + # Opt-in: notify existing attendees -> full event body sent upstream. + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}¬ify_existing_attendees=true), headers: Mock::Headers.office365_no_auth, body: { + name: "User Three", + email: "user-three@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(200) + + captured_patch_bodies.size.should eq(2) + full_body = JSON.parse(captured_patch_bodies[1]).as_h + full_body.has_key?("attendees").should be_true + full_body.keys.size.should be > 1 + end + it "#delete_attendee should ONLY allow removing self from PRIVATE events" do WebMock.stub(:post, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events") .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) diff --git a/src/controllers/events.cr b/src/controllers/events.cr index c0dee3fe..e8467d8d 100644 --- a/src/controllers/events.cr +++ b/src/controllers/events.cr @@ -564,6 +564,8 @@ class Events < Application associated_system : String? = nil, @[AC::Param::Info(name: "calendar", description: "the calendar associated with this event id", example: "user@org.com")] user_cal : String? = nil, + @[AC::Param::Info(description: "(office365 only) set to false when the only change is adding attendees, so that existing attendees aren't notified by email. Warning: while false, only the attendee changes are propagated to the provider - any other edits in the request body are ignored", example: "false")] + notify_existing_attendees : Bool = true, ) : PlaceCalendar::Event changes.id = event_id = original_id system_id = (associated_system || changes.system_id).presence @@ -690,7 +692,7 @@ class Events < Application end end - updated_event = client.update_event(user_id: host, event: changes, calendar_id: host) + updated_event = client.update_event(user_id: host, event: changes, calendar_id: host, notify_existing_attendees: notify_existing_attendees) raise Error::BadUpstreamResponse.new("failed to update event #{event_id} as #{host}") unless updated_event if system @@ -876,6 +878,8 @@ class Events < Application system_id : String? = nil, @[AC::Param::Info(name: "calendar", description: "the calendar associated with this event id", example: "user@org.com")] user_cal : String? = nil, + @[AC::Param::Info(description: "(office365 only) when true, existing attendees are also emailed about the change, otherwise only the new attendee is notified", example: "false")] + notify_existing_attendees : Bool = false, ) : Attendee | PlaceCalendar::Event::Attendee placeos_client = get_placeos_client event_id = original_id @@ -929,8 +933,8 @@ class Events < Application # Add the new attendee to the event event.attendees = (event.attendees || [] of PlaceCalendar::Event::Attendee) << attendee - # Update the event with the new attendee - updated_event = client.update_event(user_id: host, event: event, calendar_id: host) + # Update the event with the new attendee. + updated_event = client.update_event(user_id: host, event: event, calendar_id: host, notify_existing_attendees: notify_existing_attendees) raise Error::BadUpstreamResponse.new("failed to update event #{event_id} as #{host}") unless updated_event if system_id