Handle event exceptions in caledar (#3589)
This ensures that we backup and restore and exceptions in the recurring events. One thing pending here is fixing up the attachments. I hope to create that as a separate PR. This can probably go in separately as well. --- #### Does this PR need a docs update or release note? - [x] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [x] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> * https://github.com/alcionai/corso/issues/2835 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
9f7a6422a0
commit
9199db8f45
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Added ProtectedResourceName to the backup list json output. ProtectedResourceName holds either a UPN or a WebURL, depending on the resource type.
|
- Added ProtectedResourceName to the backup list json output. ProtectedResourceName holds either a UPN or a WebURL, depending on the resource type.
|
||||||
- Rework base selection logic for incremental backups so it's more likely to find a valid base.
|
- Rework base selection logic for incremental backups so it's more likely to find a valid base.
|
||||||
- Improve OneDrive restore performance by paralleling item restores
|
- Improve OneDrive restore performance by paralleling item restores
|
||||||
|
- Exceptions and cancellations for recurring events are now backed up and restored
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix Exchange folder cache population error when parent folder isn't found.
|
- Fix Exchange folder cache population error when parent folder isn't found.
|
||||||
@ -23,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
- Do not display all the items that we restored at the end if there are more than 15. You can override this with `--verbose`.
|
- Do not display all the items that we restored at the end if there are more than 15. You can override this with `--verbose`.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Changes to attachments in instances of recurring events compared to the series master aren't restored
|
||||||
|
|
||||||
## [v0.8.0] (beta) - 2023-05-15
|
## [v0.8.0] (beta) - 2023-05-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -114,7 +114,10 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error
|
|||||||
func(id, now, subject, body string) []byte {
|
func(id, now, subject, body string) []byte {
|
||||||
return exchMock.EventWith(
|
return exchMock.EventWith(
|
||||||
User, subject, body, body,
|
User, subject, body, body,
|
||||||
now, now, exchMock.NoRecurrence, exchMock.NoAttendees, false)
|
exchMock.NoOriginalStartDate, now, now,
|
||||||
|
exchMock.NoRecurrence, exchMock.NoAttendees,
|
||||||
|
false, exchMock.NoCancelledOccurrences,
|
||||||
|
exchMock.NoExceptionOccurrences)
|
||||||
},
|
},
|
||||||
control.Defaults(),
|
control.Defaults(),
|
||||||
errs)
|
errs)
|
||||||
|
|||||||
@ -2,11 +2,16 @@ package exchange
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/str"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
@ -82,12 +87,14 @@ func (h eventRestoreHandler) restore(
|
|||||||
|
|
||||||
if ptr.Val(event.GetHasAttachments()) {
|
if ptr.Val(event.GetHasAttachments()) {
|
||||||
attachments = event.GetAttachments()
|
attachments = event.GetAttachments()
|
||||||
event.SetAttachments([]models.Attachmentable{})
|
// We cannot use `[]models.Attbachmentable{}` instead of nil
|
||||||
|
// for beta endpoint.
|
||||||
|
event.SetAttachments(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := h.ip.PostItem(ctx, userID, destinationID, event)
|
item, err := h.ip.PostItem(ctx, userID, destinationID, event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "restoring mail message")
|
return nil, graph.Wrap(ctx, err, "restoring calendar item")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = uploadAttachments(
|
err = uploadAttachments(
|
||||||
@ -102,8 +109,181 @@ func (h eventRestoreHandler) restore(
|
|||||||
return nil, clues.Stack(err)
|
return nil, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Have to parse event again as we modified the original event and
|
||||||
|
// removed cancelled and exceptions events form it
|
||||||
|
event, err = api.BytesToEventable(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "creating event from bytes").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix up event instances in case we have a recurring event
|
||||||
|
err = updateRecurringEvents(ctx, h.ac, userID, destinationID, ptr.Val(item.GetId()), event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
info := api.EventInfo(event)
|
info := api.EventInfo(event)
|
||||||
info.Size = int64(len(body))
|
info.Size = int64(len(body))
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateRecurringEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
ac api.Events,
|
||||||
|
userID, containerID, itemID string,
|
||||||
|
event models.Eventable,
|
||||||
|
) error {
|
||||||
|
if event.GetRecurrence() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancellations and exceptions are currently in additional data
|
||||||
|
// but will get their own fields once the beta API lands and
|
||||||
|
// should be moved then
|
||||||
|
cancelledOccurrences := event.GetAdditionalData()["cancelledOccurrences"]
|
||||||
|
exceptionOccurrences := event.GetAdditionalData()["exceptionOccurrences"]
|
||||||
|
|
||||||
|
err := updateCancelledOccurrences(ctx, ac, userID, itemID, cancelledOccurrences)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "update cancelled occurrences")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateExceptionOccurrences(ctx, ac, userID, itemID, exceptionOccurrences)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "update exception occurrences")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateExceptionOccurrences take events that have exceptions, uses
|
||||||
|
// the originalStart date to find the instance and modify it to match
|
||||||
|
// the backup by updating the instance to match the backed up one
|
||||||
|
func updateExceptionOccurrences(
|
||||||
|
ctx context.Context,
|
||||||
|
ac api.Events,
|
||||||
|
userID string,
|
||||||
|
itemID string,
|
||||||
|
exceptionOccurrences any,
|
||||||
|
) error {
|
||||||
|
if exceptionOccurrences == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eo, ok := exceptionOccurrences.([]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting exceptionOccurrences to []any").
|
||||||
|
With("type", fmt.Sprintf("%T", exceptionOccurrences))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range eo {
|
||||||
|
instance, ok := instance.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting instance to map[string]any").
|
||||||
|
With("type", fmt.Sprintf("%T", instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
evt, err := api.EventFromMap(instance)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "parsing exception event")
|
||||||
|
}
|
||||||
|
|
||||||
|
start := ptr.Val(evt.GetOriginalStart())
|
||||||
|
startStr := dttm.FormatTo(start, dttm.DateOnly)
|
||||||
|
endStr := dttm.FormatTo(start.Add(24*time.Hour), dttm.DateOnly)
|
||||||
|
|
||||||
|
// Get all instances on the day of the instance which should
|
||||||
|
// just the one we need to modify
|
||||||
|
evts, err := ac.GetItemInstances(ctx, userID, itemID, startStr, endStr)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting instances")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the min recurrence interval is 1 day and we are
|
||||||
|
// querying for only a single day worth of instances, we
|
||||||
|
// should not have more than one instance here.
|
||||||
|
if len(evts) != 1 {
|
||||||
|
return clues.New("invalid number of instances for modified").
|
||||||
|
With("instances_count", len(evts), "search_start", startStr, "search_end", endStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt = toEventSimplified(evt)
|
||||||
|
|
||||||
|
// TODO(meain): Update attachments (might have to diff the
|
||||||
|
// attachments using ids and delete or add). We will have
|
||||||
|
// to get the id of the existing attachments, diff them
|
||||||
|
// with what we need a then create/delete items kinda like
|
||||||
|
// permissions
|
||||||
|
_, err = ac.PatchItem(ctx, userID, ptr.Val(evts[0].GetId()), evt)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "updating event instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCancelledOccurrences get the cancelled occurrences which is a
|
||||||
|
// list of strings of the format "<id>.<date>", parses the date out of
|
||||||
|
// that and uses the to get the event instance at that date to delete.
|
||||||
|
func updateCancelledOccurrences(
|
||||||
|
ctx context.Context,
|
||||||
|
ac api.Events,
|
||||||
|
userID string,
|
||||||
|
itemID string,
|
||||||
|
cancelledOccurrences any,
|
||||||
|
) error {
|
||||||
|
if cancelledOccurrences == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
co, ok := cancelledOccurrences.([]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting cancelledOccurrences to []any").
|
||||||
|
With("type", fmt.Sprintf("%T", cancelledOccurrences))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: We can fetch a date range instead of fetching
|
||||||
|
// instances if we have multiple cancelled events which are nearby
|
||||||
|
// and reduce the number of API calls that we have to make
|
||||||
|
for _, instance := range co {
|
||||||
|
instance, err := str.AnyToString(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
splits := strings.Split(instance, ".")
|
||||||
|
|
||||||
|
startStr := splits[len(splits)-1]
|
||||||
|
|
||||||
|
start, err := dttm.ParseTime(startStr)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "parsing cancelled event date")
|
||||||
|
}
|
||||||
|
|
||||||
|
endStr := dttm.FormatTo(start.Add(24*time.Hour), dttm.DateOnly)
|
||||||
|
|
||||||
|
// Get all instances on the day of the instance which should
|
||||||
|
// just the one we need to modify
|
||||||
|
evts, err := ac.GetItemInstances(ctx, userID, itemID, startStr, endStr)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting instances")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the min recurrence interval is 1 day and we are
|
||||||
|
// querying for only a single day worth of instances, we
|
||||||
|
// should not have more than one instance here.
|
||||||
|
if len(evts) != 1 {
|
||||||
|
return clues.New("invalid number of instances for cancelled").
|
||||||
|
With("instances_count", len(evts), "search_start", startStr, "search_end", endStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ac.DeleteItem(ctx, userID, ptr.Val(evts[0].GetId()))
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "deleting event instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -25,11 +25,6 @@ import (
|
|||||||
//nolint:lll
|
//nolint:lll
|
||||||
const (
|
const (
|
||||||
eventTmpl = `{
|
eventTmpl = `{
|
||||||
"id":"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA=",
|
|
||||||
"calendar@odata.navigationLink":"https://graph.microsoft.com/v1.0/users('foobar@8qzvrj.onmicrosoft.com')/calendars('AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAAA=')",
|
|
||||||
"calendar@odata.associationLink":"https://graph.microsoft.com/v1.0/users('foobar@8qzvrj.onmicrosoft.com')/calendars('AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAAA=')/$ref",
|
|
||||||
"@odata.etag":"W/\"0hATW1CAfUS+njw3hdxSGAAAJIxNug==\"",
|
|
||||||
"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users('foobar%%408qzvrj.onmicrosoft.com')/events/$entity",
|
|
||||||
"categories":[],
|
"categories":[],
|
||||||
"changeKey":"0hATW1CAfUS+njw3hdxSGAAAJIxNug==",
|
"changeKey":"0hATW1CAfUS+njw3hdxSGAAAJIxNug==",
|
||||||
"createdDateTime":"2022-03-28T03:42:03Z",
|
"createdDateTime":"2022-03-28T03:42:03Z",
|
||||||
@ -46,7 +41,6 @@ const (
|
|||||||
"timeZone":"UTC"
|
"timeZone":"UTC"
|
||||||
},
|
},
|
||||||
"hideAttendees":false,
|
"hideAttendees":false,
|
||||||
"iCalUId":"040000008200E00074C5B7101A82E0080000000035723BC75542D801000000000000000010000000E1E7C8F785242E4894DA13AEFB947B85",
|
|
||||||
"importance":"normal",
|
"importance":"normal",
|
||||||
"isAllDay":false,
|
"isAllDay":false,
|
||||||
"isCancelled":false,
|
"isCancelled":false,
|
||||||
@ -75,6 +69,7 @@ const (
|
|||||||
"name":"Anu Pierson"
|
"name":"Anu Pierson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
%s
|
||||||
"originalEndTimeZone":"UTC",
|
"originalEndTimeZone":"UTC",
|
||||||
"originalStartTimeZone":"UTC",
|
"originalStartTimeZone":"UTC",
|
||||||
"reminderMinutesBeforeStart":15,
|
"reminderMinutesBeforeStart":15,
|
||||||
@ -90,11 +85,13 @@ const (
|
|||||||
"timeZone":"UTC"
|
"timeZone":"UTC"
|
||||||
},
|
},
|
||||||
"subject":"%s",
|
"subject":"%s",
|
||||||
"type":"singleInstance",
|
"type":"%s",
|
||||||
"hasAttachments":%v,
|
"hasAttachments":%v,
|
||||||
%s
|
%s
|
||||||
"webLink":"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA%%3D&exvsurl=1&path=/calendar/item",
|
"webLink":"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA%%3D&exvsurl=1&path=/calendar/item",
|
||||||
"recurrence":%s,
|
"recurrence":%s,
|
||||||
|
%s
|
||||||
|
%s
|
||||||
"attendees":%s
|
"attendees":%s
|
||||||
}`
|
}`
|
||||||
|
|
||||||
@ -151,13 +148,16 @@ const (
|
|||||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}],"
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}],"
|
||||||
|
|
||||||
|
originalStartDateFormat = `"originalStart": "%s",`
|
||||||
|
NoOriginalStartDate = ``
|
||||||
|
|
||||||
NoRecurrence = `null`
|
NoRecurrence = `null`
|
||||||
recurrenceTmpl = `{
|
recurrenceTmpl = `{
|
||||||
"pattern": {
|
"pattern": {
|
||||||
"type": "absoluteYearly",
|
"type": "absoluteYearly",
|
||||||
"interval": 1,
|
"interval": 1,
|
||||||
"month": 1,
|
"month": %s,
|
||||||
"dayOfMonth": 1,
|
"dayOfMonth": %s,
|
||||||
"firstDayOfWeek": "sunday",
|
"firstDayOfWeek": "sunday",
|
||||||
"index": "first"
|
"index": "first"
|
||||||
},
|
},
|
||||||
@ -170,6 +170,13 @@ const (
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
cancelledOccurrencesFormat = `"cancelledOccurrences": [%s],`
|
||||||
|
cancelledOccurrenceInstanceFormat = `"OID.AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAENAABBFDg0JJk7TY1fmsJrh7tNAADHGTZoAAA=.%s"`
|
||||||
|
NoCancelledOccurrences = ""
|
||||||
|
|
||||||
|
exceptionOccurrencesFormat = `"exceptionOccurrences": [%s],`
|
||||||
|
NoExceptionOccurrences = ""
|
||||||
|
|
||||||
NoAttendees = `[]`
|
NoAttendees = `[]`
|
||||||
attendeesTmpl = `[{
|
attendeesTmpl = `[{
|
||||||
"emailAddress": {
|
"emailAddress": {
|
||||||
@ -227,7 +234,8 @@ func EventWithSubjectBytes(subject string) []byte {
|
|||||||
return EventWith(
|
return EventWith(
|
||||||
defaultEventOrganizer, subject,
|
defaultEventOrganizer, subject,
|
||||||
defaultEventBody, defaultEventBodyPreview,
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
atTime, endTime, NoRecurrence, NoAttendees, false,
|
NoOriginalStartDate, atTime, endTime, NoRecurrence, NoAttendees,
|
||||||
|
false, NoCancelledOccurrences, NoExceptionOccurrences,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +247,8 @@ func EventWithAttachment(subject string) []byte {
|
|||||||
return EventWith(
|
return EventWith(
|
||||||
defaultEventOrganizer, subject,
|
defaultEventOrganizer, subject,
|
||||||
defaultEventBody, defaultEventBodyPreview,
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
atTime, atTime, NoRecurrence, NoAttendees, true,
|
NoOriginalStartDate, atTime, atTime, NoRecurrence, NoAttendees,
|
||||||
|
true, NoCancelledOccurrences, NoExceptionOccurrences,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +260,8 @@ func EventWithRecurrenceBytes(subject, recurrenceTimeZone string) []byte {
|
|||||||
|
|
||||||
recurrence := string(fmt.Sprintf(
|
recurrence := string(fmt.Sprintf(
|
||||||
recurrenceTmpl,
|
recurrenceTmpl,
|
||||||
|
strconv.Itoa(int(at.Month())),
|
||||||
|
strconv.Itoa(at.Day()),
|
||||||
timeSlice[0],
|
timeSlice[0],
|
||||||
recurrenceTimeZone,
|
recurrenceTimeZone,
|
||||||
))
|
))
|
||||||
@ -258,7 +269,70 @@ func EventWithRecurrenceBytes(subject, recurrenceTimeZone string) []byte {
|
|||||||
return EventWith(
|
return EventWith(
|
||||||
defaultEventOrganizer, subject,
|
defaultEventOrganizer, subject,
|
||||||
defaultEventBody, defaultEventBodyPreview,
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
atTime, atTime, recurrence, attendeesTmpl, true,
|
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
|
||||||
|
true, NoCancelledOccurrences, NoExceptionOccurrences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EventWithRecurrenceAndCancellationBytes(subject string) []byte {
|
||||||
|
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
|
||||||
|
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
|
||||||
|
atTime := dttm.Format(at)
|
||||||
|
timeSlice := strings.Split(atTime, "T")
|
||||||
|
nextYear := tomorrow.AddDate(1, 0, 0)
|
||||||
|
|
||||||
|
recurrence := string(fmt.Sprintf(
|
||||||
|
recurrenceTmpl,
|
||||||
|
strconv.Itoa(int(at.Month())),
|
||||||
|
strconv.Itoa(at.Day()),
|
||||||
|
timeSlice[0],
|
||||||
|
`"UTC"`,
|
||||||
|
))
|
||||||
|
|
||||||
|
cancelledInstances := []string{fmt.Sprintf(cancelledOccurrenceInstanceFormat, dttm.FormatTo(nextYear, dttm.DateOnly))}
|
||||||
|
cancelledOccurrences := fmt.Sprintf(cancelledOccurrencesFormat, strings.Join(cancelledInstances, ","))
|
||||||
|
|
||||||
|
return EventWith(
|
||||||
|
defaultEventOrganizer, subject,
|
||||||
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
|
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
|
||||||
|
true, cancelledOccurrences, NoExceptionOccurrences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EventWithRecurrenceAndExceptionBytes(subject string) []byte {
|
||||||
|
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
|
||||||
|
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
|
||||||
|
atTime := dttm.Format(at)
|
||||||
|
timeSlice := strings.Split(atTime, "T")
|
||||||
|
newTime := dttm.Format(tomorrow.AddDate(0, 0, 1))
|
||||||
|
originalStartDate := dttm.FormatTo(at, dttm.TabularOutput)
|
||||||
|
|
||||||
|
recurrence := string(fmt.Sprintf(
|
||||||
|
recurrenceTmpl,
|
||||||
|
strconv.Itoa(int(at.Month())),
|
||||||
|
strconv.Itoa(at.Day()),
|
||||||
|
timeSlice[0],
|
||||||
|
`"UTC"`,
|
||||||
|
))
|
||||||
|
|
||||||
|
exceptionEvent := EventWith(
|
||||||
|
defaultEventOrganizer, subject+"(modified)",
|
||||||
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
|
fmt.Sprintf(originalStartDateFormat, originalStartDate),
|
||||||
|
newTime, newTime, NoRecurrence, attendeesTmpl,
|
||||||
|
false, NoCancelledOccurrences, NoExceptionOccurrences,
|
||||||
|
)
|
||||||
|
exceptionOccurrences := fmt.Sprintf(
|
||||||
|
exceptionOccurrencesFormat,
|
||||||
|
strings.Join([]string{string(exceptionEvent)}, ","),
|
||||||
|
)
|
||||||
|
|
||||||
|
return EventWith(
|
||||||
|
defaultEventOrganizer, subject,
|
||||||
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
|
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
|
||||||
|
true, NoCancelledOccurrences, exceptionOccurrences,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +344,8 @@ func EventWithAttendeesBytes(subject string) []byte {
|
|||||||
return EventWith(
|
return EventWith(
|
||||||
defaultEventOrganizer, subject,
|
defaultEventOrganizer, subject,
|
||||||
defaultEventBody, defaultEventBodyPreview,
|
defaultEventBody, defaultEventBodyPreview,
|
||||||
atTime, atTime, NoRecurrence, attendeesTmpl, true,
|
NoOriginalStartDate, atTime, atTime, NoRecurrence, attendeesTmpl,
|
||||||
|
true, NoCancelledOccurrences, NoExceptionOccurrences,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,8 +356,8 @@ func EventWithAttendeesBytes(subject string) []byte {
|
|||||||
// Body must contain a well-formatted string, consumable in a json payload. IE: no unescaped newlines.
|
// Body must contain a well-formatted string, consumable in a json payload. IE: no unescaped newlines.
|
||||||
func EventWith(
|
func EventWith(
|
||||||
organizer, subject, body, bodyPreview,
|
organizer, subject, body, bodyPreview,
|
||||||
startDateTime, endDateTime, recurrence, attendees string,
|
originalStartDate, startDateTime, endDateTime, recurrence, attendees string,
|
||||||
hasAttachments bool,
|
hasAttachments bool, cancelledOccurrences, exceptionOccurrences string,
|
||||||
) []byte {
|
) []byte {
|
||||||
var attachments string
|
var attachments string
|
||||||
if hasAttachments {
|
if hasAttachments {
|
||||||
@ -300,17 +375,26 @@ func EventWith(
|
|||||||
endDateTime += ".0000000"
|
endDateTime += ".0000000"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventType := "singleInstance"
|
||||||
|
if recurrence != "null" {
|
||||||
|
eventType = "seriesMaster"
|
||||||
|
}
|
||||||
|
|
||||||
return []byte(fmt.Sprintf(
|
return []byte(fmt.Sprintf(
|
||||||
eventTmpl,
|
eventTmpl,
|
||||||
body,
|
body,
|
||||||
bodyPreview,
|
bodyPreview,
|
||||||
endDateTime,
|
endDateTime,
|
||||||
organizer,
|
organizer,
|
||||||
|
originalStartDate,
|
||||||
startDateTime,
|
startDateTime,
|
||||||
subject,
|
subject,
|
||||||
|
eventType,
|
||||||
hasAttachments,
|
hasAttachments,
|
||||||
attachments,
|
attachments,
|
||||||
recurrence,
|
recurrence,
|
||||||
|
cancelledOccurrences,
|
||||||
|
exceptionOccurrences,
|
||||||
attendees,
|
attendees,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,14 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
|
|||||||
name: "Test recurrenceTimeZone: Empty",
|
name: "Test recurrenceTimeZone: Empty",
|
||||||
bytes: exchMock.EventWithRecurrenceBytes(subject, `""`),
|
bytes: exchMock.EventWithRecurrenceBytes(subject, `""`),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Test cancelledOccurrences",
|
||||||
|
bytes: exchMock.EventWithRecurrenceAndCancellationBytes(subject),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test exceptionOccurrences",
|
||||||
|
bytes: exchMock.EventWithRecurrenceAndExceptionBytes(subject),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@ -70,7 +70,6 @@ func toEventSimplified(orig models.Eventable) models.Eventable {
|
|||||||
newContent := insertStringToBody(origBody, attendees)
|
newContent := insertStringToBody(origBody, attendees)
|
||||||
newBody := models.NewItemBody()
|
newBody := models.NewItemBody()
|
||||||
newBody.SetContentType(origBody.GetContentType())
|
newBody.SetContentType(origBody.GetContentType())
|
||||||
newBody.SetAdditionalData(origBody.GetAdditionalData())
|
|
||||||
newBody.SetOdataType(origBody.GetOdataType())
|
newBody.SetOdataType(origBody.GetOdataType())
|
||||||
newBody.SetContent(&newContent)
|
newBody.SetContent(&newContent)
|
||||||
orig.SetBody(newBody)
|
orig.SetBody(newBody)
|
||||||
@ -89,6 +88,14 @@ func toEventSimplified(orig models.Eventable) models.Eventable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove exceptions for recurring events
|
||||||
|
// These will be present in objects once we start using the API
|
||||||
|
// that is currently in beta
|
||||||
|
additionalData := orig.GetAdditionalData()
|
||||||
|
delete(additionalData, "cancelledOccurrences")
|
||||||
|
delete(additionalData, "exceptionOccurrences")
|
||||||
|
orig.SetAdditionalData(additionalData)
|
||||||
|
|
||||||
return orig
|
return orig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -121,6 +121,32 @@ func (suite *TransformUnitTest) TestToEventSimplified_recurrence() {
|
|||||||
return ptr.Val(e.GetRecurrence().GetRange().GetRecurrenceTimeZone()) == "Pacific Standard Time"
|
return ptr.Val(e.GetRecurrence().GetRange().GetRecurrenceTimeZone()) == "Pacific Standard Time"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Test cancelledOccurrences",
|
||||||
|
event: func() models.Eventable {
|
||||||
|
bytes := exchMock.EventWithRecurrenceAndCancellationBytes(subject)
|
||||||
|
event, err := api.BytesToEventable(bytes)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
|
||||||
|
validateOutput: func(e models.Eventable) bool {
|
||||||
|
return e.GetAdditionalData()["cancelledOccurrences"] == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test exceptionOccurrences",
|
||||||
|
event: func() models.Eventable {
|
||||||
|
bytes := exchMock.EventWithRecurrenceAndExceptionBytes(subject)
|
||||||
|
event, err := api.BytesToEventable(bytes)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
|
||||||
|
validateOutput: func(e models.Eventable) bool {
|
||||||
|
return e.GetAdditionalData()["exceptionOccurrences"] == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@ -813,7 +813,10 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
|
|||||||
eventDBF := func(id, timeStamp, subject, body string) []byte {
|
eventDBF := func(id, timeStamp, subject, body string) []byte {
|
||||||
return exchMock.EventWith(
|
return exchMock.EventWith(
|
||||||
suite.user, subject, body, body,
|
suite.user, subject, body, body,
|
||||||
now, now, exchMock.NoRecurrence, exchMock.NoAttendees, false)
|
exchMock.NoOriginalStartDate, now, now,
|
||||||
|
exchMock.NoRecurrence, exchMock.NoAttendees,
|
||||||
|
false, exchMock.NoCancelledOccurrences,
|
||||||
|
exchMock.NoExceptionOccurrences)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test data set
|
// test data set
|
||||||
|
|||||||
@ -3,8 +3,10 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -15,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/dttm"
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/str"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
@ -189,6 +192,14 @@ func (c Events) PatchCalendar(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Beta version cannot have /calendars/%s for get and Patch
|
||||||
|
// https://stackoverflow.com/questions/50492177/microsoft-graph-get-user-calendar-event-with-beta-version
|
||||||
|
eventExceptionsBetaURLTemplate = "https://graph.microsoft.com/beta/users/%s/events/%s?$expand=exceptionOccurrences"
|
||||||
|
eventPostBetaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events"
|
||||||
|
eventPatchBetaURLTemplate = "https://graph.microsoft.com/beta/users/%s/events/%s"
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// items
|
// items
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -208,41 +219,216 @@ func (c Events) GetItem(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
event, err = c.Stable.
|
// Beta endpoint helps us fetch the event exceptions, but since we
|
||||||
|
// don't use the beta SDK, the exceptionOccurrences and
|
||||||
|
// cancelledOccurrences end up in AdditionalData
|
||||||
|
// https://learn.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-beta#properties
|
||||||
|
rawURL := fmt.Sprintf(eventExceptionsBetaURLTemplate, userID, itemID)
|
||||||
|
builder := users.NewItemEventsEventItemRequestBuilder(rawURL, c.Stable.Adapter())
|
||||||
|
|
||||||
|
event, err = builder.Get(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, graph.Stack(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateCancelledOccurrences(event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, clues.Wrap(err, "verify cancelled occurrences")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fixupExceptionOccurrences(ctx, c, event, immutableIDs, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, clues.Wrap(err, "fixup exception occurrences")
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments, err := c.getAttachments(ctx, event, immutableIDs, userID, itemID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
event.SetAttachments(attachments)
|
||||||
|
|
||||||
|
return event, EventInfo(event), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixupExceptionOccurrences gets attachments and converts the data
|
||||||
|
// into a format that gets serialized when storing to kopia
|
||||||
|
func fixupExceptionOccurrences(
|
||||||
|
ctx context.Context,
|
||||||
|
client Events,
|
||||||
|
event models.Eventable,
|
||||||
|
immutableIDs bool,
|
||||||
|
userID string,
|
||||||
|
) error {
|
||||||
|
// Fetch attachments for exceptions
|
||||||
|
exceptionOccurrences := event.GetAdditionalData()["exceptionOccurrences"]
|
||||||
|
if exceptionOccurrences == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eo, ok := exceptionOccurrences.([]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting exceptionOccurrences to []any").
|
||||||
|
With("type", fmt.Sprintf("%T", exceptionOccurrences))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range eo {
|
||||||
|
instance, ok := instance.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting instance to map[string]any").
|
||||||
|
With("type", fmt.Sprintf("%T", instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
evt, err := EventFromMap(instance)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "parsing exception event")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: We don't have to store any of the
|
||||||
|
// attachments that carry over from the original
|
||||||
|
attachments, err := client.getAttachments(ctx, evt, immutableIDs, userID, ptr.Val(evt.GetId()))
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting exception attachments").
|
||||||
|
With("exception_event_id", ptr.Val(evt.GetId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This odd roundabout way of doing this is required as
|
||||||
|
// the json serialization at the end does not serialize if
|
||||||
|
// you just pass in a models.Attachmentable
|
||||||
|
convertedAttachments := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
am, err := parseableToMap(attachment)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "converting attachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedAttachments = append(convertedAttachments, am)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance["attachments"] = convertedAttachments
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding checks to ensure that the data is in the format that we expect M365 to return
|
||||||
|
func validateCancelledOccurrences(event models.Eventable) error {
|
||||||
|
cancelledOccurrences := event.GetAdditionalData()["cancelledOccurrences"]
|
||||||
|
if cancelledOccurrences != nil {
|
||||||
|
co, ok := cancelledOccurrences.([]any)
|
||||||
|
if !ok {
|
||||||
|
return clues.New("converting cancelledOccurrences to []any").
|
||||||
|
With("type", fmt.Sprintf("%T", cancelledOccurrences))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range co {
|
||||||
|
instance, err := str.AnyToString(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// There might be multiple `.` in the ID and hence >2
|
||||||
|
splits := strings.Split(instance, ".")
|
||||||
|
if len(splits) < 2 {
|
||||||
|
return clues.New("unexpected cancelled event format").
|
||||||
|
With("instance", instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
startStr := splits[len(splits)-1]
|
||||||
|
|
||||||
|
_, err = dttm.ParseTime(startStr)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "parsing cancelled event date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseableToMap(att serialization.Parsable) (map[string]any, error) {
|
||||||
|
var item map[string]any
|
||||||
|
|
||||||
|
writer := kjson.NewJsonSerializationWriter()
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
if err := writer.WriteObjectValue("", att); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ats, err := writer.GetSerializedContent()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(ats, &item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "unmarshalling serialized attachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Events) getAttachments(
|
||||||
|
ctx context.Context,
|
||||||
|
event models.Eventable,
|
||||||
|
immutableIDs bool,
|
||||||
|
userID string,
|
||||||
|
itemID string,
|
||||||
|
) ([]models.Attachmentable, error) {
|
||||||
|
if !ptr.Val(event.GetHasAttachments()) && !HasAttachments(event.GetBody()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
|
||||||
|
Expand: []string{"microsoft.graph.itemattachment/item"},
|
||||||
|
},
|
||||||
|
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
attached, err := c.LargeItem.
|
||||||
Client().
|
Client().
|
||||||
Users().
|
Users().
|
||||||
ByUserId(userID).
|
ByUserId(userID).
|
||||||
Events().
|
Events().
|
||||||
ByEventId(itemID).
|
ByEventId(itemID).
|
||||||
|
Attachments().
|
||||||
Get(ctx, config)
|
Get(ctx, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, graph.Stack(ctx, err)
|
return nil, graph.Wrap(ctx, err, "event attachment download")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
|
return attached.GetValue(), nil
|
||||||
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
|
}
|
||||||
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
|
|
||||||
Expand: []string{"microsoft.graph.itemattachment/item"},
|
|
||||||
},
|
|
||||||
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)),
|
|
||||||
}
|
|
||||||
|
|
||||||
attached, err := c.LargeItem.
|
func (c Events) GetItemInstances(
|
||||||
Client().
|
ctx context.Context,
|
||||||
Users().
|
userID, itemID string,
|
||||||
ByUserId(userID).
|
startDate, endDate string,
|
||||||
Events().
|
) ([]models.Eventable, error) {
|
||||||
ByEventId(itemID).
|
config := &users.ItemEventsItemInstancesRequestBuilderGetRequestConfiguration{
|
||||||
Attachments().
|
QueryParameters: &users.ItemEventsItemInstancesRequestBuilderGetQueryParameters{
|
||||||
Get(ctx, config)
|
Select: []string{"id"},
|
||||||
if err != nil {
|
StartDateTime: ptr.To(startDate),
|
||||||
return nil, nil, graph.Wrap(ctx, err, "event attachment download")
|
EndDateTime: ptr.To(endDate),
|
||||||
}
|
},
|
||||||
|
|
||||||
event.SetAttachments(attached.GetValue())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return event, EventInfo(event), nil
|
events, err := c.Stable.
|
||||||
|
Client().
|
||||||
|
Users().
|
||||||
|
ByUserId(userID).
|
||||||
|
Events().
|
||||||
|
ByEventId(itemID).
|
||||||
|
Instances().
|
||||||
|
Get(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Stack(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.GetValue(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Events) PostItem(
|
func (c Events) PostItem(
|
||||||
@ -250,14 +436,10 @@ func (c Events) PostItem(
|
|||||||
userID, containerID string,
|
userID, containerID string,
|
||||||
body models.Eventable,
|
body models.Eventable,
|
||||||
) (models.Eventable, error) {
|
) (models.Eventable, error) {
|
||||||
itm, err := c.Stable.
|
rawURL := fmt.Sprintf(eventPostBetaURLTemplate, userID, containerID)
|
||||||
Client().
|
builder := users.NewItemCalendarsItemEventsRequestBuilder(rawURL, c.Stable.Adapter())
|
||||||
Users().
|
|
||||||
ByUserId(userID).
|
itm, err := builder.Post(ctx, body, nil)
|
||||||
Calendars().
|
|
||||||
ByCalendarId(containerID).
|
|
||||||
Events().
|
|
||||||
Post(ctx, body, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "creating calendar event")
|
return nil, graph.Wrap(ctx, err, "creating calendar event")
|
||||||
}
|
}
|
||||||
@ -265,6 +447,22 @@ func (c Events) PostItem(
|
|||||||
return itm, nil
|
return itm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Events) PatchItem(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, eventID string,
|
||||||
|
body models.Eventable,
|
||||||
|
) (models.Eventable, error) {
|
||||||
|
rawURL := fmt.Sprintf(eventPatchBetaURLTemplate, userID, eventID)
|
||||||
|
builder := users.NewItemCalendarsItemEventsEventItemRequestBuilder(rawURL, c.Stable.Adapter())
|
||||||
|
|
||||||
|
itm, err := builder.Patch(ctx, body, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, graph.Wrap(ctx, err, "updating calendar event")
|
||||||
|
}
|
||||||
|
|
||||||
|
return itm, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c Events) DeleteItem(
|
func (c Events) DeleteItem(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, itemID string,
|
userID, itemID string,
|
||||||
@ -472,3 +670,17 @@ func EventInfo(evt models.Eventable) *details.ExchangeInfo {
|
|||||||
Modified: ptr.OrNow(evt.GetLastModifiedDateTime()),
|
Modified: ptr.OrNow(evt.GetLastModifiedDateTime()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EventFromMap(ev map[string]any) (models.Eventable, error) {
|
||||||
|
instBytes, err := json.Marshal(ev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "marshaling event exception instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := BytesToEventable(instBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "converting exception event bytes to Eventable")
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -26,3 +26,5 @@ included in backup and restore.
|
|||||||
* SharePoint document library data can't be restored after the library has been deleted.
|
* SharePoint document library data can't be restored after the library has been deleted.
|
||||||
|
|
||||||
* Sharing information of items in OneDrive/SharePoint using sharing links aren't backed up and restored.
|
* Sharing information of items in OneDrive/SharePoint using sharing links aren't backed up and restored.
|
||||||
|
|
||||||
|
* Changes to attachments in instances of recurring events compared to the series master aren't restored
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user