Skip to content

Commit

Permalink
Added support for activity update (#51)
Browse files Browse the repository at this point in the history
* Added support for activity update

* Update samples/activity-update/README.md

Co-authored-by: Prasad Ghangal <[email protected]>
  • Loading branch information
sbawaskar and PrasadG193 authored Mar 13, 2021
1 parent ceff03e commit da77b9e
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
**/.vscode/**
20 changes: 18 additions & 2 deletions connector/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
type Client interface {
Post(url url.URL, activity schema.Activity) error
Delete(url url.URL, activity schema.Activity) error
Put(url url.URL, activity schema.Activity) error
}

// ConnectorClient implements Client to send HTTP requests to the connector service.
Expand Down Expand Up @@ -67,7 +68,7 @@ func (client *ConnectorClient) Post(target url.URL, activity schema.Activity) er
if err != nil {
return err
}

fmt.Println(target.String())
req, err := http.NewRequest(http.MethodPost, target.String(), bytes.NewBuffer(jsonStr))
if err != nil {
return err
Expand All @@ -87,6 +88,22 @@ func (client *ConnectorClient) Delete(target url.URL, activity schema.Activity)
return client.sendRequest(req, activity)
}

// Put an activity.
//
// Creates a HTTP PUT request with the provided activity payload and a Bearer token in the header.
// Returns any error as received from the call to connector service.
func (client *ConnectorClient) Put(target url.URL, activity schema.Activity) error {
jsonStr, err := json.Marshal(activity)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, target.String(), bytes.NewBuffer(jsonStr))
if err != nil {
return err
}
return client.sendRequest(req, activity)
}

func (client *ConnectorClient) sendRequest(req *http.Request, activity schema.Activity) error {
token, err := client.getToken()
if err != nil {
Expand All @@ -109,7 +126,6 @@ func (client *ConnectorClient) checkRespError(resp *http.Response, err error) er
}
}
defer resp.Body.Close()

// Check if resp allowed
for _, code := range allowedResp {
if code == resp.StatusCode {
Expand Down
23 changes: 19 additions & 4 deletions core/activity/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import (
type Response interface {
SendActivity(activity schema.Activity) error
DeleteActivity(activity schema.Activity) error
UpdateActivity(activity schema.Activity) error
}

const (
// APIVersion for response URLs
APIVersion = "v3"

sendToConversationURL = "/%s/conversations/%s/activities"
replyToActivityURL = "/%s/conversations/%s/activities/%s"
deleteActivityURL = "/%s/conversations/%s/activities/%s"
activityResourceURL = "/%s/conversations/%s/activities/%s"
)

// DefaultResponse is the default implementation of Response.
Expand All @@ -56,7 +56,7 @@ func (response *DefaultResponse) DeleteActivity(activity schema.Activity) error
return errors.Wrapf(err, "Failed to parse ServiceURL %s.", activity.ServiceURL)
}

respPath := fmt.Sprintf(deleteActivityURL, APIVersion, activity.Conversation.ID, activity.ID)
respPath := fmt.Sprintf(activityResourceURL, APIVersion, activity.Conversation.ID, activity.ID)

// Send activity to client
u.Path = path.Join(u.Path, respPath)
Expand All @@ -75,7 +75,7 @@ func (response *DefaultResponse) SendActivity(activity schema.Activity) error {

// if ReplyToID is set in the activity, we send reply to that particular activity
if activity.ReplyToID != "" {
respPath = fmt.Sprintf(replyToActivityURL, APIVersion, activity.Conversation.ID, activity.ID)
respPath = fmt.Sprintf(activityResourceURL, APIVersion, activity.Conversation.ID, activity.ID)
}

// Send activity to client
Expand All @@ -84,6 +84,21 @@ func (response *DefaultResponse) SendActivity(activity schema.Activity) error {
return errors.Wrap(err, "Failed to send response.")
}

// UpdateActivity sends a Put activity method to the BOT connector service.
func (response *DefaultResponse) UpdateActivity(activity schema.Activity) error {
u, err := url.Parse(activity.ServiceURL)
if err != nil {
return errors.Wrapf(err, "Failed to parse ServiceURL %s.", activity.ServiceURL)
}

respPath := fmt.Sprintf(activityResourceURL, APIVersion, activity.Conversation.ID, activity.ID)

// Send activity to client
u.Path = path.Join(u.Path, respPath)
err = response.Client.Put(*u, activity)
return errors.Wrap(err, "Failed to update response.")
}

// NewActivityResponse provides a DefaultResponse implementaton of Response.
func NewActivityResponse(connectorClient client.Client) (Response, error) {
if connectorClient == nil {
Expand Down
11 changes: 11 additions & 0 deletions core/bot_framework_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Adapter interface {
ProcessActivity(ctx context.Context, req schema.Activity, handler activity.Handler) error
ProactiveMessage(ctx context.Context, ref schema.ConversationReference, handler activity.Handler) error
DeleteActivity(ctx context.Context, activityID string, ref schema.ConversationReference) error
UpdateActivity(ctx context.Context, activity schema.Activity) error
}

// AdapterSetting is the configuration for the Adapter.
Expand Down Expand Up @@ -155,3 +156,13 @@ func (bf *BotFrameworkAdapter) authenticateRequest(ctx context.Context, req sche

return errors.Wrap(err, "Authentication failed.")
}

// UpdateActivity Updates an existing activity
func (bf *BotFrameworkAdapter) UpdateActivity(ctx context.Context, req schema.Activity) error {
response, err := activity.NewActivityResponse(bf.Client)

if err != nil {
return errors.Wrap(err, "Failed to create response object.")
}
return response.UpdateActivity(req)
}
73 changes: 70 additions & 3 deletions core/bot_framework_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ import (
"github.com/stretchr/testify/assert"
)

func serverMock() *httptest.Server {
func serverMock(t *testing.T) *httptest.Server {
handler := http.NewServeMux()
handler.HandleFunc("/v3/conversations/abcd1234/activities", msTeamsMockMock)

h1 := &msTeamsActivityUpdateMock{t: t}
handler.Handle("/v3/conversations/TestActivityUpdate/activities", h1)
srv := httptest.NewServer(handler)

return srv
Expand All @@ -50,6 +51,19 @@ func msTeamsMockMock(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("{\"id\":\"1\"}"))
}

type msTeamsActivityUpdateMock struct {
t *testing.T
}

func (th *msTeamsActivityUpdateMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
assert.Equal(th.t, "PUT", r.Method, "Expect PUT method")
activity := schema.Activity{}
err := json.NewDecoder(r.Body).Decode(&activity)
assert.Equal(th.t, "TestLabel", activity.Label, "Expect PUT method")
assert.Nil(th.t, err, fmt.Sprintf("Failed with error %s", err))
_, _ = w.Write([]byte("{\"id\":\"1\"}"))
}

// Create a handler that defines operations to be performed on respective events.
// Following defines the operation to be performed on the 'message' event.
var customHandler = activity.HandlerFuncs{
Expand All @@ -59,7 +73,7 @@ var customHandler = activity.HandlerFuncs{
}

func TestExample(t *testing.T) {
srv := serverMock()
srv := serverMock(t)
// activity depicts a request as received from a client
activity := schema.Activity{
Type: schema.Message,
Expand Down Expand Up @@ -110,3 +124,56 @@ func TestExample(t *testing.T) {
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 200, "Expect 200 response status")
}

func TestActivityUpdate(t *testing.T) {
srv := serverMock(t)

activity := schema.Activity{
Type: schema.Message,
From: schema.ChannelAccount{
ID: "12345678",
Name: "Pepper's News Feed",
},
Conversation: schema.ConversationAccount{
ID: "TestActivityUpdate",
Name: "Convo1",
},
Recipient: schema.ChannelAccount{
ID: "1234abcd",
Name: "SteveW",
},
Text: "Message from Teams Client",
ReplyToID: "5d5cdc723",
ServiceURL: srv.URL,
}

handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := context.Background()
setting := core.AdapterSetting{
AppID: "asdasd",
AppPassword: "cfg.MicrosoftTeams.AppPassword",
}
setting.CredentialProvider = auth.SimpleCredentialProvider{
AppID: setting.AppID,
Password: setting.AppPassword,
}
clientConfig, err := client.NewClientConfig(setting.CredentialProvider, auth.ToChannelFromBotLoginURL[0])
assert.Nil(t, err, fmt.Sprintf("Failed with error %s", err))
connectorClient, err := client.NewClient(clientConfig)
assert.Nil(t, err, fmt.Sprintf("Failed with error %s", err))
adapter := core.BotFrameworkAdapter{setting, &core.MockTokenValidator{}, connectorClient}
act, err := adapter.ParseRequest(ctx, req)
act.Label = "TestLabel"
err = adapter.UpdateActivity(ctx, act)
assert.Nil(t, err, fmt.Sprintf("Failed with error %s", err))
})
rr := httptest.NewRecorder()
bodyJSON, err := json.Marshal(activity)
assert.Nil(t, err, fmt.Sprintf("Failed with error %s", err))
bodyBytes := bytes.NewReader(bodyJSON)
req, err := http.NewRequest(http.MethodPost, "/api/messages", bodyBytes)
assert.Nil(t, err, fmt.Sprintf("Failed with error %s", err))
req.Header.Set("Authorization", "Bearer abc123")
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 200, "Expect 200 response status")
}
116 changes: 116 additions & 0 deletions samples/activity-update/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Bot Framework activity update sample.

This Microsoft Teams bot uses the [msbotbuilder-go](https://github.com/infracloudio/msbotbuilder-go) library. It shows how to create a simple bot that accepts input from the user and echoes response with an attachment.

## Run the example

#### Step 1: Register MS BOT

Follow the official [documentation](https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/create-a-bot-for-teams#register-your-web-service-with-the-bot-framework) to create and register your BOT.

Copy `APP_ID` and `APP_PASSWORD` generated for your BOT.

Do not set any messaging endpoint for now.

#### Step 2: Run echo local server

Set two variables for the session as `APP_ID` and `APP_PASSWORD` to the values of your BotFramework `APP_ID` and `APP_PASSWORD`. Then run the `main.go` file.

```bash
export APP_PASSWORD=MICROSOFT_APP_PASSWORD
export APP_ID=MICROSOFT_APP_ID

go run main.go
```

This will start a server which will listen on port `3978`

#### Step 3: Expose local server with ngrok

Now, in separate terminal, run [ngrok](https://ngrok.com/download) command to expose your local server to outside world.

```sh
$ ngrok http 3978
```

Copy `https` endpoint, go to [Bot Framework](https://dev.botframework.com/bots) dashboard and set messaging endpoint under Settings.

#### Step 4: Test the BOT

You can either test BOT on BotFramework portal or you can create app manifest and install the App on Teams as mentioned [here](https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/create-a-bot-for-teams#create-your-app-manifest-and-package).


## Understanding the example

The program starts by creating a handler struct of type `activity.HandlerFuncs`.

This struct contains a definition for the `OnMessageFunc` field which is treated as a callback by the library on the respective event.
In this sample, when the GetCard command is issued from the chat application, an adaptive card with an input box is sent from this bot. Once submit button on the card is clicked the card is deleted. Also, the bot updates the activity and sets the text message to activity as "Changed Activity" after the card is deleted.

```bash
var customHandler = activity.HandlerFuncs{
OnMessageFunc: func(turn *activity.TurnContext) (schema.Activity, error) {
if turn.Activity.Text == "getCard" {
sJson := `{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Sample"
},
{
"type": "Input.Text",
"id": "firstName",
"placeholder": "What is your first name?"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
}`
var obj map[string]interface{}
err := json.Unmarshal(([]byte(sJson)), &obj)
if err != nil {
return schema.Activity{}, nil
}
attachments := []schema.Attachment{
{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: obj,
},
}
return turn.SendActivity(activity.MsgOptionAttachments(attachments))
}
if turn.Activity.Value != nil {
fmt.Println("Activity=", turn.Activity.Value)
activityID = turn.Activity.ReplyToID
}

return turn.SendActivity(activity.MsgOptionText("Echo: " + turn.Activity.Text))
},
}
```
The `init` function picks up the `APP_ID` and `APP_PASSWORD` from the environment session and creates an `adapter` using this.
A webserver is started with a handler which passes the received payload to `adapter.ParseRequest`. This methods authenticates the payload, parses the request and returns an Activity value.
```
activity, err := adapter.ParseRequest(ctx, req)
```
The Activity is then passed to `adapter.ProcessActivity` with the handler created to process the activity as per the handler functions and send the response to the connector service.
```
err = adapter.ProcessActivity(ctx, activity, customHandler)
```
In case of no error, this web server responds with a 200 status.
Loading

0 comments on commit da77b9e

Please sign in to comment.