Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat Features: Create, Get, List, Update #219

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion environments/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ var (
Endpoint: SynapsePublicEndpoint,
}

SynapseUSGov = Api {
SynapseUSGov = Api{
AppId: PublishedApis["AzureSynapseGateway"],
Endpoint: SynapseUSGovEndpoint,
}
Expand Down
2 changes: 1 addition & 1 deletion environments/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ const (
StoragePublicEndpoint ApiEndpoint = "https://storage.azure.com"

SynapsePublicEndpoint ApiEndpoint = "https://dev.azuresynapse.net"
SynapseUSGovEndpoint ApiEndpoint = "https://dev.azuresynapse.usgovcloudapi.net"
SynapseUSGovEndpoint ApiEndpoint = "https://dev.azuresynapse.usgovcloudapi.net"
)
1 change: 1 addition & 0 deletions internal/test/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type Test struct {
AppRoleAssignedToClient *msgraph.AppRoleAssignedToClient
AuthenticationMethodsClient *msgraph.AuthenticationMethodsClient
B2CUserFlowClient *msgraph.B2CUserFlowClient
ChatClient *msgraph.ChatClient
Threpio marked this conversation as resolved.
Show resolved Hide resolved
ClaimsMappingPolicyClient *msgraph.ClaimsMappingPolicyClient
ConditionalAccessPoliciesClient *msgraph.ConditionalAccessPoliciesClient
ConnectedOrganizationClient *msgraph.ConnectedOrganizationClient
Expand Down
152 changes: 152 additions & 0 deletions msgraph/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package msgraph

import (
"context"
"encoding/json"
"fmt"
"github.com/manicminer/hamilton/odata"
"io"
"net/http"
Threpio marked this conversation as resolved.
Show resolved Hide resolved
)

type ChatClient struct {
BaseClient Client
}

func NewChatClient(tenantId string) *ChatClient {
return &ChatClient{
BaseClient: NewClient(VersionBeta, tenantId),
}
}

// Create creates a new chat.
func (c *ChatClient) Create(ctx context.Context, chat Chat) (*Chat, int, error) {
var status int

body, err := json.Marshal(chat)
if err != nil {
return nil, status, fmt.Errorf("json.Marshal(): %v", err)
}

resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{
Body: body,
OData: odata.Query{
Metadata: odata.MetadataFull,
},
ValidStatusCodes: []int{http.StatusCreated},
Uri: Uri{
Entity: "/chats",
HasTenantId: true,
},
})

if err != nil {
return nil, status, fmt.Errorf("ChatsClient.BaseClient.Post(): %v", err)
}

defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, status, fmt.Errorf("io.ReadAll(): %v", err)
}

var newChat Chat
if err := json.Unmarshal(respBody, &newChat); err != nil {
return nil, status, fmt.Errorf("json.Unmarshal(): %v", err)
}

return &newChat, status, nil
}

// Get retrieves a chat.
func (c *ChatClient) Get(ctx context.Context, id string, query odata.Query) (*Chat, int, error) {
query.Metadata = odata.MetadataFull

resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{
ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc,
OData: query,
ValidStatusCodes: []int{http.StatusOK},
Uri: Uri{
Entity: fmt.Sprintf("/chats/%s", id),
HasTenantId: true,
},
})
if err != nil {
return nil, status, fmt.Errorf("ChatsClient.BaseClient.Get(): %v", err)
}

defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, status, fmt.Errorf("io.ReadAll(): %v", err)
}

var chat Chat
if err := json.Unmarshal(respBody, &chat); err != nil {
return nil, status, fmt.Errorf("json.Unmarshal(): %v", err)
}

return &chat, status, nil
}

// List returns a list of chats as Chat objects.
// To return just a lost of IDs then place the query to be Odata.Query{Select: "id"}.
func (c *ChatClient) List(ctx context.Context, userID string, query odata.Query) (*[]Chat, int, error) {
var status int

query.Metadata = odata.MetadataFull

resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{
ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc,
OData: query,
ValidStatusCodes: []int{http.StatusOK},
Uri: Uri{
Entity: fmt.Sprintf("/users/%s/chats", userID),
HasTenantId: false,
},
})
if err != nil {
return nil, status, fmt.Errorf("ChatsClient.BaseClient.Get(): %v", err)
}

defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, status, fmt.Errorf("io.ReadAll(): %v", err)
}

var chatList struct {
Value []Chat `json:"value"`
}
if err := json.Unmarshal(respBody, &chatList); err != nil {
return nil, status, fmt.Errorf("json.Unmarshal(): %v", err)
}

return &chatList.Value, status, nil

}

// Update updates a chat.
func (c *ChatClient) Update(ctx context.Context, chat Chat) (int, error) {
var status int

body, err := json.Marshal(chat)
if err != nil {
return status, fmt.Errorf("json.Marshal(): %v", err)
}

_, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{
Body: body,
ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc,
ValidStatusCodes: []int{http.StatusNoContent},
Uri: Uri{
Entity: fmt.Sprintf("/chats/%s", *chat.ID),
HasTenantId: true,
},
})
if err != nil {
return status, fmt.Errorf("ChatsClient.BaseClient.Patch(): %v", err)
}

return status, nil
}
116 changes: 116 additions & 0 deletions msgraph/chat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package msgraph_test

import (
"fmt"
"github.com/manicminer/hamilton/internal/utils"
"testing"

"github.com/manicminer/hamilton/internal/test"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"
Threpio marked this conversation as resolved.
Show resolved Hide resolved
)

