From 75454f08b3e33f809761cc5397970cbf8b1a565f Mon Sep 17 00:00:00 2001 From: Jeff Walter Date: Sun, 10 Dec 2017 13:17:09 -0600 Subject: [PATCH] Refactoring --- .idea/vcs.xml | 6 + account.go | 145 ++++++++++++++- arlo.go | 56 ++++++ arlo_test.go | 1 + arloclient.go | 338 ----------------------------------- arloclient_test.go | 1 - const.go | 2 +- devices.go | 140 +++++++++++++-- internal/request/client.go | 131 ++++++++++++++ internal/request/request.go | 159 ---------------- internal/request/response.go | 72 ++++++++ internal/util/util.go | 34 +--- library.go | 87 ++++++++- responses.go | 23 ++- types.go | 51 +++++- 15 files changed, 690 insertions(+), 556 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 arlo.go create mode 100644 arlo_test.go delete mode 100644 arloclient.go delete mode 100644 arloclient_test.go create mode 100644 internal/request/client.go create mode 100644 internal/request/response.go diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/account.go b/account.go index 322e6bf..ff15573 100644 --- a/account.go +++ b/account.go @@ -1,4 +1,8 @@ -package arloclient +package arlo + +import ( + "github.com/pkg/errors" +) // Account is the account data. type Account struct { @@ -16,3 +20,142 @@ type Account struct { Arlo bool `json:"arlo"` DateCreated float64 `json:"dateCreated"` } + +type Friend struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Devices DeviceOrder `json:"devices"` + LastModified float64 `json:"lastModified"` + AdminUser bool `json:"adminUser"` + Email string `json:"email"` + Id string `json:"id"` +} + +func Login(user string, pass string) (*Arlo, error) { + + a := newArlo(user, pass) + + body := map[string]string{"email": a.user, "password": a.pass} + resp, err := a.client.Post(LoginUri, body, nil) + + if err != nil { + return nil, errors.WithMessage(err, "login request failed") + } + + var loginResponse LoginResponse + if err := resp.Decode(&loginResponse); err != nil { + return nil, err + } + + if loginResponse.Success { + // Cache the auth token. + a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) + + // Save the account info with the Arlo struct. + a.Account = &loginResponse.Data + + if deviceResponse, err := a.GetDevices(); err != nil { + return nil, err + } else { + if !deviceResponse.Success { + return nil, err + } + + // Cache the devices as their respective types. + a.Basestations = deviceResponse.Data.Basestations() + a.Cameras = deviceResponse.Data.Cameras() + + // Set the XCloudId header for future requests. You can override this on a per-request basis if needed. + a.client.BaseHttpHeader.Add("xCloudId", deviceResponse.Data[0].XCloudId) + } + } else { + return nil, errors.New("failed to login") + } + + return a, nil +} + +func (a *Arlo) Logout() (*Status, error) { + + resp, err := a.client.Put(LogoutUri, nil, nil) + + if err != nil { + return nil, errors.WithMessage(err, "logout request failed") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} + +// UpdateProfile takes a first and last name, and updates the user profile with that information. +func (a *Arlo) UpdateProfile(firstName, lastName string) (*Status, error) { + + body := map[string]string{"firstName": firstName, "lastName": lastName} + resp, err := a.client.Put(UserProfileUri, body, nil) + + if err != nil { + return nil, errors.WithMessage(err, "failed to update profile") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} + +func (a *Arlo) UpdatePassword(pass string) (*Status, error) { + + body := map[string]string{"currentPassword": a.pass, "newPassword": pass} + resp, err := a.client.Post(UserChangePasswordUri, body, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to update password") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + if status.Success { + a.pass = pass + } + + return &status, nil +} + +/* + This is an example of the json you would pass in the body to UpdateFriends(): +{ + "firstName":"Some", + "lastName":"Body", + "devices":{ + "XXXXXXXXXXXXX":"Camera 1", + "XXXXXXXXXXXXX":"Camera 2 ", + "XXXXXXXXXXXXX":"Camera 3" + }, + "lastModified":1463977440911, + "adminUser":true, + "email":"user@example.com", + "id":"XXX-XXXXXXX" +} +*/ +func (a *Arlo) UpdateFriends(f Friend) (*Status, error) { + + resp, err := a.client.Put(UserFriendsUri, f, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to update friends") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} diff --git a/arlo.go b/arlo.go new file mode 100644 index 0000000..099e3e9 --- /dev/null +++ b/arlo.go @@ -0,0 +1,56 @@ +package arlo + +import ( + "github.com/jeffreydwalter/arlo/internal/request" +) + +type Arlo struct { + user string + pass string + client *request.Client + Account *Account + Basestations *Basestations + Cameras *Cameras +} + +func newArlo(user string, pass string) *Arlo { + + c, _ := request.NewClient(BaseUrl) + arlo := &Arlo{ + user: user, + pass: pass, + client: c, + } + + return arlo +} + +/* +## +# This function causes the camera to record a snapshot. +# +# You can get the timezone from GetDevices(). +## +func (a *Arlo) TakeSnapshot(camera): +stream_url = self.StartStream(camera) +a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) +return stream_url; + +## +# This function causes the camera to start recording. +# +# You can get the timezone from GetDevices(). +## +func (a *Arlo) StartRecording(camera): +stream_url = self.StartStream(camera) +a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) +return stream_url + +## +# This function causes the camera to stop recording. +# +# You can get the timezone from GetDevices(). +## +func (a *Arlo) StopRecording(camera): +return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) +*/ diff --git a/arlo_test.go b/arlo_test.go new file mode 100644 index 0000000..d0aa6f2 --- /dev/null +++ b/arlo_test.go @@ -0,0 +1 @@ +package arlo diff --git a/arloclient.go b/arloclient.go deleted file mode 100644 index 2d94642..0000000 --- a/arloclient.go +++ /dev/null @@ -1,338 +0,0 @@ -package arloclient - -import ( - "time" - - "github.com/jeffreydwalter/arloclient/internal/request" - "github.com/jeffreydwalter/arloclient/internal/util" - - "github.com/pkg/errors" -) - -type Arlo struct { - user string - pass string - client *request.Client - Account *Account - Devices *Devices -} - -func newArlo(user string, pass string) *Arlo { - - c, _ := request.NewClient(BaseUrl) - arlo := &Arlo{ - user: user, - pass: pass, - client: c, - } - - return arlo -} - -func Login(user string, pass string) (*Arlo, error) { - - a := newArlo(user, pass) - - body := map[string]string{"email": a.user, "password": a.pass} - resp, err := a.client.Post(LoginUri, body, nil) - - if err != nil { - return nil, errors.WithMessage(err, "login request failed") - } - - var loginResponse LoginResponse - if err := util.Decode(resp.ParsedBody, &loginResponse); err != nil { - return nil, err - } - - if loginResponse.Success { - // Cache the auth token. - a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) - - // Save the account info with the Arlo struct. - a.Account = &loginResponse.Data - - if deviceResponse, err := a.GetDevices(); err != nil { - return nil, err - } else { - if !deviceResponse.Success { - return nil, err - } - a.Devices = &deviceResponse.Data - } - } else { - return nil, errors.New("failed to login") - } - - return a, nil -} - -func (a *Arlo) Logout() (*Status, error) { - - resp, err := a.client.Put(LogoutUri, nil, nil) - - if err != nil { - return nil, errors.WithMessage(err, "logout request failed") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil -} - -func (a *Arlo) GetDevices() (*DeviceResponse, error) { - - resp, err := a.client.Get(DevicesUri, nil) - - if err != nil { - return nil, errors.WithMessage(err, "get devices request failed") - } - - var deviceResponse DeviceResponse - if err := util.Decode(resp.ParsedBody, &deviceResponse); err != nil { - return nil, err - } - - return &deviceResponse, nil -} - -func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (*LibraryMetaDataResponse, error) { - - body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} - resp, err := a.client.Post(LibraryMetadataUri, body, nil) - - if err != nil { - return nil, errors.WithMessage(err, "failed to get library metadata") - } - - var libraryMetaDataResponse LibraryMetaDataResponse - if err := util.Decode(resp.ParsedBody, &libraryMetaDataResponse); err != nil { - return nil, err - } - - return &libraryMetaDataResponse, nil -} - -func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) { - - body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} - resp, err := a.client.Post(LibraryUri, body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to get library") - } - - var libraryResponse LibraryResponse - if err := util.Decode(resp.ParsedBody, &libraryResponse); err != nil { - return nil, err - } - - return &libraryResponse, nil -} - -func (a *Arlo) UpdateDeviceName(d Device, name string) (*Status, error) { - - body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId} - resp, err := a.client.Put(DeviceRenameUri, body, nil) - - if err != nil { - return nil, errors.WithMessage(err, "failed to update device name") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil - - return nil, errors.New("Device not found") -} - -// UpdateProfile takes a first and last name, and updates the user profile with that information. -func (a *Arlo) UpdateProfile(firstName, lastName string) (*Status, error) { - - body := map[string]string{"firstName": firstName, "lastName": lastName} - resp, err := a.client.Put(UserProfileUri, body, nil) - - if err != nil { - return nil, errors.WithMessage(err, "failed to update profile") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil -} - -func (a *Arlo) UpdatePassword(password string) (*Status, error) { - - body := map[string]string{"currentPassword": a.pass, "newPassword": password} - resp, err := a.client.Post(UserChangePasswordUri, body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to update password") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - if status.Success { - a.pass = password - } - - return &status, nil -} - -/* - This is an example of the json you would pass in the body to UpdateFriends(): -{ - "firstName":"Some", - "lastName":"Body", - "devices":{ - "XXXXXXXXXXXXX":"Camera 1", - "XXXXXXXXXXXXX":"Camera 2 ", - "XXXXXXXXXXXXX":"Camera 3" - }, - "lastModified":1463977440911, - "adminUser":true, - "email":"user@example.com", - "id":"XXX-XXXXXXX" -} -*/ -func (a *Arlo) UpdateFriends(f Friend) (*Status, error) { - - resp, err := a.client.Put(UserFriendsUri, f, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to update friends") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil -} - -func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) { - - resp, err := a.client.Post(DeviceDisplayOrderUri, d, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to update display order") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil -} - -/* -## -# Delete a single video recording from Arlo. -# -# All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call. -# -## -*/ -func (a *Arlo) DeleteRecording(r *Recording) (*Status, error) { - - body := map[string]map[string]interface{}{"data": {"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}} - resp, err := a.client.Post(LibraryRecycleUri, body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to delete recording") - } - - var status Status - if err := util.Decode(resp.ParsedBody, &status); err != nil { - return nil, err - } - - return &status, nil -} - -/* -## -# Delete a batch of video recordings from Arlo. -# -# The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for. -# If you want to delete some other batch of videos, then you need to send an array of objects representing each video you want to delete. -# -#[ -# { -# "createdDate":"20160904", -# "utcCreatedDate":1473010280395, -# "deviceId":"XXXXXXXXXXXXX" -# }, -# { -# "createdDate":"20160904", -# "utcCreatedDate":1473010280395, -# "deviceId":"XXXXXXXXXXXXX" -# } -#] -## -func (a *Arlo) BatchDeleteRecordings(recording_metadata): -return a.client.Post('https://arlo.netgear.com/hmsweb/users/library/recycle', {'data':recording_metadata}) - -## -# Returns the whole video from the presignedContentUrl. -# -# Obviously, this function is generic and could be used to download anything. :) -## -func (a *Arlo) GetRecording(url, chunk_size=4096): -video = '' -r = requests.get(url, stream=True) -r.raise_for_status() - -for chunk in r.iter_content(chunk_size): -if chunk: video += chunk -return video - - -## -# This function returns a json object containing the rtmps url to the requested video stream. -# You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp -# -# The request to /users/devices/startStream returns: -#{ "url":"rtmps://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } -# -## -func (a *Arlo) StartStream(camera): -return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')}) - -## -# This function causes the camera to record a snapshot. -# -# You can get the timezone from GetDevices(). -## -func (a *Arlo) TakeSnapshot(camera): -stream_url = self.StartStream(camera) -a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) -return stream_url; - -## -# This function causes the camera to start recording. -# -# You can get the timezone from GetDevices(). -## -func (a *Arlo) StartRecording(camera): -stream_url = self.StartStream(camera) -a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) -return stream_url - -## -# This function causes the camera to stop recording. -# -# You can get the timezone from GetDevices(). -## -func (a *Arlo) StopRecording(camera): -return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) -*/ diff --git a/arloclient_test.go b/arloclient_test.go deleted file mode 100644 index fac0d50..0000000 --- a/arloclient_test.go +++ /dev/null @@ -1 +0,0 @@ -package arloclient diff --git a/const.go b/const.go index 26ffff9..194e6be 100644 --- a/const.go +++ b/const.go @@ -1,4 +1,4 @@ -package arloclient +package arlo const ( BaseUrl = "https://arlo.netgear.com/hmsweb" diff --git a/devices.go b/devices.go index d017299..90d23b3 100644 --- a/devices.go +++ b/devices.go @@ -1,6 +1,10 @@ -package arloclient +package arlo -// Device is the device data. +import ( + "github.com/pkg/errors" +) + +// A Device is the device data, this can be a camera, basestation, arloq, etc. type Device struct { DeviceType string `json:"deviceType"` XCloudId string `json:"xCloudId"` @@ -26,7 +30,22 @@ type Device struct { // Devices is an array of Device objects. type Devices []Device -// DeviceOrder is a hash of # XXXXXXXXXXXXX is the device id of each camera. You can get this from GetDevices(). +// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.). +// This type is here just for semantics. Some methods explicitly require a device of a certain type. +type Basestation Device + +// Basestations is an array of Basestation objects. +type Basestations []Basestation + +// A Camera is a Device of type "camera". +// This type is here just for semantics. Some methods explicitly require a device of a certain type. +type Camera Device + +// Cameras is an array of Camera objects. +type Cameras []Camera + +// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order. +// Device order is mainly used by the UI to determine which order to show the devices. /* { "devices":{ @@ -36,9 +55,10 @@ type Devices []Device } */ type DeviceOrder struct { - Devices map[string]int + Devices map[string]int `json:"devices"` } +// Find returns a device with the device id passed in. func (ds *Devices) Find(deviceId string) *Device { for _, d := range *ds { if d.DeviceId == deviceId { @@ -49,22 +69,120 @@ func (ds *Devices) Find(deviceId string) *Device { return nil } -func (ds *Devices) BaseStations() *Devices { - var basestations Devices +// Basestations returns a Basestations object containing all devices that are NOT type "camera". +// I did this because some device types, like arloq, don't have a basestation. +// So, when interacting with them you must treat them like a basestation and a camera. +// Cameras also includes decvices of this type, so you can get the same data there or cast. +func (ds *Devices) Basestations() *Basestations { + var basestations Basestations for _, d := range *ds { - if d.DeviceType == "basestation" { - basestations = append(basestations, d) + if d.DeviceType != "camera" { + basestations = append(basestations, Basestation(d)) } } return &basestations } -func (ds *Devices) Cameras() *Devices { - var cameras Devices +// Cameras returns a Cameras object containing all devices that are of type "camera". +// I did this because some device types, like arloq, don't have a basestation. +// So, when interacting with them you must treat them like a basestation and a camera. +// Basestations also includes decvices of this type, so you can get the same data there or cast. +func (ds *Devices) Cameras() *Cameras { + var cameras Cameras for _, d := range *ds { if d.DeviceType != "basestation" { - cameras = append(cameras, d) + cameras = append(cameras, Camera(d)) } } return &cameras } + +// GetDevices returns an array of all devices. +// When you call Login, this method is called and all devices are cached in the Arlo object. +func (a *Arlo) GetDevices() (*DeviceResponse, error) { + + resp, err := a.client.Get(DevicesUri, nil) + + if err != nil { + return nil, errors.WithMessage(err, "failed to get devices") + } + + var deviceResponse DeviceResponse + if err := resp.Decode(&deviceResponse); err != nil { + return nil, err + } + + if len(deviceResponse.Data) == 0 { + return nil, errors.New("no devices found") + } + + return &deviceResponse, nil +} + +// UpdateDeviceName sets the name of the given device to the name argument. +func (a *Arlo) UpdateDeviceName(d Device, name string) (*Status, error) { + + body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId} + resp, err := a.client.Put(DeviceRenameUri, body, nil) + + if err != nil { + return nil, errors.WithMessage(err, "failed to update device name") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil + + return nil, errors.New("device not found") +} + +// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given. +func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) { + + resp, err := a.client.Post(DeviceDisplayOrderUri, d, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to update display order") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} + +// StartStream returns a json object containing the rtmps url to the requested video stream. +// You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp +// +// The request to /users/devices/startStream returns: +// NOTE: { "url":"rtmps://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } +func (a *Arlo) StartStream(c Camera) (*StartStreamResponse, error) { + + var n Notification + n.To = c.ParentId + n.From = c.UserId + n.Resource = "cameras/" + c.DeviceId + n.Action = "set" + n.PublishResponse = true + n.TransId = "" + n.Properties.ActivityState = "startUserStream" + n.Properties.CameraId = c.DeviceId + + // {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')} + resp, err := a.client.Post(DeviceStartStreamUri, n, nil) + + if err != nil { + return nil, errors.WithMessage(err, "failed to start stream") + } + + var startStreamResponse StartStreamResponse + if err := resp.Decode(&startStreamResponse); err != nil { + return nil, err + } + + return &startStreamResponse, nil +} diff --git a/internal/request/client.go b/internal/request/client.go new file mode 100644 index 0000000..d20e184 --- /dev/null +++ b/internal/request/client.go @@ -0,0 +1,131 @@ +package request + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + + "github.com/pkg/errors" +) + +type Client struct { + BaseURL *url.URL + BaseHttpHeader http.Header + httpClient http.Client +} + +func NewClient(baseurl string) (*Client, error) { + var err error + var jar *cookiejar.Jar + + options := cookiejar.Options{} + + if jar, err = cookiejar.New(&options); err != nil { + return nil, errors.Wrap(err, "failed to create client object") + } + + var u *url.URL + if u, err = url.Parse(baseurl); err != nil { + return nil, errors.Wrap(err, "failed to create client object") + } + + header := make(http.Header) + header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36") + header.Add("Content-Type", "application/json") + header.Add("Accept", "application/json") + + return &Client{ + BaseURL: u, + BaseHttpHeader: header, + httpClient: http.Client{Jar: jar}, + }, nil +} + +func (c *Client) Get(uri string, header http.Header) (*Response, error) { + req, err := c.newRequest("GET", uri, nil, header) + if err != nil { + return nil, errors.WithMessage(err, "get request "+uri+" failed") + } + return c.do(req) +} + +func (c *Client) Post(uri string, body interface{}, header http.Header) (*Response, error) { + req, err := c.newRequest("POST", uri, body, header) + if err != nil { + return nil, errors.WithMessage(err, "post request "+uri+" failed") + } + return c.do(req) +} + +func (c *Client) Put(uri string, body interface{}, header http.Header) (*Response, error) { + req, err := c.newRequest("PUT", uri, body, header) + if err != nil { + return nil, errors.WithMessage(err, "put request "+uri+" failed") + } + return c.do(req) +} + +func (c *Client) newRequest(method string, uri string, body interface{}, header http.Header) (*Request, error) { + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, errors.Wrap(err, "failed to create request object") + } + } + log.Printf("JSON: %v", buf) + u := c.BaseURL.String() + uri + req, err := http.NewRequest(method, u, buf) + if err != nil { + return nil, errors.Wrap(err, "failed to create request object") + } + + for k, v := range c.BaseHttpHeader { + for _, h := range v { + //fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) + req.Header.Add(k, h) + } + } + + for k, v := range header { + for _, h := range v { + //fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) + req.Header.Add(k, h) + } + } + + return &Request{ + Request: *req, + }, nil +} + +func (c *Client) newResponse(resp *http.Response) (*Response, error) { + + return &Response{ + Response: *resp, + }, nil +} + +func (c *Client) do(req *Request) (*Response, error) { + + //fmt.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.httpClient.Jar.Cookies(req.URL)) + //fmt.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header) + + resp, err := c.httpClient.Do(&req.Request) + if err != nil { + return nil, errors.Wrap(err, "failed to execute http request") + } + + if resp.StatusCode >= 400 { + defer resp.Body.Close() + return nil, errors.New("http request failed with status: " + resp.Status) + } + + return c.newResponse(resp) +} diff --git a/internal/request/request.go b/internal/request/request.go index 4000d18..ec5ebc5 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -1,168 +1,9 @@ package request import ( - "bytes" - "encoding/json" - "io" - "io/ioutil" - "log" - "mime" "net/http" - "net/http/cookiejar" - "net/url" - - "github.com/pkg/errors" ) -type Client struct { - BaseURL *url.URL - BaseHttpHeader http.Header - httpClient http.Client -} - type Request struct { http.Request } -type Response struct { - http.Response - ParsedBody interface{} -} - -func NewClient(baseurl string) (*Client, error) { - var err error - var jar *cookiejar.Jar - - options := cookiejar.Options{} - - if jar, err = cookiejar.New(&options); err != nil { - return nil, errors.Wrap(err, "failed to create client object") - } - - var u *url.URL - if u, err = url.Parse(baseurl); err != nil { - return nil, errors.Wrap(err, "failed to create client object") - } - - header := make(http.Header) - header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36") - header.Add("Content-Type", "application/json") - header.Add("Accept", "application/json") - - return &Client{ - BaseURL: u, - BaseHttpHeader: header, - httpClient: http.Client{Jar: jar}, - }, nil -} - -func (c *Client) Get(uri string, header http.Header) (*Response, error) { - req, err := c.newRequest("GET", uri, nil, header) - if err != nil { - return nil, errors.WithMessage(err, "get request "+uri+" failed") - } - return c.do(req) -} - -func (c *Client) Post(uri string, body interface{}, header http.Header) (*Response, error) { - req, err := c.newRequest("POST", uri, body, header) - if err != nil { - return nil, errors.WithMessage(err, "post request "+uri+" failed") - } - return c.do(req) -} - -func (c *Client) Put(uri string, body interface{}, header http.Header) (*Response, error) { - req, err := c.newRequest("PUT", uri, body, header) - if err != nil { - return nil, errors.WithMessage(err, "put request "+uri+" failed") - } - return c.do(req) -} - -func GetContentType(ct string) (string, error) { - mediaType, _, err := mime.ParseMediaType(ct) - - if err != nil { - return "", errors.Wrap(err, "failed to get content type") - } - return mediaType, nil -} - -func (c *Client) newRequest(method string, uri string, body interface{}, header http.Header) (*Request, error) { - - var buf io.ReadWriter - if body != nil { - buf = new(bytes.Buffer) - err := json.NewEncoder(buf).Encode(body) - if err != nil { - return nil, errors.Wrap(err, "failed to create request object") - } - } - log.Printf("JSON: %v", buf) - u := c.BaseURL.String() + uri - req, err := http.NewRequest(method, u, buf) - if err != nil { - return nil, errors.Wrap(err, "failed to create request object") - } - - for k, v := range c.BaseHttpHeader { - for _, h := range v { - //fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) - req.Header.Add(k, h) - } - } - - for k, v := range header { - for _, h := range v { - //fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) - req.Header.Add(k, h) - } - } - - return &Request{ - Request: *req, - }, nil -} - -func (c *Client) newResponse(resp *http.Response) (*Response, error) { - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - // log.Printf("DATA: %v", string(data)) - - mediaType, err := GetContentType(resp.Header.Get("Content-Type")) - if err != nil { - return nil, errors.WithMessage(err, "failed to create response object") - } - - var pb interface{} - switch mediaType { - case "application/json": - err = json.Unmarshal([]byte(body), &pb) - if err != nil { - return nil, errors.Wrap(err, "failed to create response object") - } - } - - return &Response{ - Response: *resp, - ParsedBody: pb, - }, nil -} - -func (c *Client) do(req *Request) (*Response, error) { - - //fmt.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.httpClient.Jar.Cookies(req.URL)) - //fmt.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header) - - resp, err := c.httpClient.Do(&req.Request) - if err != nil { - return nil, errors.Wrap(err, "failed to execute http request") - } - defer resp.Body.Close() - - return c.newResponse(resp) -} diff --git a/internal/request/response.go b/internal/request/response.go new file mode 100644 index 0000000..7ed3c25 --- /dev/null +++ b/internal/request/response.go @@ -0,0 +1,72 @@ +package request + +import ( + "encoding/json" + "io" + "log" + "mime" + "net/http" + "os" + "reflect" + + "github.com/pkg/errors" +) + +type Response struct { + http.Response +} + +func (resp *Response) GetContentType() (string, error) { + + mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + + if err != nil { + return "", errors.Wrap(err, "failed to get content type") + } + return mediaType, nil +} + +func (resp *Response) Decode(s interface{}) error { + + defer resp.Body.Close() + + mediaType, err := resp.GetContentType() + if err != nil { + return errors.WithMessage(err, "failed to decode response body") + } + + switch mediaType { + case "application/json": + err := json.NewDecoder(resp.Body).Decode(&s) + if err != nil { + return errors.Wrap(err, "failed to create "+reflect.TypeOf(s).String()+" object") + } + default: + return errors.New("unsupported content type: " + mediaType) + } + return nil +} + +func (resp *Response) Download(to string) (error, int64) { + + defer resp.Body.Close() + + // Create output file + newFile, err := os.Create(to) + if err != nil { + log.Fatal(err) + } + defer newFile.Close() + + // Write bytes from HTTP response to file. + // response.Body satisfies the reader interface. + // newFile satisfies the writer interface. + // That allows us to use io.Copy which accepts + // any type that implements reader and writer interface + bytesWritten, err := io.Copy(newFile, resp.Body) + if err != nil { + log.Fatal(err) + } + + return nil, bytesWritten +} diff --git a/internal/util/util.go b/internal/util/util.go index ab4d699..8b8b386 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -3,10 +3,6 @@ package util import ( "encoding/json" "fmt" - "reflect" - - "github.com/mitchellh/mapstructure" - "github.com/pkg/errors" ) func PrettyPrint(data interface{}) string { @@ -17,27 +13,13 @@ func PrettyPrint(data interface{}) string { return fmt.Sprint(string(j)) } -/* -type Timestamp time.Time +const TransIdPrefix = "web" -func (t *Timestamp) MarshalJSON() ([]byte, error) { - ts := time.Time(*t).Unix() - stamp := fmt.Sprint(ts) - return []byte(stamp), nil -} -func (t *Timestamp) UnmarshalJSON(b []byte) error { - ts, err := strconv.Atoi(string(b)) - if err != nil { - return err - } - *t = Timestamp(time.Unix(int64(ts), 0)) - return nil -} -*/ - -func Decode(b interface{}, s interface{}) error { - if err := mapstructure.Decode(b, s); err != nil { - return errors.Wrap(err, "failed to create "+reflect.TypeOf(s).String()+" object") - } - return nil +func GenTransId(transType string) { + /* + func divmod(numerator, denominator int64) (quotient, remainder int64) { + quotient = numerator / denominator // integer division, decimals are truncated + remainder = numerator % denominator + return + }*/ } diff --git a/library.go b/library.go index 5cfa18f..2e4d099 100644 --- a/library.go +++ b/library.go @@ -1,4 +1,10 @@ -package arloclient +package arlo + +import ( + "time" + + "github.com/pkg/errors" +) // LibraryMetaData is the library meta data. type LibraryMetaData struct { @@ -29,3 +35,82 @@ type Recording struct { } type Library []Recording + +func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (*LibraryMetaDataResponse, error) { + + body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} + resp, err := a.client.Post(LibraryMetadataUri, body, nil) + + if err != nil { + return nil, errors.WithMessage(err, "failed to get library metadata") + } + + var libraryMetaDataResponse LibraryMetaDataResponse + if err := resp.Decode(&libraryMetaDataResponse); err != nil { + return nil, err + } + + return &libraryMetaDataResponse, nil +} + +func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) { + + body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} + resp, err := a.client.Post(LibraryUri, body, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to get library") + } + + var libraryResponse LibraryResponse + if err := resp.Decode(&libraryResponse); err != nil { + return nil, err + } + + return &libraryResponse, nil +} + +/* + Delete a single video recording from Arlo. + + All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call. + + NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. +*/ +func (a *Arlo) DeleteRecording(r Recording) (*Status, error) { + + body := map[string]Library{"data": {r}} + resp, err := a.client.Post(LibraryRecycleUri, body, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to delete recording") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} + +/* + Delete a batch of video recordings from Arlo. + + The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for. + + NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. +*/ +func (a *Arlo) BatchDeleteRecordings(l Library) (*Status, error) { + + body := map[string]Library{"data": l} + resp, err := a.client.Post(LibraryRecycleUri, body, nil) + if err != nil { + return nil, errors.WithMessage(err, "failed to delete recordings") + } + + var status Status + if err := resp.Decode(&status); err != nil { + return nil, err + } + + return &status, nil +} diff --git a/responses.go b/responses.go index 0365cde..1804346 100644 --- a/responses.go +++ b/responses.go @@ -1,4 +1,4 @@ -package arloclient +package arlo // UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call. type Status struct { @@ -7,23 +7,28 @@ type Status struct { // LoginResponse is an intermediate struct used when parsing data from the Login() call. type LoginResponse struct { - Data Account - Success bool `json:"success"` + Data Account + *Status } // DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call. type DeviceResponse struct { - Data Devices - Success bool `json:"success"` + Data Devices + *Status } // LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call. type LibraryMetaDataResponse struct { - Data LibraryMetaData - Success bool `json:"success"` + Data LibraryMetaData + *Status } type LibraryResponse struct { - Data Library - Success bool `json:"success"` + Data Library + *Status +} + +type StartStreamResponse struct { + Data StreamUrl + *Status } diff --git a/types.go b/types.go index 51f309c..faccafb 100644 --- a/types.go +++ b/types.go @@ -1,4 +1,4 @@ -package arloclient +package arlo /* // Credentials is the login credential data. @@ -45,12 +45,45 @@ type Favorite struct { Favorite uint8 `json:"Favorite"` } -type Friend struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Devices DeviceOrder `json:"devices"` - LastModified float64 `json:"lastModified"` - AdminUser bool `json:"adminUser"` - Email string `json:"email"` - Id string `json:"id"` +/* +type Device struct { + DeviceType string `json:"deviceType"` + XCloudId string `json:"xCloudId"` + DisplayOrder uint8 `json:"displayOrder"` + State string `json:"state"` + ModelId string `json:"modelId"` + InterfaceVersion string `json:"interfaceVersion"` + ParentId string `json:"parentId"` + UserId string `json:"userId"` + DeviceName string `json:"deviceName"` + FirmwareVersion string `json:"firmwareVersion"` + MediaObjectCount uint8 `json:"mediaObjectCount"` + DateCreated float64 `json:"dateCreated"` + Owner Owner `json:"owner"` + Properties Properties `json:"properties"` + UniqueId string `json:"uniqueId"` + LastModified float64 `json:"lastModified"` + UserRole string `json:"userRole"` + InterfaceSchemaVer string `json:"interfaceSchemaVer"` + DeviceId string `json:"deviceId"` +} +*/ + +type StreamUrl struct { + Url string `json:"url"` +} + +type NotificationProperties struct { + ActivityState string `json:"activityState"` + CameraId string `json:"cameraId"` +} + +type Notification struct { + To string `json:"to"` + From string `json:"from"` + Resource string `json:"resource"` + Action string `json:"action"` + PublishResponse bool `json:"publishResourcec"` + TransId string `json:"transId"` + Properties NotificationProperties `json:"properties"` }