mirror of
				https://github.com/AlexxIT/go2rtc.git
				synced 2025-10-31 11:46:26 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			546 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			546 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package ring
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/sha256"
 | |
| 	"encoding/base64"
 | |
| 	"encoding/hex"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| type RefreshTokenAuth struct {
 | |
| 	RefreshToken string
 | |
| }
 | |
| 
 | |
| type EmailAuth struct {
 | |
| 	Email    string
 | |
| 	Password string
 | |
| }
 | |
| 
 | |
| // AuthConfig represents the decoded refresh token data
 | |
| type AuthConfig struct {
 | |
| 	RT  string `json:"rt"`  // Refresh Token
 | |
| 	HID string `json:"hid"` // Hardware ID
 | |
| }
 | |
| 
 | |
| // AuthTokenResponse represents the response from the authentication endpoint
 | |
| type AuthTokenResponse struct {
 | |
| 	AccessToken  string `json:"access_token"`
 | |
| 	ExpiresIn    int    `json:"expires_in"`
 | |
| 	RefreshToken string `json:"refresh_token"`
 | |
| 	Scope        string `json:"scope"`      // Always "client"
 | |
| 	TokenType    string `json:"token_type"` // Always "Bearer"
 | |
| }
 | |
| 
 | |
| type Auth2faResponse struct {
 | |
| 	Error            string `json:"error"`
 | |
| 	ErrorDescription string `json:"error_description"`
 | |
| 	TSVState         string `json:"tsv_state"`
 | |
| 	Phone            string `json:"phone"`
 | |
| 	NextTimeInSecs   int    `json:"next_time_in_secs"`
 | |
| }
 | |
| 
 | |
| // SocketTicketRequest represents the request to get a socket ticket
 | |
| type SocketTicketResponse struct {
 | |
| 	Ticket            string `json:"ticket"`
 | |
| 	ResponseTimestamp int64  `json:"response_timestamp"`
 | |
| }
 | |
| 
 | |
| // RingRestClient handles authentication and requests to Ring API
 | |
| type RingRestClient struct {
 | |
| 	httpClient     *http.Client
 | |
| 	authConfig     *AuthConfig
 | |
| 	hardwareID     string
 | |
| 	authToken      *AuthTokenResponse
 | |
| 	Using2FA       bool
 | |
| 	PromptFor2FA   string
 | |
| 	RefreshToken   string
 | |
| 	auth           interface{} // EmailAuth or RefreshTokenAuth
 | |
| 	onTokenRefresh func(string)
 | |
| }
 | |
| 
 | |
| // CameraKind represents the different types of Ring cameras
 | |
| type CameraKind string
 | |
| 
 | |
| // CameraData contains common fields for all camera types
 | |
| type CameraData struct {
 | |
| 	ID          float64 `json:"id"`
 | |
| 	Description string  `json:"description"`
 | |
| 	DeviceID    string  `json:"device_id"`
 | |
| 	Kind        string  `json:"kind"`
 | |
| 	LocationID  string  `json:"location_id"`
 | |
| }
 | |
| 
 | |
| // RingDeviceType represents different types of Ring devices
 | |
| type RingDeviceType string
 | |
| 
 | |
| // RingDevicesResponse represents the response from the Ring API
 | |
