corso/src/pkg/services/m365/api/conversations.go
Abhishek Pandey fad3dfe769
Download attachments for group mailbox posts (#4992)
<!-- PR description-->

* Attachment download needed an expand operation. Added that. 
* Added test coverage with gock. Tested with manual backup of posts which contain attachments(embedded/non-embedded). We can't add e2e tests with attachments, since API to create new conversations requires delegated access.
* Note that https://github.com/alcionai/corso/issues/4991 is still an open item. We don't have a resolution for it right now, since attachment endpoint requires delegated token. Defaulting to let the backup fail for "too many attachments" error case. We don't know yet if we'd see that with group mailboxes, and whether it'd be the same error that we saw with exchange (recurring 503s).
---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### 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.-->
- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
2024-01-10 04:49:04 +00:00

195 lines
5.1 KiB
Go

package api
import (
"context"
"github.com/alcionai/clues"
"github.com/jaytaylor/html2text"
"github.com/microsoftgraph/msgraph-sdk-go/groups"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Conversations() Conversations {
return Conversations{c}
}
// Conversations is an interface-compliant provider of the client.
type Conversations struct {
Client
}
// ---------------------------------------------------------------------------
// Item (conversation thread post)
// ---------------------------------------------------------------------------
func (c Conversations) GetConversationPost(
ctx context.Context,
groupID, conversationID, threadID, postID string,
cc CallConfig,
) (models.Postable, *details.GroupsInfo, error) {
config := &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetRequestConfiguration{
QueryParameters: &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetQueryParameters{},
}
if len(cc.Select) > 0 {
config.QueryParameters.Select = cc.Select
}
if len(cc.Expand) > 0 {
config.QueryParameters.Expand = append(config.QueryParameters.Expand, cc.Expand...)
}
post, err := c.Stable.
Client().
Groups().
ByGroupId(groupID).
Conversations().
ByConversationId(conversationID).
Threads().
ByConversationThreadId(threadID).
Posts().
ByPostId(postID).
Get(ctx, config)
if err != nil {
return nil, nil, graph.Stack(ctx, err)
}
preview, contentLen, err := getConversationPostContentPreview(post)
if err != nil {
preview = "malformed or unparseable content body: " + preview
}
if !ptr.Val(post.GetHasAttachments()) && !HasAttachments(post.GetBody()) {
return post, conversationPostInfo(post, contentLen, preview), nil
}
attachments, totalSize, err := c.getAttachments(
ctx,
groupID,
conversationID,
threadID,
postID)
if err != nil {
// Similar to exchange, a failure can happen if a post has a lot of attachments.
// We don't have a fallback option here to fetch attachments one by one. See
// issue #4991.
//
// Resort to failing the post backup for now since we don't know yet how this
// error might manifest itself for posts.
logger.CtxErr(ctx, err).Info("failed to get post attachments")
return nil, nil, clues.Stack(err)
}
contentLen += totalSize
post.SetAttachments(attachments)
return post, conversationPostInfo(post, contentLen, preview), graph.Stack(ctx, err).OrNil()
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func conversationPostInfo(
post models.Postable,
size int64,
preview string,
) *details.GroupsInfo {
if post == nil {
return nil
}
var sender string
if post.GetSender() != nil && post.GetSender().GetEmailAddress() != nil {
sender = ptr.Val(post.GetSender().GetEmailAddress().GetAddress())
}
cpi := details.ConversationPostInfo{
CreatedAt: ptr.Val(post.GetCreatedDateTime()),
Creator: sender,
Preview: preview,
Size: size,
}
return &details.GroupsInfo{
ItemType: details.GroupsConversationPost,
Modified: ptr.Val(post.GetLastModifiedDateTime()),
Post: cpi,
}
}
func getConversationPostContentPreview(post models.Postable) (string, int64, error) {
content, origSize, err := stripConversationPostHTML(post)
return str.Preview(content, 128), origSize, clues.Stack(err).OrNil()
}
func stripConversationPostHTML(post models.Postable) (string, int64, error) {
var (
content string
origSize int64
)
if post.GetBody() != nil {
content = ptr.Val(post.GetBody().GetContent())
}
origSize = int64(len(content))
content, err := html2text.FromString(content)
return content, origSize, clues.Stack(err).OrNil()
}
// getAttachments attempts to get all attachments, including their content, in a singe query.
func (c Conversations) getAttachments(
ctx context.Context,
groupID, conversationID, threadID, postID string,
) ([]models.Attachmentable, int64, error) {
var (
result = []models.Attachmentable{}
totalSize int64
)
cfg := &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetRequestConfiguration{
QueryParameters: &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetQueryParameters{
Expand: []string{"attachments"},
},
}
post, err := c.LargeItem.
Client().
Groups().
ByGroupId(groupID).
Conversations().
ByConversationId(conversationID).
Threads().
ByConversationThreadId(threadID).
Posts().
ByPostId(postID).
Get(ctx, cfg)
if err != nil {
return nil, 0, graph.Stack(ctx, err)
}
attachments := post.GetAttachments()
for _, a := range attachments {
totalSize += int64(ptr.Val(a.GetSize()))
result = append(result, a)
}
return result, totalSize, nil
}