Compare commits

...

10 Commits

Author SHA1 Message Date
neha-Gupta1
07e772ee0e remove extra methods 2023-08-23 19:16:41 +05:30
neha-Gupta1
63080485a8 remove YAGNI 2023-08-23 16:03:03 +05:30
neha-Gupta1
27990e6174 item pager for channels 2023-08-22 12:48:11 +05:30
neha-Gupta1
7de04b98ab test cases channel api 2023-08-22 10:19:36 +05:30
neha-Gupta1
aef4c688ac lint code 2023-08-18 23:34:31 +05:30
neha-Gupta1
54d5e05950 channels and messages API 2023-08-18 18:27:03 +05:30
neha-Gupta1
5fbc37fbc1 Merge branch 'main' of https://github.com/alcionai/corso into channelHandlers 2023-08-18 13:41:01 +05:30
neha-Gupta1
d1f0d683af message handler 2023-08-18 13:40:42 +05:30
neha-Gupta1
fa1a432a87 lint changes 2023-08-17 16:42:14 +05:30
neha-Gupta1
8d4277b1c7 add handers for channels 2023-08-17 16:24:38 +05:30
7 changed files with 473 additions and 0 deletions

View File

@ -0,0 +1,17 @@
package groups
import (
"context"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type BackupMessagesHandler interface {
GetMessage(ctx context.Context, teamID, channelID, itemID string) (models.ChatMessageable, error)
NewMessagePager(teamID, channelID string) api.MessageItemDeltaEnumerator
GetChannel(ctx context.Context, teamID, channelID string) (models.Channelable, error)
GetReply(ctx context.Context, teamID, channelID, messageID string) (serialization.Parsable, error)
}

View File

@ -28,6 +28,7 @@ const (
TestCfgSiteURL = "m365siteurl"
TestCfgTeamID = "m365teamid"
TestCfgGroupID = "m365groupid"
TestCfgChannelID = "m365channelid"
TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid"
TestCfgTertiaryUserID = "tertiarym365userid"
@ -45,6 +46,7 @@ const (
EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL"
EnvCorsoM365TestTeamID = "CORSO_M365_TEST_TEAM_ID"
EnvCorsoM365TestGroupID = "CORSO_M365_TEST_GROUP_ID"
EnvCorsoM365TestChannelID = "CORSO_M365_TEST_CHANNEL_ID"
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
EnvCorsoSecondaryM365TestSiteID = "CORSO_SECONDARY_M365_TEST_SITE_ID"
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
@ -166,6 +168,12 @@ func ReadTestConfig() (map[string]string, error) {
os.Getenv(EnvCorsoM365TestGroupID),
vpr.GetString(TestCfgGroupID),
"6f24b40d-b13d-4752-980f-f5fb9fba7aa0")
fallbackTo(
testEnv,
TestCfgChannelID,
os.Getenv(EnvCorsoM365TestChannelID),
vpr.GetString(TestCfgChannelID),
"19:nY6QHZ3hnHJ6ylarBxPjCOLRJNvrL3oKI5iW15QxTPA1@thread.tacv2")
fallbackTo(
testEnv,
TestCfgSiteURL,

View File

@ -246,3 +246,14 @@ func M365GroupID(t *testing.T) string {
return strings.ToLower(cfg[TestCfgTeamID])
}
// M365ChannelID returns a channelID string representing the m365TeamsID described
// by either the env var CORSO_M365_TEST_CHANNEL_ID, the corso_test.toml config
// file or the default value (in that order of priority). The default is a
// last-attempt fallback that will only work on alcion's testing org.
func M365ChannelID(t *testing.T) string {
cfg, err := ReadTestConfig()
require.NoError(t, err, "retrieving m365 channel id from test configuration: %+v", clues.ToCore(err))
return cfg[TestCfgChannelID]
}

View File

@ -36,6 +36,9 @@ const (
// Folder Management(30x)
FolderItem ItemType = 306
// GroupChannelMessage(40x)
GroupChannelMessage ItemType = 407
)
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {

View File

@ -0,0 +1,198 @@
package api
import (
"context"
"fmt"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Channels() Channels {
return Channels{c}
}
// Channels is an interface-compliant provider of the client.
type Channels struct {
Client
}
// ---------------------------------------------------------------------------
// containers
// ---------------------------------------------------------------------------
func (c Channels) GetChannel(
ctx context.Context,
teamID, containerID string,
) (models.Channelable, error) {
config := &teams.ItemChannelsChannelItemRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsChannelItemRequestBuilderGetQueryParameters{
Select: idAnd("displayName"),
},
}
resp, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Get(ctx, config)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
// GetChannelByName fetches a channel by name
func (c Channels) GetChannelByName(
ctx context.Context,
teamID, containerName string,
) (models.Channelable, error) {
ctx = clues.Add(ctx, "channel_name", containerName)
filter := fmt.Sprintf("displayName eq '%s'", containerName)
options := &teams.ItemChannelsRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
resp, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
Get(ctx, options)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("channel not found").WithClues(ctx)
}
// We only allow the api to match one channel with the provided name.
// If we match multiples, we'll eagerly return the first one.
logger.Ctx(ctx).Debugw("channels matched the name search")
// Sanity check ID and name
cal := gv[0]
if err := CheckIDAndName(cal); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return cal, nil
}
// ---------------------------------------------------------------------------
// message
// ---------------------------------------------------------------------------
// GetMessage retrieves a ChannelMessage item.
func (c Channels) GetMessage(
ctx context.Context,
teamID, channelID, itemID string,
errs *fault.Bus,
) (serialization.Parsable, *details.GroupsInfo, error) {
var (
size int64
)
message, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
ByChatMessageId(itemID).
Get(ctx, nil)
if err != nil {
return nil, nil, graph.Stack(ctx, err)
}
return message, ChannelMessageInfo(message, size), nil
}
// ---------------------------------------------------------------------------
// replies
// ---------------------------------------------------------------------------
// GetReplies retrieves a Messageable item.
func (c Channels) GetReplies(
ctx context.Context,
teamID, channelID, itemID string,
) (serialization.Parsable, error) {
replies, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
ByChatMessageId(itemID).
Replies().
Get(ctx, nil)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return replies, nil
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func ChannelMessageInfo(msg models.ChatMessageable, size int64) *details.GroupsInfo {
var (
created = ptr.Val(msg.GetCreatedDateTime())
)
return &details.GroupsInfo{
ItemType: details.GroupChannelMessage,
Size: size,
Created: created,
Modified: ptr.OrNow(msg.GetLastModifiedDateTime()),
}
}
// ---------------------------------------------------------------------------
// helper funcs
// ---------------------------------------------------------------------------
// CheckIDAndName is a validator that ensures the ID
// and name are populated and not zero valued.
func CheckIDAndName(c models.Channelable) error {
if c == nil {
return clues.New("nil container")
}
id := ptr.Val(c.GetId())
if len(id) == 0 {
return clues.New("container missing ID")
}
dn := ptr.Val(c.GetDisplayName())
if len(dn) == 0 {
return clues.New("container missing display name").With("container_id", id)
}
return nil
}

View File

@ -0,0 +1,144 @@
package api
import (
"context"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
)
// ---------------------------------------------------------------------------
// item pager
// ---------------------------------------------------------------------------
type MessageItemDeltaEnumerator interface {
GetPage(context.Context) (PageLinker, error)
SetNext(nextLink string)
}
var _ MessageItemDeltaEnumerator = &messagePageCtrl{}
type messagePageCtrl struct {
gs graph.Servicer
builder *teams.ItemChannelsItemMessagesDeltaRequestBuilder
options *teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration
}
func (c Channels) NewMessagePager(
teamID,
channelID string,
fields []string,
) *messagePageCtrl {
requestConfig := &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetQueryParameters{
Select: fields,
},
}
res := &messagePageCtrl{
gs: c.Stable,
options: requestConfig,
builder: c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
Delta(),
}
return res
}
func (p *messagePageCtrl) SetNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter())
}
func (p *messagePageCtrl) GetPage(ctx context.Context) (PageLinker, error) {
var (
resp PageLinker
err error
)
resp, err = p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
type MessageItemIDType struct {
ItemID string
}
type channelItemPageCtrl struct {
gs graph.Servicer
builder *teams.ItemChannelsItemMessagesRequestBuilder
options *teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration
}
func (c Channels) GetItemIDsInContainer(
ctx context.Context,
teamID, channelID string,
) (map[string]MessageItemIDType, error) {
ctx = clues.Add(ctx, "channel_id", channelID)
pager := c.NewChannelItemPager(teamID, channelID)
items, err := enumerateItems(ctx, pager)
if err != nil {
return nil, graph.Wrap(ctx, err, "enumerating contacts")
}
m := map[string]MessageItemIDType{}
for _, item := range items {
m[ptr.Val(item.GetId())] = MessageItemIDType{
ItemID: ptr.Val(item.GetId()),
}
}
return m, nil
}
func (c Channels) NewChannelItemPager(
teamID, containerID string,
selectProps ...string,
) itemPager[models.ChatMessageable] {
options := &teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsItemMessagesRequestBuilderGetQueryParameters{},
}
if len(selectProps) > 0 {
options.QueryParameters.Select = selectProps
}
builder := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages()
return &channelItemPageCtrl{c.Stable, builder, options}
}
//lint:ignore U1000 False Positive
func (p *channelItemPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.ChatMessageable], error) {
page, err := p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return EmptyDeltaLinker[models.ChatMessageable]{PageLinkValuer: page}, nil
}
//lint:ignore U1000 False Positive
func (p *channelItemPageCtrl) setNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
}

View File

@ -0,0 +1,92 @@
package api_test
import (
"testing"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ChannelPagerIntgSuite struct {
tester.Suite
its intgTesterSetup
}
func TestChannelPagerIntgSuite(t *testing.T) {
suite.Run(t, &ChannelPagerIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tconfig.M365AcctCredEnvs}),
})
}
func (suite *ChannelPagerIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *ChannelPagerIntgSuite) TestChannels_GetPage() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
teamID := tconfig.M365TeamsID(t)
channelID := tconfig.M365ChannelID(t)
pager := suite.its.ac.Channels().NewMessagePager(teamID, channelID, []string{})
a, err := pager.GetPage(ctx)
assert.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, a)
}
func (suite *ChannelPagerIntgSuite) TestChannels_Get() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
containerName = "General"
teamID = tconfig.M365TeamsID(t)
chanClient = suite.its.ac.Channels()
)
// GET channel -should be found
channel, err := chanClient.GetChannelByName(ctx, teamID, containerName)
assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, ptr.Val(channel.GetDisplayName()), containerName)
// GET channel -should not be found anymore
_, err = chanClient.GetChannel(ctx, teamID, ptr.Val(channel.GetId()))
assert.Error(t, err, clues.ToCore(err))
}
// func (suite *ChannelPagerIntgSuite) TestMessages_CreateGetAndDelete() {
// t := suite.T()
// ctx, flush := tester.NewContext(t)
// defer flush()
// var (
// teamID = tconfig.M365TeamsID(t)
// channelID = tconfig.M365ChannelID(t)
// credentials = suite.its.ac.Credentials
// chanClient = suite.its.ac.Channels()
// )
// // GET channel - should be not found
// message, _, err := chanClient.GetMessage(ctx, teamID, channelID, "", "")
// assert.Error(t, err, clues.ToCore(err))
// // POST channel
// // patchBody := models.NewChatMessage()
// // body := models.NewItemBody()
// // content := "Hello World"
// // body.SetContent(&content)
// // patchBody.SetBody(body)
// // _, := suite.its.ac.Channels().PostMessage(ctx, teamID, channelID, patchBody)
// // assert.NoError(t, err, clues.ToCore(err))
// }