| type RingDevicesResponse struct {
 | |
| 	Doorbots           []CameraData             `json:"doorbots"`
 | |
| 	AuthorizedDoorbots []CameraData             `json:"authorized_doorbots"`
 | |
| 	StickupCams        []CameraData             `json:"stickup_cams"`
 | |
| 	AllCameras         []CameraData             `json:"all_cameras"`
 | |
| 	Chimes             []CameraData             `json:"chimes"`
 | |
| 	Other              []map[string]interface{} `json:"other"`
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	Doorbot             CameraKind = "doorbot"
 | |
| 	Doorbell            CameraKind = "doorbell"
 | |
| 	DoorbellV3          CameraKind = "doorbell_v3"
 | |
| 	DoorbellV4          CameraKind = "doorbell_v4"
 | |
| 	DoorbellV5          CameraKind = "doorbell_v5"
 | |
| 	DoorbellOyster      CameraKind = "doorbell_oyster"
 | |
| 	DoorbellPortal      CameraKind = "doorbell_portal"
 | |
| 	DoorbellScallop     CameraKind = "doorbell_scallop"
 | |
| 	DoorbellScallopLite CameraKind = "doorbell_scallop_lite"
 | |
| 	DoorbellGraham      CameraKind = "doorbell_graham_cracker"
 | |
| 	LpdV1               CameraKind = "lpd_v1"
 | |
| 	LpdV2               CameraKind = "lpd_v2"
 | |
| 	LpdV4               CameraKind = "lpd_v4"
 | |
| 	JboxV1              CameraKind = "jbox_v1"
 | |
| 	StickupCam          CameraKind = "stickup_cam"
 | |
| 	StickupCamV3        CameraKind = "stickup_cam_v3"
 | |
| 	StickupCamElite     CameraKind = "stickup_cam_elite"
 | |
| 	StickupCamLongfin   CameraKind = "stickup_cam_longfin"
 | |
| 	StickupCamLunar     CameraKind = "stickup_cam_lunar"
 | |
| 	SpotlightV2         CameraKind = "spotlightw_v2"
 | |
| 	HpCamV1             CameraKind = "hp_cam_v1"
 | |
| 	HpCamV2             CameraKind = "hp_cam_v2"
 | |
| 	StickupCamV4        CameraKind = "stickup_cam_v4"
 | |
| 	FloodlightV1        CameraKind = "floodlight_v1"
 | |
| 	FloodlightV2        CameraKind = "floodlight_v2"
 | |
| 	FloodlightPro       CameraKind = "floodlight_pro"
 | |
| 	CocoaCamera         CameraKind = "cocoa_camera"
 | |
| 	CocoaDoorbell       CameraKind = "cocoa_doorbell"
 | |
| 	CocoaFloodlight     CameraKind = "cocoa_floodlight"
 | |
| 	CocoaSpotlight      CameraKind = "cocoa_spotlight"
 | |
| 	StickupCamMini      CameraKind = "stickup_cam_mini"
 | |
| 	OnvifCamera         CameraKind = "onvif_camera"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	IntercomHandsetAudio RingDeviceType = "intercom_handset_audio"
 | |
| 	OnvifCameraType      RingDeviceType = "onvif_camera"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	clientAPIBaseURL   = "https://api.ring.com/clients_api/"
 | |
| 	deviceAPIBaseURL   = "https://api.ring.com/devices/v1/"
 | |
| 	commandsAPIBaseURL = "https://api.ring.com/commands/v1/"
 | |
| 	appAPIBaseURL      = "https://prd-api-us.prd.rings.solutions/api/v1/"
 | |
| 	oauthURL           = "https://oauth.ring.com/oauth/token"
 | |
| 	apiVersion         = 11
 | |
| 	defaultTimeout     = 20 * time.Second
 | |
| 	maxRetries         = 3
 | |
| )
 | |
| 
 | |
| // NewRingRestClient creates a new Ring client instance
 | |
| func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) {
 | |
| 	client := &RingRestClient{
 | |
| 		httpClient:     &http.Client{Timeout: defaultTimeout},
 | |
| 		onTokenRefresh: onTokenRefresh,
 | |
| 		hardwareID:     generateHardwareID(),
 | |
| 		auth:           auth,
 | |
| 	}
 | |
| 
 | |
| 	switch a := auth.(type) {
 | |
| 	case RefreshTokenAuth:
 | |
| 		if a.RefreshToken == "" {
 | |
| 			return nil, fmt.Errorf("refresh token is required")
 | |
| 		}
 | |
| 
 | |
| 		config, err := parseAuthConfig(a.RefreshToken)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to parse refresh token: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		client.authConfig = config
 | |
| 		client.hardwareID = config.HID
 | |
| 		client.RefreshToken = a.RefreshToken
 | |
| 	case EmailAuth:
 | |
| 		if a.Email == "" || a.Password == "" {
 | |
| 			return nil, fmt.Errorf("email and password are required")
 | |
| 		}
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("invalid auth type")
 | |
| 	}
 | |
| 
 | |
| 	return client, nil
 | |
| }
 | |
| 
 | |
| // Request makes an authenticated request to the Ring API
 | |
