Skip to content

Commit

Permalink
Add support to upload long response as a file
Browse files Browse the repository at this point in the history
Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>
  • Loading branch information
PrasadG193 committed Apr 12, 2020
1 parent 95cc0b2 commit f901912
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 30 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/gorilla/websocket v1.4.1 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/infracloudio/msbotbuilder-go v0.2.0
github.com/infracloudio/msbotbuilder-go v0.2.1-0.20200411121620-e6b496f9febf
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattermost/gorp v2.0.0+incompatible // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
Expand Down Expand Up @@ -174,6 +175,8 @@ github.com/infracloudio/msbotbuilder-go v0.1.1-0.20200128183632-9d11322f671e h1:
github.com/infracloudio/msbotbuilder-go v0.1.1-0.20200128183632-9d11322f671e/go.mod h1:zTFZH9V4x9YQMXrBw2CNsI6hO6blIQ8jHNvdnjbAqZM=
github.com/infracloudio/msbotbuilder-go v0.2.0 h1:Tnoc04aJ7zIyxa6f6OlhF+kclUcGJ5JDiJZViC7YpYk=
github.com/infracloudio/msbotbuilder-go v0.2.0/go.mod h1:zTFZH9V4x9YQMXrBw2CNsI6hO6blIQ8jHNvdnjbAqZM=
github.com/infracloudio/msbotbuilder-go v0.2.1-0.20200411121620-e6b496f9febf h1:oRSRni/lJYTELBzNprJAn291ZkpjbbKAMKU4OJV4blg=
github.com/infracloudio/msbotbuilder-go v0.2.1-0.20200411121620-e6b496f9febf/go.mod h1:zTFZH9V4x9YQMXrBw2CNsI6hO6blIQ8jHNvdnjbAqZM=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
Expand Down
248 changes: 219 additions & 29 deletions pkg/bot/teams.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
// Copyright (c) 2020 InfraCloud Technologies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package bot

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/infracloudio/botkube/pkg/config"
Expand All @@ -17,37 +38,57 @@ import (
)

const (
defaultMsgPath = "/api/messages"
defaultPort = "3978"
defaultMsgPath = "/api/messages"
defaultPort = "3978"
consentBufferSize = 100
longRespNotice = "Response is too long. Sending last few lines. Please send DM to BotKube to get complete response."
convTypePersonal = "personal"
channelSetCmd = "set default channel"
maxMessageSize = 15700
)

var _ Bot = (*Teams)(nil)

// Teams contains credentials to start Teams backend server
type Teams struct {
AppID string
AppPassword string
MessagePath string
Port string
AllowKubectl bool
RestrictAccess bool
ClusterName string
NotifType config.NotifType
Adapter core.Adapter

ConversationRef schema.ConversationReference
AppID string
AppPassword string
MessagePath string
Port string
AllowKubectl bool
RestrictAccess bool
ClusterName string
NotifType config.NotifType
Adapter core.Adapter
ProcessedConsents chan processedConsent
CleanupDone chan bool

ConversationRef *schema.ConversationReference
}

type processedConsent struct {
ID string
conversationRef schema.ConversationReference
}

type ConsentContext struct {
Command string
}

// NewTeamsBot returns Teams instance
func NewTeamsBot(c *config.Config) *Teams {
logging.Logger.Infof("Config:: %+v", c.Communications.Teams)
return &Teams{
AppID: c.Communications.Teams.AppID,
AppPassword: c.Communications.Teams.AppPassword,
NotifType: c.Communications.Teams.NotifType,
MessagePath: defaultMsgPath,
Port: defaultPort,
AllowKubectl: c.Settings.AllowKubectl,
RestrictAccess: c.Settings.RestrictAccess,
ClusterName: c.Settings.ClusterName,
AppID: c.Communications.Teams.AppID,
AppPassword: c.Communications.Teams.AppPassword,
NotifType: c.Communications.Teams.NotifType,
MessagePath: defaultMsgPath,
Port: defaultPort,
AllowKubectl: c.Settings.AllowKubectl,
RestrictAccess: c.Settings.RestrictAccess,
ClusterName: c.Settings.ClusterName,
ProcessedConsents: make(chan processedConsent, consentBufferSize),
CleanupDone: make(chan bool),
}
}