func TestChatClient(t *testing.T) {
c := test.NewTest(t)
defer c.CancelFunc()

self := testDirectoryObjectsClient_Get(t, c, c.Claims.ObjectId)

// To create a chat two users need to be assigned to the chat.
// An owner needs to be assigned
user1 := testUsersClient_Create(t, c, msgraph.User{
AccountEnabled: utils.BoolPtr(true),
DisplayName: utils.StringPtr("test-user1"),
MailNickname: utils.StringPtr(fmt.Sprintf("test-user-%s", c.RandomString)),
UserPrincipalName: utils.StringPtr(fmt.Sprintf("test-user-%s@%s", c.RandomString, c.Connections["default"].DomainName)),
PasswordProfile: &msgraph.UserPasswordProfile{
Password: utils.StringPtr(fmt.Sprintf("IrPa55w0rd%s", c.RandomString)),
},
})
user2 := testUsersClient_Create(t, c, msgraph.User{
AccountEnabled: utils.BoolPtr(true),
DisplayName: utils.StringPtr("test-user2"),
MailNickname: utils.StringPtr(fmt.Sprintf("test-user-%s", c.RandomString)),
UserPrincipalName: utils.StringPtr(fmt.Sprintf("test-user-%s@%s", c.RandomString, c.Connections["default"].DomainName)),
PasswordProfile: &msgraph.UserPasswordProfile{
Password: utils.StringPtr(fmt.Sprintf("IrPa55w0rd%s", c.RandomString)),
},
})

// Check that a group chat and a OneOnOne chat can be created
newChat := msgraph.Chat{
Topic: utils.StringPtr(fmt.Sprintf("test-chat-%s", c.RandomString)),
ChatType: utils.StringPtr(msgraph.ChatTypeGroup),
Members: &[]msgraph.ConversationMember{
{
ID: user1.Id,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are bind records, it looks like the odata type is required here, and also the ID needs to be sent as a full odata ID.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I apologize for taking this PR on without understanding this part.

Can you link an example of where the OdataID is passed in a full context? I must be missing the apparent examples elsewhere in the code.

My understanding from your first part is that it needs to be:

			{
                                 ODataType: odata.TypeConversationMember
				ID:    user1.Id,
				Roles: &[]string{"owner"},
			},

And then the odata.TypeConversationMember be added as a type?

Copy link
Owner

@manicminer manicminer Feb 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Threpio No apologies are needed - thank you for working on this, it is very appreciated.

Perhaps it is also possible to specify the regular ID and a type instead of the OData ID?

I noted my original comment from the docs where there is an example showing members being set using an OData bind field:

Screenshot 2023-02-27 at 22 36 14

There are other places in the SDK where we handle this type of field, for example see the Group model:

Screenshot 2023-02-27 at 22 38 36

The Members field marshals to members@odata.bind and it should be populated by the user with OData IDs, which take the form: https://graph.microsoft.com/v1/users/00000000-0000-0000-0000-000000000000

You should be able to re-use the Members type here too, as this has its own marshaler and unmarshaler funcs to help with parsing values.

Hope this helps!

Roles: &[]string{"owner"},
},
{
ID: user2.Id,
Roles: &[]string{"owner"},
},
{
ID: self.Id,
Roles: &[]string{"owner"},
},
},
}

chat := testChatClient_Create(t, c, newChat)
testChatClient_Get(t, c, *chat.ID)
testChatClient_List(t, c, *self.Id)
testChatClient_Update(t, c, *chat)
Threpio marked this conversation as resolved.
Show resolved Hide resolved

}

func testChatClient_Create(t *testing.T, c *test.Test, newChat msgraph.Chat) (chat *msgraph.Chat) {
chat, status, err := c.ChatClient.Create(c.Context, newChat)
if err != nil {
t.Fatalf("ChatClient.Create(): %v", err)
}
if status < 200 || status >= 300 {
t.Fatalf("ChatClient.Create(): invalid status: %d", status)
}
if chat == nil {
t.Fatal("ChatClient.Create(): chat was nil")
}
return
}

func testChatClient_Get(t *testing.T, c *test.Test, id string) (chat *msgraph.Chat) {
chat, status, err := c.ChatClient.Get(c.Context, id, odata.Query{})
if err != nil {
t.Fatalf("ChatClient.Get(): %v", err)
}
if status < 200 || status >= 300 {
t.Fatalf("ChatClient.Get(): invalid status: %d", status)
}
if chat == nil {
t.Fatal("ChatClient.Get(): chat was nil")
}
return
}

func testChatClient_List(t *testing.T, c *test.Test, userID string) (chats *[]msgraph.Chat) {
chats, _, err := c.ChatClient.List(c.Context, userID, odata.Query{Top: 10})
if err != nil {
t.Fatalf("ChatClient.List(): %v", err)
}
if chats == nil {
t.Fatal("ChatClient.List(): chats was nil")
}
return
}

func testChatClient_Update(t *testing.T, c *test.Test, chat msgraph.Chat) (updatedChat *msgraph.Chat) {
chat.Topic = utils.StringPtr(fmt.Sprintf("test-chat-%s", c.RandomString))
status, err := c.ChatClient.Update(c.Context, chat)
if err != nil {
t.Fatalf("ChatClient.Update(): %v", err)
}
if status < 200 || status >= 300 {
t.Fatalf("ChatClient.Update(): invalid status: %d", status)
}
return
}
Loading