get details working (and other cleanup) (#5116)

details wasn't properly listing backed up items. This fixes the details
display, and contains some code clean-up that occurred along the way.
This commit is contained in:
Keepers 2024-02-15 14:32:25 -07:00 committed by GitHub
parent 316e5f0195
commit 0efe25bb71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 291 additions and 164 deletions

View File

@ -213,7 +213,6 @@ func runDetailsTeamsChatsCmd(cmd *cobra.Command) error {
opts := utils.MakeTeamsChatsOpts(cmd) opts := utils.MakeTeamsChatsOpts(cmd)
sel := utils.IncludeTeamsChatsRestoreDataSelectors(ctx, opts) sel := utils.IncludeTeamsChatsRestoreDataSelectors(ctx, opts)
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
utils.FilterTeamsChatsRestoreInfoSelectors(sel, opts) utils.FilterTeamsChatsRestoreInfoSelectors(sel, opts)
ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector) ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector)

View File

@ -89,7 +89,10 @@ func IncludeTeamsChatsRestoreDataSelectors(ctx context.Context, opts TeamsChatsO
users = selectors.Any() 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. // FilterTeamsChatsRestoreInfoSelectors builds the common info-selector filters.

View File

@ -115,11 +115,8 @@ func populateCollection[I chatsItemer](
) )
ctx = clues.AddLabelCounter(ctx, cl.PlainAdder()) 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 { if err != nil {
errs.AddRecoverable(ctx, clues.Stack(err)) errs.AddRecoverable(ctx, clues.Stack(err))
return collection, clues.Stack(errs.Failure()).OrNil() return collection, clues.Stack(errs.Failure()).OrNil()

View File

@ -49,14 +49,6 @@ type mockBackupHandler struct {
doNotInclude bool 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] { func (bh mockBackupHandler) container() container[models.Chatable] {
return chatContainer() return chatContainer()
} }
@ -71,7 +63,6 @@ func (bh mockBackupHandler) getContainer(
func (bh mockBackupHandler) getItemIDs( func (bh mockBackupHandler) getItemIDs(
_ context.Context, _ context.Context,
_ api.CallConfig,
) ([]models.Chatable, error) { ) ([]models.Chatable, error) {
return bh.chats, bh.chatsErr return bh.chats, bh.chatsErr
} }

View File

@ -46,8 +46,11 @@ func (bh usersChatsBackupHandler) getContainer(
//lint:ignore U1000 required for interface compliance //lint:ignore U1000 required for interface compliance
func (bh usersChatsBackupHandler) getItemIDs( func (bh usersChatsBackupHandler) getItemIDs(
ctx context.Context, ctx context.Context,
cc api.CallConfig,
) ([]models.Chatable, error) { ) ([]models.Chatable, error) {
cc := api.CallConfig{
Expand: []string{"lastMessagePreview"},
}
return bh.ac.GetChats( return bh.ac.GetChats(
ctx, ctx,
bh.protectedResourceID, bh.protectedResourceID,
@ -89,26 +92,21 @@ func (bh usersChatsBackupHandler) getItem(
chatID := ptr.Val(chat.GetId()) chatID := ptr.Val(chat.GetId())
cc := api.CallConfig{ msgs, err := bh.ac.GetChatMessages(ctx, chatID, api.CallConfig{})
Expand: []string{"lastMessagePreview"},
}
msgs, err := bh.ac.GetChatMessages(ctx, chatID, cc)
if err != nil { if err != nil {
return nil, nil, clues.Stack(err) return nil, nil, clues.Stack(err)
} }
chat.SetMessages(msgs) 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 chat.SetMembers(members)
func (bh usersChatsBackupHandler) augmentItemInfo(
dgi *details.TeamsChatsInfo, return chat, api.TeamsChatInfo(chat), nil
c models.Chatable,
) {
// no-op
} }
func chatContainer() container[models.Chatable] { func chatContainer() container[models.Chatable] {

View File

@ -66,7 +66,7 @@ func updateStatus(
// or notMoved (if they match). // or notMoved (if they match).
func NewCollection[I chatsItemer]( func NewCollection[I chatsItemer](
baseCol data.BaseCollection, baseCol data.BaseCollection,
getAndAugment getItemAndAugmentInfoer[I], getter getItemer[I],
protectedResource string, protectedResource string,
items []I, items []I,
contains container[I], contains container[I],
@ -76,7 +76,7 @@ func NewCollection[I chatsItemer](
BaseCollection: baseCol, BaseCollection: baseCol,
items: items, items: items,
contains: contains, contains: contains,
getAndAugment: getAndAugment, getter: getter,
statusUpdater: statusUpdater, statusUpdater: statusUpdater,
stream: make(chan data.Item, collectionChannelBufferSize), stream: make(chan data.Item, collectionChannelBufferSize),
protectedResource: protectedResource, protectedResource: protectedResource,
@ -96,7 +96,7 @@ type lazyFetchCollection[I chatsItemer] struct {
items []I items []I
getAndAugment getItemAndAugmentInfoer[I] getter getItemer[I]
statusUpdater support.StatusUpdater statusUpdater support.StatusUpdater
} }
@ -167,13 +167,13 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault.
col.stream <- data.NewLazyItemWithInfo( col.stream <- data.NewLazyItemWithInfo(
ictx, ictx,
&lazyItemGetter[I]{ &lazyItemGetter[I]{
modTime: modTime, modTime: modTime,
getAndAugment: col.getAndAugment, getter: col.getter,
resourceID: col.protectedResource, resourceID: col.protectedResource,
item: item, item: item,
containerIDs: col.FullPath().Folders(), containerIDs: col.FullPath().Folders(),
contains: col.contains, contains: col.contains,
parentPath: col.LocationPath().String(), parentPath: col.LocationPath().String(),
}, },
itemID, itemID,
modTime, modTime,
@ -192,13 +192,13 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault.
} }
type lazyItemGetter[I chatsItemer] struct { type lazyItemGetter[I chatsItemer] struct {
getAndAugment getItemAndAugmentInfoer[I] getter getItemer[I]
resourceID string resourceID string
item I item I
parentPath string parentPath string
containerIDs path.Elements containerIDs path.Elements
modTime time.Time modTime time.Time
contains container[I] contains container[I]
} }
func (lig *lazyItemGetter[I]) GetData( func (lig *lazyItemGetter[I]) GetData(
@ -208,7 +208,7 @@ func (lig *lazyItemGetter[I]) GetData(
writer := kjson.NewJsonSerializationWriter() writer := kjson.NewJsonSerializationWriter()
defer writer.Close() defer writer.Close()
item, info, err := lig.getAndAugment.getItem( item, info, err := lig.getter.getItem(
ctx, ctx,
lig.resourceID, lig.resourceID,
lig.item) lig.item)
@ -229,8 +229,6 @@ func (lig *lazyItemGetter[I]) GetData(
return nil, nil, false, err return nil, nil, false, err
} }
lig.getAndAugment.augmentItemInfo(info, lig.contains.container)
if err := writer.WriteObjectValue("", item); err != nil { if err := writer.WriteObjectValue("", item); err != nil {
err = clues.WrapWC(ctx, err, "writing item to serializer").Label(fault.LabelForceNoBackupCreation) err = clues.WrapWC(ctx, err, "writing item to serializer").Label(fault.LabelForceNoBackupCreation)
errs.AddRecoverable(ctx, err) errs.AddRecoverable(ctx, err)

View File

@ -163,11 +163,6 @@ func (m getAndAugmentChat) getItem(
return chat, &details.TeamsChatsInfo{}, m.err 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() { func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
var ( var (
t = suite.T() t = suite.T()
@ -225,7 +220,7 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
count.New()), count.New()),
items: test.items, items: test.items,
contains: container[models.Chatable]{}, contains: container[models.Chatable]{},
getAndAugment: getterAugmenter, getter: getterAugmenter,
stream: make(chan data.Item), stream: make(chan data.Item),
statusUpdater: statusUpdater, statusUpdater: statusUpdater,
} }
@ -285,11 +280,11 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() {
li := data.NewLazyItemWithInfo( li := data.NewLazyItemWithInfo(
ctx, ctx,
&lazyItemGetter[models.Chatable]{ &lazyItemGetter[models.Chatable]{
resourceID: "resourceID", resourceID: "resourceID",
item: chat, item: chat,
getAndAugment: &m, getter: &m,
modTime: now, modTime: now,
parentPath: parentPath, parentPath: parentPath,
}, },
ptr.Val(chat.GetId()), ptr.Val(chat.GetId()),
now, now,
@ -329,11 +324,11 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig
li := data.NewLazyItemWithInfo( li := data.NewLazyItemWithInfo(
ctx, ctx,
&lazyItemGetter[models.Chatable]{ &lazyItemGetter[models.Chatable]{
resourceID: "resourceID", resourceID: "resourceID",
item: chat, item: chat,
getAndAugment: &m, getter: &m,
modTime: now, modTime: now,
parentPath: parentPath, parentPath: parentPath,
}, },
ptr.Val(chat.GetId()), ptr.Val(chat.GetId()),
now, now,
@ -368,11 +363,11 @@ func (suite *CollectionUnitSuite) TestLazyItem() {
li := data.NewLazyItemWithInfo( li := data.NewLazyItemWithInfo(
ctx, ctx,
&lazyItemGetter[models.Chatable]{ &lazyItemGetter[models.Chatable]{
resourceID: "resourceID", resourceID: "resourceID",
item: chat, item: chat,
getAndAugment: &m, getter: &m,
modTime: now, modTime: now,
parentPath: parentPath, parentPath: parentPath,
}, },
ptr.Val(chat.GetId()), ptr.Val(chat.GetId()),
now, now,

View File

@ -22,7 +22,7 @@ type chatsItemer interface {
type backupHandler[I chatsItemer] interface { type backupHandler[I chatsItemer] interface {
getContainerer[I] getContainerer[I]
getItemAndAugmentInfoer[I] getItemer[I]
getItemer[I] getItemer[I]
getItemIDser[I] getItemIDser[I]
includeItemer[I] includeItemer[I]
@ -39,22 +39,10 @@ type getContainerer[I chatsItemer] interface {
) (container[I], error) ) (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 // gets all item IDs in the container
type getItemIDser[I chatsItemer] interface { type getItemIDser[I chatsItemer] interface {
getItemIDs( getItemIDs(
ctx context.Context, ctx context.Context,
cc api.CallConfig,
) ([]I, error) ) ([]I, error)
} }

View File

@ -103,6 +103,12 @@ func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) {
} }
baseLoc = path.Builder{}.Append(p.Root).Append(p.Folders...) 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 { if baseLoc == nil {
@ -141,26 +147,23 @@ func (de Entry) MinimumPrintable() any {
// Headers returns the human-readable names of properties in a DetailsEntry // Headers returns the human-readable names of properties in a DetailsEntry
// for printing out to a terminal in a columnar display. // for printing out to a terminal in a columnar display.
func (de Entry) Headers(skipID bool) []string { 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() hs = de.ItemInfo.Folder.Headers()
} case de.ItemInfo.Exchange != nil:
if de.ItemInfo.Exchange != nil {
hs = de.ItemInfo.Exchange.Headers() hs = de.ItemInfo.Exchange.Headers()
} case de.ItemInfo.SharePoint != nil:
if de.ItemInfo.SharePoint != nil {
hs = de.ItemInfo.SharePoint.Headers() hs = de.ItemInfo.SharePoint.Headers()
} case de.ItemInfo.OneDrive != nil:
if de.ItemInfo.OneDrive != nil {
hs = de.ItemInfo.OneDrive.Headers() hs = de.ItemInfo.OneDrive.Headers()
} case de.ItemInfo.Groups != nil:
if de.ItemInfo.Groups != nil {
hs = de.ItemInfo.Groups.Headers() hs = de.ItemInfo.Groups.Headers()
case de.ItemInfo.TeamsChats != nil:
hs = de.ItemInfo.TeamsChats.Headers()
default:
hs = []string{"ERROR - Service not recognized"}
} }
if skipID { if skipID {
@ -172,26 +175,23 @@ func (de Entry) Headers(skipID bool) []string {
// Values returns the values matching the Headers list. // Values returns the values matching the Headers list.
func (de Entry) Values(skipID bool) []string { 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() vs = de.ItemInfo.Folder.Values()
} case de.ItemInfo.Exchange != nil:
if de.ItemInfo.Exchange != nil {
vs = de.ItemInfo.Exchange.Values() vs = de.ItemInfo.Exchange.Values()
} case de.ItemInfo.SharePoint != nil:
if de.ItemInfo.SharePoint != nil {
vs = de.ItemInfo.SharePoint.Values() vs = de.ItemInfo.SharePoint.Values()
} case de.ItemInfo.OneDrive != nil:
if de.ItemInfo.OneDrive != nil {
vs = de.ItemInfo.OneDrive.Values() vs = de.ItemInfo.OneDrive.Values()
} case de.ItemInfo.Groups != nil:
if de.ItemInfo.Groups != nil {
vs = de.ItemInfo.Groups.Values() vs = de.ItemInfo.Groups.Values()
case de.ItemInfo.TeamsChats != nil:
vs = de.ItemInfo.TeamsChats.Values()
default:
vs = []string{"ERROR - Service not recognized"}
} }
if skipID { if skipID {

View File

@ -91,19 +91,14 @@ func (i ItemInfo) infoType() ItemType {
switch { switch {
case i.Folder != nil: case i.Folder != nil:
return i.Folder.ItemType return i.Folder.ItemType
case i.Exchange != nil: case i.Exchange != nil:
return i.Exchange.ItemType return i.Exchange.ItemType
case i.SharePoint != nil: case i.SharePoint != nil:
return i.SharePoint.ItemType return i.SharePoint.ItemType
case i.OneDrive != nil: case i.OneDrive != nil:
return i.OneDrive.ItemType return i.OneDrive.ItemType
case i.Groups != nil: case i.Groups != nil:
return i.Groups.ItemType return i.Groups.ItemType
case i.TeamsChats != nil: case i.TeamsChats != nil:
return i.TeamsChats.ItemType return i.TeamsChats.ItemType
} }
@ -115,19 +110,14 @@ func (i ItemInfo) size() int64 {
switch { switch {
case i.Exchange != nil: case i.Exchange != nil:
return i.Exchange.Size return i.Exchange.Size
case i.OneDrive != nil: case i.OneDrive != nil:
return i.OneDrive.Size return i.OneDrive.Size
case i.SharePoint != nil: case i.SharePoint != nil:
return i.SharePoint.Size return i.SharePoint.Size
case i.Groups != nil: case i.Groups != nil:
return i.Groups.Size return i.Groups.Size
case i.Folder != nil: case i.Folder != nil:
return i.Folder.Size return i.Folder.Size
case i.TeamsChats != nil: case i.TeamsChats != nil:
return int64(i.TeamsChats.Chat.MessageCount) return int64(i.TeamsChats.Chat.MessageCount)
} }
@ -139,19 +129,14 @@ func (i ItemInfo) Modified() time.Time {
switch { switch {
case i.Exchange != nil: case i.Exchange != nil:
return i.Exchange.Modified return i.Exchange.Modified
case i.OneDrive != nil: case i.OneDrive != nil:
return i.OneDrive.Modified return i.OneDrive.Modified
case i.SharePoint != nil: case i.SharePoint != nil:
return i.SharePoint.Modified return i.SharePoint.Modified
case i.Groups != nil: case i.Groups != nil:
return i.Groups.Modified return i.Groups.Modified
case i.Folder != nil: case i.Folder != nil:
return i.Folder.Modified return i.Folder.Modified
case i.TeamsChats != nil: case i.TeamsChats != nil:
return i.TeamsChats.Modified return i.TeamsChats.Modified
} }
@ -163,19 +148,14 @@ func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
switch { switch {
case i.Exchange != nil: case i.Exchange != nil:
return i.Exchange.uniqueLocation(baseLoc) return i.Exchange.uniqueLocation(baseLoc)
case i.OneDrive != nil: case i.OneDrive != nil:
return i.OneDrive.uniqueLocation(baseLoc) return i.OneDrive.uniqueLocation(baseLoc)
case i.SharePoint != nil: case i.SharePoint != nil:
return i.SharePoint.uniqueLocation(baseLoc) return i.SharePoint.uniqueLocation(baseLoc)
case i.Groups != nil: case i.Groups != nil:
return i.Groups.uniqueLocation(baseLoc) return i.Groups.uniqueLocation(baseLoc)
case i.TeamsChats != nil: case i.TeamsChats != nil:
return i.TeamsChats.uniqueLocation(baseLoc) return i.TeamsChats.uniqueLocation(baseLoc)
default: default:
return nil, clues.New("unsupported type") return nil, clues.New("unsupported type")
} }
@ -185,19 +165,14 @@ func (i ItemInfo) updateFolder(f *FolderInfo) error {
switch { switch {
case i.Exchange != nil: case i.Exchange != nil:
return i.Exchange.updateFolder(f) return i.Exchange.updateFolder(f)
case i.OneDrive != nil: case i.OneDrive != nil:
return i.OneDrive.updateFolder(f) return i.OneDrive.updateFolder(f)
case i.SharePoint != nil: case i.SharePoint != nil:
return i.SharePoint.updateFolder(f) return i.SharePoint.updateFolder(f)
case i.Groups != nil: case i.Groups != nil:
return i.Groups.updateFolder(f) return i.Groups.updateFolder(f)
case i.TeamsChats != nil: case i.TeamsChats != nil:
return i.TeamsChats.updateFolder(f) return i.TeamsChats.updateFolder(f)
default: default:
return clues.New("unsupported type") return clues.New("unsupported type")
} }

View File

@ -44,7 +44,7 @@ type ChatInfo struct {
LastMessagePreview string `json:"preview,omitempty"` LastMessagePreview string `json:"preview,omitempty"`
Members []string `json:"members,omitempty"` Members []string `json:"members,omitempty"`
MessageCount int `json:"messageCount,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 // Headers returns the human-readable names of properties in a ChatsInfo
@ -52,7 +52,7 @@ type ChatInfo struct {
func (i TeamsChatsInfo) Headers() []string { func (i TeamsChatsInfo) Headers() []string {
switch i.ItemType { switch i.ItemType {
case TeamsChat: 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{} return []string{}
@ -75,7 +75,7 @@ func (i TeamsChatsInfo) Values() []string {
} }
return []string{ return []string{
i.Chat.Name, i.Chat.Topic,
i.Chat.LastMessagePreview, i.Chat.LastMessagePreview,
dttm.FormatToTabularDisplay(i.Chat.LastMessageAt), dttm.FormatToTabularDisplay(i.Chat.LastMessageAt),
strconv.Itoa(i.Chat.MessageCount), strconv.Itoa(i.Chat.MessageCount),

View File

@ -42,7 +42,7 @@ func (suite *ChatsUnitSuite) TestChatsPrintable() {
LastMessagePreview: "last message preview", LastMessagePreview: "last message preview",
Members: []string{"foo@bar.baz", "fnords@smarf.zoomba"}, Members: []string{"foo@bar.baz", "fnords@smarf.zoomba"},
MessageCount: 42, MessageCount: 42,
Name: "chat name", Topic: "chat name",
}, },
}, },
expectHs: []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"}, expectHs: []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"},

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity" "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/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "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. // 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.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults 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{ return []TeamsChatsScope{
makeInfoScope[TeamsChatsScope]( makeInfoScope[TeamsChatsScope](
TeamsChatsChat, TeamsChatsChat,
TeamsChatsInfoChatName, TeamsChatsInfoChatTopic,
[]string{memberID}, []string{topic},
filters.In), 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 // Categories
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -288,8 +349,12 @@ const (
TeamsChatsChat teamsChatsCategory = "TeamsChatsChat" TeamsChatsChat teamsChatsCategory = "TeamsChatsChat"
// data contained within details.ItemInfo // data contained within details.ItemInfo
TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" TeamsChatsInfoChatCreatedBefore teamsChatsCategory = "TeamsChatsInfoChatCreatedBefore"
TeamsChatsInfoChatName teamsChatsCategory = "TeamsChatsInfoChatName" TeamsChatsInfoChatCreatedAfter teamsChatsCategory = "TeamsChatsInfoChatCreatedAfter"
TeamsChatsInfoChatLastMessageBefore teamsChatsCategory = "TeamsChatsInfoChatLastMessageBefore"
TeamsChatsInfoChatLastMesasgeAfter teamsChatsCategory = "TeamsChatsInfoChatLastMesasgeAfter"
TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember"
TeamsChatsInfoChatTopic teamsChatsCategory = "TeamsChatsInfoChatName"
) )
// teamsChatsLeafProperties describes common metadata of the leaf categories // teamsChatsLeafProperties describes common metadata of the leaf categories
@ -317,7 +382,9 @@ func (ec teamsChatsCategory) String() string {
// Ex: TeamsChatsUser.leafCat() => TeamsChatsUser // Ex: TeamsChatsUser.leafCat() => TeamsChatsUser
func (ec teamsChatsCategory) leafCat() categorizer { func (ec teamsChatsCategory) leafCat() categorizer {
switch ec { switch ec {
case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatName: case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatTopic,
TeamsChatsInfoChatCreatedBefore, TeamsChatsInfoChatCreatedAfter,
TeamsChatsInfoChatLastMessageBefore, TeamsChatsInfoChatLastMesasgeAfter:
return TeamsChatsChat return TeamsChatsChat
} }
@ -505,8 +572,16 @@ func (s TeamsChatsScope) matchesInfo(dii details.ItemInfo) bool {
switch infoCat { switch infoCat {
case TeamsChatsInfoChatMember: case TeamsChatsInfoChatMember:
i = strings.Join(info.Chat.Members, ",") i = strings.Join(info.Chat.Members, ",")
case TeamsChatsInfoChatName: case TeamsChatsInfoChatTopic:
i = info.Chat.Name 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) return s.Matches(infoCat, i)

View File

@ -12,6 +12,7 @@ import (
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details" "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/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -252,14 +253,16 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() {
cs := NewTeamsChatsRestore(Any()) cs := NewTeamsChatsRestore(Any())
const ( const (
name = "smarf mcfnords" topic = "smarf mcfnords"
member = "cooks@2many.smarf" member = "cooks@2many.smarf"
subject = "I have seen the fnords!" subject = "I have seen the fnords!"
dtype = details.TeamsChat
) )
var ( var (
now = time.Now() 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 { infoWith := func(itype details.ItemType) details.ItemInfo {
@ -269,11 +272,11 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() {
Chat: details.ChatInfo{ Chat: details.ChatInfo{
CreatedAt: now, CreatedAt: now,
HasExternalMembers: false, HasExternalMembers: false,
LastMessageAt: future, LastMessageAt: now,
LastMessagePreview: "preview", LastMessagePreview: "preview",
Members: []string{member}, Members: []string{member},
MessageCount: 1, MessageCount: 1,
Name: name, Topic: topic,
}, },
}, },
} }
@ -285,12 +288,20 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() {
scope []TeamsChatsScope scope []TeamsChatsScope
expect assert.BoolAssertionFunc expect assert.BoolAssertionFunc
}{ }{
{"chat with a different member", details.TeamsChat, cs.ChatMember("blarps"), assert.False}, {"chat with a different member", dtype, cs.ChatMember("blarps"), assert.False},
{"chat with the same member", details.TeamsChat, cs.ChatMember(member), assert.True}, {"chat with the same member", dtype, cs.ChatMember(member), assert.True},
{"chat with a member submatch search", details.TeamsChat, cs.ChatMember(member[2:5]), assert.True}, {"chat with a member submatch search", dtype, cs.ChatMember(member[2:5]), assert.True},
{"chat with a different name", details.TeamsChat, cs.ChatName("blarps"), assert.False}, {"chat with a different topic", dtype, cs.ChatTopic("blarps"), assert.False},
{"chat with the same name", details.TeamsChat, cs.ChatName(name), assert.True}, {"chat with the same topic", dtype, cs.ChatTopic(topic), assert.True},
{"chat with a subname search", details.TeamsChat, cs.ChatName(name[2:5]), 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 { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {

View File

@ -103,7 +103,7 @@ func TeamsChatInfo(chat models.Chatable) *details.TeamsChatsInfo {
LastMessagePreview: preview, LastMessagePreview: preview,
Members: memberNames, Members: memberNames,
MessageCount: len(msgs), MessageCount: len(msgs),
Name: ptr.Val(chat.GetTopic()), Topic: ptr.Val(chat.GetTopic()),
}, },
} }
} }

View File

@ -12,6 +12,79 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers" "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 // chat message pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -61,6 +61,11 @@ func (suite *ChatsPagerIntgSuite) TestEnumerateChats() {
ac, ac,
chatID, chatID,
chat.GetLastMessagePreview()) 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)
}

View File

@ -64,7 +64,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
ItemType: details.TeamsChat, ItemType: details.TeamsChat,
Modified: then, Modified: then,
Chat: details.ChatInfo{ Chat: details.ChatInfo{
Name: "Hello world", Topic: "Hello world",
LastMessageAt: then, LastMessageAt: then,
LastMessagePreview: id, LastMessagePreview: id,
Members: []string{}, Members: []string{},
@ -92,7 +92,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
ItemType: details.TeamsChat, ItemType: details.TeamsChat,
Modified: then, Modified: then,
Chat: details.ChatInfo{ Chat: details.ChatInfo{
Name: "Hello world", Topic: "Hello world",
LastMessageAt: then, LastMessageAt: then,
LastMessagePreview: id, LastMessagePreview: id,
Members: []string{}, Members: []string{},
@ -120,7 +120,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
ItemType: details.TeamsChat, ItemType: details.TeamsChat,
Modified: then, Modified: then,
Chat: details.ChatInfo{ Chat: details.ChatInfo{
Name: "Hello world", Topic: "Hello world",
LastMessageAt: time.Time{}, LastMessageAt: time.Time{},
LastMessagePreview: "", LastMessagePreview: "",
Members: []string{}, Members: []string{},
@ -138,7 +138,7 @@ func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
chat, expected := test.expected() chat, expected := test.expected()
result := TeamsChatInfo(chat) result := TeamsChatInfo(chat)
assert.Equal(t, expected.Chat.Name, result.Chat.Name) assert.Equal(t, expected.Chat.Topic, result.Chat.Topic)
expectCreated := chat.GetCreatedDateTime() expectCreated := chat.GetCreatedDateTime()
if expectCreated != nil { if expectCreated != nil {