Expand All @@ -63,9 +104,26 @@ func (t *Teams) Start() {
logging.Logger.Errorf("Failed Start teams bot. %+v", err)
return
}
// Start consent cleanup
go t.cleanupConsents()
http.HandleFunc(t.MessagePath, t.processActivity)
logging.Logger.Infof("Started MS Teams server on port %s", defaultPort)
logging.Logger.Errorf("Error in MS Teams server. %v", http.ListenAndServe(fmt.Sprintf(":%s", t.Port), nil))
t.CleanupDone <- true
}

func (t *Teams) cleanupConsents() {
for {
select {
case consent := <-t.ProcessedConsents:
fmt.Printf("Deleting activity %s\n", consent.ID)
if err := t.Adapter.DeleteActivity(context.Background(), consent.ID, consent.conversationRef); err != nil {
logging.Logger.Errorf("Failed to delete activity. %s", err.Error())
}
case <-t.CleanupDone:
return
}
}
}

func (t *Teams) processActivity(w http.ResponseWriter, req *http.Request) {
Expand All @@ -79,9 +137,96 @@ func (t *Teams) processActivity(w http.ResponseWriter, req *http.Request) {

err = t.Adapter.ProcessActivity(ctx, activity, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
actjson, _ := json.MarshalIndent(turn.Activity, "", " ")
logging.Logger.Debugf("Received activity: %s", actjson)
return turn.SendActivity(coreActivity.MsgOptionText(t.processMessage(turn.Activity)))
//actjson, _ := json.MarshalIndent(turn.Activity, "", " ")
//logging.Logger.Debugf("Received activity: %s", actjson)
resp := t.processMessage(turn.Activity)
if len(resp) >= maxMessageSize {
if turn.Activity.Conversation.ConversationType == convTypePersonal {
// send file upload request
attachments := []schema.Attachment{
{
ContentType: "application/vnd.microsoft.teams.card.file.consent",
Name: "response.txt",
Content: map[string]interface{}{
"description": turn.Activity.Text,
"sizeInBytes": len(resp),
"acceptContext": map[string]interface{}{
"command": activity.Text,
},
},
},
}
return turn.SendActivity(coreActivity.MsgOptionAttachments(attachments))
}
resp = fmt.Sprintf("%s\n```\nCluster: %s\n%s", longRespNotice, t.ClusterName, resp[len(resp)-maxMessageSize:])
}
return turn.SendActivity(coreActivity.MsgOptionText(resp))
},

// handle invoke events
// https://developer.microsoft.com/en-us/microsoft-teams/blogs/working-with-files-in-your-microsoft-teams-bot/
OnInvokeFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
t.pushProcessedConsent(turn.Activity.ReplyToID, coreActivity.GetCoversationReference(turn.Activity))
if err != nil {
return schema.Activity{}, fmt.Errorf("failed to read file: %s", err.Error())
}
if turn.Activity.Value["type"] != "fileUpload" {
return schema.Activity{}, nil
}
if turn.Activity.Value["action"] != "accept" {
return schema.Activity{}, nil
}
if turn.Activity.Value["context"] == nil {
return schema.Activity{}, nil
}

// Parse upload info from invoke accept response
uploadInfo := schema.UploadInfo{}
infoJSON, err := json.Marshal(turn.Activity.Value["uploadInfo"])
if err != nil {
return schema.Activity{}, err
}
if err := json.Unmarshal(infoJSON, &uploadInfo); err != nil {
return schema.Activity{}, err
}

// Parse context
consentCtx := ConsentContext{}
ctxJSON, err := json.Marshal(turn.Activity.Value["context"])
if err != nil {
return schema.Activity{}, err
}
if err := json.Unmarshal(ctxJSON, &consentCtx); err != nil {
return schema.Activity{}, err
}

msg := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(consentCtx.Command), "<at>BotKube</at>"))
e := execute.NewDefaultExecutor(msg, t.AllowKubectl, t.RestrictAccess, t.ClusterName, true)
out := e.Execute()

