Compare commits

...

4 Commits

Author SHA1 Message Date
neha-Gupta1
ce16e12f8a reply APIs 2023-08-11 16:19:32 +05:30
neha-Gupta1
ed72e63e03 merge main 2023-08-11 11:28:39 +05:30
neha-Gupta1
c41ff6853b messages API 2023-08-09 17:15:14 +05:30
neha-Gupta1
e965a2ec5c channel pager API 2023-08-07 18:59:43 +05:30
2 changed files with 673 additions and 0 deletions

View File

@ -0,0 +1,380 @@
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/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"
)
// ---------------------------------------------------------------------------
// Currently implemented-
// - Channels CRUD
// - Items i.e. messages CRUD
// Pending
// - teams CRUD
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Channels() Channels {
return Channels{c}
}
// Channels is an interface-compliant provider of the client.
type Channels struct {
Client
}
// ---------------------------------------------------------------------------
// containers
// ---------------------------------------------------------------------------
// CreateContainer makes an channels with the name in the team
func (c Channels) CreateContainer(
ctx context.Context,
// parentContainerID needed for iface, doesn't apply to events
teamID, _, containerName string,
) (graph.Container, error) {
body := models.NewChannel()
body.SetDisplayName(&containerName)
container, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating channel")
}
return ChannelsDisplayable{Channelable: container}, nil
}
// DeleteContainer removes a channel from user's M365 account
func (c Channels) DeleteContainer(
ctx context.Context,
teamID, containerID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Delete(ctx, nil)
if err != nil {
return graph.Stack(ctx, err)
}
return nil
}
// prefer GetContainerByID where possible.
// use this only in cases where the models.Channelable
// is required.
func (c Channels) GetChannel(
ctx context.Context,
teamID, containerID string,
) (models.Channelable, error) {
config := &teams.ItemChannelsChannelItemRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsChannelItemRequestBuilderGetQueryParameters{
Select: idAnd("name", "owner"),
},
}
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
}
// interface-compliant wrapper of GetCalendar
func (c Channels) GetContainerByID(
ctx context.Context,
teamID, containerID string,
) (graph.Container, error) {
channel, err := c.GetChannel(ctx, teamID, containerID)
if err != nil {
return nil, err
}
return ChannelsDisplayable{Channelable: channel}, nil
}
// GetContainerByName fetches a calendar by name
func (c Channels) GetContainerByName(
ctx context.Context,
// parentContainerID needed for iface, doesn't apply to events
teamID, _, containerName string,
) (graph.Container, error) {
// TODO: check container filter
// filter := fmt.Sprintf("name eq '%s'", containerName)
// options := &teams.ItemChannelsChannelItemRequestBuilderGetRequestConfiguration{
// QueryParameters: &teams.ItemChannelsChannelItemRequestBuilderGetQueryParameters{
// Filter: &filter,
// },
// }
ctx = clues.Add(ctx, "container_name", containerName)
resp, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
Get(ctx, nil)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("container not found").WithClues(ctx)
}
// We only allow the api to match one calendar with the provided name.
// If we match multiples, we'll eagerly return the first one.
logger.Ctx(ctx).Debugw("calendars matched the name search", "calendar_count", len(gv))
// Sanity check ID and name
cal := gv[0]
container := ChannelsDisplayable{Channelable: cal}
if err := graph.CheckIDAndName(container); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return container, nil
}
func (c Channels) PatchChannel(
ctx context.Context,
teamID, containerID string,
body models.Channelable,
) error {
_, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Patch(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "patching event calendar")
}
return nil
}
// ---------------------------------------------------------------------------
// items
// ---------------------------------------------------------------------------
// GetItem retrieves a Messageable item.
func (c Channels) GetItem(
ctx context.Context,
teamID, channelID, itemID string,
immutableIDs bool,
errs *fault.Bus,
) (serialization.Parsable, *details.ChannelsInfo, error) {
var (
size int64
)
// is preferImmutableIDs headers required here
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, ChannelsInfo(message, size), nil
}
func (c Channels) PostItem(
ctx context.Context,
teamID, containerID string,
body models.ChatMessageable,
) (models.ChatMessageable, error) {
itm, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating mail message")
}
if itm == nil {
return nil, clues.New("nil response mail message creation").WithClues(ctx)
}
return itm, nil
}
func (c Channels) DeleteItem(
ctx context.Context,
teamID, itemID, containerID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting mail message")
}
return nil
}
// ---------------------------------------------------------------------------
// replies
// ---------------------------------------------------------------------------
// GetItem 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
}
func (c Channels) PostReply(
ctx context.Context,
teamID, containerID, messageID string,
body models.ChatMessageable,
) (models.ChatMessageable, error) {
itm, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(messageID).
Replies().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating reply message")
}
if itm == nil {
return nil, clues.New("nil response reply to message creation").WithClues(ctx)
}
return itm, nil
}
func (c Channels) DeleteReply(
ctx context.Context,
teamID, itemID, containerID, replyID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(itemID).
Replies().
ByChatMessageId1(replyID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting mail message")
}
return nil
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func ChannelsInfo(msg models.ChatMessageable, size int64) *details.ChannelsInfo {
var (
sender = ptr.Val(msg.GetFrom().GetUser().GetDisplayName())
created = ptr.Val(msg.GetCreatedDateTime())
)
return &details.ChannelsInfo{
ItemType: details.ExchangeMail,
Sender: sender,
Size: size,
Created: created,
Modified: ptr.OrNow(msg.GetLastModifiedDateTime()),
}
}

View File

@ -0,0 +1,293 @@
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/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
)
// ---------------------------------------------------------------------------
// container pager
// ---------------------------------------------------------------------------
// EnumerateContainers iterates through all of the users teams
// channels, converting each to a graph.CacheFolder, and
// calling fn(cf) on each one.
// Folder hierarchy is represented in its current state, and does
// not contain historical data.
func (c Channels) EnumerateContainers(
ctx context.Context,
userID, baseContainerID string,
fn func(graph.CachedContainer) error,
errs *fault.Bus,
) error {
var (
el = errs.Local()
config = &teams.ItemChannelsRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsRequestBuilderGetQueryParameters{
Select: idAnd("name"),
},
}
builder = c.Stable.
Client().
Teams().
ByTeamId(userID).
Channels()
)
for {
if el.Failure() != nil {
break
}
resp, err := builder.Get(ctx, config)
if err != nil {
return graph.Stack(ctx, err)
}
for _, channel := range resp.GetValue() {
if el.Failure() != nil {
break
}
cd := ChannelsDisplayable{Channelable: channel}
if err := graph.CheckIDAndName(cd); err != nil {
errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
continue
}
fctx := clues.Add(
ctx,
"container_id", ptr.Val(channel.GetId()),
"container_name", ptr.Val(channel.GetDisplayName()))
temp := graph.NewCacheFolder(
// TODO: understand about it. Is path required and so on.
cd,
path.Builder{}.Append(ptr.Val(channel.GetId())), // storage path
path.Builder{}.Append(ptr.Val(channel.GetDisplayName()))) // display location
if err := fn(&temp); err != nil {
errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue
}
}
link, ok := ptr.ValOK(resp.GetOdataNextLink())
if !ok {
break
}
builder = teams.NewItemChannelsRequestBuilder(link, c.Stable.Adapter())
}
return el.Failure()
}
// ---------------------------------------------------------------------------
// item pager
// ---------------------------------------------------------------------------
var _ itemPager[models.ChatMessageable] = &channelsPageCtrl{}
type channelsPageCtrl struct {
gs graph.Servicer
builder *teams.ItemChannelsItemMessagesRequestBuilder
options *teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration
}
func (c Channels) NewChannelsPager(
teamID, containerID string,
selectProps ...string,
) itemPager[models.ChatMessageable] {
options := &teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)),
QueryParameters: &teams.ItemChannelsItemMessagesRequestBuilderGetQueryParameters{
// do NOT set Top. It limits the total items received.
},
}
if len(selectProps) > 0 {
options.QueryParameters.Select = selectProps
}
builder := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages()
return &channelsPageCtrl{c.Stable, builder, options}
}
//lint:ignore U1000 False Positive
func (p *channelsPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.ChatMessageable], error) {
resp, err := p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
//lint:ignore U1000 False Positive
func (p *channelsPageCtrl) setNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
}
// ---------------------------------------------------------------------------
// item ID pager
// ---------------------------------------------------------------------------
var _ itemIDPager = &channelIDPager{}
type channelIDPager struct {
gs graph.Servicer
builder *teams.ItemChannelsItemMessagesRequestBuilder
options *teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration
}
func (c Channels) NewChannelIDsPager(
ctx context.Context,
teamID, containerID string,
immutableIDs bool,
) (itemIDPager, error) {
options := &teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)),
QueryParameters: &teams.ItemChannelsItemMessagesRequestBuilderGetQueryParameters{
// do NOT set Top. It limits the total items received.
},
}
builder := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages()
return &channelIDPager{c.Stable, builder, options}, nil
}
func (p *channelIDPager) getPage(ctx context.Context) (DeltaPageLinker, error) {
resp, err := p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return EmptyDeltaLinker[models.ChatMessageable]{PageLinkValuer: resp}, nil
}
func (p *channelIDPager) setNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
}
// non delta pagers don't need reset
func (p *channelIDPager) reset(context.Context) {}
func (p *channelIDPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) {
return toValues[models.ChatMessageable](pl)
}
// ---------------------------------------------------------------------------
// delta item ID pager
// ---------------------------------------------------------------------------
var _ itemIDPager = &channelDeltaIDPager{}
type channelDeltaIDPager struct {
gs graph.Servicer
userID string
containerID string
builder *teams.ItemChannelsItemMessagesDeltaRequestBuilder
options *teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration
}
func (c Channels) NewChannelsDeltaIDsPager(
ctx context.Context,
userID, containerID, oldDelta string,
immutableIDs bool,
) (itemIDPager, error) {
options := &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferPageSize(c.options.DeltaPageSize), preferImmutableIDs(immutableIDs)),
QueryParameters: &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetQueryParameters{
// do NOT set Top. It limits the total items received.
},
}
var builder *teams.ItemChannelsItemMessagesDeltaRequestBuilder
if oldDelta == "" {
builder = getChannelsDeltaBuilder(ctx, c.Stable, userID, containerID, options)
} else {
builder = teams.NewItemChannelsItemMessagesDeltaRequestBuilder(oldDelta, c.Stable.Adapter())
}
return &channelDeltaIDPager{c.Stable, userID, containerID, builder, options}, nil
}
func getChannelsDeltaBuilder(
ctx context.Context,
gs graph.Servicer,
teamID, containerID string,
options *teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration,
) *teams.ItemChannelsItemMessagesDeltaRequestBuilder {
builder := gs.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
Delta()
return builder
}
func (p *channelDeltaIDPager) getPage(ctx context.Context) (DeltaPageLinker, error) {
resp, err := p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
func (p *channelDeltaIDPager) setNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter())
}
func (p *channelDeltaIDPager) reset(ctx context.Context) {
p.builder = getChannelsDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options)
}
func (p *channelDeltaIDPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) {
return toValues[models.Channelable](pl)
}
// ---------------------------------------------------------------------------
// helper funcs
// ---------------------------------------------------------------------------
// ChannelsDisplayable is a wrapper that complies with the
// models.Channelable interface with the graph.Container
// interfaces.
type ChannelsDisplayable struct {
models.Channelable
}
// GetParentFolderId returns the default channe name address
//nolint:revive
func (c ChannelsDisplayable) GetParentFolderId() *string {
return nil
}