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

[MM-54589] Implement job status API #542

Merged
merged 5 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ require (
github.com/mattermost/calls-offloader v0.3.2
github.com/mattermost/calls-recorder v0.4.2
github.com/mattermost/logr/v2 v2.0.16
github.com/mattermost/mattermost-plugin-calls/server/public v0.0.1
github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403
github.com/mattermost/rtcd v0.11.1
github.com/mattermost/rtcd v0.11.3-0.20230913232654-bdaaa43e3e1c
github.com/mattermost/squirrel v0.2.0
github.com/pion/interceptor v0.1.12
github.com/pion/rtp v1.7.13
Expand All @@ -29,6 +30,8 @@ require (

replace github.com/pion/interceptor v0.1.12 => github.com/streamer45/interceptor v0.0.0-20230202152215-57f3ac9e7696

replace github.com/mattermost/mattermost-plugin-calls/server/public => ./server/public
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I actually tried to use Go workspaces to avoid this but ended up in a bit of rabbit hole with some unexpected failures on dependencies. Will look into this at another time as it's working fine for now.


require (
git.mills.io/prologic/bitcask v1.0.2 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
Expand Down Expand Up @@ -76,7 +79,7 @@ require (
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/pyroscope-io/godeltaprof v0.1.1 // indirect
github.com/pyroscope-io/godeltaprof v0.1.2 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ github.com/mattermost/logr/v2 v2.0.16 h1:jnePX4cPskC3WDFvUardh/xZfxNdsFXbEERJQ1k
github.com/mattermost/logr/v2 v2.0.16/go.mod h1:1dm/YhTpozsqANXxo5Pi5zYLBsal2xY0pX+JZNbzYJY=
github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403 h1:/rxsEaisu+Rb5mWfoIEnbFqscJeKVkspj+BWzchUAfs=
github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403/go.mod h1:sgXQrYzs+IJy51mB8E8OBljagk2u3YwQRoYlBH5goiw=
github.com/mattermost/rtcd v0.11.1 h1:xaP/s0/WX8rDqHq05l8b4QLLJuRZXucr9Qh6cHTQSHk=
github.com/mattermost/rtcd v0.11.1/go.mod h1:ketmoC7+9IOjynE5YJgR6GrFTG1b78byVlkVsLcCDa0=
github.com/mattermost/rtcd v0.11.3-0.20230913232654-bdaaa43e3e1c h1:I869qzi119z/b5hEcsFGrbDTkS9f8HAVg+IO56Q6diw=
github.com/mattermost/rtcd v0.11.3-0.20230913232654-bdaaa43e3e1c/go.mod h1:7C412PFWeVSZK0E8Ruht5ArHP+zxhSr/sMDJvnUFDe4=
github.com/mattermost/squirrel v0.2.0 h1:8ZWeyf+MWQ2cL7hu9REZgLtz2IJi51qqZEovI3T3TT8=
github.com/mattermost/squirrel v0.2.0/go.mod h1:NPPtk+CdpWre4GxMGoOpzEVFVc0ZoEFyJBZGCtn9nSU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
Expand Down Expand Up @@ -433,8 +433,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/pyroscope-io/godeltaprof v0.1.1 h1:+Mmi+b9gR3s/qufuQSxOBjyXZR1fmvS/C12Q73PIPvw=
github.com/pyroscope-io/godeltaprof v0.1.1/go.mod h1:psMITXp90+8pFenXkKIpNhrfmI9saQnPbba27VIaiQE=
github.com/pyroscope-io/godeltaprof v0.1.2 h1:MdlEmYELd5w+lvIzmZvXGNMVzW2Qc9jDMuJaPOR75g4=
github.com/pyroscope-io/godeltaprof v0.1.2/go.mod h1:psMITXp90+8pFenXkKIpNhrfmI9saQnPbba27VIaiQE=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down
90 changes: 90 additions & 0 deletions server/bot_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import (
"regexp"
"time"

"github.com/mattermost/mattermost-plugin-calls/server/public"

"github.com/mattermost/mattermost/server/public/model"
)

var botChRE = regexp.MustCompile(`^\/bot\/channels\/([a-z0-9]+)$`)
var botUserImageRE = regexp.MustCompile(`^\/bot\/users\/([a-z0-9]+)\/image$`)
var botUploadsRE = regexp.MustCompile(`^\/bot\/uploads\/?([a-z0-9]+)?$`)
var botRecordingsRE = regexp.MustCompile(`^\/bot\/calls\/([a-z0-9]+)\/recordings$`)
var botJobsStatusRE = regexp.MustCompile(`^\/bot\/calls\/([a-z0-9]+)\/jobs\/([a-z0-9]+)\/status$`)

func (p *Plugin) getBotID() string {
if p.botSession != nil {
Expand Down Expand Up @@ -245,6 +248,88 @@ func (p *Plugin) handleBotPostRecordings(w http.ResponseWriter, r *http.Request,
res.Msg = "success"
}

func (p *Plugin) handleBotPostJobsStatus(w http.ResponseWriter, r *http.Request, callID, jobID string) {
var res httpResponse
defer p.httpAudit("handleBotPostJobsStatus", &res, w, r)

var status public.JobStatus
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, requestBodyMaxSizeBytes)).Decode(&status); err != nil {
res.Err = "failed to decode request body: " + err.Error()
res.Code = http.StatusBadRequest
return
}

state, err := p.lockCall(callID)
if err != nil {
res.Err = fmt.Errorf("failed to lock call: %w", err).Error()
res.Code = http.StatusInternalServerError
return
}
defer p.unlockCall(callID)

if state == nil || state.Call == nil {
res.Err = "no call ongoing"
res.Code = http.StatusBadRequest
return
}

if status.JobType == public.JobTypeRecording {
if state.Call.Recording == nil {
res.Err = "no recording ongoing"
res.Code = http.StatusBadRequest
return
}

if state.Call.Recording.ID != jobID {
res.Err = "invalid recording job ID"
res.Code = http.StatusBadRequest
return
}

if state.Call.Recording.EndAt > 0 {
res.Err = "recording has ended"
res.Code = http.StatusBadRequest
return
}

if status.Status == public.JobStatusTypeFailed {
p.LogDebug("recording has failed", "jobID", jobID)
state.Call.Recording.EndAt = time.Now().UnixMilli()
state.Call.Recording.Err = status.Error
} else if status.Status == public.JobStatusTypeStarted {
if state.Call.Recording.StartAt > 0 {
res.Err = "recording has already started"
res.Code = http.StatusBadRequest
return
}
p.LogDebug("recording has started", "jobID", jobID)
state.Call.Recording.StartAt = time.Now().UnixMilli()
} else {
res.Err = "unsupported status type"
res.Code = http.StatusBadRequest
return
}

if err := p.kvSetChannelState(callID, state); err != nil {
res.Err = fmt.Errorf("failed to set channel state: %w", err).Error()
res.Code = http.StatusInternalServerError
return
}

p.publishWebSocketEvent(wsEventCallRecordingState, map[string]interface{}{
"callID": callID,
"recState": state.Call.Recording.getClientState().toMap(),
}, &model.WebsocketBroadcast{ChannelId: callID, ReliableClusterSend: true})

res.Code = http.StatusOK
res.Msg = "success"
return
}

res.Err = "bad request"
res.Code = http.StatusBadRequest
}

func (p *Plugin) handleBotAPI(w http.ResponseWriter, r *http.Request) {
if !p.licenseChecker.RecordingsAllowed() {
http.Error(w, "Forbidden", http.StatusForbidden)
Expand Down Expand Up @@ -286,6 +371,11 @@ func (p *Plugin) handleBotAPI(w http.ResponseWriter, r *http.Request) {
p.handleBotPostRecordings(w, r, matches[1])
return
}

if matches := botJobsStatusRE.FindStringSubmatch(r.URL.Path); len(matches) == 3 {
p.handleBotPostJobsStatus(w, r, matches[1], matches[2])
return
}
}

http.NotFound(w, r)
Expand Down
3 changes: 3 additions & 0 deletions server/public/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/mattermost/mattermost-plugin-calls/server/public

go 1.19
20 changes: 20 additions & 0 deletions server/public/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package public

type JobType string

const (
JobTypeRecording JobType = "recording"
)

type JobStatusType string

const (
JobStatusTypeStarted JobStatusType = "started"
JobStatusTypeFailed = "failed"
)

type JobStatus struct {
JobType JobType
Status JobStatusType
Error string `json:"omitempty"`
}
9 changes: 5 additions & 4 deletions server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,16 @@ func (p *Plugin) addUserSession(state *channelState, userID, connID, channelID,
return nil, fmt.Errorf("user cannot join because of limits")
}

// When the bot joins the call it means the recording has started.
// When the bot joins the call it means the recording is starting. The actual
// start time is when the bot sends the status update through the API.
if userID == p.getBotID() {
if state.Call.Recording != nil && state.Call.Recording.StartAt == 0 {
if state.Call.Recording != nil && state.Call.Recording.StartAt == 0 && state.Call.Recording.BotConnID == "" {
if state.Call.Recording.ID != jobID {
return nil, fmt.Errorf("invalid job ID for recording")
}
state.Call.Recording.StartAt = time.Now().UnixMilli()
p.LogDebug("bot joined, recording is starting", "jobID", jobID)
state.Call.Recording.BotConnID = connID
} else if state.Call.Recording == nil || state.Call.Recording.StartAt > 0 {
} else if state.Call.Recording == nil || state.Call.Recording.StartAt > 0 || state.Call.Recording.BotConnID != "" {
// In this case we should fail to prevent the bot from recording
// without consent.
return nil, fmt.Errorf("recording not in progress or already started")
Expand Down