Skip to content

Commit

Permalink
Conferences (#64)
Browse files Browse the repository at this point in the history
* Implement GetConference API

* start conference participant

* implement rest of conference & participant APIs

* support full ConferenceParticipant attributes

* EarlyMedia should be a bool

* rename to capitals

* Hold + Announce support for conferences

* use proper URL params for updating conference

* typo in AccountUrl
  • Loading branch information
andrewpage authored and sfreiberg committed Jan 13, 2020
1 parent 3818799 commit 06f83df
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 16 deletions.
215 changes: 215 additions & 0 deletions conference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package gotwilio

import (
"encoding/json"
"fmt"
"github.com/google/go-querystring/query"
"net/http"
)

// Conference represents a Twilio Voice conference call
type Conference struct {
Sid string `json:"sid"`
FriendlyName string `json:"friendly_name"`
Status string `json:"status"`
Region string `json:"region"`
}

// ConferenceOptions are used for updating Conferences
type ConferenceOptions struct {
Status string `url:"Status,omitempty"`
AnnounceURL string `url:"AnnounceUrl,omitempty"`
AnnounceMethod string `url:"AnnounceMethod,omitempty"`
}

// ConferenceParticipant represents a Participant in responses from the Twilio API
type ConferenceParticipant struct {
CallSid string `json:"call_sid"`
ConferenceSid string `json:"conference_sid"`
Muted bool `json:"muted"`
Hold bool `json:"hold"`
Status string `json:"status"`
StartConferenceOnEnter bool `json:"start_conference_on_enter"`
EndConferenceOnExit bool `json:"end_conference_on_exit"`
Coaching bool `json:"coaching"`
CallSidToCoach string `json:"call_sid_to_coach"`
}

// ConferenceParticipantOptions are used for creating and updating Conference Participants.
type ConferenceParticipantOptions struct {
From string `url:"From,omitempty"`
To string `url:"To,omitempty"`
StatusCallback string `url:"StatusCallback,omitempty"`
StatusCallbackMethod string `url:"StatusCallbackMethod,omitempty"`
StatusCallbackEvent string `url:"statusCallbackEvent,omitempty"`
Timeout int `url:"Timeout"`
Record *bool `url:"Record,omitempty"`
Beep *bool `url:"Beep,omitempty"`
Muted *bool `url:"Muted,omitempty"`
Hold *bool `url:"Hold,omitempty"`
HoldURL *string `url:"HoldURL,omitempty"`
HoldMethod *string `url:"HoldMethod,omitempty"`
AnnounceURL *string `url:"AnnounceURL,omitempty"`
AnnounceMethod *string `url:"AnnounceMethod,omitempty"`
StartConferenceOnEnter *bool `url:"StartConferenceOnEnter,omitempty"`
EndConferenceOnExit *bool `url:"EndConferenceOnExit,omitempty"`
WaitURL string `url:"WaitURL,omitempty"`
WaitMethod string `url:"WaitMethod,omitempty"`
EarlyMedia *bool `url:"EarlyMedia,omitempty"`
MaxParticipants int `url:"MaxParticipants"`
ConferenceRecord string `url:"ConferenceRecord,omitempty"`
ConferenceTrim string `url:"ConferenceTrim,omitempty"`
ConferenceStatusCallback string `url:"ConferenceStatusCallback,omitempty"`
ConferenceStatusCallbackMethod string `url:"ConferenceStatusCallbackMethod,omitempty"`
ConferenceStatusCallbackEvent string `url:"ConferenceStatusCallbackEvent,omitempty"`
RecordingChannels string `url:"RecordingChannels,omitempty"`
RecordingStatusCallback string `url:"RecordingStatusCallback,omitempty"`
RecordingStatusCallbackMethod string `url:"RecordingStatusCallbackMethod,omitempty"`
RecordingStatusCallbackEvent string `url:"RecordingStatusCallbackEvent,omitempty"`
SipAuthUsername string `url:"SipAuthUsername,omitempty"`
SipAuthPassword string `url:"SipAuthPassword,omitempty"`
Region string `url:"Region,omitempty"`
ConferenceRecordingStatusCallback string `url:"ConferenceRecordingStatusCallback,omitempty"`
ConferenceRecordingStatusCallbackMethod string `url:"ConferenceRecordingStatusCallbackMethod,omitempty"`
Coaching *bool `url:"Coaching,omitempty"`
CallSidToCoach string `url:"CallSidToCoach,omitempty"`
}

// GetConference fetches details for a single conference instance
// https://www.twilio.com/docs/voice/api/conference-resource#fetch-a-conference-resource
func (twilio *Twilio) GetConference(conferenceSid string) (*Conference, *Exception, error) {
res, err := twilio.get(twilio.buildUrl(fmt.Sprintf("Conferences/%s.json", conferenceSid)))
if err != nil {
return nil, nil, err
}

decoder := json.NewDecoder(res.Body)

// handle NULL response
if res.StatusCode != http.StatusOK {
exception := new(Exception)
err = decoder.Decode(exception)
return nil, exception, err
}

conf := new(Conference)
err = decoder.Decode(conf)
return conf, nil, err
}

// UpdateConference to end it or play an announcement
// https://www.twilio.com/docs/voice/api/conference-resource#update-a-conference-resource
func (twilio *Twilio) UpdateConference(conferenceSid string, options *ConferenceOptions) (*Conference, *Exception, error) {
form, err := query.Values(options)
if err != nil {
return nil, nil, err
}

res, err := twilio.post(form, twilio.buildUrl(fmt.Sprintf("Conferences/%s.json", conferenceSid)))
if err != nil {
return nil, nil, err
}

decoder := json.NewDecoder(res.Body)

if res.StatusCode != http.StatusOK {
exception := new(Exception)
err = decoder.Decode(exception)
return nil, exception, err
}

c := new(Conference)
err = decoder.Decode(c)
return c, nil, err
}

// GetConferenceParticipant fetches details for a conference participant resource
// https://www.twilio.com/docs/voice/api/conference-participant-resource#fetch-a-participant-resource
func (twilio *Twilio) GetConferenceParticipant(conferenceSid, callSid string) (*ConferenceParticipant, *Exception, error) {
res, err := twilio.get(twilio.buildUrl(fmt.Sprintf("Conferences/%s/Participants/%s.json", conferenceSid, callSid)))
if err != nil {
return nil, nil, err
}

decoder := json.NewDecoder(res.Body)

// handle NULL response
if res.StatusCode != http.StatusOK {
exception := new(Exception)
err = decoder.Decode(exception)
return nil, exception, err
}

conf := new(ConferenceParticipant)
err = decoder.Decode(conf)
return conf, nil, err
}

// AddConferenceParticipant adds a Participant to a conference by dialing out a new call
// https://www.twilio.com/docs/voice/api/conference-participant-resource#create-a-participant-agent-conference-only
func (twilio *Twilio) AddConferenceParticipant(conferenceSid string, participant *ConferenceParticipantOptions) (*ConferenceParticipant, *Exception, error) {
form, err := query.Values(participant)
if err != nil {
return nil, nil, err
}

res, err := twilio.post(form, twilio.buildUrl(fmt.Sprintf("Conferences/%s/Participants.json", conferenceSid)))
if err != nil {
return nil, nil, err
}

decoder := json.NewDecoder(res.Body)

if res.StatusCode != http.StatusCreated {
exception := new(Exception)
err = decoder.Decode(exception)
return nil, exception, err
}

conf := new(ConferenceParticipant)
err = decoder.Decode(conf)
return conf, nil, err
}

// UpdateConferenceParticipant
// https://www.twilio.com/docs/voice/api/conference-participant-resource#create-a-participant-agent-conference-only
func (twilio *Twilio) UpdateConferenceParticipant(conferenceSid string, callSid string, participant *ConferenceParticipantOptions) (*ConferenceParticipant, *Exception, error) {
form, err := query.Values(participant)
if err != nil {
return nil, nil, err
}

res, err := twilio.post(form, twilio.buildUrl(fmt.Sprintf("Conferences/%s/Participants/%s.json", conferenceSid, callSid)))
if err != nil {
return nil, nil, err
}

decoder := json.NewDecoder(res.Body)

if res.StatusCode != http.StatusOK {
exception := new(Exception)
err = decoder.Decode(exception)
return nil, exception, err
}

p := new(ConferenceParticipant)
err = decoder.Decode(p)
return p, nil, err
}

// DeleteConferenceParticipant
func (twilio *Twilio) DeleteConferenceParticipant(conferenceSid string, callSid string) (*Exception, error) {
res, err := twilio.delete(twilio.buildUrl(fmt.Sprintf("Conferences/%s/Participants/%s.json", conferenceSid, callSid)))
if err != nil {
return nil, err
}

if res.StatusCode != http.StatusOK {
decoder := json.NewDecoder(res.Body)
exception := new(Exception)
err = decoder.Decode(exception)
return exception, err
}

return nil, err
}
82 changes: 82 additions & 0 deletions conference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package gotwilio

import (
"os"
"testing"

log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)

var testConferenceSid = os.Getenv("TEST_CONFERENCE_SID")
var testPhoneNumberTo = os.Getenv("TEST_PHONE_NUMBER_TO")
var testPhoneNumberFrom = os.Getenv("TEST_PHONE_NUMBER_FROM")

func TestTwilio_GetConference(t *testing.T) {
log.SetLevel(log.DebugLevel)
client := initTestTwilioClient()

res, exception, err := client.GetConference(testConferenceSid)
validateTwilioException(t, exception)
assert.NoError(t, err)
assert.NotNil(t, res)

assert.Equal(t, testConferenceSid, res.Sid)
assert.NotEmpty(t, res.FriendlyName)
}

// Test Conference functionality end to end. Real Twilio Account SID and Auth Token are required.
// A real conference call must also be active.
func TestTwilio_Conference(t *testing.T) {
log.SetLevel(log.DebugLevel)
client := initTestTwilioClient()

// cannot create a new conference in code

conf, exception, err := client.GetConference(testConferenceSid)
validateTwilioException(t, exception)
assert.NoError(t, err)
assert.NotNil(t, conf)
assert.Equal(t, testConferenceSid, conf.Sid)
assert.NotEmpty(t, conf.FriendlyName)

// add participant to call
participant, exception, err := client.AddConferenceParticipant(conf.Sid, &ConferenceParticipantOptions{
From: testPhoneNumberFrom,
To: testPhoneNumberTo,
Timeout: 15,
Record: NewBoolean(false),
Muted: NewBoolean(false),
})
validateTwilioException(t, exception)
assert.NoError(t, err)
assert.NotNil(t, participant)
assert.NotEmpty(t, participant.CallSid)

// get same participant's data
participant2, exception, err := client.GetConferenceParticipant(conf.Sid, participant.CallSid)
validateTwilioException(t, exception)
assert.NoError(t, err)
assert.NotNil(t, participant2)
assert.Equal(t, participant.CallSid, participant2.CallSid)

// update the conference
_, exception, err = client.UpdateConference(conf.Sid, &ConferenceOptions{
AnnounceURL: "https://google.com",
AnnounceMethod: "GET",
})
validateTwilioException(t, exception)
assert.NoError(t, err)

// update the participant
_, exception, err = client.UpdateConferenceParticipant(conf.Sid, participant.CallSid, &ConferenceParticipantOptions{
Muted: NewBoolean(true),
})
validateTwilioException(t, exception)
assert.NoError(t, err)

// delete the participant
exception, err = client.DeleteConferenceParticipant(conf.Sid, participant.CallSid)
validateTwilioException(t, exception)
assert.NoError(t, err)
}
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -6,8 +7,11 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -19,6 +23,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 changes: 21 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gotwilio

import (
"os"
"testing"
)

var (
testTwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID")
testTwilioAuthToken = os.Getenv("TWILIO_AUTH_TOKEN")
)

func initTestTwilioClient() *Twilio {
return NewTwilioClient(testTwilioAccountSID, testTwilioAuthToken)
}

func validateTwilioException(t *testing.T, e *Exception) {
if e != nil {
t.Errorf("twilio exception. status: %d, message: %s, code: %d, more_info: %s", e.Status, e.Message, e.Code, e.MoreInfo)
}
}
16 changes: 0 additions & 16 deletions phonenumbers_test.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
package gotwilio

import (
"os"
"testing"

"github.com/stretchr/testify/assert"

log "github.com/sirupsen/logrus"
)

var (
testTwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID")
testTwilioAuthToken = os.Getenv("TWILIO_AUTH_TOKEN")
)

func initTestTwilioClient() *Twilio {
return NewTwilioClient(testTwilioAccountSID, testTwilioAuthToken)
}

func validateTwilioException(t *testing.T, e *Exception) {
if e != nil {
t.Errorf("twilio exception. status: %d, message: %s, code: %d, more_info: %s", e.Status, e.Message, e.Code, e.MoreInfo)
}
}

func TestGetAvailablePhoneNumbers(t *testing.T) {
log.SetLevel(log.DebugLevel)

Expand Down

0 comments on commit 06f83df

Please sign in to comment.