now that exchange api has been folded in with the rest of the m365 api, it doesn't make sense to maintain an options file with only exchange functionality. Since all calls in the file were used 1:1 with some api func, those options have been moved into their respective api funcs. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #1996 #### Test Plan - [x] 💚 E2E
530 lines
15 KiB
Go
530 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/microsoft/kiota-abstractions-go/serialization"
|
|
kjson "github.com/microsoft/kiota-serialization-json-go"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/users"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
|
"github.com/alcionai/corso/src/internal/connector/graph/api"
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// controller
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (c Client) Events() Events {
|
|
return Events{c}
|
|
}
|
|
|
|
// Events is an interface-compliant provider of the client.
|
|
type Events struct {
|
|
Client
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// methods
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// CreateCalendar makes an event Calendar with the name in the user's M365 exchange account
|
|
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go
|
|
func (c Events) CreateCalendar(
|
|
ctx context.Context,
|
|
user, calendarName string,
|
|
) (models.Calendarable, error) {
|
|
requestbody := models.NewCalendar()
|
|
requestbody.SetName(&calendarName)
|
|
|
|
mdl, err := c.Stable.Client().Users().ByUserId(user).Calendars().Post(ctx, requestbody, nil)
|
|
if err != nil {
|
|
return nil, graph.Wrap(ctx, err, "creating calendar")
|
|
}
|
|
|
|
return mdl, nil
|
|
}
|
|
|
|
// DeleteContainer removes a calendar from user's M365 account
|
|
// Reference: https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=go
|
|
func (c Events) DeleteContainer(
|
|
ctx context.Context,
|
|
user, calendarID string,
|
|
) error {
|
|
// deletes require unique http clients
|
|
// https://github.com/alcionai/corso/issues/2707
|
|
srv, err := NewService(c.Credentials)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
err = srv.Client().Users().ByUserId(user).Calendars().ByCalendarId(calendarID).Delete(ctx, nil)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c Events) GetContainerByID(
|
|
ctx context.Context,
|
|
userID, containerID string,
|
|
) (graph.Container, error) {
|
|
service, err := c.Service()
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
queryParams := &users.ItemCalendarsCalendarItemRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemCalendarsCalendarItemRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "name", "owner"},
|
|
},
|
|
}
|
|
|
|
cal, err := service.Client().Users().ByUserId(userID).Calendars().ByCalendarId(containerID).Get(ctx, queryParams)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err).WithClues(ctx)
|
|
}
|
|
|
|
return graph.CalendarDisplayable{Calendarable: cal}, nil
|
|
}
|
|
|
|
// GetContainerByName fetches a calendar by name
|
|
func (c Events) GetContainerByName(
|
|
ctx context.Context,
|
|
userID, name string,
|
|
) (models.Calendarable, error) {
|
|
filter := fmt.Sprintf("name eq '%s'", name)
|
|
options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{
|
|
Filter: &filter,
|
|
},
|
|
}
|
|
|
|
ctx = clues.Add(ctx, "calendar_name", name)
|
|
|
|
resp, err := c.Stable.Client().Users().ByUserId(userID).Calendars().Get(ctx, options)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err).WithClues(ctx)
|
|
}
|
|
|
|
// We only allow the api to match one calendar with provided name.
|
|
// Return an error if multiple calendars exist (unlikely) or if no calendar
|
|
// is found.
|
|
if len(resp.GetValue()) != 1 {
|
|
err = clues.New("unexpected number of calendars returned").
|
|
With("returned_calendar_count", len(resp.GetValue()))
|
|
return nil, err
|
|
}
|
|
|
|
// Sanity check ID and name
|
|
cal := resp.GetValue()[0]
|
|
cd := CalendarDisplayable{Calendarable: cal}
|
|
|
|
if err := graph.CheckIDAndName(cd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cal, nil
|
|
}
|
|
|
|
// GetItem retrieves an Eventable item.
|
|
func (c Events) GetItem(
|
|
ctx context.Context,
|
|
user, itemID string,
|
|
immutableIDs bool,
|
|
errs *fault.Bus,
|
|
) (serialization.Parsable, *details.ExchangeInfo, error) {
|
|
var (
|
|
err error
|
|
event models.Eventable
|
|
header = buildPreferHeaders(false, immutableIDs)
|
|
itemOpts = &users.ItemEventsEventItemRequestBuilderGetRequestConfiguration{
|
|
Headers: header,
|
|
}
|
|
)
|
|
|
|
event, err = c.Stable.Client().Users().ByUserId(user).Events().ByEventId(itemID).Get(ctx, itemOpts)
|
|
if err != nil {
|
|
return nil, nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
|
|
options := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
|
|
Expand: []string{"microsoft.graph.itemattachment/item"},
|
|
},
|
|
Headers: header,
|
|
}
|
|
|
|
attached, err := c.LargeItem.
|
|
Client().
|
|
Users().
|
|
ByUserId(user).
|
|
Events().
|
|
ByEventId(itemID).
|
|
Attachments().
|
|
Get(ctx, options)
|
|
if err != nil {
|
|
return nil, nil, graph.Wrap(ctx, err, "event attachment download")
|
|
}
|
|
|
|
event.SetAttachments(attached.GetValue())
|
|
}
|
|
|
|
return event, EventInfo(event), nil
|
|
}
|
|
|
|
// EnumerateContainers iterates through all of the users current
|
|
// calendars, converting each to a graph.CacheFolder, and
|
|
// calling fn(cf) on each one.
|
|
// Folder hierarchy is represented in its current state, and does
|
|
// not contain historical data.
|
|
func (c Events) EnumerateContainers(
|
|
ctx context.Context,
|
|
userID, baseDirID string,
|
|
fn func(graph.CacheFolder) error,
|
|
errs *fault.Bus,
|
|
) error {
|
|
service, err := c.Service()
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
queryParams := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "name"},
|
|
},
|
|
}
|
|
|
|
el := errs.Local()
|
|
builder := service.Client().Users().ByUserId(userID).Calendars()
|
|
|
|
for {
|
|
if el.Failure() != nil {
|
|
break
|
|
}
|
|
|
|
resp, err := builder.Get(ctx, queryParams)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
for _, cal := range resp.GetValue() {
|
|
if el.Failure() != nil {
|
|
break
|
|
}
|
|
|
|
cd := CalendarDisplayable{Calendarable: cal}
|
|
if err := graph.CheckIDAndName(cd); err != nil {
|
|
errs.AddRecoverable(graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
|
|
continue
|
|
}
|
|
|
|
fctx := clues.Add(
|
|
ctx,
|
|
"container_id", ptr.Val(cal.GetId()),
|
|
"container_name", ptr.Val(cal.GetName()))
|
|
|
|
temp := graph.NewCacheFolder(
|
|
cd,
|
|
path.Builder{}.Append(ptr.Val(cd.GetId())), // storage path
|
|
path.Builder{}.Append(ptr.Val(cd.GetDisplayName()))) // display location
|
|
if err := fn(temp); err != nil {
|
|
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
|
|
continue
|
|
}
|
|
}
|
|
|
|
link, ok := ptr.ValOK(resp.GetOdataNextLink())
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
builder = users.NewItemCalendarsRequestBuilder(link, service.Adapter())
|
|
}
|
|
|
|
return el.Failure()
|
|
}
|
|
|
|
const (
|
|
eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// item pager
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ itemPager = &eventPager{}
|
|
|
|
type eventPager struct {
|
|
gs graph.Servicer
|
|
builder *users.ItemCalendarsItemEventsRequestBuilder
|
|
options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration
|
|
}
|
|
|
|
func NewEventPager(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user, calendarID string,
|
|
immutableIDs bool,
|
|
) (itemPager, error) {
|
|
options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{
|
|
Headers: buildPreferHeaders(true, immutableIDs),
|
|
}
|
|
|
|
builder := gs.Client().Users().ByUserId(user).Calendars().ByCalendarId(calendarID).Events()
|
|
|
|
return &eventPager{gs, builder, options}, nil
|
|
}
|
|
|
|
func (p *eventPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) {
|
|
resp, err := p.builder.Get(ctx, p.options)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return api.EmptyDeltaLinker[models.Eventable]{PageLinkValuer: resp}, nil
|
|
}
|
|
|
|
func (p *eventPager) setNext(nextLink string) {
|
|
p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter())
|
|
}
|
|
|
|
// non delta pagers don't need reset
|
|
func (p *eventPager) reset(context.Context) {}
|
|
|
|
func (p *eventPager) valuesIn(pl api.PageLinker) ([]getIDAndAddtler, error) {
|
|
return toValues[models.Eventable](pl)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// delta item pager
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ itemPager = &eventDeltaPager{}
|
|
|
|
type eventDeltaPager struct {
|
|
gs graph.Servicer
|
|
user string
|
|
calendarID string
|
|
builder *users.ItemCalendarsItemEventsDeltaRequestBuilder
|
|
options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration
|
|
}
|
|
|
|
func NewEventDeltaPager(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user, calendarID, deltaURL string,
|
|
immutableIDs bool,
|
|
) (itemPager, error) {
|
|
options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{
|
|
Headers: buildPreferHeaders(true, immutableIDs),
|
|
}
|
|
|
|
var builder *users.ItemCalendarsItemEventsDeltaRequestBuilder
|
|
|
|
if deltaURL == "" {
|
|
builder = getEventDeltaBuilder(ctx, gs, user, calendarID, options)
|
|
} else {
|
|
builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(deltaURL, gs.Adapter())
|
|
}
|
|
|
|
return &eventDeltaPager{gs, user, calendarID, builder, options}, nil
|
|
}
|
|
|
|
func getEventDeltaBuilder(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user string,
|
|
calendarID string,
|
|
options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration,
|
|
) *users.ItemCalendarsItemEventsDeltaRequestBuilder {
|
|
// Graph SDK only supports delta queries against events on the beta version, so we're
|
|
// manufacturing use of the beta version url to make the call instead.
|
|
// See: https://learn.microsoft.com/ko-kr/graph/api/event-delta?view=graph-rest-beta&tabs=http
|
|
// Note that the delta item body is skeletal compared to the actual event struct. Lucky
|
|
// for us, we only need the item ID. As a result, even though we hacked the version, the
|
|
// response body parses properly into the v1.0 structs and complies with our wanted interfaces.
|
|
// Likewise, the NextLink and DeltaLink odata tags carry our hack forward, so the rest of the code
|
|
// works as intended (until, at least, we want to _not_ call the beta anymore).
|
|
rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, user, calendarID)
|
|
builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, gs.Adapter())
|
|
|
|
return builder
|
|
}
|
|
|
|
func (p *eventDeltaPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) {
|
|
resp, err := p.builder.Get(ctx, p.options)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (p *eventDeltaPager) setNext(nextLink string) {
|
|
p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter())
|
|
}
|
|
|
|
func (p *eventDeltaPager) reset(ctx context.Context) {
|
|
p.builder = getEventDeltaBuilder(ctx, p.gs, p.user, p.calendarID, p.options)
|
|
}
|
|
|
|
func (p *eventDeltaPager) valuesIn(pl api.PageLinker) ([]getIDAndAddtler, error) {
|
|
return toValues[models.Eventable](pl)
|
|
}
|
|
|
|
func (c Events) GetAddedAndRemovedItemIDs(
|
|
ctx context.Context,
|
|
user, calendarID, oldDelta string,
|
|
immutableIDs bool,
|
|
canMakeDeltaQueries bool,
|
|
) ([]string, []string, DeltaUpdate, error) {
|
|
service, err := c.Service()
|
|
if err != nil {
|
|
return nil, nil, DeltaUpdate{}, err
|
|
}
|
|
|
|
ctx = clues.Add(
|
|
ctx,
|
|
"container_id", calendarID)
|
|
|
|
pager, err := NewEventPager(ctx, service, user, calendarID, immutableIDs)
|
|
if err != nil {
|
|
return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating non-delta pager")
|
|
}
|
|
|
|
deltaPager, err := NewEventDeltaPager(ctx, service, user, calendarID, oldDelta, immutableIDs)
|
|
if err != nil {
|
|
return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating delta pager")
|
|
}
|
|
|
|
return getAddedAndRemovedItemIDs(ctx, service, pager, deltaPager, oldDelta, canMakeDeltaQueries)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Serialization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Serialize transforms the event into a byte slice.
|
|
func (c Events) Serialize(
|
|
ctx context.Context,
|
|
item serialization.Parsable,
|
|
user, itemID string,
|
|
) ([]byte, error) {
|
|
event, ok := item.(models.Eventable)
|
|
if !ok {
|
|
return nil, clues.New(fmt.Sprintf("item is not an Eventable: %T", item))
|
|
}
|
|
|
|
ctx = clues.Add(ctx, "item_id", ptr.Val(event.GetId()))
|
|
|
|
var (
|
|
err error
|
|
writer = kjson.NewJsonSerializationWriter()
|
|
)
|
|
|
|
defer writer.Close()
|
|
|
|
if err = writer.WriteObjectValue("", event); err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
bs, err := writer.GetSerializedContent()
|
|
if err != nil {
|
|
return nil, graph.Wrap(ctx, err, "serializing event")
|
|
}
|
|
|
|
return bs, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helper funcs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// CalendarDisplayable is a wrapper that complies with the
|
|
// models.Calendarable interface with the graph.Container
|
|
// interfaces. Calendars do not have a parentFolderID.
|
|
// Therefore, that value will always return nil.
|
|
type CalendarDisplayable struct {
|
|
models.Calendarable
|
|
}
|
|
|
|
// GetDisplayName returns the *string of the models.Calendable
|
|
// variant: calendar.GetName()
|
|
func (c CalendarDisplayable) GetDisplayName() *string {
|
|
return c.GetName()
|
|
}
|
|
|
|
// GetParentFolderId returns the default calendar name address
|
|
// EventCalendars have a flat hierarchy and Calendars are rooted
|
|
// at the default
|
|
//
|
|
//nolint:revive
|
|
func (c CalendarDisplayable) GetParentFolderId() *string {
|
|
return nil
|
|
}
|
|
|
|
func EventInfo(evt models.Eventable) *details.ExchangeInfo {
|
|
var (
|
|
organizer string
|
|
subject = ptr.Val(evt.GetSubject())
|
|
recurs bool
|
|
start = time.Time{}
|
|
end = time.Time{}
|
|
created = ptr.Val(evt.GetCreatedDateTime())
|
|
)
|
|
|
|
if evt.GetOrganizer() != nil &&
|
|
evt.GetOrganizer().GetEmailAddress() != nil {
|
|
organizer = ptr.Val(evt.GetOrganizer().GetEmailAddress().GetAddress())
|
|
}
|
|
|
|
if evt.GetRecurrence() != nil {
|
|
recurs = true
|
|
}
|
|
|
|
if evt.GetStart() != nil && len(ptr.Val(evt.GetStart().GetDateTime())) > 0 {
|
|
// timeString has 'Z' literal added to ensure the stored
|
|
// DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
startTime := ptr.Val(evt.GetStart().GetDateTime()) + "Z"
|
|
|
|
output, err := dttm.ParseTime(startTime)
|
|
if err == nil {
|
|
start = output
|
|
}
|
|
}
|
|
|
|
if evt.GetEnd() != nil && len(ptr.Val(evt.GetEnd().GetDateTime())) > 0 {
|
|
// timeString has 'Z' literal added to ensure the stored
|
|
// DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
endTime := ptr.Val(evt.GetEnd().GetDateTime()) + "Z"
|
|
|
|
output, err := dttm.ParseTime(endTime)
|
|
if err == nil {
|
|
end = output
|
|
}
|
|
}
|
|
|
|
return &details.ExchangeInfo{
|
|
ItemType: details.ExchangeEvent,
|
|
Organizer: organizer,
|
|
Subject: subject,
|
|
EventStart: start,
|
|
EventEnd: end,
|
|
EventRecurs: recurs,
|
|
Created: created,
|
|
Modified: ptr.OrNow(evt.GetLastModifiedDateTime()),
|
|
}
|
|
}
|