diff --git a/src/cli/backup/teamschats.go b/src/cli/backup/teamschats.go index 190189380..487cc41e4 100644 --- a/src/cli/backup/teamschats.go +++ b/src/cli/backup/teamschats.go @@ -213,7 +213,6 @@ func runDetailsTeamsChatsCmd(cmd *cobra.Command) error { opts := utils.MakeTeamsChatsOpts(cmd) sel := utils.IncludeTeamsChatsRestoreDataSelectors(ctx, opts) - sel.Configure(selectors.Config{OnlyMatchItemNames: true}) utils.FilterTeamsChatsRestoreInfoSelectors(sel, opts) ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector) diff --git a/src/cli/utils/teamschats.go b/src/cli/utils/teamschats.go index 1467949a3..f91609bbc 100644 --- a/src/cli/utils/teamschats.go +++ b/src/cli/utils/teamschats.go @@ -89,7 +89,10 @@ func IncludeTeamsChatsRestoreDataSelectors(ctx context.Context, opts TeamsChatsO users = selectors.Any() } - return selectors.NewTeamsChatsRestore(users) + sel := selectors.NewTeamsChatsRestore(users) + sel.Include(sel.Chats(selectors.Any())) + + return sel } // FilterTeamsChatsRestoreInfoSelectors builds the common info-selector filters. diff --git a/src/internal/m365/collection/teamschats/backup.go b/src/internal/m365/collection/teamschats/backup.go index 732d32286..f554540eb 100644 --- a/src/internal/m365/collection/teamschats/backup.go +++ b/src/internal/m365/collection/teamschats/backup.go @@ -115,11 +115,8 @@ func populateCollection[I chatsItemer]( ) ctx = clues.AddLabelCounter(ctx, cl.PlainAdder()) - cc := api.CallConfig{ - CanMakeDeltaQueries: false, - } - items, err := bh.getItemIDs(ctx, cc) + items, err := bh.getItemIDs(ctx) if err != nil { errs.AddRecoverable(ctx, clues.Stack(err)) return collection, clues.Stack(errs.Failure()).OrNil() diff --git a/src/internal/m365/collection/teamschats/backup_test.go b/src/internal/m365/collection/teamschats/backup_test.go index 9ca745b55..3bf22a776 100644 --- a/src/internal/m365/collection/teamschats/backup_test.go +++ b/src/internal/m365/collection/teamschats/backup_test.go @@ -49,14 +49,6 @@ type mockBackupHandler struct { doNotInclude bool } -//lint:ignore U1000 false linter issue due to generics -func (bh mockBackupHandler) augmentItemInfo( - *details.TeamsChatsInfo, - models.Chatable, -) { - // no-op -} - func (bh mockBackupHandler) container() container[models.Chatable] { return chatContainer() } @@ -71,7 +63,6 @@ func (bh mockBackupHandler) getContainer( func (bh mockBackupHandler) getItemIDs( _ context.Context, - _ api.CallConfig, ) ([]models.Chatable, error) { return bh.chats, bh.chatsErr } diff --git a/src/internal/m365/collection/teamschats/chat_handler.go b/src/internal/m365/collection/teamschats/chat_handler.go index b87f4dd1b..adbd1103b 100644 --- a/src/internal/m365/collection/teamschats/chat_handler.go +++ b/src/internal/m365/collection/teamschats/chat_handler.go @@ -46,8 +46,11 @@ func (bh usersChatsBackupHandler) getContainer( //lint:ignore U1000 required for interface compliance func (bh usersChatsBackupHandler) getItemIDs( ctx context.Context, - cc api.CallConfig, ) ([]models.Chatable, error) { + cc := api.CallConfig{ + Expand: []string{"lastMessagePreview"}, + } + return bh.ac.GetChats( ctx, bh.protectedResourceID, @@ -89,26 +92,21 @@ func (bh usersChatsBackupHandler) getItem( chatID := ptr.Val(chat.GetId()) - cc := api.CallConfig{ - Expand: []string{"lastMessagePreview"}, - } - - msgs, err := bh.ac.GetChatMessages(ctx, chatID, cc) + msgs, err := bh.ac.GetChatMessages(ctx, chatID, api.CallConfig{}) if err != nil { return nil, nil, clues.Stack(err) } chat.SetMessages(msgs) - return chat, api.TeamsChatInfo(chat), nil -} + members, err := bh.ac.GetChatMembers(ctx, chatID, api.CallConfig{}) + if err != nil { + return nil, nil, clues.Stack(err) + } -//lint:ignore U1000 false linter issue due to generics -func (bh usersChatsBackupHandler) augmentItemInfo( - dgi *details.TeamsChatsInfo, - c models.Chatable, -) { - // no-op + chat.SetMembers(members) + + return chat, api.TeamsChatInfo(chat), nil } func chatContainer() container[models.Chatable] { diff --git a/src/internal/m365/collection/teamschats/collection.go b/src/internal/m365/collection/teamschats/collection.go index 6c32bd5cd..e2fe532db 100644 --- a/src/internal/m365/collection/teamschats/collection.go +++ b/src/internal/m365/collection/teamschats/collection.go @@ -66,7 +66,7 @@ func updateStatus( // or notMoved (if they match). func NewCollection[I chatsItemer]( baseCol data.BaseCollection, - getAndAugment getItemAndAugmentInfoer[I], + getter getItemer[I], protectedResource string, items []I, contains container[I], @@ -76,7 +76,7 @@ func NewCollection[I chatsItemer]( BaseCollection: baseCol, items: items, contains: contains, - getAndAugment: getAndAugment, + getter: getter, statusUpdater: statusUpdater, stream: make(chan data.Item, collectionChannelBufferSize), protectedResource: protectedResource, @@ -96,7 +96,7 @@ type lazyFetchCollection[I chatsItemer] struct { items []I - getAndAugment getItemAndAugmentInfoer[I] + getter getItemer[I] statusUpdater support.StatusUpdater } @@ -167,13 +167,13 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault. col.stream <- data.NewLazyItemWithInfo( ictx, &lazyItemGetter[I]{ - modTime: modTime, - getAndAugment: col.getAndAugment, - resourceID: col.protectedResource, - item: item, - containerIDs: col.FullPath().Folders(), - contains: col.contains, - parentPath: col.LocationPath().String(), + modTime: modTime, + getter: col.getter, + resourceID: col.protectedResource, + item: item, + containerIDs: col.FullPath().Folders(), + contains: col.contains, + parentPath: col.LocationPath().String(), }, itemID, modTime, @@ -192,13 +192,13 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault. } type lazyItemGetter[I chatsItemer] struct { - getAndAugment getItemAndAugmentInfoer[I] - resourceID string - item I - parentPath string - containerIDs path.Elements - modTime time.Time - contains container[I] + getter getItemer[I] + resourceID string + item I + parentPath string + containerIDs path.Elements + modTime time.Time + contains container[I] } func (lig *lazyItemGetter[I]) GetData( @@ -208,7 +208,7 @@ func (lig *lazyItemGetter[I]) GetData( writer := kjson.NewJsonSerializationWriter() defer writer.Close() - item, info, err := lig.getAndAugment.getItem( + item, info, err := lig.getter.getItem( ctx, lig.resourceID, lig.item) @@ -229,8 +229,6 @@ func (lig *lazyItemGetter[I]) GetData( return nil, nil, false, err } - lig.getAndAugment.augmentItemInfo(info, lig.contains.container) - if err := writer.WriteObjectValue("", item); err != nil { err = clues.WrapWC(ctx, err, "writing item to serializer").Label(fault.LabelForceNoBackupCreation) errs.AddRecoverable(ctx, err) diff --git a/src/internal/m365/collection/teamschats/collection_test.go b/src/internal/m365/collection/teamschats/collection_test.go index 1eb8a237c..b1296e40f 100644 --- a/src/internal/m365/collection/teamschats/collection_test.go +++ b/src/internal/m365/collection/teamschats/collection_test.go @@ -163,11 +163,6 @@ func (m getAndAugmentChat) getItem( return chat, &details.TeamsChatsInfo{}, m.err } -//lint:ignore U1000 false linter issue due to generics -func (getAndAugmentChat) augmentItemInfo(*details.TeamsChatsInfo, models.Chatable) { - // no-op -} - func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() { var ( t = suite.T() @@ -225,7 +220,7 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() { count.New()), items: test.items, contains: container[models.Chatable]{}, - getAndAugment: getterAugmenter, + getter: getterAugmenter, stream: make(chan data.Item), statusUpdater: statusUpdater, } @@ -285,11 +280,11 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() { li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - item: chat, - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, ptr.Val(chat.GetId()), now, @@ -329,11 +324,11 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - item: chat, - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, ptr.Val(chat.GetId()), now, @@ -368,11 +363,11 @@ func (suite *CollectionUnitSuite) TestLazyItem() { li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - item: chat, - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, ptr.Val(chat.GetId()), now, diff --git a/src/internal/m365/collection/teamschats/handlers.go b/src/internal/m365/collection/teamschats/handlers.go index 2763f8704..7865de489 100644 --- a/src/internal/m365/collection/teamschats/handlers.go +++ b/src/internal/m365/collection/teamschats/handlers.go @@ -22,7 +22,7 @@ type chatsItemer interface { type backupHandler[I chatsItemer] interface { getContainerer[I] - getItemAndAugmentInfoer[I] + getItemer[I] getItemer[I] getItemIDser[I] includeItemer[I] @@ -39,22 +39,10 @@ type getContainerer[I chatsItemer] interface { ) (container[I], error) } -type getItemAndAugmentInfoer[I chatsItemer] interface { - getItemer[I] - augmentItemInfoer[I] -} - -type augmentItemInfoer[I chatsItemer] interface { - // augmentItemInfo completes the teamChatsInfo population with any data - // owned by the container and not accessible to the item. - augmentItemInfo(*details.TeamsChatsInfo, I) -} - // gets all item IDs in the container type getItemIDser[I chatsItemer] interface { getItemIDs( ctx context.Context, - cc api.CallConfig, ) ([]I, error) } diff --git a/src/pkg/backup/details/entry.go b/src/pkg/backup/details/entry.go index 0d040eb67..a020598d1 100644 --- a/src/pkg/backup/details/entry.go +++ b/src/pkg/backup/details/entry.go @@ -103,6 +103,12 @@ func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) { } baseLoc = path.Builder{}.Append(p.Root).Append(p.Folders...) + + case TeamsChat: + baseLoc = &path.Builder{} + + default: + return nil, clues.New("undentified item type").With("item_type", de.ItemInfo.infoType()) } if baseLoc == nil { @@ -141,26 +147,23 @@ func (de Entry) MinimumPrintable() any { // Headers returns the human-readable names of properties in a DetailsEntry // for printing out to a terminal in a columnar display. func (de Entry) Headers(skipID bool) []string { - hs := []string{} + var hs []string - if de.ItemInfo.Folder != nil { + switch { + case de.ItemInfo.Folder != nil: hs = de.ItemInfo.Folder.Headers() - } - - if de.ItemInfo.Exchange != nil { + case de.ItemInfo.Exchange != nil: hs = de.ItemInfo.Exchange.Headers() - } - - if de.ItemInfo.SharePoint != nil { + case de.ItemInfo.SharePoint != nil: hs = de.ItemInfo.SharePoint.Headers() - } - - if de.ItemInfo.OneDrive != nil { + case de.ItemInfo.OneDrive != nil: hs = de.ItemInfo.OneDrive.Headers() - } - - if de.ItemInfo.Groups != nil { + case de.ItemInfo.Groups != nil: hs = de.ItemInfo.Groups.Headers() + case de.ItemInfo.TeamsChats != nil: + hs = de.ItemInfo.TeamsChats.Headers() + default: + hs = []string{"ERROR - Service not recognized"} } if skipID { @@ -172,26 +175,23 @@ func (de Entry) Headers(skipID bool) []string { // Values returns the values matching the Headers list. func (de Entry) Values(skipID bool) []string { - vs := []string{} + var vs []string - if de.ItemInfo.Folder != nil { + switch { + case de.ItemInfo.Folder != nil: vs = de.ItemInfo.Folder.Values() - } - - if de.ItemInfo.Exchange != nil { + case de.ItemInfo.Exchange != nil: vs = de.ItemInfo.Exchange.Values() - } - - if de.ItemInfo.SharePoint != nil { + case de.ItemInfo.SharePoint != nil: vs = de.ItemInfo.SharePoint.Values() - } - - if de.ItemInfo.OneDrive != nil { + case de.ItemInfo.OneDrive != nil: vs = de.ItemInfo.OneDrive.Values() - } - - if de.ItemInfo.Groups != nil { + case de.ItemInfo.Groups != nil: vs = de.ItemInfo.Groups.Values() + case de.ItemInfo.TeamsChats != nil: + vs = de.ItemInfo.TeamsChats.Values() + default: + vs = []string{"ERROR - Service not recognized"} } if skipID { diff --git a/src/pkg/backup/details/iteminfo.go b/src/pkg/backup/details/iteminfo.go index 22391d2d8..656d7d92a 100644 --- a/src/pkg/backup/details/iteminfo.go +++ b/src/pkg/backup/details/iteminfo.go @@ -91,19 +91,14 @@ func (i ItemInfo) infoType() ItemType { switch { case i.Folder != nil: return i.Folder.ItemType - case i.Exchange != nil: return i.Exchange.ItemType - case i.SharePoint != nil: return i.SharePoint.ItemType - case i.OneDrive != nil: return i.OneDrive.ItemType - case i.Groups != nil: return i.Groups.ItemType - case i.TeamsChats != nil: return i.TeamsChats.ItemType } @@ -115,19 +110,14 @@ func (i ItemInfo) size() int64 { switch { case i.Exchange != nil: return i.Exchange.Size - case i.OneDrive != nil: return i.OneDrive.Size - case i.SharePoint != nil: return i.SharePoint.Size - case i.Groups != nil: return i.Groups.Size - case i.Folder != nil: return i.Folder.Size - case i.TeamsChats != nil: return int64(i.TeamsChats.Chat.MessageCount) } @@ -139,19 +129,14 @@ func (i ItemInfo) Modified() time.Time { switch { case i.Exchange != nil: return i.Exchange.Modified - case i.OneDrive != nil: return i.OneDrive.Modified - case i.SharePoint != nil: return i.SharePoint.Modified - case i.Groups != nil: return i.Groups.Modified - case i.Folder != nil: return i.Folder.Modified - case i.TeamsChats != nil: return i.TeamsChats.Modified } @@ -163,19 +148,14 @@ func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) { switch { case i.Exchange != nil: return i.Exchange.uniqueLocation(baseLoc) - case i.OneDrive != nil: return i.OneDrive.uniqueLocation(baseLoc) - case i.SharePoint != nil: return i.SharePoint.uniqueLocation(baseLoc) - case i.Groups != nil: return i.Groups.uniqueLocation(baseLoc) - case i.TeamsChats != nil: return i.TeamsChats.uniqueLocation(baseLoc) - default: return nil, clues.New("unsupported type") } @@ -185,19 +165,14 @@ func (i ItemInfo) updateFolder(f *FolderInfo) error { switch { case i.Exchange != nil: return i.Exchange.updateFolder(f) - case i.OneDrive != nil: return i.OneDrive.updateFolder(f) - case i.SharePoint != nil: return i.SharePoint.updateFolder(f) - case i.Groups != nil: return i.Groups.updateFolder(f) - case i.TeamsChats != nil: return i.TeamsChats.updateFolder(f) - default: return clues.New("unsupported type") } diff --git a/src/pkg/backup/details/teamsChats.go b/src/pkg/backup/details/teamsChats.go index 0974941a3..4cd159492 100644 --- a/src/pkg/backup/details/teamsChats.go +++ b/src/pkg/backup/details/teamsChats.go @@ -44,7 +44,7 @@ type ChatInfo struct { LastMessagePreview string `json:"preview,omitempty"` Members []string `json:"members,omitempty"` MessageCount int `json:"messageCount,omitempty"` - Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` } // Headers returns the human-readable names of properties in a ChatsInfo @@ -52,7 +52,7 @@ type ChatInfo struct { func (i TeamsChatsInfo) Headers() []string { switch i.ItemType { case TeamsChat: - return []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"} + return []string{"Topic", "Last message", "Last message at", "Message count", "Created", "Members"} } return []string{} @@ -75,7 +75,7 @@ func (i TeamsChatsInfo) Values() []string { } return []string{ - i.Chat.Name, + i.Chat.Topic, i.Chat.LastMessagePreview, dttm.FormatToTabularDisplay(i.Chat.LastMessageAt), strconv.Itoa(i.Chat.MessageCount), diff --git a/src/pkg/backup/details/teamsChats_test.go b/src/pkg/backup/details/teamsChats_test.go index 476a32341..d3f646f1d 100644 --- a/src/pkg/backup/details/teamsChats_test.go +++ b/src/pkg/backup/details/teamsChats_test.go @@ -42,7 +42,7 @@ func (suite *ChatsUnitSuite) TestChatsPrintable() { LastMessagePreview: "last message preview", Members: []string{"foo@bar.baz", "fnords@smarf.zoomba"}, MessageCount: 42, - Name: "chat name", + Topic: "chat name", }, }, expectHs: []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"}, diff --git a/src/pkg/selectors/teamsChats.go b/src/pkg/selectors/teamsChats.go index 60df1a08b..3e749c7e2 100644 --- a/src/pkg/selectors/teamsChats.go +++ b/src/pkg/selectors/teamsChats.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/identity" + "github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/path" @@ -254,21 +255,81 @@ func (sr *TeamsChatsRestore) ChatMember(memberID string) []TeamsChatsScope { } } -// ChatName produces one or more teamsChats chat name info scopes. +// ChatTopic produces one or more teamsChats chat name info scopes. // Matches any chat whose name contains the provided string. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] -func (sr *TeamsChatsRestore) ChatName(memberID string) []TeamsChatsScope { +func (sr *TeamsChatsRestore) ChatTopic(topic string) []TeamsChatsScope { return []TeamsChatsScope{ makeInfoScope[TeamsChatsScope]( TeamsChatsChat, - TeamsChatsInfoChatName, - []string{memberID}, + TeamsChatsInfoChatTopic, + []string{topic}, filters.In), } } +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose creation datetime is before the given datetime. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +func (sr *TeamsChatsRestore) ChatCreatedBefore(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatCreatedBefore, + []string{datetime}, + filters.Greater), + } +} + +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose creation datetime is after the given datetime. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +func (sr *TeamsChatsRestore) ChatCreatedAfter(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatCreatedAfter, + []string{datetime}, + filters.Less), + } +} + +// ChatLastMessageBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose most recent message (if it has messages) is before the given datetime. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +func (sr *TeamsChatsRestore) ChatLastMessageBefore(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatLastMessageBefore, + []string{datetime}, + filters.Greater), + } +} + +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose most recent message (if it has messages) is after the given datetime. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +func (sr *TeamsChatsRestore) ChatLastMessageAfter(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatLastMesasgeAfter, + []string{datetime}, + filters.Less), + } +} + // --------------------------------------------------------------------------- // Categories // --------------------------------------------------------------------------- @@ -288,8 +349,12 @@ const ( TeamsChatsChat teamsChatsCategory = "TeamsChatsChat" // data contained within details.ItemInfo - TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" - TeamsChatsInfoChatName teamsChatsCategory = "TeamsChatsInfoChatName" + TeamsChatsInfoChatCreatedBefore teamsChatsCategory = "TeamsChatsInfoChatCreatedBefore" + TeamsChatsInfoChatCreatedAfter teamsChatsCategory = "TeamsChatsInfoChatCreatedAfter" + TeamsChatsInfoChatLastMessageBefore teamsChatsCategory = "TeamsChatsInfoChatLastMessageBefore" + TeamsChatsInfoChatLastMesasgeAfter teamsChatsCategory = "TeamsChatsInfoChatLastMesasgeAfter" + TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" + TeamsChatsInfoChatTopic teamsChatsCategory = "TeamsChatsInfoChatName" ) // teamsChatsLeafProperties describes common metadata of the leaf categories @@ -317,7 +382,9 @@ func (ec teamsChatsCategory) String() string { // Ex: TeamsChatsUser.leafCat() => TeamsChatsUser func (ec teamsChatsCategory) leafCat() categorizer { switch ec { - case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatName: + case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatTopic, + TeamsChatsInfoChatCreatedBefore, TeamsChatsInfoChatCreatedAfter, + TeamsChatsInfoChatLastMessageBefore, TeamsChatsInfoChatLastMesasgeAfter: return TeamsChatsChat } @@ -505,8 +572,16 @@ func (s TeamsChatsScope) matchesInfo(dii details.ItemInfo) bool { switch infoCat { case TeamsChatsInfoChatMember: i = strings.Join(info.Chat.Members, ",") - case TeamsChatsInfoChatName: - i = info.Chat.Name + case TeamsChatsInfoChatTopic: + i = info.Chat.Topic + case TeamsChatsInfoChatCreatedBefore, TeamsChatsInfoChatCreatedAfter: + i = dttm.Format(info.Chat.CreatedAt) + case TeamsChatsInfoChatLastMessageBefore, TeamsChatsInfoChatLastMesasgeAfter: + if info.Chat.MessageCount < 1 { + return false + } + + i = dttm.Format(info.Chat.LastMessageAt) } return s.Matches(infoCat, i) diff --git a/src/pkg/selectors/teamsChats_test.go b/src/pkg/selectors/teamsChats_test.go index f3b695494..cc7f73727 100644 --- a/src/pkg/selectors/teamsChats_test.go +++ b/src/pkg/selectors/teamsChats_test.go @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/path" @@ -252,14 +253,16 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { cs := NewTeamsChatsRestore(Any()) const ( - name = "smarf mcfnords" + topic = "smarf mcfnords" member = "cooks@2many.smarf" subject = "I have seen the fnords!" + dtype = details.TeamsChat ) var ( now = time.Now() - future = now.Add(1 * time.Minute) + past = dttm.Format(now.Add(-1 * time.Minute)) + future = dttm.Format(now.Add(1 * time.Minute)) ) infoWith := func(itype details.ItemType) details.ItemInfo { @@ -269,11 +272,11 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { Chat: details.ChatInfo{ CreatedAt: now, HasExternalMembers: false, - LastMessageAt: future, + LastMessageAt: now, LastMessagePreview: "preview", Members: []string{member}, MessageCount: 1, - Name: name, + Topic: topic, }, }, } @@ -285,12 +288,20 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { scope []TeamsChatsScope expect assert.BoolAssertionFunc }{ - {"chat with a different member", details.TeamsChat, cs.ChatMember("blarps"), assert.False}, - {"chat with the same member", details.TeamsChat, cs.ChatMember(member), assert.True}, - {"chat with a member submatch search", details.TeamsChat, cs.ChatMember(member[2:5]), assert.True}, - {"chat with a different name", details.TeamsChat, cs.ChatName("blarps"), assert.False}, - {"chat with the same name", details.TeamsChat, cs.ChatName(name), assert.True}, - {"chat with a subname search", details.TeamsChat, cs.ChatName(name[2:5]), assert.True}, + {"chat with a different member", dtype, cs.ChatMember("blarps"), assert.False}, + {"chat with the same member", dtype, cs.ChatMember(member), assert.True}, + {"chat with a member submatch search", dtype, cs.ChatMember(member[2:5]), assert.True}, + {"chat with a different topic", dtype, cs.ChatTopic("blarps"), assert.False}, + {"chat with the same topic", dtype, cs.ChatTopic(topic), assert.True}, + {"chat with a subtopic search", dtype, cs.ChatTopic(topic[2:5]), assert.True}, + {"chat created after", dtype, cs.ChatCreatedAfter(past), assert.True}, + {"chat not created after", dtype, cs.ChatCreatedAfter(future), assert.False}, + {"chat created before", dtype, cs.ChatCreatedBefore(future), assert.True}, + {"chat not created before", dtype, cs.ChatCreatedBefore(past), assert.False}, + {"chat last message after", dtype, cs.ChatLastMessageAfter(past), assert.True}, + {"chat last message not after", dtype, cs.ChatLastMessageAfter(future), assert.False}, + {"chat last message before", dtype, cs.ChatLastMessageBefore(future), assert.True}, + {"chat last message not before", dtype, cs.ChatLastMessageBefore(past), assert.False}, } for _, test := range table { suite.Run(test.name, func() { diff --git a/src/pkg/services/m365/api/teamsChats.go b/src/pkg/services/m365/api/teamsChats.go index 3aca53c40..04e1b7459 100644 --- a/src/pkg/services/m365/api/teamsChats.go +++ b/src/pkg/services/m365/api/teamsChats.go @@ -103,7 +103,7 @@ func TeamsChatInfo(chat models.Chatable) *details.TeamsChatsInfo { LastMessagePreview: preview, Members: memberNames, MessageCount: len(msgs), - Name: ptr.Val(chat.GetTopic()), + Topic: ptr.Val(chat.GetTopic()), }, } } diff --git a/src/pkg/services/m365/api/teamsChats_pager.go b/src/pkg/services/m365/api/teamsChats_pager.go index fe5a126bb..77bee30e5 100644 --- a/src/pkg/services/m365/api/teamsChats_pager.go +++ b/src/pkg/services/m365/api/teamsChats_pager.go @@ -12,6 +12,79 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api/pagers" ) +// --------------------------------------------------------------------------- +// chat members pager +// --------------------------------------------------------------------------- + +// delta queries are not supported +var _ pagers.NonDeltaHandler[models.ConversationMemberable] = &chatMembersPageCtrl{} + +type chatMembersPageCtrl struct { + chatID string + gs graph.Servicer + builder *chats.ItemMembersRequestBuilder + options *chats.ItemMembersRequestBuilderGetRequestConfiguration +} + +func (p *chatMembersPageCtrl) SetNextLink(nextLink string) { + p.builder = chats.NewItemMembersRequestBuilder(nextLink, p.gs.Adapter()) +} + +func (p *chatMembersPageCtrl) GetPage( + ctx context.Context, +) (pagers.NextLinkValuer[models.ConversationMemberable], error) { + resp, err := p.builder.Get(ctx, p.options) + return resp, graph.Stack(ctx, err).OrNil() +} + +func (p *chatMembersPageCtrl) ValidModTimes() bool { + return true +} + +func (c Chats) NewChatMembersPager( + chatID string, + cc CallConfig, +) *chatMembersPageCtrl { + builder := c.Stable. + Client(). + Chats(). + ByChatId(chatID). + Members() + + options := &chats.ItemMembersRequestBuilderGetRequestConfiguration{ + QueryParameters: &chats.ItemMembersRequestBuilderGetQueryParameters{}, + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)), + } + + if len(cc.Select) > 0 { + options.QueryParameters.Select = cc.Select + } + + if len(cc.Expand) > 0 { + options.QueryParameters.Expand = cc.Expand + } + + return &chatMembersPageCtrl{ + chatID: chatID, + builder: builder, + gs: c.Stable, + options: options, + } +} + +// GetChatMembers fetches a delta of all members in the chat. +func (c Chats) GetChatMembers( + ctx context.Context, + chatID string, + cc CallConfig, +) ([]models.ConversationMemberable, error) { + ctx = clues.Add(ctx, "chat_id", chatID) + pager := c.NewChatMembersPager(chatID, cc) + items, err := pagers.BatchEnumerateItems[models.ConversationMemberable](ctx, pager) + + return items, graph.Stack(ctx, err).OrNil() +} + // --------------------------------------------------------------------------- // chat message pager // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/teamsChats_pager_test.go b/src/pkg/services/m365/api/teamsChats_pager_test.go index c74d5f733..b276b245e 100644 --- a/src/pkg/services/m365/api/teamsChats_pager_test.go +++ b/src/pkg/services/m365/api/teamsChats_pager_test.go @@ -61,6 +61,11 @@ func (suite *ChatsPagerIntgSuite) TestEnumerateChats() { ac, chatID, chat.GetLastMessagePreview()) + + testEnumerateChatMembers( + suite.T(), + ac, + chatID) }) } } @@ -123,3 +128,22 @@ func testEnumerateChatMessages( } } } + +func testEnumerateChatMembers( + t *testing.T, + ac Chats, + chatID string, +) { + ctx, flush := tester.NewContext(t) + defer flush() + + cc := CallConfig{} + + members, err := ac.GetChatMembers(ctx, chatID, cc) + require.NoError(t, err, clues.ToCore(err)) + + // no good way to test members right now. Even though + // the graph api response contains the `userID` and `email` + // properties, we can't access them in the sdk model + assert.NotEmpty(t, members) +} diff --git a/src/pkg/services/m365/api/teamsChats_test.go b/src/pkg/services/m365/api/teamsChats_test.go index 9d2c762f8..2f17cff92 100644 --- a/src/pkg/services/m365/api/teamsChats_test.go +++ b/src/pkg/services/m365/api/teamsChats_test.go @@ -64,7 +64,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() { ItemType: details.TeamsChat, Modified: then, Chat: details.ChatInfo{ - Name: "Hello world", + Topic: "Hello world", LastMessageAt: then, LastMessagePreview: id, Members: []string{}, @@ -92,7 +92,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() { ItemType: details.TeamsChat, Modified: then, Chat: details.ChatInfo{ - Name: "Hello world", + Topic: "Hello world", LastMessageAt: then, LastMessagePreview: id, Members: []string{}, @@ -120,7 +120,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() { ItemType: details.TeamsChat, Modified: then, Chat: details.ChatInfo{ - Name: "Hello world", + Topic: "Hello world", LastMessageAt: time.Time{}, LastMessagePreview: "", Members: []string{}, @@ -138,7 +138,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() { chat, expected := test.expected() result := TeamsChatInfo(chat) - assert.Equal(t, expected.Chat.Name, result.Chat.Name) + assert.Equal(t, expected.Chat.Topic, result.Chat.Topic) expectCreated := chat.GetCreatedDateTime() if expectCreated != nil {