| func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte, error) {
 | |
| 	// Ensure we have a valid auth token
 | |
| 	if err := c.ensureAuth(); err != nil {
 | |
| 		return nil, fmt.Errorf("authentication failed: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var bodyReader io.Reader
 | |
| 	if body != nil {
 | |
| 		jsonBody, err := json.Marshal(body)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to marshal request body: %w", err)
 | |
| 		}
 | |
| 		bodyReader = bytes.NewReader(jsonBody)
 | |
| 	}
 | |
| 
 | |
| 	// Create request
 | |
| 	req, err := http.NewRequest(method, url, bodyReader)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create request: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Set headers
 | |
| 	req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken)
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Accept", "application/json")
 | |
| 	req.Header.Set("hardware_id", c.hardwareID)
 | |
| 	req.Header.Set("User-Agent", "android:com.ringapp")
 | |
| 
 | |
| 	// Make request with retries
 | |
| 	var resp *http.Response
 | |
| 	var responseBody []byte
 | |
| 
 | |
| 	for attempt := 0; attempt <= maxRetries; attempt++ {
 | |
| 		resp, err = c.httpClient.Do(req)
 | |
| 		if err != nil {
 | |
| 			if attempt == maxRetries {
 | |
| 				return nil, fmt.Errorf("request failed after %d retries: %w", maxRetries, err)
 | |
| 			}
 | |
| 			time.Sleep(5 * time.Second)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer resp.Body.Close()
 | |
| 
 | |
| 		responseBody, err = io.ReadAll(resp.Body)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to read response body: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Handle 401 by refreshing auth and retrying
 | |
| 		if resp.StatusCode == http.StatusUnauthorized {
 | |
| 			c.authToken = nil // Force token refresh
 | |
| 			if attempt == maxRetries {
 | |
| 				return nil, fmt.Errorf("authentication failed after %d retries", maxRetries)
 | |
| 			}
 | |
| 			if err := c.ensureAuth(); err != nil {
 | |
| 				return nil, fmt.Errorf("failed to refresh authentication: %w", err)
 | |
| 			}
 | |
| 			req.Header.Set("Authorization", "Bearer "+c.authToken.AccessToken)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Handle other error status codes
 | |
| 		if resp.StatusCode >= 400 {
 | |
| 			return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(responseBody))
 | |
| 		}
 | |
| 
 | |
| 		break
 | |
| 	}
 | |
| 
 | |
| 	return responseBody, nil
 | |
| }
 | |
| 
 | |
| // ensureAuth ensures we have a valid auth token
 | |
| func (c *RingRestClient) ensureAuth() error {
 | |
| 	if c.authToken != nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	var grantData = map[string]string{
 | |
| 		"grant_type":    "refresh_token",
 | |
| 		"refresh_token": c.authConfig.RT,
 | |
| 	}
 | |
| 
 | |
| 	// Add common fields
 | |
| 	grantData["client_id"] = "ring_official_android"
 | |
| 	grantData["scope"] = "client"
 | |
| 
 | |
| 	// Make auth request
 | |
| 	body, err := json.Marshal(grantData)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to marshal auth request: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body))
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to create auth request: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Accept", "application/json")
 | |
| 	req.Header.Set("hardware_id", c.hardwareID)
 | |
| 	req.Header.Set("User-Agent", "android:com.ringapp")
 | |
| 	req.Header.Set("2fa-support", "true")
 | |
| 
 | |
| 	resp, err := c.httpClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("auth request failed: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	if resp.StatusCode == http.StatusPreconditionFailed {
 | |
| 		return fmt.Errorf("2FA required. Please see documentation for handling 2FA")
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode != http.StatusOK {
 | |
| 		body, _ := io.ReadAll(resp.Body)
 | |
| 		return fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body))
 | |
| 	}
 | |
| 
 | |
| 	var authResp AuthTokenResponse
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
 | |
