Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pkg/connector/dbmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ func (gv *GVConnector) GetDBMetaTypes() database.MetaTypes {
},
Message: nil,
UserLogin: func() any {
return &UserLoginMetadata{}
return &UserLoginMetadata{
Cookies: make(map[string]string),
}
},
}
}
Expand Down
27 changes: 25 additions & 2 deletions pkg/libgv/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import (
const reqChooseServer = `[[null,null,null,[7,5],null,[null,[null,1],[[["3"]]]]]]`

func (c *Client) SubscribeRealtimeChannel(ctx context.Context) (string, *gvproto.WebChannelSession, error) {
if c == nil {
return "", nil, fmt.Errorf("google Voice client is not initialized - ensure you call NewClient() with valid cookies before subscribing to channels")
}
chooseServerResp, err := ReadProtoResponse[*gvproto.RespChooseServer](
c.MakeRequest(ctx, http.MethodPost, EndpointRealtimeChooseServer, nil, http.Header{
"Content-Type": {ContentTypePBLite},
Expand Down Expand Up @@ -76,6 +79,12 @@ func (c *Client) SubscribeRealtimeChannel(ctx context.Context) (string, *gvproto
if err != nil {
return "", nil, fmt.Errorf("failed to create channel: %w", err)
}
if createChannelResp.GetData() == nil {
return "", nil, fmt.Errorf("create channel response has no data - this may indicate authentication issues or Google Voice API changes")
}
if createChannelResp.GetData().GetSession() == nil {
return "", nil, fmt.Errorf("create channel response has no session - verify your Google Voice account is properly configured")
}
return chooseServerResp.GSessionID, createChannelResp.GetData().GetSession(), nil
}

Expand All @@ -88,6 +97,9 @@ func (c *Client) RunRealtimeChannel(ctx context.Context) error {
if err != nil {
return err
}
if channel == nil {
return fmt.Errorf("channel is nil after subscription - failed to establish realtime connection, check network and authentication")
}
lastResubscribe := time.Now()
log := zerolog.Ctx(ctx)
var ackID uint64
Expand All @@ -99,6 +111,9 @@ func (c *Client) RunRealtimeChannel(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to re-subscribe after timeout: %w", err)
}
if channel == nil {
return fmt.Errorf("channel is nil after re-subscription - failed to re-establish realtime connection, verify authentication")
}
lastResubscribe = time.Now()
ackID = 0
}
Expand All @@ -121,7 +136,7 @@ func (c *Client) RunRealtimeChannel(ctx context.Context) error {
resp, err := c.MakeRequest(ctx, http.MethodGet, EndpointRealtimeChannel, query, nil, nil)
if err != nil {
var httpErr *ResponseError
if errors.As(err, &httpErr) && bytes.Contains(httpErr.Body, []byte("Unknown SID")) {
if errors.As(err, &httpErr) && httpErr.Body != nil && bytes.Contains(httpErr.Body, []byte("Unknown SID")) {
failedRequests++
if failedRequests > 10 {
return errors.New("too many repeated unknown SID errors")
Expand All @@ -133,6 +148,9 @@ func (c *Client) RunRealtimeChannel(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to re-subscribe after unknown SID: %w", err)
}
if channel == nil {
return fmt.Errorf("channel is nil after re-subscription following unknown SID - connection lost, try restarting the bridge")
}
lastResubscribe = time.Now()
ackID = 0
continue
Expand Down Expand Up @@ -184,13 +202,18 @@ func (c *Client) RunRealtimeChannel(ctx context.Context) error {
log.Debug().RawJSON("failed_entry", entry).Msg("Failed to parse channel entry")
return fmt.Errorf("failed to parse entry #%d: %w", i+1, err)
}
if len(parsed.GetDataWrapper()) == 1 && parsed.GetDataWrapper()[0].GetAltData().GetReconnect() {
if len(parsed.GetDataWrapper()) == 1 &&
parsed.GetDataWrapper()[0].GetAltData() != nil &&
parsed.GetDataWrapper()[0].GetAltData().GetReconnect() {
_ = resp.Body.Close()
log.Debug().Msg("Got event that probably means we need to reconnect")
gSessionID, channel, err = c.SubscribeRealtimeChannel(ctx)
if err != nil {
return fmt.Errorf("failed to re-subscribe after timeout: %w", err)
}
if channel == nil {
return fmt.Errorf("channel is nil after re-subscription following reconnect event - persistent connection issues, check network stability")
}
lastResubscribe = time.Now()
ackID = 0
break ReadLoop
Expand Down
9 changes: 9 additions & 0 deletions pkg/libgv/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type Client struct {
}

func NewClient(cookies map[string]string) *Client {
if cookies == nil {
cookies = make(map[string]string)
}
return &Client{
HTTP: &http.Client{Timeout: 120 * time.Second},
cookies: cookies,
Expand All @@ -52,6 +55,9 @@ func NewClient(cookies map[string]string) *Client {
func (c *Client) GetCookies() map[string]string {
c.cookiesLock.RLock()
defer c.cookiesLock.RUnlock()
if c.cookies == nil {
return make(map[string]string)
}
return maps.Clone(c.cookies)
}

Expand All @@ -68,6 +74,9 @@ func (c *Client) updateCookies(ctx context.Context, resp *http.Response) {
}
c.cookiesLock.Lock()
defer c.cookiesLock.Unlock()
if c.cookies == nil {
c.cookies = make(map[string]string)
}
cookiesChanged := false
for _, cookie := range respCookies {
if cookie.Expires.Before(time.Now()) || cookie.MaxAge < 0 {
Expand Down
31 changes: 24 additions & 7 deletions pkg/libgv/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func SAPISIDHash(origin, sapisid string) string {
}

func (c *Client) prepareHeaders(req *http.Request) {
if c == nil {
return
}
req.Header.Set("Sec-Ch-Ua", CHUserAgent)
req.Header.Set("Sec-Ch-Ua-Platform", CHPlatform)
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
Expand Down Expand Up @@ -84,13 +87,15 @@ func (c *Client) prepareHeaders(req *http.Request) {
}
c.cookiesLock.RLock()
defer c.cookiesLock.RUnlock()
for name, value := range c.cookies {
req.AddCookie(&http.Cookie{
Name: name,
Value: value,
})
if name == "SAPISID" {
req.Header.Set("Authorization", SAPISIDHash(Origin, value))
if c.cookies != nil {
for name, value := range c.cookies {
req.AddCookie(&http.Cookie{
Name: name,
Value: value,
})
if name == "SAPISID" {
req.Header.Set("Authorization", SAPISIDHash(Origin, value))
}
}
}
}
Expand All @@ -108,12 +113,21 @@ type ResponseError struct {
}

func (re *ResponseError) Error() string {
if re == nil {
return "response error object is nil - this indicates a programming error in error handling"
}
if re.Resp == nil {
return "request failed with no response - check network connectivity and authentication"
}
return fmt.Sprintf("unexpected status code %d", re.Resp.StatusCode)
}

const MaxRetryCount = 10

func (c *Client) MakeRequest(ctx context.Context, method, baseAddr string, query url.Values, headers http.Header, body any) (*http.Response, error) {
if c == nil {
return nil, fmt.Errorf("google Voice client is not initialized - ensure you call NewClient() with valid cookies before making requests")
}
parsedAddr, err := url.Parse(baseAddr)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
Expand Down Expand Up @@ -219,6 +233,9 @@ func (c *Client) makeRequestDirect(ctx context.Context, method string, parsedAdd
}
req.Header = headers.Clone()
c.prepareHeaders(req)
if c.HTTP == nil {
return req, nil, fmt.Errorf("HTTP client is not configured - this indicates a programming error in client initialization")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return req, nil, fmt.Errorf("failed to send request: %w", err)
Expand Down
Loading