Skip to content

Commit

Permalink
Fix meeting ended webhook (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickmister authored May 22, 2020
1 parent 80cb1f6 commit 37a1b57
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 39 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go 1.12

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/schema v1.1.0
github.com/mattermost/mattermost-server/v5 v5.23.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.5.1
Expand Down
74 changes: 47 additions & 27 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"crypto/subtle"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"

"github.com/gorilla/schema"
"github.com/mattermost/mattermost-plugin-zoom/server/zoom"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
Expand Down Expand Up @@ -153,7 +153,7 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request)

user, _ := p.API.GetUser(userID)

_, appErr = p.postMeeting(user.Username, zoomUser.Pmi, channelID, "")
_, appErr = p.postMeeting(user, zoomUser.Pmi, channelID, "")
if appErr != nil {
http.Error(w, appErr.Error(), appErr.StatusCode)
return
Expand Down Expand Up @@ -183,42 +183,55 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
return
}

if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request body", http.StatusBadRequest)
if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
res := fmt.Sprintf("Expected Content-Type 'application/json' for webhook request, received '%s'.", r.Header.Get("Content-Type"))
http.Error(w, res, http.StatusBadRequest)
return
}

var webhook zoom.Webhook
decoder := schema.NewDecoder()

// Try to decode to standard webhook
if err := decoder.Decode(&webhook, r.PostForm); err != nil {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

p.handleStandardWebhook(w, r, &webhook)
var webhook zoom.Webhook
err = json.Unmarshal(b, &webhook)
if err != nil {
p.API.LogError("Error unmarshaling webhook", "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// TODO: handle recording webhook
}
if webhook.Event != zoom.EventTypeMeetingEnded {
w.WriteHeader(http.StatusNotImplemented)
return
}

func (p *Plugin) handleStandardWebhook(w http.ResponseWriter, r *http.Request, webhook *zoom.Webhook) {
if webhook.Status != zoom.WebhookStatusEnded {
var meetingWebhook zoom.MeetingWebhook
err = json.Unmarshal(b, &meetingWebhook)
if err != nil {
p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.handleMeetingEnded(w, r, &meetingWebhook)
}

key := fmt.Sprintf("%v%v", postMeetingKey, webhook.ID)
func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, webhook *zoom.MeetingWebhook) {
key := fmt.Sprintf("%v%v", postMeetingKey, webhook.Payload.Object.ID)
b, appErr := p.API.KVGet(key)
if appErr != nil {
http.Error(w, appErr.Error(), appErr.StatusCode)
return
}

if b == nil {
http.Error(w, "Stored meeting not found", http.StatusNotFound)
return
}
postID := string(b)

postID := string(b)
post, appErr := p.API.GetPost(postID)
if appErr != nil {
http.Error(w, appErr.Error(), appErr.StatusCode)
Expand All @@ -228,17 +241,20 @@ func (p *Plugin) handleStandardWebhook(w http.ResponseWriter, r *http.Request, w
post.Message = "Meeting has ended."
post.Props["meeting_status"] = zoom.WebhookStatusEnded

if _, appErr := p.API.UpdatePost(post); appErr != nil {
_, appErr = p.API.UpdatePost(post)
if appErr != nil {
http.Error(w, appErr.Error(), appErr.StatusCode)
return
}

if appErr := p.API.KVDelete(key); appErr != nil {
appErr = p.API.KVDelete(key)
if appErr != nil {
p.API.LogWarn("failed to delete db entry", "error", appErr.Error())
return
}

if _, err := w.Write([]byte(post.ToJson())); err != nil {
_, err := w.Write([]byte(post.ToJson()))
if err != nil {
p.API.LogWarn("failed to write response", "error", err.Error())
}
}
Expand Down Expand Up @@ -281,7 +297,8 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
}

var req startMeetingRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Expand All @@ -292,20 +309,22 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
return
}

if _, appErr = p.API.GetChannelMember(req.ChannelID, userID); appErr != nil {
_, appErr = p.API.GetChannelMember(req.ChannelID, userID)
if appErr != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

if forceCreate := r.URL.Query().Get("force"); forceCreate == "" {
if r.URL.Query().Get("force") == "" {
recentMeeting, recentMeetindID, creatorName, cpmErr := p.checkPreviousMessages(req.ChannelID)
if cpmErr != nil {
http.Error(w, cpmErr.Error(), cpmErr.StatusCode)
return
}

if recentMeeting {
if _, err := w.Write([]byte(`{"meeting_url": ""}`)); err != nil {
_, err = w.Write([]byte(`{"meeting_url": ""}`))
if err != nil {
p.API.LogWarn("failed to write response", "error", err.Error())
}
p.postConfirm(recentMeetindID, req.ChannelID, req.Topic, userID, creatorName)
Expand All @@ -315,7 +334,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {

zoomUser, authErr := p.authenticateAndFetchZoomUser(userID, user.Email, req.ChannelID)
if authErr != nil {
if _, err := w.Write([]byte(`{"meeting_url": ""}`)); err != nil {
if _, err = w.Write([]byte(`{"meeting_url": ""}`)); err != nil {
p.API.LogWarn("failed to write response", "error", err.Error())
}
p.postConnect(req.ChannelID, userID)
Expand All @@ -330,14 +349,15 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
return
}

if appErr = p.API.KVSet(fmt.Sprintf("%v%v", postMeetingKey, meetingID), []byte(createdPost.Id)); appErr != nil {
appErr = p.API.KVSet(fmt.Sprintf("%v%v", postMeetingKey, meetingID), []byte(createdPost.Id))
if appErr != nil {
http.Error(w, appErr.Error(), appErr.StatusCode)
return
}

meetingURL := p.getMeetingURL(meetingID)

if _, err := w.Write([]byte(fmt.Sprintf(`{"meeting_url": "%s"}`, meetingURL))); err != nil {
_, err = w.Write([]byte(fmt.Sprintf(`{"meeting_url": "%s"}`, meetingURL)))
if err != nil {
p.API.LogWarn("failed to write response", "error", err.Error())
}
}
Expand Down
16 changes: 9 additions & 7 deletions server/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ func TestPlugin(t *testing.T) {
personalMeetingRequest := httptest.NewRequest("POST", "/api/v1/meetings", strings.NewReader("{\"channel_id\": \"thechannelid\", \"personal\": true}"))
personalMeetingRequest.Header.Add("Mattermost-User-Id", "theuserid")

validWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader("id=234&uuid=1dnv2x3XRiMdoVIwzms5lA%3D%3D&status=ENDED&host_id=iQZt4-f1ZQp2tgWwx-p1mQ"))
validWebhookRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
endedPayload := `{"event": "meeting.ended", "payload": {"object": {"id": "234"}}}`
validStoppedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(endedPayload))

validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader("id=234&uuid=1dnv2x3XRiMdoVIwzms5lA%3D%3D&status=STARTED&host_id=iQZt4-f1ZQp2tgWwx-p1mQ"))
validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(`{"event": "meeting.started"}`))

noSecretWebhookRequest := httptest.NewRequest("POST", "/webhook", strings.NewReader("id=234&uuid=1dnv2x3XRiMdoVIwzms5lA%3D%3D&status=ENDED&host_id=iQZt4-f1ZQp2tgWwx-p1mQ"))
noSecretWebhookRequest := httptest.NewRequest("POST", "/webhook", strings.NewReader(endedPayload))

for name, tc := range map[string]struct {
Request *http.Request
Expand All @@ -63,13 +63,13 @@ func TestPlugin(t *testing.T) {
Request: personalMeetingRequest,
ExpectedStatusCode: http.StatusOK,
},
"ValidWebhookRequest": {
Request: validWebhookRequest,
"ValidStoppedWebhookRequest": {
Request: validStoppedWebhookRequest,
ExpectedStatusCode: http.StatusOK,
},
"ValidStartedWebhookRequest": {
Request: validStartedWebhookRequest,
ExpectedStatusCode: http.StatusOK,
ExpectedStatusCode: http.StatusNotImplemented,
},
"NoSecretWebhookRequest": {
Request: noSecretWebhookRequest,
Expand Down Expand Up @@ -131,6 +131,8 @@ func TestPlugin(t *testing.T) {
err = p.OnActivate()
require.Nil(t, err)

tc.Request.Header.Add("Content-Type", "application/json")

w := httptest.NewRecorder()
p.ServeHTTP(&plugin.Context{}, w, tc.Request)
assert.Equal(t, tc.ExpectedStatusCode, w.Result().StatusCode)
Expand Down
33 changes: 29 additions & 4 deletions server/zoom/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,43 @@ import (
"time"
)

type EventType string

const (
WebhookStatusStarted = "STARTED"
WebhookStatusEnded = "ENDED"
RecordingWebhookTypeComplete = "RECORDING_MEETING_COMPLETED"
RecentlyCreated = "RECENTLY_CREATED"

EventTypeMeetingStarted EventType = "meeting.started"
EventTypeMeetingEnded EventType = "meeting.ended"
)

type MeetingWebhookObject struct {
Duration int `json:"duration"`
StartTime time.Time `json:"start_time"`
Timezone string `json:"timezone"`
EndTime time.Time `json:"end_time"`
Topic string `json:"topic"`
ID string `json:"id"`
Type int `json:"type"`
UUID string `json:"uuid"`
HostID string `json:"host_id"`
}

type MeetingWebhookPayload struct {
AccountID string `json:"account_id"`
Object MeetingWebhookObject `json:"object"`
}

type MeetingWebhook struct {
Event EventType `json:"event"`
Payload MeetingWebhookPayload `json:"payload"`
}

type Webhook struct {
ID int `schema:"id"`
UUID string `schema:"uuid"`
Status string `schema:"status"`
HostID string `schema:"host_id"`
Event EventType `json:"event"`
Payload interface{} `json:"payload"`
}

type RecordingWebhook struct {
Expand Down

0 comments on commit 37a1b57

Please sign in to comment.