diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index aeb86a7b..df81bace 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -15,7 +15,18 @@ TtsLanguageCode, ) from .input_types import Dtmf, Speech -from .ncco import Connect, Conversation, Input, NccoAction, Notify, Record, Stream, Talk +from .ncco import ( + Connect, + Conversation, + Input, + NccoAction, + Notify, + Record, + Stream, + Talk, + Transfer, + Wait, +) from .requests import ( AudioStreamOptions, CreateCallRequest, @@ -33,41 +44,43 @@ ) __all__ = [ - 'AdvancedMachineDetection', - 'AppEndpoint', - 'AudioStreamOptions', - 'CallInfo', - 'CallList', - 'CallMessage', - 'CallState', - 'Channel', - 'Connect', - 'ConnectEndpointType', - 'Conversation', - 'CreateCallRequest', - 'CreateCallResponse', - 'Dtmf', - 'Embedded', - 'Input', - 'ListCallsFilter', - 'HalLinks', - 'NccoAction', - 'NccoActionType', - 'Notify', - 'OnAnswer', - 'Phone', - 'PhoneEndpoint', - 'Record', - 'Sip', - 'SipEndpoint', - 'Speech', - 'Stream', - 'Talk', - 'ToPhone', - 'TtsLanguageCode', - 'TtsStreamOptions', - 'Vbc', - 'VbcEndpoint', - 'Websocket', - 'WebsocketEndpoint', + "AdvancedMachineDetection", + "AppEndpoint", + "AudioStreamOptions", + "CallInfo", + "CallList", + "CallMessage", + "CallState", + "Channel", + "Connect", + "ConnectEndpointType", + "Conversation", + "CreateCallRequest", + "CreateCallResponse", + "Dtmf", + "Embedded", + "Input", + "ListCallsFilter", + "HalLinks", + "NccoAction", + "NccoActionType", + "Notify", + "OnAnswer", + "Phone", + "PhoneEndpoint", + "Record", + "Sip", + "SipEndpoint", + "Speech", + "Stream", + "Talk", + "Transfer", + "Wait", + "ToPhone", + "TtsLanguageCode", + "TtsStreamOptions", + "Vbc", + "VbcEndpoint", + "Websocket", + "WebsocketEndpoint", ] diff --git a/voice/src/vonage_voice/models/enums.py b/voice/src/vonage_voice/models/enums.py index faa007ca..868abce4 100644 --- a/voice/src/vonage_voice/models/enums.py +++ b/voice/src/vonage_voice/models/enums.py @@ -16,6 +16,8 @@ class NccoActionType(str, Enum): STREAM = 'stream' INPUT = 'input' NOTIFY = 'notify' + WAIT = 'wait' + TRANSFER = 'transfer' class ConnectEndpointType(str, Enum): diff --git a/voice/src/vonage_voice/models/ncco.py b/voice/src/vonage_voice/models/ncco.py index 72388719..6309dc4c 100644 --- a/voice/src/vonage_voice/models/ncco.py +++ b/voice/src/vonage_voice/models/ncco.py @@ -267,3 +267,66 @@ class Notify(NccoAction): eventUrl: list[str] eventMethod: Optional[str] = None action: NccoActionType = NccoActionType.NOTIFY + + +class Wait(NccoAction): + """Use the Wait action to add a pause to an NCCO. + + The wait period starts when the action is executed and ends after the provided + or default timeout value. Execution of the NCCO then resumes with the next action. + + Args: + timeout (Optional[float]): Duration of the wait period in seconds. Valid values + are from 0.1 to 7200. Values below 0.1 are treated as 0.1; values above + 7200 are treated as 7200. If not specified, defaults to 10 seconds. + """ + + timeout: Optional[float] = 10.0 + action: NccoActionType = NccoActionType.WAIT + + @model_validator(mode='after') + def clamp_timeout(self): + if self.timeout is None: + self.timeout = 10.0 + elif self.timeout < 0.1: + self.timeout = 0.1 + elif self.timeout > 7200: + self.timeout = 7200.0 + return self + + +class Transfer(NccoAction): + """Use the Transfer action to move all legs from the current conversation into another + existing conversation. + + The transfer action is synchronous and terminal for the current conversation. + The target conversation's NCCO continues to control its behaviour. + + Args: + conversationId (str): The target conversation ID. + canHear (Optional[list[str]]): Leg UUIDs this participant can hear. If not + provided, the participant can hear everyone. If an empty list is provided, + the participant will not hear any other participants. + canSpeak (Optional[list[str]]): Leg UUIDs this participant can be heard by. If + not provided, the participant can be heard by everyone. If an empty list is + provided, the participant will not be heard by anyone. + mute (Optional[bool]): Set to `True` to mute the participant. When using + `canSpeak`, the `mute` parameter is not supported. + + Raises: + NccoActionError: If the `mute` option is used with the `canSpeak` option. + """ + + conversationId: str + canHear: Optional[list[str]] = None + canSpeak: Optional[list[str]] = None + mute: Optional[bool] = None + action: NccoActionType = NccoActionType.TRANSFER + + @model_validator(mode='after') + def validate_mute_and_can_speak(self): + if self.canSpeak and self.mute: + raise NccoActionError( + 'Cannot use mute option if canSpeak option is specified.' + ) + return self diff --git a/voice/tests/test_ncco_actions.py b/voice/tests/test_ncco_actions.py index 4e1a3c75..73764591 100644 --- a/voice/tests/test_ncco_actions.py +++ b/voice/tests/test_ncco_actions.py @@ -325,3 +325,62 @@ def test_notify_options(): 'eventMethod': 'POST', 'action': 'notify', } + + +def test_wait_default_timeout(): + wait = ncco.Wait() + assert wait.model_dump(by_alias=True, exclude_none=True) == { + 'timeout': 10.0, + 'action': 'wait', + } + + +def test_wait_custom_timeout(): + wait = ncco.Wait(timeout=0.5) + assert wait.model_dump(by_alias=True, exclude_none=True) == { + 'timeout': 0.5, + 'action': 'wait', + } + + +def test_wait_timeout_clamped_min_max(): + wait_min = ncco.Wait(timeout=0.01) + assert wait_min.timeout == 0.1 + + wait_max = ncco.Wait(timeout=10000) + assert wait_max.timeout == 7200.0 + + +def test_transfer_basic(): + transfer = ncco.Transfer(conversationId='CON-1234567890') + assert transfer.model_dump(by_alias=True, exclude_none=True) == { + 'conversationId': 'CON-1234567890', + 'action': 'transfer', + } + + +def test_transfer_options(): + transfer = ncco.Transfer( + conversationId='CON-1234567890', + canHear=['leg-a'], + canSpeak=['leg-b', 'leg-c'], + mute=False, + ) + assert transfer.model_dump(by_alias=True, exclude_none=True) == { + 'conversationId': 'CON-1234567890', + 'canHear': ['leg-a'], + 'canSpeak': ['leg-b', 'leg-c'], + 'mute': False, + 'action': 'transfer', + } + + +def test_transfer_mute_with_canspeak_error(): + with raises(NccoActionError) as e: + ncco.Transfer( + conversationId='CON-1234567890', + canSpeak=['leg-a'], + mute=True, + ) + + assert e.match('Cannot use mute option if canSpeak option is specified.')