mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 04:36:12 +08:00
Correcting code formatting after #1567
This commit is contained in:
@@ -1,102 +0,0 @@
|
|||||||
package ring
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ring"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
|
|
||||||
return ring.Dial(source)
|
|
||||||
})
|
|
||||||
|
|
||||||
api.HandleFunc("api/ring", apiRing)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
|
||||||
query := r.URL.Query()
|
|
||||||
var ringAPI *ring.RingRestClient
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Check auth method
|
|
||||||
if email := query.Get("email"); email != "" {
|
|
||||||
// Email/Password Flow
|
|
||||||
password := query.Get("password")
|
|
||||||
code := query.Get("code")
|
|
||||||
|
|
||||||
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
|
||||||
Email: email,
|
|
||||||
Password: password,
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try authentication (this will trigger 2FA if needed)
|
|
||||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
|
||||||
if ringAPI.Using2FA {
|
|
||||||
// Return 2FA prompt
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"needs_2fa": true,
|
|
||||||
"prompt": ringAPI.PromptFor2FA,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Refresh Token Flow
|
|
||||||
refreshToken := query.Get("refresh_token")
|
|
||||||
if refreshToken == "" {
|
|
||||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
|
||||||
RefreshToken: refreshToken,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch devices
|
|
||||||
devices, err := ringAPI.FetchRingDevices()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create clean query with only required parameters
|
|
||||||
cleanQuery := url.Values{}
|
|
||||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
|
||||||
|
|
||||||
var items []*api.Source
|
|
||||||
for _, camera := range devices.AllCameras {
|
|
||||||
cleanQuery.Set("device_id", camera.DeviceID)
|
|
||||||
|
|
||||||
// Stream source
|
|
||||||
items = append(items, &api.Source{
|
|
||||||
Name: camera.Description,
|
|
||||||
URL: "ring:?" + cleanQuery.Encode(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Snapshot source
|
|
||||||
items = append(items, &api.Source{
|
|
||||||
Name: camera.Description + " Snapshot",
|
|
||||||
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.ResponseSources(w, items)
|
|
||||||
}
|
|
102
internal/ring/ring.go
Normal file
102
internal/ring/ring.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package ring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ring"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
|
||||||
|
return ring.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.HandleFunc("api/ring", apiRing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
var ringAPI *ring.RingRestClient
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Check auth method
|
||||||
|
if email := query.Get("email"); email != "" {
|
||||||
|
// Email/Password Flow
|
||||||
|
password := query.Get("password")
|
||||||
|
code := query.Get("code")
|
||||||
|
|
||||||
|
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
||||||
|
Email: email,
|
||||||
|
Password: password,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try authentication (this will trigger 2FA if needed)
|
||||||
|
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||||
|
if ringAPI.Using2FA {
|
||||||
|
// Return 2FA prompt
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"needs_2fa": true,
|
||||||
|
"prompt": ringAPI.PromptFor2FA,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Refresh Token Flow
|
||||||
|
refreshToken := query.Get("refresh_token")
|
||||||
|
if refreshToken == "" {
|
||||||
|
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch devices
|
||||||
|
devices, err := ringAPI.FetchRingDevices()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create clean query with only required parameters
|
||||||
|
cleanQuery := url.Values{}
|
||||||
|
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||||
|
|
||||||
|
var items []*api.Source
|
||||||
|
for _, camera := range devices.AllCameras {
|
||||||
|
cleanQuery.Set("device_id", camera.DeviceID)
|
||||||
|
|
||||||
|
// Stream source
|
||||||
|
items = append(items, &api.Source{
|
||||||
|
Name: camera.Description,
|
||||||
|
URL: "ring:?" + cleanQuery.Encode(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Snapshot source
|
||||||
|
items = append(items, &api.Source{
|
||||||
|
Name: camera.Description + " Snapshot",
|
||||||
|
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponseSources(w, items)
|
||||||
|
}
|
302
pkg/ring/api.go
302
pkg/ring/api.go
@@ -19,8 +19,8 @@ type RefreshTokenAuth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EmailAuth struct {
|
type EmailAuth struct {
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthConfig represents the decoded refresh token data
|
// AuthConfig represents the decoded refresh token data
|
||||||
@@ -31,38 +31,38 @@ type AuthConfig struct {
|
|||||||
|
|
||||||
// AuthTokenResponse represents the response from the authentication endpoint
|
// AuthTokenResponse represents the response from the authentication endpoint
|
||||||
type AuthTokenResponse struct {
|
type AuthTokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
Scope string `json:"scope"` // Always "client"
|
Scope string `json:"scope"` // Always "client"
|
||||||
TokenType string `json:"token_type"` // Always "Bearer"
|
TokenType string `json:"token_type"` // Always "Bearer"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth2faResponse struct {
|
type Auth2faResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
ErrorDescription string `json:"error_description"`
|
ErrorDescription string `json:"error_description"`
|
||||||
TSVState string `json:"tsv_state"`
|
TSVState string `json:"tsv_state"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
NextTimeInSecs int `json:"next_time_in_secs"`
|
NextTimeInSecs int `json:"next_time_in_secs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SocketTicketRequest represents the request to get a socket ticket
|
// SocketTicketRequest represents the request to get a socket ticket
|
||||||
type SocketTicketResponse struct {
|
type SocketTicketResponse struct {
|
||||||
Ticket string `json:"ticket"`
|
Ticket string `json:"ticket"`
|
||||||
ResponseTimestamp int64 `json:"response_timestamp"`
|
ResponseTimestamp int64 `json:"response_timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RingRestClient handles authentication and requests to Ring API
|
// RingRestClient handles authentication and requests to Ring API
|
||||||
type RingRestClient struct {
|
type RingRestClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
authConfig *AuthConfig
|
authConfig *AuthConfig
|
||||||
hardwareID string
|
hardwareID string
|
||||||
authToken *AuthTokenResponse
|
authToken *AuthTokenResponse
|
||||||
Using2FA bool
|
Using2FA bool
|
||||||
PromptFor2FA string
|
PromptFor2FA string
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
auth interface{} // EmailAuth or RefreshTokenAuth
|
auth interface{} // EmailAuth or RefreshTokenAuth
|
||||||
onTokenRefresh func(string)
|
onTokenRefresh func(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CameraKind represents the different types of Ring cameras
|
// CameraKind represents the different types of Ring cameras
|
||||||
@@ -70,11 +70,11 @@ type CameraKind string
|
|||||||
|
|
||||||
// CameraData contains common fields for all camera types
|
// CameraData contains common fields for all camera types
|
||||||
type CameraData struct {
|
type CameraData struct {
|
||||||
ID float64 `json:"id"`
|
ID float64 `json:"id"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
DeviceID string `json:"device_id"`
|
DeviceID string `json:"device_id"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
LocationID string `json:"location_id"`
|
LocationID string `json:"location_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RingDeviceType represents different types of Ring devices
|
// RingDeviceType represents different types of Ring devices
|
||||||
@@ -82,12 +82,12 @@ type RingDeviceType string
|
|||||||
|
|
||||||
// RingDevicesResponse represents the response from the Ring API
|
// RingDevicesResponse represents the response from the Ring API
|
||||||
type RingDevicesResponse struct {
|
type RingDevicesResponse struct {
|
||||||
Doorbots []CameraData `json:"doorbots"`
|
Doorbots []CameraData `json:"doorbots"`
|
||||||
AuthorizedDoorbots []CameraData `json:"authorized_doorbots"`
|
AuthorizedDoorbots []CameraData `json:"authorized_doorbots"`
|
||||||
StickupCams []CameraData `json:"stickup_cams"`
|
StickupCams []CameraData `json:"stickup_cams"`
|
||||||
AllCameras []CameraData `json:"all_cameras"`
|
AllCameras []CameraData `json:"all_cameras"`
|
||||||
Chimes []CameraData `json:"chimes"`
|
Chimes []CameraData `json:"chimes"`
|
||||||
Other []map[string]interface{} `json:"other"`
|
Other []map[string]interface{} `json:"other"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -131,48 +131,48 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
clientAPIBaseURL = "https://api.ring.com/clients_api/"
|
clientAPIBaseURL = "https://api.ring.com/clients_api/"
|
||||||
deviceAPIBaseURL = "https://api.ring.com/devices/v1/"
|
deviceAPIBaseURL = "https://api.ring.com/devices/v1/"
|
||||||
commandsAPIBaseURL = "https://api.ring.com/commands/v1/"
|
commandsAPIBaseURL = "https://api.ring.com/commands/v1/"
|
||||||
appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/"
|
appAPIBaseURL = "https://prd-api-us.prd.rings.solutions/api/v1/"
|
||||||
oauthURL = "https://oauth.ring.com/oauth/token"
|
oauthURL = "https://oauth.ring.com/oauth/token"
|
||||||
apiVersion = 11
|
apiVersion = 11
|
||||||
defaultTimeout = 20 * time.Second
|
defaultTimeout = 20 * time.Second
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRingRestClient creates a new Ring client instance
|
// NewRingRestClient creates a new Ring client instance
|
||||||
func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) {
|
func NewRingRestClient(auth interface{}, onTokenRefresh func(string)) (*RingRestClient, error) {
|
||||||
client := &RingRestClient{
|
client := &RingRestClient{
|
||||||
httpClient: &http.Client{Timeout: defaultTimeout},
|
httpClient: &http.Client{Timeout: defaultTimeout},
|
||||||
onTokenRefresh: onTokenRefresh,
|
onTokenRefresh: onTokenRefresh,
|
||||||
hardwareID: generateHardwareID(),
|
hardwareID: generateHardwareID(),
|
||||||
auth: auth,
|
auth: auth,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch a := auth.(type) {
|
||||||
|
case RefreshTokenAuth:
|
||||||
|
if a.RefreshToken == "" {
|
||||||
|
return nil, fmt.Errorf("refresh token is required")
|
||||||
|
}
|
||||||
|
|
||||||
switch a := auth.(type) {
|
|
||||||
case RefreshTokenAuth:
|
|
||||||
if a.RefreshToken == "" {
|
|
||||||
return nil, fmt.Errorf("refresh token is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := parseAuthConfig(a.RefreshToken)
|
config, err := parseAuthConfig(a.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse refresh token: %w", err)
|
return nil, fmt.Errorf("failed to parse refresh token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.authConfig = config
|
client.authConfig = config
|
||||||
client.hardwareID = config.HID
|
client.hardwareID = config.HID
|
||||||
client.RefreshToken = a.RefreshToken
|
client.RefreshToken = a.RefreshToken
|
||||||
case EmailAuth:
|
case EmailAuth:
|
||||||
if a.Email == "" || a.Password == "" {
|
if a.Email == "" || a.Password == "" {
|
||||||
return nil, fmt.Errorf("email and password are required")
|
return nil, fmt.Errorf("email and password are required")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid auth type")
|
return nil, fmt.Errorf("invalid auth type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request makes an authenticated request to the Ring API
|
// Request makes an authenticated request to the Ring API
|
||||||
@@ -207,7 +207,7 @@ func (c *RingRestClient) Request(method, url string, body interface{}) ([]byte,
|
|||||||
// Make request with retries
|
// Make request with retries
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var responseBody []byte
|
var responseBody []byte
|
||||||
|
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||||
resp, err = c.httpClient.Do(req)
|
resp, err = c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -318,104 +318,104 @@ func (c *RingRestClient) ensureAuth() error {
|
|||||||
|
|
||||||
// getAuth makes an authentication request to the Ring API
|
// getAuth makes an authentication request to the Ring API
|
||||||
func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) {
|
func (c *RingRestClient) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) {
|
||||||
var grantData map[string]string
|
var grantData map[string]string
|
||||||
|
|
||||||
if c.authConfig != nil && twoFactorAuthCode == "" {
|
if c.authConfig != nil && twoFactorAuthCode == "" {
|
||||||
grantData = map[string]string{
|
grantData = map[string]string{
|
||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"refresh_token": c.authConfig.RT,
|
"refresh_token": c.authConfig.RT,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authEmail, ok := c.auth.(EmailAuth)
|
authEmail, ok := c.auth.(EmailAuth)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid auth type for email authentication")
|
return nil, fmt.Errorf("invalid auth type for email authentication")
|
||||||
}
|
}
|
||||||
grantData = map[string]string{
|
grantData = map[string]string{
|
||||||
"grant_type": "password",
|
"grant_type": "password",
|
||||||
"username": authEmail.Email,
|
"username": authEmail.Email,
|
||||||
"password": authEmail.Password,
|
"password": authEmail.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grantData["client_id"] = "ring_official_android"
|
grantData["client_id"] = "ring_official_android"
|
||||||
grantData["scope"] = "client"
|
grantData["scope"] = "client"
|
||||||
|
|
||||||
body, err := json.Marshal(grantData)
|
body, err := json.Marshal(grantData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal auth request: %w", err)
|
return nil, fmt.Errorf("failed to marshal auth request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body))
|
req, err := http.NewRequest("POST", oauthURL, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("hardware_id", c.hardwareID)
|
req.Header.Set("hardware_id", c.hardwareID)
|
||||||
req.Header.Set("User-Agent", "android:com.ringapp")
|
req.Header.Set("User-Agent", "android:com.ringapp")
|
||||||
req.Header.Set("2fa-support", "true")
|
req.Header.Set("2fa-support", "true")
|
||||||
if twoFactorAuthCode != "" {
|
if twoFactorAuthCode != "" {
|
||||||
req.Header.Set("2fa-code", twoFactorAuthCode)
|
req.Header.Set("2fa-code", twoFactorAuthCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Handle 2FA Responses
|
// Handle 2FA Responses
|
||||||
if resp.StatusCode == http.StatusPreconditionFailed ||
|
if resp.StatusCode == http.StatusPreconditionFailed ||
|
||||||
(resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get("WWW-Authenticate"), "Verification Code")) {
|
(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
|
var tfaResp Auth2faResponse
|
||||||
if resp.StatusCode == http.StatusBadRequest {
|
if err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil {
|
||||||
c.PromptFor2FA = "Invalid 2fa code entered. Please try again."
|
return nil, err
|
||||||
return nil, fmt.Errorf("invalid 2FA code")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tfaResp.TSVState != "" {
|
c.Using2FA = true
|
||||||
prompt := "from your authenticator app"
|
if resp.StatusCode == http.StatusBadRequest {
|
||||||
if tfaResp.TSVState != "totp" {
|
c.PromptFor2FA = "Invalid 2fa code entered. Please try again."
|
||||||
prompt = fmt.Sprintf("sent to %s via %s", tfaResp.Phone, tfaResp.TSVState)
|
return nil, fmt.Errorf("invalid 2FA code")
|
||||||
}
|
}
|
||||||
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")
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
// Handle errors
|
return nil, fmt.Errorf("2FA required")
|
||||||
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
|
// Handle errors
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("failed to decode auth response: %w", err)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
}
|
return nil, fmt.Errorf("auth request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
c.authToken = &authResp
|
var authResp AuthTokenResponse
|
||||||
c.authConfig = &AuthConfig{
|
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
|
||||||
RT: authResp.RefreshToken,
|
return nil, fmt.Errorf("failed to decode auth response: %w", err)
|
||||||
HID: c.hardwareID,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.RefreshToken = encodeAuthConfig(c.authConfig)
|
c.authToken = &authResp
|
||||||
if c.onTokenRefresh != nil {
|
c.authConfig = &AuthConfig{
|
||||||
c.onTokenRefresh(c.RefreshToken)
|
RT: authResp.RefreshToken,
|
||||||
}
|
HID: c.hardwareID,
|
||||||
|
}
|
||||||
|
|
||||||
return c.authToken, nil
|
c.RefreshToken = encodeAuthConfig(c.authConfig)
|
||||||
|
if c.onTokenRefresh != nil {
|
||||||
|
c.onTokenRefresh(c.RefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.authToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for auth config encoding/decoding
|
// Helper functions for auth config encoding/decoding
|
||||||
@@ -542,4 +542,4 @@ func interfaceSlice(slice interface{}) []CameraData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
@@ -16,422 +16,422 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
api *RingRestClient
|
api *RingRestClient
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
prod core.Producer
|
prod core.Producer
|
||||||
camera *CameraData
|
camera *CameraData
|
||||||
dialogID string
|
dialogID string
|
||||||
sessionID string
|
sessionID string
|
||||||
wsMutex sync.Mutex
|
wsMutex sync.Mutex
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionBody struct {
|
type SessionBody struct {
|
||||||
DoorbotID int `json:"doorbot_id"`
|
DoorbotID int `json:"doorbot_id"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnswerMessage struct {
|
type AnswerMessage struct {
|
||||||
Method string `json:"method"` // "sdp"
|
Method string `json:"method"` // "sdp"
|
||||||
Body struct {
|
Body struct {
|
||||||
SessionBody
|
SessionBody
|
||||||
SDP string `json:"sdp"`
|
SDP string `json:"sdp"`
|
||||||
Type string `json:"type"` // "answer"
|
Type string `json:"type"` // "answer"
|
||||||
} `json:"body"`
|
} `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IceCandidateMessage struct {
|
type IceCandidateMessage struct {
|
||||||
Method string `json:"method"` // "ice"
|
Method string `json:"method"` // "ice"
|
||||||
Body struct {
|
Body struct {
|
||||||
SessionBody
|
SessionBody
|
||||||
Ice string `json:"ice"`
|
Ice string `json:"ice"`
|
||||||
MLineIndex int `json:"mlineindex"`
|
MLineIndex int `json:"mlineindex"`
|
||||||
} `json:"body"`
|
} `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionMessage struct {
|
type SessionMessage struct {
|
||||||
Method string `json:"method"` // "session_created" or "session_started"
|
Method string `json:"method"` // "session_created" or "session_started"
|
||||||
Body SessionBody `json:"body"`
|
Body SessionBody `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PongMessage struct {
|
type PongMessage struct {
|
||||||
Method string `json:"method"` // "pong"
|
Method string `json:"method"` // "pong"
|
||||||
Body SessionBody `json:"body"`
|
Body SessionBody `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationMessage struct {
|
type NotificationMessage struct {
|
||||||
Method string `json:"method"` // "notification"
|
Method string `json:"method"` // "notification"
|
||||||
Body struct {
|
Body struct {
|
||||||
SessionBody
|
SessionBody
|
||||||
IsOK bool `json:"is_ok"`
|
IsOK bool `json:"is_ok"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
} `json:"body"`
|
} `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamInfoMessage struct {
|
type StreamInfoMessage struct {
|
||||||
Method string `json:"method"` // "stream_info"
|
Method string `json:"method"` // "stream_info"
|
||||||
Body struct {
|
Body struct {
|
||||||
SessionBody
|
SessionBody
|
||||||
Transcoding bool `json:"transcoding"`
|
Transcoding bool `json:"transcoding"`
|
||||||
TranscodingReason string `json:"transcoding_reason"`
|
TranscodingReason string `json:"transcoding_reason"`
|
||||||
} `json:"body"`
|
} `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CloseMessage struct {
|
type CloseMessage struct {
|
||||||
Method string `json:"method"` // "close"
|
Method string `json:"method"` // "close"
|
||||||
Body struct {
|
Body struct {
|
||||||
SessionBody
|
SessionBody
|
||||||
Reason struct {
|
Reason struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
} `json:"reason"`
|
} `json:"reason"`
|
||||||
} `json:"body"`
|
} `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseMessage struct {
|
type BaseMessage struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Body map[string]any `json:"body"`
|
Body map[string]any `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close reason codes
|
// Close reason codes
|
||||||
const (
|
const (
|
||||||
CloseReasonNormalClose = 0
|
CloseReasonNormalClose = 0
|
||||||
CloseReasonAuthenticationFailed = 5
|
CloseReasonAuthenticationFailed = 5
|
||||||
CloseReasonTimeout = 6
|
CloseReasonTimeout = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
func Dial(rawURL string) (*Client, error) {
|
func Dial(rawURL string) (*Client, error) {
|
||||||
// 1. Parse URL and validate basic params
|
// 1. Parse URL and validate basic params
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
query := u.Query()
|
query := u.Query()
|
||||||
encodedToken := query.Get("refresh_token")
|
encodedToken := query.Get("refresh_token")
|
||||||
deviceID := query.Get("device_id")
|
deviceID := query.Get("device_id")
|
||||||
_, isSnapshot := query["snapshot"]
|
_, isSnapshot := query["snapshot"]
|
||||||
|
|
||||||
if encodedToken == "" || deviceID == "" {
|
if encodedToken == "" || deviceID == "" {
|
||||||
return nil, errors.New("ring: wrong query")
|
return nil, errors.New("ring: wrong query")
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL-decode the refresh token
|
// URL-decode the refresh token
|
||||||
refreshToken, err := url.QueryUnescape(encodedToken)
|
refreshToken, err := url.QueryUnescape(encodedToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err)
|
return nil, fmt.Errorf("ring: invalid refresh token encoding: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Ring API client
|
// Initialize Ring API client
|
||||||
ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil)
|
ringAPI, err := NewRingRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get camera details
|
// Get camera details
|
||||||
devices, err := ringAPI.FetchRingDevices()
|
devices, err := ringAPI.FetchRingDevices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var camera *CameraData
|
var camera *CameraData
|
||||||
for _, cam := range devices.AllCameras {
|
for _, cam := range devices.AllCameras {
|
||||||
if fmt.Sprint(cam.DeviceID) == deviceID {
|
if fmt.Sprint(cam.DeviceID) == deviceID {
|
||||||
camera = &cam
|
camera = &cam
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if camera == nil {
|
if camera == nil {
|
||||||
return nil, errors.New("ring: camera not found")
|
return nil, errors.New("ring: camera not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create base client
|
// Create base client
|
||||||
client := &Client{
|
client := &Client{
|
||||||
api: ringAPI,
|
api: ringAPI,
|
||||||
camera: camera,
|
camera: camera,
|
||||||
dialogID: uuid.NewString(),
|
dialogID: uuid.NewString(),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if snapshot request
|
// Check if snapshot request
|
||||||
if isSnapshot {
|
if isSnapshot {
|
||||||
client.prod = NewSnapshotProducer(ringAPI, camera)
|
client.prod = NewSnapshotProducer(ringAPI, camera)
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not snapshot, continue with WebRTC setup
|
// If not snapshot, continue with WebRTC setup
|
||||||
ticket, err := ringAPI.GetSocketTicket()
|
ticket, err := ringAPI.GetSocketTicket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create WebSocket connection
|
// Create WebSocket connection
|
||||||
wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s",
|
wsURL := fmt.Sprintf("wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s",
|
||||||
uuid.NewString(), url.QueryEscape(ticket.Ticket))
|
uuid.NewString(), url.QueryEscape(ticket.Ticket))
|
||||||
|
|
||||||
client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{
|
client.ws, _, err = websocket.DefaultDialer.Dial(wsURL, map[string][]string{
|
||||||
"User-Agent": {"android:com.ringapp"},
|
"User-Agent": {"android:com.ringapp"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Peer Connection
|
// Create Peer Connection
|
||||||
conf := pion.Configuration{
|
conf := pion.Configuration{
|
||||||
ICEServers: []pion.ICEServer{
|
ICEServers: []pion.ICEServer{
|
||||||
{URLs: []string{
|
{URLs: []string{
|
||||||
"stun:stun.kinesisvideo.us-east-1.amazonaws.com:443",
|
"stun:stun.kinesisvideo.us-east-1.amazonaws.com:443",
|
||||||
"stun:stun.kinesisvideo.us-east-2.amazonaws.com:443",
|
"stun:stun.kinesisvideo.us-east-2.amazonaws.com:443",
|
||||||
"stun:stun.kinesisvideo.us-west-2.amazonaws.com:443",
|
"stun:stun.kinesisvideo.us-west-2.amazonaws.com:443",
|
||||||
"stun:stun.l.google.com:19302",
|
"stun:stun.l.google.com:19302",
|
||||||
"stun:stun1.l.google.com:19302",
|
"stun:stun1.l.google.com:19302",
|
||||||
"stun:stun2.l.google.com:19302",
|
"stun:stun2.l.google.com:19302",
|
||||||
"stun:stun3.l.google.com:19302",
|
"stun:stun3.l.google.com:19302",
|
||||||
"stun:stun4.l.google.com:19302",
|
"stun:stun4.l.google.com:19302",
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
ICETransportPolicy: pion.ICETransportPolicyAll,
|
ICETransportPolicy: pion.ICETransportPolicyAll,
|
||||||
BundlePolicy: pion.BundlePolicyBalanced,
|
BundlePolicy: pion.BundlePolicyBalanced,
|
||||||
}
|
}
|
||||||
|
|
||||||
api, err := webrtc.NewAPI()
|
api, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.ws.Close()
|
client.ws.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := api.NewPeerConnection(conf)
|
pc, err := api.NewPeerConnection(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.ws.Close()
|
client.ws.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// protect from sending ICE candidate before Offer
|
// protect from sending ICE candidate before Offer
|
||||||
var sendOffer core.Waiter
|
var sendOffer core.Waiter
|
||||||
|
|
||||||
// protect from blocking on errors
|
// protect from blocking on errors
|
||||||
defer sendOffer.Done(nil)
|
defer sendOffer.Done(nil)
|
||||||
|
|
||||||
// waiter will wait PC error or WS error or nil (connection OK)
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.FormatName = "ring/webrtc"
|
prod.FormatName = "ring/webrtc"
|
||||||
prod.Mode = core.ModeActiveProducer
|
prod.Mode = core.ModeActiveProducer
|
||||||
prod.Protocol = "ws"
|
prod.Protocol = "ws"
|
||||||
prod.URL = rawURL
|
prod.URL = rawURL
|
||||||
|
|
||||||
client.prod = prod
|
client.prod = prod
|
||||||
|
|
||||||
prod.Listen(func(msg any) {
|
prod.Listen(func(msg any) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
_ = sendOffer.Wait()
|
_ = sendOffer.Wait()
|
||||||
|
|
||||||
iceCandidate := msg.ToJSON()
|
iceCandidate := msg.ToJSON()
|
||||||
|
|
||||||
// skip empty ICE candidates
|
// skip empty ICE candidates
|
||||||
if iceCandidate.Candidate == "" {
|
if iceCandidate.Candidate == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
icePayload := map[string]interface{}{
|
icePayload := map[string]interface{}{
|
||||||
"ice": iceCandidate.Candidate,
|
"ice": iceCandidate.Candidate,
|
||||||
"mlineindex": iceCandidate.SDPMLineIndex,
|
"mlineindex": iceCandidate.SDPMLineIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = client.sendSessionMessage("ice", icePayload); err != nil {
|
|
||||||
connState.Done(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case pion.PeerConnectionState:
|
if err = client.sendSessionMessage("ice", icePayload); err != nil {
|
||||||
switch msg {
|
connState.Done(err)
|
||||||
case pion.PeerConnectionStateConnecting:
|
return
|
||||||
case pion.PeerConnectionStateConnected:
|
}
|
||||||
connState.Done(nil)
|
|
||||||
default:
|
|
||||||
connState.Done(errors.New("ring: " + msg.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup media configuration
|
case pion.PeerConnectionState:
|
||||||
medias := []*core.Media{
|
switch msg {
|
||||||
{
|
case pion.PeerConnectionStateConnecting:
|
||||||
Kind: core.KindAudio,
|
case pion.PeerConnectionStateConnected:
|
||||||
Direction: core.DirectionSendRecv,
|
connState.Done(nil)
|
||||||
Codecs: []*core.Codec{
|
default:
|
||||||
{
|
connState.Done(errors.New("ring: " + msg.String()))
|
||||||
Name: "opus",
|
}
|
||||||
ClockRate: 48000,
|
}
|
||||||
Channels: 2,
|
})
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: core.KindVideo,
|
|
||||||
Direction: core.DirectionRecvonly,
|
|
||||||
Codecs: []*core.Codec{
|
|
||||||
{
|
|
||||||
Name: "H264",
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create offer
|
// Setup media configuration
|
||||||
offer, err := prod.CreateOffer(medias)
|
medias := []*core.Media{
|
||||||
if err != nil {
|
{
|
||||||
client.Stop()
|
Kind: core.KindAudio,
|
||||||
return nil, err
|
Direction: core.DirectionSendRecv,
|
||||||
}
|
Codecs: []*core.Codec{
|
||||||
|
{
|
||||||
|
Name: "opus",
|
||||||
|
ClockRate: 48000,
|
||||||
|
Channels: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{
|
||||||
|
Name: "H264",
|
||||||
|
ClockRate: 90000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Send offer
|
// Create offer
|
||||||
offerPayload := map[string]interface{}{
|
offer, err := prod.CreateOffer(medias)
|
||||||
"stream_options": map[string]bool{
|
if err != nil {
|
||||||
"audio_enabled": true,
|
client.Stop()
|
||||||
"video_enabled": true,
|
return nil, err
|
||||||
},
|
}
|
||||||
"sdp": offer,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.sendSessionMessage("live_view", offerPayload); err != nil {
|
// Send offer
|
||||||
client.Stop()
|
offerPayload := map[string]interface{}{
|
||||||
return nil, err
|
"stream_options": map[string]bool{
|
||||||
}
|
"audio_enabled": true,
|
||||||
|
"video_enabled": true,
|
||||||
|
},
|
||||||
|
"sdp": offer,
|
||||||
|
}
|
||||||
|
|
||||||
sendOffer.Done(nil)
|
if err = client.sendSessionMessage("live_view", offerPayload); err != nil {
|
||||||
|
client.Stop()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Ring expects a ping message every 5 seconds
|
sendOffer.Done(nil)
|
||||||
go client.startPingLoop(pc)
|
|
||||||
go client.startMessageLoop(&connState)
|
|
||||||
|
|
||||||
if err = connState.Wait(); err != nil {
|
// Ring expects a ping message every 5 seconds
|
||||||
return nil, err
|
go client.startPingLoop(pc)
|
||||||
}
|
go client.startMessageLoop(&connState)
|
||||||
|
|
||||||
return client, nil
|
if err = connState.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startPingLoop(pc *pion.PeerConnection) {
|
func (c *Client) startPingLoop(pc *pion.PeerConnection) {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if pc.ConnectionState() == pion.PeerConnectionStateConnected {
|
if pc.ConnectionState() == pion.PeerConnectionStateConnected {
|
||||||
if err := c.sendSessionMessage("ping", nil); err != nil {
|
if err := c.sendSessionMessage("ping", nil); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startMessageLoop(connState *core.Waiter) {
|
func (c *Client) startMessageLoop(connState *core.Waiter) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// will be closed when conn will be closed
|
// will be closed when conn will be closed
|
||||||
defer func() {
|
defer func() {
|
||||||
connState.Done(err)
|
connState.Done(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
var res BaseMessage
|
var res BaseMessage
|
||||||
if err = c.ws.ReadJSON(&res); err != nil {
|
if err = c.ws.ReadJSON(&res); err != nil {
|
||||||
select {
|
select {
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Stop()
|
c.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if "doorbot_id" is present
|
// check if "doorbot_id" is present
|
||||||
if _, ok := res.Body["doorbot_id"]; !ok {
|
if _, ok := res.Body["doorbot_id"]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the message is from the correct doorbot
|
|
||||||
doorbotID := res.Body["doorbot_id"].(float64)
|
|
||||||
if doorbotID != float64(c.camera.ID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the message is from the correct session
|
// check if the message is from the correct doorbot
|
||||||
if res.Method == "session_created" || res.Method == "session_started" {
|
doorbotID := res.Body["doorbot_id"].(float64)
|
||||||
if _, ok := res.Body["session_id"]; ok && c.sessionID == "" {
|
if doorbotID != float64(c.camera.ID) {
|
||||||
c.sessionID = res.Body["session_id"].(string)
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := res.Body["session_id"]; ok {
|
// check if the message is from the correct session
|
||||||
if res.Body["session_id"].(string) != c.sessionID {
|
if res.Method == "session_created" || res.Method == "session_started" {
|
||||||
continue
|
if _, ok := res.Body["session_id"]; ok && c.sessionID == "" {
|
||||||
}
|
c.sessionID = res.Body["session_id"].(string)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rawMsg, _ := json.Marshal(res)
|
if _, ok := res.Body["session_id"]; ok {
|
||||||
|
if res.Body["session_id"].(string) != c.sessionID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch res.Method {
|
rawMsg, _ := json.Marshal(res)
|
||||||
case "sdp":
|
|
||||||
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
|
||||||
// Get answer
|
|
||||||
var msg AnswerMessage
|
|
||||||
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
|
||||||
c.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = prod.SetAnswer(msg.Body.SDP); err != nil {
|
|
||||||
c.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = c.activateSession(); err != nil {
|
|
||||||
c.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ice":
|
|
||||||
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
|
||||||
// Continue to receiving candidates
|
|
||||||
var msg IceCandidateMessage
|
|
||||||
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for empty ICE candidate
|
switch res.Method {
|
||||||
if msg.Body.Ice == "" {
|
case "sdp":
|
||||||
break
|
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
}
|
// Get answer
|
||||||
|
var msg AnswerMessage
|
||||||
|
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
||||||
|
c.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = prod.SetAnswer(msg.Body.SDP); err != nil {
|
||||||
|
c.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = c.activateSession(); err != nil {
|
||||||
|
c.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err = prod.AddCandidate(msg.Body.Ice); err != nil {
|
case "ice":
|
||||||
c.Stop()
|
if prod, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
return
|
// Continue to receiving candidates
|
||||||
}
|
var msg IceCandidateMessage
|
||||||
}
|
if err = json.Unmarshal(rawMsg, &msg); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "close":
|
// check for empty ICE candidate
|
||||||
c.Stop()
|
if msg.Body.Ice == "" {
|
||||||
return
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "pong":
|
if err = prod.AddCandidate(msg.Body.Ice); err != nil {
|
||||||
// Ignore
|
c.Stop()
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
case "close":
|
||||||
|
c.Stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
case "pong":
|
||||||
|
// Ignore
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) activateSession() error {
|
func (c *Client) activateSession() error {
|
||||||
@@ -453,7 +453,7 @@ func (c *Client) activateSession() error {
|
|||||||
|
|
||||||
func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error {
|
func (c *Client) sendSessionMessage(method string, body map[string]interface{}) error {
|
||||||
c.wsMutex.Lock()
|
c.wsMutex.Lock()
|
||||||
defer c.wsMutex.Unlock()
|
defer c.wsMutex.Unlock()
|
||||||
|
|
||||||
if body == nil {
|
if body == nil {
|
||||||
body = make(map[string]interface{})
|
body = make(map[string]interface{})
|
||||||
@@ -486,18 +486,18 @@ func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
if media.Kind == core.KindAudio {
|
if media.Kind == core.KindAudio {
|
||||||
// Enable speaker
|
// Enable speaker
|
||||||
speakerPayload := map[string]interface{}{
|
speakerPayload := map[string]interface{}{
|
||||||
"stealth_mode": false,
|
"stealth_mode": false,
|
||||||
}
|
}
|
||||||
_ = c.sendSessionMessage("camera_options", speakerPayload)
|
_ = c.sendSessionMessage("camera_options", speakerPayload)
|
||||||
}
|
}
|
||||||
return webrtcProd.AddTrack(media, codec, track)
|
return webrtcProd.AddTrack(media, codec, track)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("add track not supported for snapshot")
|
return fmt.Errorf("add track not supported for snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
func (c *Client) Start() error {
|
||||||
@@ -534,9 +534,9 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
if webrtcProd, ok := c.prod.(*webrtc.Conn); ok {
|
||||||
return webrtcProd.MarshalJSON()
|
return webrtcProd.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("ring: can't marshal")
|
return nil, errors.New("ring: can't marshal")
|
||||||
}
|
}
|
||||||
|
@@ -8,57 +8,57 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SnapshotProducer struct {
|
type SnapshotProducer struct {
|
||||||
core.Connection
|
core.Connection
|
||||||
|
|
||||||
client *RingRestClient
|
client *RingRestClient
|
||||||
camera *CameraData
|
camera *CameraData
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer {
|
func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotProducer {
|
||||||
return &SnapshotProducer{
|
return &SnapshotProducer{
|
||||||
Connection: core.Connection{
|
Connection: core.Connection{
|
||||||
ID: core.NewID(),
|
ID: core.NewID(),
|
||||||
FormatName: "ring/snapshot",
|
FormatName: "ring/snapshot",
|
||||||
Protocol: "https",
|
Protocol: "https",
|
||||||
Medias: []*core.Media{
|
Medias: []*core.Media{
|
||||||
{
|
{
|
||||||
Kind: core.KindVideo,
|
Kind: core.KindVideo,
|
||||||
Direction: core.DirectionRecvonly,
|
Direction: core.DirectionRecvonly,
|
||||||
Codecs: []*core.Codec{
|
Codecs: []*core.Codec{
|
||||||
{
|
{
|
||||||
Name: core.CodecJPEG,
|
Name: core.CodecJPEG,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
PayloadType: core.PayloadTypeRAW,
|
PayloadType: core.PayloadTypeRAW,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
client: client,
|
client: client,
|
||||||
camera: camera,
|
camera: camera,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SnapshotProducer) Start() error {
|
func (p *SnapshotProducer) Start() error {
|
||||||
// Fetch snapshot
|
// Fetch snapshot
|
||||||
response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil)
|
response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get snapshot: %w", err)
|
return fmt.Errorf("failed to get snapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pkt := &rtp.Packet{
|
pkt := &rtp.Packet{
|
||||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||||
Payload: response,
|
Payload: response,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to all receivers
|
// Send to all receivers
|
||||||
for _, receiver := range p.Receivers {
|
for _, receiver := range p.Receivers {
|
||||||
receiver.WriteRTP(pkt)
|
receiver.WriteRTP(pkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SnapshotProducer) Stop() error {
|
func (p *SnapshotProducer) Stop() error {
|
||||||
return p.Connection.Stop()
|
return p.Connection.Stop()
|
||||||
}
|
}
|
||||||
|
@@ -247,7 +247,7 @@
|
|||||||
|
|
||||||
const r = await fetch(url, {cache: 'no-cache'});
|
const r = await fetch(url, {cache: 'no-cache'});
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
|
||||||
if (data.needs_2fa) {
|
if (data.needs_2fa) {
|
||||||
document.getElementById('tfa-field').style.display = 'block';
|
document.getElementById('tfa-field').style.display = 'block';
|
||||||
document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code';
|
document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code';
|
||||||
|
Reference in New Issue
Block a user