Downloading Webex Recordings with Go


Using the Cisco Webex Meetings XML APIs

This particular requirement is how I started my Go journey. I’d taken an initial look with other languages – it looked doable but not that straight forward. Plus, I ideally wanted a command line tool that I could give to other people. Python and Node just weren’t going to cut it. I was keen to take a look at Go and thought I would see if it could meet my needs. As it turned out, it was super simple and definitely the right choice.

Firstly, I guess I should explain why I would want to do such a thing. Well, not too long ago, Webex didn’t have any mechanism for managing Webex recordings. However, more recently, they added “Recording Management” to the menu which is definitely a useful addition.

recording management

On top of that, the ability to bulk manage and archive recordings on a scheduled basis programmatically was also a requirement, in order to manage our “Recording Storage Allocation”.

To build the tool, I used the following elements:

  • Cobra : a library for creating powerful modern CLI applications
  • Webex Meetings API : API documentation for interacting with Webex.

The help file for the resulting command currently looks like this:

$ wt

WebEx Tool (wt) is a tool for IT Administrators to manage their WebEx Tenant.

Usage:
  wt [command]

Available Commands:
  delete      Delete WebEx Recordings
  download    Download WebEx Recordings
  help        Help about any command
  list        List WebEx Recordings
  user        User based commands
  version     Print the version number of wt

Flags:
  -h, --help   help for wt

Use "wt [command] --help" for more information about a command.

For the purposes of this post, I’ll focus specifically on the interaction with Webex. There are plenty of resources on using Cobra, but I’m happy to provide more information in another post if it’s something people would like.

In addition, on the Webex side, to keep the post relatively short, I’ll just concentrate on obtaining the recording itself with the “download” command. I won’t go into detail on the other API calls that are used, such as:

  • LstRecording : Allows users to see a list of all recordings within a certain date range. Site Admin users can return the recording information for all users on the site.
  • GetRecordingInfo : allows users to retrieve information about a recording.
  • DelRecording : Allows the user to delete an NBR (Network Based Recording) file.
  • GetUser : Retrieves detailed information about the specified user.
  • SetUser : Allows site administrators to update the information of an existing user, for example to unlock their account.

The API for downloading recordings is slightly separate from the other APIs mentioned above in that it falls under the NBR Web Services API. Specifically, it is the getNBRStorageFile API call outlined under the API Functions.

The Webex API for this is currently SOAP based and as such we have to pass it XML. I use the encoding/xml library to decode various responses, and strictly speaking we should maybe use that to generate the body, but since it is pretty basic, I decided to just use a formatted string:


xmlBody := fmt.Sprintf("
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
    <soapenv:Envelope 
        xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" 
        xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" 
        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">
        <soapenv:Body>
            <ns1:downloadNBRStorageFile soapenv:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:ns1=\"NBRStorageService\">
                <siteId xsi:type=\"xsd:long\">%s</siteId>
                <recordId xsi:type=\"xsd:long\">%s</recordId>
                <ticket xsi:type=\"xsd:string\">%s</ticket>
            </ns1:downloadNBRStorageFile>
        </soapenv:Body>
    </soapenv:Envelope>", siteID, recordingID, token)

The values passed into this function and used by the string formatting are:

  • siteID : This is a six digit number that you can get from the Site Information on your Webex Administration page
  • recordingID : This is obtained from listing the recordings
  • token : this is obtained using another function after prompting the user for their username and password

We then need to create a connection to Webex and send the XML body. In addition, we need to specify the Accept header as application/soap+xml, application/dime, multipart/related, text/* and an empty SOAPAction:

	
client := &http.Client{}
req, _ := http.NewRequest("POST", URL, strings.NewReader(xmlBody))
req.Header.Set("Content-Type", "application/xml; charset=utf-8")
req.Header.Set("SOAPAction", "''")
req.Header.Set("Accept", "application/soap+xml, application/dime, multipart/related, text/*")
resp, err := client.Do(req)
if err != nil {
    return "", err
}
defer resp.Body.Close()

The URL referred to is http://[data center specific domain]/nbr/services/nbrXMLService where [data center specific domain] is something you have to obtain from Cisco and would be different for different tenants.

At this point, we’ve made the request and now have a response body we can process. The response is a multipart body consisting of three parts: the XML file response; a file telling us the size and name; and the binary recording.

To process this I used the “mime” and “mime/multipart” libraries which is what makes this much easier than other languages I tried. Essentially we just loop through the three parts and when we get to the binary file, we create a new local file and write the bytes straight into it.


mediaType, params, error := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if error != nil {
    return "", error
}
if strings.HasPrefix(mediaType, "multipart/") {
    mr := multipart.NewReader(resp.Body, params["boundary"])
    embeddedfilename := ""
    filename := recording.HostWebExID + "_" + recording.RecordingID + "_"
    // Since we always get three parts:
    for i := 0; i < 3; i++ {
        p, err := mr.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return "", err
        }
        // XML File
        if i == 0 {
            //do nothing since we don't need the xml file
        }
        // File name and Size and Encryption
        if i == 1 {
            slurp, err := ioutil.ReadAll(p)
            if err != nil {
                return "", err
            }
            embeddedfilename = strings.Split(string(slurp), "\n")[0]
            filename += embeddedfilename
            fmt.Println("Downloading " + filename)
        }
        // The Recording
        if i == 2 && embeddedfilename != "" {
            fo, err := os.Create(filename)
            if err != nil {
                return "", err
            }
            defer fo.Close()
            w := bufio.NewWriter(fo)
            buf := make([]byte, 1024)
            for {
                // Reading
                n, err := p.Read(buf)
                if err != nil && err != io.EOF {
                    return "", err
                }
                if n == 0 {
                    break
                }
                // Write to file
                if _, err := w.Write(buf[:n]); err != nil {
                    return "", err
                }
            }
        }
    }

}

The output from running the list command followed by the download command is as follows:


$ wt list -u dparkinson

Enter Password: 

Host WebExID	RecordingID   Size	    CreateTime	          Name
------------	-----------   ----	    ----------	          ----
  dparkinson       66768892   460.049   05/04/2019 16:44:48	  Test Meeting

$ wt download -r 66768892  

Enter Username: dparkinson
Enter Password: 

Downloading Test Meeting.mp4

Clearly whilst looking at any of this code, bear in mind that this is the first Go code I had ever written. In this instance it was a simple function, but if I were to build this again, I’d almost certainly use methods on a type and allow the caller to pass in their own http client.

The good news is that a lot of this will get easier with the release of the new “RESTful API” which is coming soon. Apparently it will be similar to the Webex Teams API so will be more clearly documented and a whole lot simpler to use – watch this space.

comments powered by Disqus