This commit is contained in:
gsinghpal
2026-03-09 15:21:22 -04:00
parent a3e85a23ef
commit acd3fc455e
243 changed files with 20459 additions and 4197 deletions

View File

@@ -465,7 +465,7 @@ class RcCallHistory(models.Model):
media_url = f'{content_uri}?access_token={config.access_token}'
data = config._api_post(
'/ai/speech-to-text/v1/async?webhook=false',
'/ai/audio/v1/async/speech-to-text?webhook=false',
data={
'contentUri': media_url,
'source': 'CallRecording',

View File

@@ -172,46 +172,84 @@ class RcConfig(models.Model):
return False
def _refresh_token(self):
"""Refresh the access token using the refresh token."""
"""Refresh the access token using the refresh token.
Uses SELECT ... FOR UPDATE to prevent concurrent cron workers
from racing on the same refresh token (RC rotates tokens on
each refresh, so only the first caller wins).
"""
self.ensure_one()
if not self.refresh_token:
self.write({'state': 'error'})
return False
self.env.cr.execute(
"SELECT refresh_token, state, token_expiry "
"FROM rc_config WHERE id = %s FOR UPDATE SKIP LOCKED",
(self.id,),
)
row = self.env.cr.fetchone()
if not row:
_logger.debug("RC config %s locked by another worker, skipping refresh", self.id)
return True
if not row[0]:
return False
if row[1] == 'connected' and row[2] and row[2] > datetime.utcnow() + timedelta(minutes=2):
return True
current_refresh = row[0]
try:
resp = requests.post(
f'{self.server_url}/restapi/oauth/token',
data={
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
'refresh_token': current_refresh,
},
auth=(self.client_id, self.client_secret),
timeout=15,
verify=self.ssl_verify,
proxies=self._get_proxies(),
)
resp.raise_for_status()
data = resp.json()
if resp.status_code != 200:
try:
err_data = resp.json()
rc_error = err_data.get('error', 'unknown')
rc_desc = err_data.get('error_description', '')
rc_codes = [e.get('errorCode', '') for e in err_data.get('errors', [])]
_logger.error(
"RingCentral token refresh failed: %s - %s (codes: %s). "
"Manual OAuth reconnect required.",
rc_error, rc_desc, ', '.join(rc_codes),
)
except Exception:
_logger.error(
"RingCentral token refresh failed: HTTP %d", resp.status_code,
)
self.write({'state': 'error'})
return False
data = resp.json()
expires_in = data.get('expires_in', 3600)
self.write({
'access_token': data.get('access_token'),
'refresh_token': data.get('refresh_token', self.refresh_token),
'refresh_token': data.get('refresh_token', current_refresh),
'token_expiry': datetime.utcnow() + timedelta(seconds=expires_in),
'state': 'connected',
})
return True
except Exception:
_logger.exception("RingCentral token refresh failed")
_logger.exception("RingCentral token refresh failed (network error)")
self.write({'state': 'error'})
return False
def _ensure_token(self):
"""Ensure we have a valid access token, refreshing if needed."""
"""Ensure we have a valid access token, refreshing proactively."""
self.ensure_one()
if not self.access_token:
raise UserError(_('RingCentral is not connected. Please connect via OAuth first.'))
if self.token_expiry and self.token_expiry < fields.Datetime.now():
buffer = fields.Datetime.now() + timedelta(minutes=5)
if self.token_expiry and self.token_expiry < buffer:
if not self._refresh_token():
raise UserError(_('Failed to refresh RingCentral token. Please reconnect.'))
@@ -643,7 +681,7 @@ class RcConfig(models.Model):
@api.model
def _cron_refresh_tokens(self):
"""Refresh tokens for all connected configs nearing expiry."""
"""Refresh tokens for connected configs nearing expiry and recover error configs."""
soon = fields.Datetime.now() + timedelta(minutes=10)
configs = self.search([
('state', '=', 'connected'),
@@ -656,6 +694,17 @@ class RcConfig(models.Model):
except Exception:
_logger.exception("Cron: Failed to refresh token for config %s", cfg.name)
error_configs = self.search([
('state', '=', 'error'),
('refresh_token', '!=', False),
])
for cfg in error_configs:
try:
if cfg._refresh_token():
_logger.info("Cron: Recovered RC config %s from error state", cfg.name)
except Exception:
pass
# ──────────────────────────────────────────────────────────
# Cron: webhook renewal
# ──────────────────────────────────────────────────────────