Creating Daily rooms in Go

Lazer
Dailynista
Here is a quick snippet I threw together to create Daily rooms in Go. This inspired me to make a little library to interact with our REST API in general so there's likely more to come, but in the meantime here's some very basic room-creation logic. Let me know if you run into any problems!
package daily import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "time" ) const ( dailyURL = "https://api.daily.co/v1/" ) var ( // ErrInvalidTokenExpiry is returned when the caller attempts to create // a meeting token without a valid expiry time. ErrInvalidTokenExpiry = errors.New("expiry cannot be empty or in the past") // ErrInvalidAPIKey is returned when the caller attempts to provide // an invalid Daily API key. ErrInvalidAPIKey = errors.New("Daily API key is invalid") ) // Daily communicates with Daily's REST API type Daily struct { apiKey string apiURL string } // Room represents a Daily room type Room struct { ID string `json:"id"` Name string `json:"name"` Url string `json:"url"` CreatedAt time.Time `json:"created_at"` Config RoomProps `json:"config"` } type MeetingToken struct { Token string `json:"token"` } // RoomProps represents properties the user can set // when creating or updating a room // (this is an example object; in reality Daily supports // many more properties) type RoomProps struct { // Exp should be a Unix timestamp, but we'll provide // some helper methods to let caller work with time.Time // as well Exp int64 `json:"exp,omitempty"` MaxParticipants int `json:"max_participants,omitempty"` } func (p *RoomProps) SetExpiry(expiry time.Time) { p.Exp = expiry.Unix() } func (p *RoomProps) GetExpiry() time.Time { return time.Unix(p.Exp, 0) } type createRoomBody struct { Name string `json:"name,omitempty"` Privacy string `json:"privacy,omitempty"` Properties map[string]interface{} `json:"properties,omitempty"` } // NewDaily returns a new instance of Daily func NewDaily(apiKey string) (*Daily, error) { // Check that user passed in what at least COULD be a valid // API key. In a prod implementation you probably want to // have additional validity checks here. if apiKey == "" { return nil, ErrInvalidAPIKey } return &Daily{ apiKey: apiKey, // This is set on the struct instead of just reusing the // const to enable overriding for unit tests. apiURL: dailyURL, }, nil } // CreateRoom creates a Daily room using Daily's REST API func (d *Daily) CreateRoom(name string, isPrivate bool, props RoomProps, additionalProps map[string]interface{}) (*Room, error) { // Make the request body for room creation reqBody, err := d.makeCreateRoomBody(name, isPrivate, props, additionalProps) if err != nil { return nil, fmt.Errorf("failed to make room creation request body: %w", err) } // Make the actual HTTP request req, err := http.NewRequest("POST", d.roomsEndpoint(), reqBody) if err != nil { return nil, fmt.Errorf("failed to create POST request to rooms endpoint: %w", err) } // Prepare auth and content-type headers for request d.prepHeaders(req) // Do the thing!!! res, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to create room: %w", err) } // Parse the response resBody, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to create room with status code %d %s", res.StatusCode, resBody) } var room Room if err := json.Unmarshal(resBody, &room); err != nil { return nil, fmt.Errorf("failed to unmarshal body into Room: %w", err) } return &room, nil } func (d *Daily) makeCreateRoomBody(name string, isPrivate bool, props RoomProps, additionalProps map[string]interface{}) (*bytes.Buffer, error) { // Concatenate original and additional properties into a JSON blob propsData, err := d.concatRoomProperties(props, additionalProps) if err != nil { return nil, fmt.Errorf("failed to build room props JSON: %w", err) } // Prep request body reqBody := createRoomBody{ Name: name, Properties: propsData, } // Rooms are public by default if isPrivate { reqBody.Privacy = "private" } bodyBlob, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } return bytes.NewBuffer(bodyBlob), nil } func (d *Daily) concatRoomProperties(props RoomProps, additionalProps map[string]interface{}) (map[string]interface{}, error) { data, err := json.Marshal(&props) if err != nil { return nil, fmt.Errorf("failed to marshal room props: %w", err) } // Unmarshal all the original props into a map for us to work with var mProps map[string]interface{} if err := json.Unmarshal(data, &mProps); err != nil { return nil, fmt.Errorf("failed to unmarshal props: %w", err) } // Add additional props to prop map, but only if given key // does not already exist in original props. for k, v := range additionalProps { if _, ok := mProps[k]; ok { // This key already exists, skip it continue } mProps[k] = v } return mProps, nil } func (d *Daily) roomsEndpoint() string { return fmt.Sprintf("%srooms", d.apiURL) } func (d *Daily) meetingTokensEndpoint() string { return fmt.Sprintf("%smeeting-tokens", d.apiURL) } func (d *Daily) prepHeaders(req *http.Request) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiKey)) } package daily import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "time" ) const ( dailyURL = "https://api.daily.co/v1/" ) var ( // ErrInvalidTokenExpiry is returned when the caller attempts to create // a meeting token without a valid expiry time. ErrInvalidTokenExpiry = errors.New("expiry cannot be empty or in the past") // ErrInvalidAPIKey is returned when the caller attempts to provide // an invalid Daily API key. ErrInvalidAPIKey = errors.New("Daily API key is invalid") ) // Daily communicates with Daily's REST API type Daily struct { apiKey string apiURL string } // Room represents a Daily room type Room struct { ID string `json:"id"` Name string `json:"name"` Url string `json:"url"` CreatedAt time.Time `json:"created_at"` Config RoomProps `json:"config"` } type MeetingToken struct { Token string `json:"token"` } // RoomProps represents properties the user can set // when creating or updating a room // (this is an example object; in reality Daily supports // many more properties) type RoomProps struct { // Exp should be a Unix timestamp, but we'll provide // some helper methods to let caller work with time.Time // as well Exp int64 `json:"exp,omitempty"` MaxParticipants int `json:"max_participants,omitempty"` } func (p *RoomProps) SetExpiry(expiry time.Time) { p.Exp = expiry.Unix() } func (p *RoomProps) GetExpiry() time.Time { return time.Unix(p.Exp, 0) } type createRoomBody struct { Name string `json:"name,omitempty"` Privacy string `json:"privacy,omitempty"` Properties map[string]interface{} `json:"properties,omitempty"` } // NewDaily returns a new instance of Daily func NewDaily(apiKey string) (*Daily, error) { // Check that user passed in what at least COULD be a valid // API key. In a prod implementation you probably want to // have additional validity checks here. if apiKey == "" { return nil, ErrInvalidAPIKey } return &Daily{ apiKey: apiKey, // This is set on the struct instead of just reusing the // const to enable overriding for unit tests. apiURL: dailyURL, }, nil } // CreateRoom creates a Daily room using Daily's REST API func (d *Daily) CreateRoom(name string, isPrivate bool, props RoomProps, additionalProps map[string]interface{}) (*Room, error) { // Make the request body for room creation reqBody, err := d.makeCreateRoomBody(name, isPrivate, props, additionalProps) if err != nil { return nil, fmt.Errorf("failed to make room creation request body: %w", err) } // Make the actual HTTP request req, err := http.NewRequest("POST", d.roomsEndpoint(), reqBody) if err != nil { return nil, fmt.Errorf("failed to create POST request to rooms endpoint: %w", err) } // Prepare auth and content-type headers for request d.prepHeaders(req) // Do the thing!!! res, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to create room: %w", err) } // Parse the response resBody, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to create room with status code %d %s", res.StatusCode, resBody) } var room Room if err := json.Unmarshal(resBody, &room); err != nil { return nil, fmt.Errorf("failed to unmarshal body into Room: %w", err) } return &room, nil } func (d *Daily) makeCreateRoomBody(name string, isPrivate bool, props RoomProps, additionalProps map[string]interface{}) (*bytes.Buffer, error) { // Concatenate original and additional properties into a JSON blob propsData, err := d.concatRoomProperties(props, additionalProps) if err != nil { return nil, fmt.Errorf("failed to build room props JSON: %w", err) } // Prep request body reqBody := createRoomBody{ Name: name, Properties: propsData, } // Rooms are public by default if isPrivate { reqBody.Privacy = "private" } bodyBlob, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } return bytes.NewBuffer(bodyBlob), nil } func (d *Daily) concatRoomProperties(props RoomProps, additionalProps map[string]interface{}) (map[string]interface{}, error) { data, err := json.Marshal(&props) if err != nil { return nil, fmt.Errorf("failed to marshal room props: %w", err) } // Unmarshal all the original props into a map for us to work with var mProps map[string]interface{} if err := json.Unmarshal(data, &mProps); err != nil { return nil, fmt.Errorf("failed to unmarshal props: %w", err) } // Add additional props to prop map, but only if given key // does not already exist in original props. for k, v := range additionalProps { if _, ok := mProps[k]; ok { // This key already exists, skip it continue } mProps[k] = v } return mProps, nil } func (d *Daily) roomsEndpoint() string { return fmt.Sprintf("%srooms", d.apiURL) } func (d *Daily) meetingTokensEndpoint() string { return fmt.Sprintf("%smeeting-tokens", d.apiURL) } func (d *Daily) prepHeaders(req *http.Request) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiKey)) }
Tagged:
3