Compare commits
2 Commits
main
...
5063-chats
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0efe25bb71 | ||
|
|
316e5f0195 |
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -96,15 +87,13 @@ func (bh mockBackupHandler) CanonicalPath() (path.Path, error) {
|
||||
func (bh mockBackupHandler) getItem(
|
||||
_ context.Context,
|
||||
_ string,
|
||||
itemID string,
|
||||
chat models.Chatable,
|
||||
) (models.Chatable, *details.TeamsChatsInfo, error) {
|
||||
chat := models.NewChat()
|
||||
chatID := ptr.Val(chat.GetId())
|
||||
|
||||
chat.SetId(ptr.To(itemID))
|
||||
chat.SetTopic(ptr.To(itemID))
|
||||
chat.SetMessages(bh.chatMessages[itemID])
|
||||
chat.SetMessages(bh.chatMessages[chatID])
|
||||
|
||||
return chat, bh.info[itemID], bh.getMessageErr[itemID]
|
||||
return chat, bh.info[chatID], bh.getMessageErr[chatID]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/selectors"
|
||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||
@ -45,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,
|
||||
@ -80,18 +84,29 @@ func (bh usersChatsBackupHandler) CanonicalPath() (path.Path, error) {
|
||||
func (bh usersChatsBackupHandler) getItem(
|
||||
ctx context.Context,
|
||||
userID string,
|
||||
chatID string,
|
||||
chat models.Chatable,
|
||||
) (models.Chatable, *details.TeamsChatsInfo, error) {
|
||||
// FIXME: should retrieve and populate all messages in the chat.
|
||||
return nil, nil, clues.New("not implemented")
|
||||
}
|
||||
if chat == nil {
|
||||
return nil, nil, clues.Stack(core.ErrNotFound)
|
||||
}
|
||||
|
||||
//lint:ignore U1000 false linter issue due to generics
|
||||
func (bh usersChatsBackupHandler) augmentItemInfo(
|
||||
dgi *details.TeamsChatsInfo,
|
||||
c models.Chatable,
|
||||
) {
|
||||
// no-op
|
||||
chatID := ptr.Val(chat.GetId())
|
||||
|
||||
msgs, err := bh.ac.GetChatMessages(ctx, chatID, api.CallConfig{})
|
||||
if err != nil {
|
||||
return nil, nil, clues.Stack(err)
|
||||
}
|
||||
|
||||
chat.SetMessages(msgs)
|
||||
|
||||
members, err := bh.ac.GetChatMembers(ctx, chatID, api.CallConfig{})
|
||||
if err != nil {
|
||||
return nil, nil, clues.Stack(err)
|
||||
}
|
||||
|
||||
chat.SetMembers(members)
|
||||
|
||||
return chat, api.TeamsChatInfo(chat), nil
|
||||
}
|
||||
|
||||
func chatContainer() container[models.Chatable] {
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -152,33 +152,30 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault.
|
||||
break
|
||||
}
|
||||
|
||||
itemID := ptr.Val(item.GetId())
|
||||
modTime := ptr.Val(item.GetLastUpdatedDateTime())
|
||||
|
||||
wg.Add(1)
|
||||
semaphoreCh <- struct{}{}
|
||||
|
||||
go func(id string, modTime time.Time) {
|
||||
go func(item I, modTime time.Time) {
|
||||
defer wg.Done()
|
||||
defer func() { <-semaphoreCh }()
|
||||
|
||||
ictx := clues.Add(
|
||||
ctx,
|
||||
"item_id", id,
|
||||
"parent_path", path.LoggableDir(col.LocationPath().String()))
|
||||
itemID := ptr.Val(item.GetId())
|
||||
ictx := clues.Add(ctx, "item_id", itemID)
|
||||
|
||||
col.stream <- data.NewLazyItemWithInfo(
|
||||
ictx,
|
||||
&lazyItemGetter[I]{
|
||||
modTime: modTime,
|
||||
getAndAugment: col.getAndAugment,
|
||||
resourceID: col.protectedResource,
|
||||
itemID: id,
|
||||
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(),
|
||||
},
|
||||
id,
|
||||
itemID,
|
||||
modTime,
|
||||
col.Counter,
|
||||
el)
|
||||
@ -188,20 +185,20 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault.
|
||||
if progressMessage != nil {
|
||||
progressMessage <- struct{}{}
|
||||
}
|
||||
}(itemID, modTime)
|
||||
}(item, modTime)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type lazyItemGetter[I chatsItemer] struct {
|
||||
getAndAugment getItemAndAugmentInfoer[I]
|
||||
resourceID string
|
||||
itemID string
|
||||
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(
|
||||
@ -211,10 +208,10 @@ 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.itemID)
|
||||
lig.item)
|
||||
if err != nil {
|
||||
// For items that were deleted in flight, add the skip label so that
|
||||
// they don't lead to recoverable failures during backup.
|
||||
@ -232,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)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/google/uuid"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -155,20 +156,13 @@ type getAndAugmentChat struct {
|
||||
func (m getAndAugmentChat) getItem(
|
||||
_ context.Context,
|
||||
_ string,
|
||||
itemID string,
|
||||
chat models.Chatable,
|
||||
) (models.Chatable, *details.TeamsChatsInfo, error) {
|
||||
chat := models.NewChat()
|
||||
chat.SetId(ptr.To(itemID))
|
||||
chat.SetTopic(ptr.To(itemID))
|
||||
chat.SetTopic(chat.GetId())
|
||||
|
||||
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()
|
||||
@ -226,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,
|
||||
}
|
||||
@ -277,6 +271,8 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() {
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
chat := testdata.StubChats(uuid.NewString())[0]
|
||||
|
||||
m := getAndAugmentChat{
|
||||
err: test.getErr,
|
||||
}
|
||||
@ -284,13 +280,13 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() {
|
||||
li := data.NewLazyItemWithInfo(
|
||||
ctx,
|
||||
&lazyItemGetter[models.Chatable]{
|
||||
resourceID: "resourceID",
|
||||
itemID: "itemID",
|
||||
getAndAugment: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
resourceID: "resourceID",
|
||||
item: chat,
|
||||
getter: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
},
|
||||
"itemID",
|
||||
ptr.Val(chat.GetId()),
|
||||
now,
|
||||
count.New(),
|
||||
fault.New(true))
|
||||
@ -319,6 +315,8 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
chat := testdata.StubChats(uuid.NewString())[0]
|
||||
|
||||
m := getAndAugmentChat{
|
||||
err: core.ErrNotFound,
|
||||
}
|
||||
@ -326,13 +324,13 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig
|
||||
li := data.NewLazyItemWithInfo(
|
||||
ctx,
|
||||
&lazyItemGetter[models.Chatable]{
|
||||
resourceID: "resourceID",
|
||||
itemID: "itemID",
|
||||
getAndAugment: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
resourceID: "resourceID",
|
||||
item: chat,
|
||||
getter: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
},
|
||||
"itemID",
|
||||
ptr.Val(chat.GetId()),
|
||||
now,
|
||||
count.New(),
|
||||
fault.New(true))
|
||||
@ -359,18 +357,19 @@ func (suite *CollectionUnitSuite) TestLazyItem() {
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
chat := testdata.StubChats(uuid.NewString())[0]
|
||||
m := getAndAugmentChat{}
|
||||
|
||||
li := data.NewLazyItemWithInfo(
|
||||
ctx,
|
||||
&lazyItemGetter[models.Chatable]{
|
||||
resourceID: "resourceID",
|
||||
itemID: "itemID",
|
||||
getAndAugment: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
resourceID: "resourceID",
|
||||
item: chat,
|
||||
getter: &m,
|
||||
modTime: now,
|
||||
parentPath: parentPath,
|
||||
},
|
||||
"itemID",
|
||||
ptr.Val(chat.GetId()),
|
||||
now,
|
||||
count.New(),
|
||||
fault.New(true))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -62,7 +50,7 @@ type getItemer[I chatsItemer] interface {
|
||||
getItem(
|
||||
ctx context.Context,
|
||||
protectedResource string,
|
||||
itemID string,
|
||||
i I,
|
||||
) (I, *details.TeamsChatsInfo, error)
|
||||
}
|
||||
|
||||
|
||||
@ -11,11 +11,21 @@ func StubChats(ids ...string) []models.Chatable {
|
||||
sl := make([]models.Chatable, 0, len(ids))
|
||||
|
||||
for _, id := range ids {
|
||||
ch := models.NewChat()
|
||||
ch.SetTopic(ptr.To(id))
|
||||
ch.SetId(ptr.To(id))
|
||||
chat := models.NewChat()
|
||||
chat.SetTopic(ptr.To(id))
|
||||
chat.SetId(ptr.To(id))
|
||||
|
||||
sl = append(sl, ch)
|
||||
// we should expect to get the latest message preview by default
|
||||
lastMsgPrv := models.NewChatMessageInfo()
|
||||
lastMsgPrv.SetId(ptr.To(uuid.NewString()))
|
||||
|
||||
body := models.NewItemBody()
|
||||
body.SetContent(ptr.To(id))
|
||||
lastMsgPrv.SetBody(body)
|
||||
|
||||
chat.SetLastMessagePreview(lastMsgPrv)
|
||||
|
||||
sl = append(sl, chat)
|
||||
}
|
||||
|
||||
return sl
|
||||
@ -24,17 +34,24 @@ func StubChats(ids ...string) []models.Chatable {
|
||||
func StubChatMessages(ids ...string) []models.ChatMessageable {
|
||||
sl := make([]models.ChatMessageable, 0, len(ids))
|
||||
|
||||
var lastMsg models.ChatMessageable
|
||||
|
||||
for _, id := range ids {
|
||||
cm := models.NewChatMessage()
|
||||
cm.SetId(ptr.To(uuid.NewString()))
|
||||
msg := models.NewChatMessage()
|
||||
msg.SetId(ptr.To(uuid.NewString()))
|
||||
|
||||
body := models.NewItemBody()
|
||||
body.SetContent(ptr.To(id))
|
||||
|
||||
cm.SetBody(body)
|
||||
msg.SetBody(body)
|
||||
|
||||
sl = append(sl, cm)
|
||||
sl = append(sl, msg)
|
||||
lastMsg = msg
|
||||
}
|
||||
|
||||
lastMsgPrv := models.NewChatMessageInfo()
|
||||
lastMsgPrv.SetId(lastMsg.GetId())
|
||||
lastMsgPrv.SetBody(lastMsg.GetBody())
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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"},
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -162,7 +162,7 @@ func channelMessageInfo(
|
||||
modTime = lastReplyAt
|
||||
}
|
||||
|
||||
preview, contentLen, err := getChatMessageContentPreview(msg)
|
||||
preview, contentLen, err := getChatMessageContentPreview(msg, msg)
|
||||
if err != nil {
|
||||
preview = "malformed or unparseable html" + preview
|
||||
}
|
||||
@ -180,7 +180,7 @@ func channelMessageInfo(
|
||||
var lr details.ChannelMessageInfo
|
||||
|
||||
if lastReply != nil {
|
||||
preview, contentLen, err = getChatMessageContentPreview(lastReply)
|
||||
preview, contentLen, err = getChatMessageContentPreview(lastReply, lastReply)
|
||||
if err != nil {
|
||||
preview = "malformed or unparseable html: " + preview
|
||||
}
|
||||
@ -239,12 +239,28 @@ func GetChatMessageFrom(msg models.ChatMessageable) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getChatMessageContentPreview(msg models.ChatMessageable) (string, int64, error) {
|
||||
content, origSize, err := stripChatMessageHTML(msg)
|
||||
// a hack for fulfilling getAttachmentser when the model doesn't
|
||||
// provide GetAttachments()
|
||||
type noAttachments struct{}
|
||||
|
||||
func (noAttachments) GetAttachments() []models.ChatMessageAttachmentable {
|
||||
return []models.ChatMessageAttachmentable{}
|
||||
}
|
||||
|
||||
type getBodyer interface {
|
||||
GetBody() models.ItemBodyable
|
||||
}
|
||||
|
||||
type getAttachmentser interface {
|
||||
GetAttachments() []models.ChatMessageAttachmentable
|
||||
}
|
||||
|
||||
func getChatMessageContentPreview(msg getBodyer, atts getAttachmentser) (string, int64, error) {
|
||||
content, origSize, err := stripChatMessageHTML(msg, atts)
|
||||
return str.Preview(content, 128), origSize, clues.Stack(err).OrNil()
|
||||
}
|
||||
|
||||
func stripChatMessageHTML(msg models.ChatMessageable) (string, int64, error) {
|
||||
func stripChatMessageHTML(msg getBodyer, atts getAttachmentser) (string, int64, error) {
|
||||
var (
|
||||
content string
|
||||
origSize int64
|
||||
@ -256,7 +272,7 @@ func stripChatMessageHTML(msg models.ChatMessageable) (string, int64, error) {
|
||||
|
||||
origSize = int64(len(content))
|
||||
|
||||
content = replaceAttachmentMarkup(content, msg.GetAttachments())
|
||||
content = replaceAttachmentMarkup(content, atts.GetAttachments())
|
||||
content, err := html2text.FromString(content)
|
||||
|
||||
return content, origSize, clues.Stack(err).OrNil()
|
||||
|
||||
@ -712,7 +712,7 @@ func (suite *ChannelsAPIUnitSuite) TestStripChatMessageContent() {
|
||||
msg.SetAttachments(test.attachments)
|
||||
|
||||
// not testing len; it's effectively covered by the content assertion
|
||||
result, _, err := stripChatMessageHTML(msg)
|
||||
result, _, err := stripChatMessageHTML(msg, msg)
|
||||
assert.Equal(t, test.expect, result)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
})
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||
)
|
||||
|
||||
type ChatsAPIUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestChatsAPIUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &ChatsAPIUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
|
||||
start := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
chatAndExpected func() (models.Chatable, *details.TeamsChatsInfo)
|
||||
}{
|
||||
{
|
||||
name: "Empty chat",
|
||||
chatAndExpected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
chat := models.NewChat()
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Chat: details.ChatInfo{},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "All fields",
|
||||
chatAndExpected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
now := time.Now()
|
||||
then := now.Add(1 * time.Hour)
|
||||
|
||||
chat := models.NewChat()
|
||||
chat.SetTopic(ptr.To("Hello world"))
|
||||
chat.SetCreatedDateTime(&now)
|
||||
chat.SetLastUpdatedDateTime(&then)
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Chat: details.ChatInfo{
|
||||
Name: "Hello world",
|
||||
},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
chat, expected := test.chatAndExpected()
|
||||
result := TeamsChatInfo(chat)
|
||||
|
||||
assert.Equal(t, expected.Chat.Name, result.Chat.Name)
|
||||
|
||||
expectLastUpdated := chat.GetLastUpdatedDateTime()
|
||||
if expectLastUpdated != nil {
|
||||
assert.Equal(t, ptr.Val(expectLastUpdated), result.Modified)
|
||||
} else {
|
||||
assert.True(t, result.Modified.After(start))
|
||||
}
|
||||
|
||||
expectCreated := chat.GetCreatedDateTime()
|
||||
if expectCreated != nil {
|
||||
assert.Equal(t, ptr.Val(expectCreated), result.Chat.CreatedAt)
|
||||
} else {
|
||||
assert.True(t, result.Chat.CreatedAt.After(start))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/chats"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
@ -62,13 +63,47 @@ func (c Chats) GetChatByID(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TeamsChatInfo(chat models.Chatable) *details.TeamsChatsInfo {
|
||||
var (
|
||||
// in case of an empty chat, we want to use Val instead of OrNow
|
||||
lastModTime = ptr.Val(chat.GetLastUpdatedDateTime())
|
||||
lastMsgPreview = chat.GetLastMessagePreview()
|
||||
lastMsgCreatedAt time.Time
|
||||
members = chat.GetMembers()
|
||||
memberNames = []string{}
|
||||
msgs = chat.GetMessages()
|
||||
preview string
|
||||
err error
|
||||
)
|
||||
|
||||
if lastMsgPreview != nil {
|
||||
preview, _, err = getChatMessageContentPreview(lastMsgPreview, noAttachments{})
|
||||
if err != nil {
|
||||
preview = "malformed or unparseable html" + preview
|
||||
}
|
||||
|
||||
// in case of an empty mod time, we want to use the chat's mod time
|
||||
// therefore Val instaed of OrNow
|
||||
lastMsgCreatedAt = ptr.Val(lastMsgPreview.GetCreatedDateTime())
|
||||
if lastModTime.Before(lastMsgCreatedAt) {
|
||||
lastModTime = lastMsgCreatedAt
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
memberNames = append(memberNames, ptr.Val(m.GetDisplayName()))
|
||||
}
|
||||
|
||||
return &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: ptr.OrNow(chat.GetLastUpdatedDateTime()),
|
||||
Modified: lastModTime,
|
||||
|
||||
Chat: details.ChatInfo{
|
||||
CreatedAt: ptr.OrNow(chat.GetCreatedDateTime()),
|
||||
Name: ptr.Val(chat.GetTopic()),
|
||||
CreatedAt: ptr.OrNow(chat.GetCreatedDateTime()),
|
||||
LastMessageAt: lastMsgCreatedAt,
|
||||
LastMessagePreview: preview,
|
||||
Members: memberNames,
|
||||
MessageCount: len(msgs),
|
||||
Topic: ptr.Val(chat.GetTopic()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -85,26 +158,6 @@ func (c Chats) GetChatMessages(
|
||||
return items, graph.Stack(ctx, err).OrNil()
|
||||
}
|
||||
|
||||
// GetChatMessageIDs fetches a delta of all messages in the chat.
|
||||
// returns two maps: addedItems, deletedItems
|
||||
func (c Chats) GetChatMessageIDs(
|
||||
ctx context.Context,
|
||||
chatID string,
|
||||
cc CallConfig,
|
||||
) (pagers.AddedAndRemoved, error) {
|
||||
aar, err := pagers.GetAddedAndRemovedItemIDs[models.ChatMessageable](
|
||||
ctx,
|
||||
c.NewChatMessagePager(chatID, CallConfig{}),
|
||||
nil,
|
||||
"",
|
||||
false, // delta queries are not supported
|
||||
0,
|
||||
pagers.AddedAndRemovedByDeletedDateTime[models.ChatMessageable],
|
||||
IsNotSystemMessage)
|
||||
|
||||
return aar, clues.Stack(err).OrNil()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// chat pager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
158
src/pkg/services/m365/api/teamsChats_test.go
Normal file
158
src/pkg/services/m365/api/teamsChats_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||
"github.com/alcionai/corso/src/internal/m365/collection/teamschats/testdata"
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||
)
|
||||
|
||||
type ChatsAPIUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestChatsAPIUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &ChatsAPIUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
|
||||
start := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expected func() (models.Chatable, *details.TeamsChatsInfo)
|
||||
}{
|
||||
{
|
||||
name: "Empty chat",
|
||||
expected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
chat := models.NewChat()
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: ptr.Val(chat.GetLastUpdatedDateTime()),
|
||||
Chat: details.ChatInfo{},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "All fields",
|
||||
expected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
now := time.Now()
|
||||
then := now.Add(1 * time.Hour)
|
||||
id := uuid.NewString()
|
||||
|
||||
chat := testdata.StubChats(id)[0]
|
||||
chat.SetTopic(ptr.To("Hello world"))
|
||||
chat.SetCreatedDateTime(&now)
|
||||
chat.SetLastUpdatedDateTime(&now)
|
||||
chat.GetLastMessagePreview().SetCreatedDateTime(&then)
|
||||
|
||||
msgs := testdata.StubChatMessages(ptr.Val(chat.GetLastMessagePreview().GetId()))
|
||||
chat.SetMessages(msgs)
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: then,
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "Hello world",
|
||||
LastMessageAt: then,
|
||||
LastMessagePreview: id,
|
||||
Members: []string{},
|
||||
MessageCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "last message preview, but no messages",
|
||||
expected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
now := time.Now()
|
||||
then := now.Add(1 * time.Hour)
|
||||
id := uuid.NewString()
|
||||
|
||||
chat := testdata.StubChats(id)[0]
|
||||
chat.SetTopic(ptr.To("Hello world"))
|
||||
chat.SetCreatedDateTime(&now)
|
||||
chat.SetLastUpdatedDateTime(&now)
|
||||
chat.GetLastMessagePreview().SetCreatedDateTime(&then)
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: then,
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "Hello world",
|
||||
LastMessageAt: then,
|
||||
LastMessagePreview: id,
|
||||
Members: []string{},
|
||||
MessageCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chat only, no messages",
|
||||
expected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||
now := time.Now()
|
||||
then := now.Add(1 * time.Hour)
|
||||
|
||||
chat := testdata.StubChats(uuid.NewString())[0]
|
||||
chat.SetTopic(ptr.To("Hello world"))
|
||||
chat.SetCreatedDateTime(&now)
|
||||
chat.SetLastUpdatedDateTime(&then)
|
||||
chat.SetLastMessagePreview(nil)
|
||||
chat.SetMessages(nil)
|
||||
|
||||
i := &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: then,
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "Hello world",
|
||||
LastMessageAt: time.Time{},
|
||||
LastMessagePreview: "",
|
||||
Members: []string{},
|
||||
MessageCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
return chat, i
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
chat, expected := test.expected()
|
||||
result := TeamsChatInfo(chat)
|
||||
|
||||
assert.Equal(t, expected.Chat.Topic, result.Chat.Topic)
|
||||
|
||||
expectCreated := chat.GetCreatedDateTime()
|
||||
if expectCreated != nil {
|
||||
assert.Equal(t, ptr.Val(expectCreated), result.Chat.CreatedAt)
|
||||
} else {
|
||||
assert.True(t, result.Chat.CreatedAt.After(start))
|
||||
}
|
||||
|
||||
assert.Truef(
|
||||
t,
|
||||
expected.Modified.Equal(result.Modified),
|
||||
"modified time doesn't match\nexpected %v\ngot %v",
|
||||
expected.Modified,
|
||||
result.Modified)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user