Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import oauth
from . import webhook
from . import widget
from . import recording

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import json
import logging
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class RcOAuthController(http.Controller):
@http.route('/ringcentral/oauth', type='http', auth='user', website=False)
def oauth_callback(self, code=None, state=None, error=None, **kw):
"""Handle the OAuth redirect from RingCentral after user authorizes."""
if error:
_logger.error("RingCentral OAuth error: %s", error)
return request.redirect('/odoo?rc_error=oauth_denied')
if not code:
return request.redirect('/odoo?rc_error=no_code')
config_id = None
if state:
try:
state_data = json.loads(state)
config_id = state_data.get('config_id')
except (json.JSONDecodeError, TypeError):
pass
if not config_id:
configs = request.env['rc.config'].search([], limit=1)
if not configs:
return request.redirect('/odoo?rc_error=no_config')
config_id = configs.id
config = request.env['rc.config'].browse(config_id)
if not config.exists():
return request.redirect('/odoo?rc_error=invalid_config')
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
redirect_uri = f'{base_url}/ringcentral/oauth'
success = config.exchange_auth_code(code, redirect_uri)
if success:
return request.redirect(f'/odoo/rc-config/{config_id}')
else:
return request.redirect(f'/odoo/rc-config/{config_id}?rc_error=token_exchange_failed')

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import logging
import requests as py_requests
from odoo import http
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
class RcRecordingController(http.Controller):
@http.route('/ringcentral/recording/<int:call_id>', type='http', auth='user')
def stream_recording(self, call_id, **kw):
"""Proxy a call recording from RingCentral with authentication."""
call = request.env['rc.call.history'].browse(call_id)
if not call.exists() or not call.recording_content_uri:
return Response('Recording not found', status=404)
config = request.env['rc.config']._get_active_config()
if not config:
return Response('RingCentral not configured', status=503)
try:
config._ensure_token()
resp = py_requests.get(
call.recording_content_uri,
headers={'Authorization': f'Bearer {config.access_token}'},
stream=True,
timeout=30,
verify=config.ssl_verify,
proxies=config._get_proxies(),
)
resp.raise_for_status()
content_type = resp.headers.get('Content-Type', 'audio/mpeg')
return Response(
resp.iter_content(chunk_size=4096),
content_type=content_type,
headers={
'Content-Disposition': f'inline; filename="recording_{call_id}.mp3"',
},
)
except Exception:
_logger.exception("Error streaming recording for call %s", call_id)
return Response('Error streaming recording', status=500)

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import json
import logging
from odoo import fields, http
from odoo.http import request, Response
_logger = logging.getLogger(__name__)
class RcWebhookController(http.Controller):
@http.route('/ringcentral/webhook', type='json', auth='none', csrf=False, methods=['POST'])
def webhook_handler(self, **kw):
"""Receive webhook events from RingCentral."""
headers = request.httprequest.headers
validation_token = headers.get('Validation-Token')
if validation_token:
return Response(
status=200,
headers={'Validation-Token': validation_token},
)
try:
body = json.loads(request.httprequest.get_data(as_text=True))
except (json.JSONDecodeError, TypeError):
_logger.warning("RingCentral webhook: invalid JSON body")
return {'status': 'error', 'message': 'Invalid JSON'}
event = body.get('event', '')
_logger.info("RingCentral webhook event: %s", event)
if 'telephony/sessions' in event:
self._handle_telephony_event(body)
return {'status': 'ok'}
def _handle_telephony_event(self, body):
"""Process a telephony session event."""
try:
event_body = body.get('body', {})
parties = event_body.get('parties', [])
for party in parties:
status_code = party.get('status', {}).get('code', '')
if status_code != 'Disconnected':
continue
direction_raw = party.get('direction', '').lower()
direction = 'inbound' if direction_raw == 'inbound' else 'outbound'
from_info = party.get('from', {})
to_info = party.get('to', {})
from_number = from_info.get('phoneNumber', '')
to_number = to_info.get('phoneNumber', '')
session_id = event_body.get('sessionId', '')
if not session_id:
continue
env = request.env(su=True)
existing = env['rc.call.history'].search_count([
('rc_session_id', '=', str(session_id)),
])
if existing:
continue
duration = party.get('duration', 0)
result = party.get('status', {}).get('reason', 'Unknown')
status_map = {
'Answered': 'answered',
'CallConnected': 'answered',
'HangUp': 'answered',
'Voicemail': 'voicemail',
'Missed': 'missed',
'NoAnswer': 'no_answer',
'Busy': 'busy',
'Rejected': 'rejected',
}
status = status_map.get(result, 'answered' if duration > 0 else 'missed')
env['rc.call.history'].create({
'rc_session_id': str(session_id),
'direction': direction,
'from_number': from_number,
'to_number': to_number,
'start_time': fields.Datetime.now(),
'duration': duration,
'status': status,
})
except Exception:
_logger.exception("Error processing telephony webhook event")

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import http
from odoo.http import request
class RcWidgetController(http.Controller):
@http.route('/ringcentral/widget-config', type='json', auth='user')
def widget_config(self, **kw):
"""Return RingCentral widget configuration for the JS frontend."""
config = request.env['rc.config']._get_active_config()
if not config:
return {'enabled': False}
return {
'enabled': config.phone_widget_enabled,
'client_id': config.client_id,
'app_server': config.server_url,
'connected': config.state == 'connected',
}