Creating Daily rooms in Go

Lazer
Lazer Dailynista
edited September 2022 in Code Share

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: