channels and messages API (#4068)

<!-- PR description-->

Message handler implementation and other APIs to fetch channels and messages data

#### Does this PR need a docs update or release note?
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
neha_gupta 2023-08-25 14:03:39 +05:30 committed by GitHub
parent 518b0a41f0
commit e1fe7f4b16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 267 additions and 0 deletions

View File

@ -31,3 +31,10 @@ type BackupHandler interface {
teamID, channelID, messageID string, teamID, channelID, messageID string,
) (serialization.Parsable, error) ) (serialization.Parsable, error)
} }
type BackupMessagesHandler interface {
GetMessage(ctx context.Context, teamID, channelID, itemID string) (models.ChatMessageable, error)
NewMessagePager(teamID, channelID string) api.ChannelMessageDeltaEnumerator
GetChannel(ctx context.Context, teamID, channelID string) (models.Channelable, error)
GetReply(ctx context.Context, teamID, channelID, messageID string) (serialization.Parsable, error)
}

View File

@ -1 +1,194 @@
package api package api
import (
"context"
"fmt"
"github.com/alcionai/clues"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
"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"
)
// ---------------------------------------------------------------------------
// 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 all replies to a Channel Message.
func (c Channels) GetReplies(
ctx context.Context,
teamID, channelID, messageID string,
) (serialization.Parsable, error) {
replies, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
ByChatMessageId(messageID).
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 {
created := ptr.Val(msg.GetCreatedDateTime())
return &details.GroupsInfo{
ItemType: details.TeamsChannelMessage,
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,67 @@
package api_test
import (
"testing"
"github.com/alcionai/clues"
"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/internal/tester/tconfig"
)
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())
}
// This will be added once 'pager' is implemented
// func (suite *ChannelPagerIntgSuite) TestChannels_GetPage() {
// t := suite.T()
// ctx, flush := tester.NewContext(t)
// defer flush()
// teamID := tconfig.M365TeamID(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.M365TeamID(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 be found
_, err = chanClient.GetChannel(ctx, teamID, ptr.Val(channel.GetId()))
assert.NoError(t, err, clues.ToCore(err))
}