| 		return fmt.Errorf("failed to decode auth response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Update auth config and refresh token
 | |
| 	c.authToken = &authResp
 | |
| 	c.authConfig = &AuthConfig{
 | |
| 		RT:  authResp.RefreshToken,
 | |
| 		HID: c.hardwareID,
 | |
| 	}
 | |
| 
 | |
| 	// Encode and notify about new refresh token
 | |
| 	if c.onTokenRefresh != nil {
 | |
| 		newRefreshToken := encodeAuthConfig(c.authConfig)
 | |
| 		c.onTokenRefresh(newRefreshToken)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // getAuth makes an authentication request to the Ring API
 | |
| func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) {
 | |
| 	var grantData map[string]string
 | |
| 
 | |
| 	if c.authConfig != nil && twoFactorAuthCode == "" {
 | |
| 		grantData = map[string]string{
 | |
| 			"grant_type":    "refresh_token",
 | |
| 			"refresh_token": c.authConfig.RT,
 | |
| 		}
 | |
| 	} else {
 | |
| 		authEmail, ok := c.auth.(EmailAuth)
 | |
| 		if !ok {
 | |
| 			return nil, fmt.Errorf("invalid auth type for email authentication")
 | |
| 		}
 | |
| 		grantData = map[string]string{
 | |
| 			"grant_type": "password",
 | |
| 			"username":   authEmail.Email,
 | |
| 			"password":   authEmail.Password,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	grantData["client_id"] = "ring_official_android"
 | |
| 	grantData["scope"] = "client"
 | |
| 
 | |
| 	body, err := json.Marshal(grantData)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to marshal auth request: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Accept", "application/json")
 | |
| 	req.Header.Set("hardware_id", c.hardwareID)
 | |
| 	req.Header.Set("User-Agent", "android:com.ringapp")
 | |
| 	req.Header.Set("2fa-support", "true")
 | |
| 	if twoFactorAuthCode != "" {
 | |
| 		req.Header.Set("2fa-code", twoFactorAuthCode)
 | |
| 	}
 | |
| 
 | |
| 	resp, err := c.httpClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	// Handle 2FA Responses
 | |
| 	if resp.StatusCode == http.StatusPreconditionFailed ||
 | |
| 		(resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) {
 | |
| 
 | |
| 		var tfaResp Auth2faResponse
 | |
| 		if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		c.Using2FA = true
 | |
| 		if resp.StatusCode == http.StatusBadRequest {
 | |
| 			c.PromptFor2FA = "Invalid 2fa code entered. Please try again."
 | |
| 			return nil, fmt.Errorf("invalid 2FA code")
 | |
| 		}
 | |
| 
 | |
| 		if tfaResp.TSVState != "" {
 | |
| 			prompt := "from your authenticator app"
 | |
| 			if tfaResp.TSVState != "totp" {
 | |
| 				prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState)
 | |
| 			}
 | |
| 			c.PromptFor2FA = fmt.Sprintf("Please enter the code %s", prompt)
 | |
| 		} else {
 | |
| 			c.PromptFor2FA = "Please enter the code sent to your text/email"
 | |
| 		}
 | |
| 
 | |
| 		return nil, fmt.Errorf("2FA required")
 | |
| 	}
 | |
| 
 | |
| 	// Handle errors
 | |
| 	if resp.StatusCode != http.StatusOK {
 | |
| 		body, _ := io.ReadAll(resp.Body)
 | |
| 		return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body))
 | |
| 	}
 | |
| 
 | |
| 	var authResp AuthTokenResponse
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode auth response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	c.authToken = &authResp
 | |
| 	c.authConfig = &AuthConfig{
 | |
| 		RT:  authResp.RefreshToken,
 | |
| 		HID: c.hardwareID,
 | |
| 	}
 | |
| 
 | |
| 	c.RefreshToken = encodeAuthConfig(c.authConfig)
 | |
| 	if c.onTokenRefresh != nil {
 | |
| 		c.onTokenRefresh(c.RefreshToken)
 | |
| 	}
 | |
| 
 | |
| 	return c.authToken, nil
 | |
| }
 | |
| 
 | |
| // Helper functions for auth config encoding/decoding
 | |
| func parseAuthConfig(refreshToken string) (*AuthConfig, error) {
 | |
| 	decoded, err := base64.StdEncoding.DecodeString(refreshToken)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var config AuthConfig
 | |
| 	if err := json.Unmarshal(decoded, &config); err != nil {
 | |
| 		// Handle legacy format where refresh token is the raw token
 | |
| 		return &AuthConfig{RT: refreshToken}, nil
 | |
| 	}
 | |
| 
 | |
| 	return &config, nil
 | |
| }
 | |