aj, _ := json.MarshalIndent(turn.Activity, "", " ")
fmt.Printf("Incoming Activity:: \n%s\n", aj)

// upload file
err = t.putRequest(uploadInfo.UploadURL, []byte(out))
if err != nil {
return schema.Activity{}, fmt.Errorf("failed to upload file: %s", err.Error())
}

// notify user about uploaded file
fileAttach := []schema.Attachment{
{
ContentType: "application/vnd.microsoft.teams.card.file.info",
ContentURL: uploadInfo.ContentURL,
Name: uploadInfo.Name,
Content: map[string]interface{}{
"uniqueId": uploadInfo.UniqueID,
"fileType": uploadInfo.FileType,
},
},
}

return turn.SendActivity(coreActivity.MsgOptionAttachments(fileAttach))
},
})
if err != nil {
Expand All @@ -94,8 +239,9 @@ func (t *Teams) processMessage(activity schema.Activity) string {
msg := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(activity.Text), "<at>BotKube</at>"))

// Parse "set default channel" command and set conversation reference
if msg == "set default channel" {
t.ConversationRef = coreActivity.GetCoversationReference(activity)
if msg == channelSetCmd {
ref := coreActivity.GetCoversationReference(activity)
t.ConversationRef = &ref
// Remove messageID from the ChannelID
if ID, ok := activity.ChannelData["teamsChannelId"]; ok {
t.ConversationRef.ChannelID = ID.(string)
Expand All @@ -106,7 +252,43 @@ func (t *Teams) processMessage(activity schema.Activity) string {

// Multicluster is not supported for Teams
e := execute.NewDefaultExecutor(msg, t.AllowKubectl, t.RestrictAccess, t.ClusterName, true)
return fmt.Sprintf("```%s\n%s```", t.ClusterName, e.Execute())
out := e.Execute()
return fmt.Sprintf("```%s```", out)
}

func (t *Teams) pushProcessedConsent(ID string, ref schema.ConversationReference) {
select {
case t.ProcessedConsents <- processedConsent{ID: ID, conversationRef: ref}:
break
default:
// Remove older ID if buffer is full
<-t.ProcessedConsents
t.ProcessedConsents <- processedConsent{ID: ID, conversationRef: ref}
}
}

func (t *Teams) putRequest(u string, data []byte) error {
client := &http.Client{}
dec, err := url.QueryUnescape(u)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, dec, bytes.NewBuffer(data))
if err != nil {
return err
}
size := fmt.Sprintf("%d", len(data))
req.Header.Set("Content-Type", "text/plain")
req.Header.Set("Content-Length", size)
req.Header.Set("Content-Range", fmt.Sprintf("bytes 0-%d/%d", len(data)-1, len(data)))
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 201 && resp.StatusCode != 200 {
return fmt.Errorf("failed to upload file with status %d", resp.StatusCode)
}
return nil
}

func (t *Teams) SendEvent(event events.Event) error {
Expand All @@ -120,7 +302,11 @@ func (t *Teams) SendEvent(event events.Event) error {

// SendMessage sends message to MsTeams
func (t *Teams) SendMessage(msg string) error {
err := t.Adapter.ProactiveMessage(context.TODO(), t.ConversationRef, coreActivity.HandlerFuncs{
if t.ConversationRef == nil {
logging.Logger.Infof("Skipping SendMessage since conversation ref not set")
return nil
}
err := t.Adapter.ProactiveMessage(context.TODO(), *t.ConversationRef, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
return turn.SendActivity(coreActivity.MsgOptionText(msg))
},
Expand All @@ -133,7 +319,11 @@ func (t *Teams) SendMessage(msg string) error {
}

func (t *Teams) sendProactiveMessage(card map[string]interface{}) error {
err := t.Adapter.ProactiveMessage(context.TODO(), t.ConversationRef, coreActivity.HandlerFuncs{
if t.ConversationRef == nil {
logging.Logger.Infof("Skipping SendMessage since conversation ref not set")
return nil
}
err := t.Adapter.ProactiveMessage(context.TODO(), *t.ConversationRef, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
attachments := []schema.Attachment{
{
Expand Down

0 comments on commit f901912

Please sign in to comment.