Skip to content
This repository has been archived by the owner on Oct 11, 2024. It is now read-only.

add chat members to the backup #5119

Merged
merged 3 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/internal/m365/collection/teamschats/chat_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func (bh usersChatsBackupHandler) getItem(

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
}

Expand Down
6 changes: 6 additions & 0 deletions src/pkg/backup/details/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
91 changes: 83 additions & 8 deletions src/pkg/selectors/teamsChats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -505,8 +572,16 @@ func (s TeamsChatsScope) matchesInfo(dii details.ItemInfo) bool {
switch infoCat {
case TeamsChatsInfoChatMember:
i = strings.Join(info.Chat.Members, ",")
case TeamsChatsInfoChatName:
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)
Expand Down
31 changes: 21 additions & 10 deletions src/pkg/selectors/teamsChats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -252,14 +253,16 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() {
cs := NewTeamsChatsRestore(Any())

const (
name = "smarf mcfnords"
topic = "smarf mcfnords"
member = "[email protected]"
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 {
Expand All @@ -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,
Topic: name,
Topic: topic,
},
},
}
Expand All @@ -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() {
Expand Down
73 changes: 73 additions & 0 deletions src/pkg/services/m365/api/teamsChats_pager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
24 changes: 24 additions & 0 deletions src/pkg/services/m365/api/teamsChats_pager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ func (suite *ChatsPagerIntgSuite) TestEnumerateChats() {
ac,
chatID,
chat.GetLastMessagePreview())

testEnumerateChatMembers(
suite.T(),
ac,
chatID)
})
}
}
Expand Down Expand Up @@ -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)
}
Loading