| 
 | |
| func encodeAuthConfig(config *AuthConfig) string {
 | |
| 	jsonBytes, _ := json.Marshal(config)
 | |
| 	return base64.StdEncoding.EncodeToString(jsonBytes)
 | |
| }
 | |
| 
 | |
| // API URL helpers
 | |
| func ClientAPI(path string) string {
 | |
| 	return clientAPIBaseURL + path
 | |
| }
 | |
| 
 | |
| func DeviceAPI(path string) string {
 | |
| 	return deviceAPIBaseURL + path
 | |
| }
 | |
| 
 | |
| func CommandsAPI(path string) string {
 | |
| 	return commandsAPIBaseURL + path
 | |
| }
 | |
| 
 | |
| func AppAPI(path string) string {
 | |
| 	return appAPIBaseURL + path
 | |
| }
 | |
| 
 | |
| // FetchRingDevices gets all Ring devices and categorizes them
 | |
| func (c *RingRestClient) FetchRingDevices() (*RingDevicesResponse, error) {
 | |
| 	response, err := c.Request("GET", ClientAPI("ring_devices"), nil)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to fetch ring devices: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var devices RingDevicesResponse
 | |
| 	if err := json.Unmarshal(response, &devices); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to unmarshal devices response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Process "other" devices
 | |
| 	var onvifCameras []CameraData
 | |
| 	var intercoms []CameraData
 | |
| 
 | |
| 	for _, device := range devices.Other {
 | |
| 		kind, ok := device["kind"].(string)
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		switch RingDeviceType(kind) {
 | |
| 		case OnvifCameraType:
 | |
| 			var camera CameraData
 | |
| 			if deviceJson, err := json.Marshal(device); err == nil {
 | |
| 				if err := json.Unmarshal(deviceJson, &camera); err == nil {
 | |
| 					onvifCameras = append(onvifCameras, camera)
 | |
| 				}
 | |
| 			}
 | |
| 		case IntercomHandsetAudio:
 | |
| 			var intercom CameraData
 | |
| 			if deviceJson, err := json.Marshal(device); err == nil {
 | |
| 				if err := json.Unmarshal(deviceJson, &intercom); err == nil {
 | |
| 					intercoms = append(intercoms, intercom)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Combine all cameras into AllCameras slice
 | |
| 	allCameras := make([]CameraData, 0)
 | |
| 	allCameras = append(allCameras, interfaceSlice(devices.Doorbots)...)
 | |
| 	allCameras = append(allCameras, interfaceSlice(devices.StickupCams)...)
 | |
| 	allCameras = append(allCameras, interfaceSlice(devices.AuthorizedDoorbots)...)
 | |
| 	allCameras = append(allCameras, interfaceSlice(onvifCameras)...)
 | |
| 	allCameras = append(allCameras, interfaceSlice(intercoms)...)
 | |
| 
 | |
| 	devices.AllCameras = allCameras
 | |
| 
 | |
| 	return &devices, nil
 | |
| }
 | |
| 
 | |
| func (c *RingRestClient) GetSocketTicket() (*SocketTicketResponse, error) {
 | |
| 	response, err := c.Request("POST", AppAPI("clap/ticket/request/signalsocket"), nil)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to fetch socket ticket: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var ticket SocketTicketResponse
 | |
| 	if err := json.Unmarshal(response, &ticket); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to unmarshal socket ticket response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return &ticket, nil
 | |
| }
 | |
| 
 | |
| func generateHardwareID() string {
 | |
| 	h := sha256.New()
 | |
| 	h.Write([]byte("ring-client-go2rtc"))
 | |
| 	return hex.EncodeToString(h.Sum(nil)[:16])
 | |
| }
 | |
| 
 | |
| func interfaceSlice(slice interface{}) []CameraData {
 | |
| 	s := reflect.ValueOf(slice)
 | |
| 	if s.Kind() != reflect.Slice {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	ret := make([]CameraData, s.Len())
 | |
| 	for i := 0; i < s.Len(); i++ {
 | |
| 		if camera, ok := s.Index(i).Interface().(CameraData); ok {
 | |
| 			ret[i] = camera
 | |
| 		}
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | 
