Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docs for 2.0.0 #205

Merged
merged 9 commits into from
Jan 11, 2024
38 changes: 23 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pyVoIP
PyVoIP is a pure python VoIP/SIP/RTP library. Currently, it supports PCMA, PCMU, and telephone-event.
PyVoIP is a pure python VoIP/SIP/RTP library. Currently, it supports PCMA, PCMU, and telephone-event.

This library does not depend on a sound library, i.e. you can use any sound library that can handle linear sound data i.e. pyaudio or even wave. Keep in mind PCMU/PCMA only supports 8000Hz, 1 channel, 8 bit audio.
This library does not depend on a sound library, i.e. you can use any sound library that can handle linear sound data such as pyaudio or even wave. Keep in mind PCMU/PCMA only supports 8000Hz, 1 channel, 8 bit audio.

## Getting Started
Simply run `pip install pyVoIP`, or if installing from source:
Expand All @@ -18,20 +18,28 @@ Don't forget to check out [the documentation](https://pyvoip.readthedocs.io/)!
This basic code will simple make a phone that will automatically answer then hang up.

```python
from pyVoIP.VoIP import VoIPPhone, InvalidStateError

def answer(call): # This will be your callback function for when you receive a phone call.
try:
call.answer()
call.hangup()
except InvalidStateError:
pass

from pyVoIP.credentials import CredentialsManager
from pyVoIP.VoIP.call import VoIPCall
from pyVoIP.VoIP.error import InvalidStateError
from pyVoIP.VoIP.phone import VoIPPhone, VoIPPhoneParamter

class Call(VoIPCall):

def ringing(self, invite_request):
try:
self.answer()
self.hangup()
except InvalidStateError:
pass

if __name__ == "__main__":
phone=VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, callCallback=answer, myIP=<Your computer's local IP>, sipPort=<Port to use for SIP (int, default 5060)>, rtpPortLow=<low end of the RTP Port Range>, rtpPortHigh=<high end of the RTP Port Range>)
phone.start()
input('Press enter to disable the phone')
phone.stop()
cm = CredentialsManager()
cm.add(<SIP server username>, <SIP server password>)
params = VoIPPhoneParamter(<SIP server IP>, <SIP server port>, <SIP server user>, cm, bind_ip=<Your computers local IP>, call_class=Call)
phone = VoIPPhone(params)
phone.start()
input('Press enter to disable the phone')
phone.stop()
```

### Sponsors
Expand Down
28 changes: 28 additions & 0 deletions docs/Credentials.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Credentials
###########

Since SIP requests can traverse multiple servers and can receive multiple challenges, the Credentials Manager was made to store multiple passwords and pyVoIP will use the appropriate password upon request.

Per `RFC 3261 Section 22.1 <https://www.rfc-editor.org/rfc/rfc3261.html#section-22.1>`_, SIP uses authentication similar to HTTP authentication (:RFC:`2617`), with the main difference being ``The realm string alone defines the protection domain.``. However, some services always use the same domain. For example, if you need to authenticate with two seperate Asterisk servers, the realm will almost certainly be ``asterisk`` for both, despite being otherwise unrelated servers. For that reason, the Credentials Manager also supports server filtering.

.. _CredentialsManager:

CredentialsManager
==================

*class* pyVoIP.credentials.\ **CredentialsManager**\ ()
**add**\ (username: str, password: str, server: Optional[str] = None, realm: Optional[str] = None, user: Optional[str] = None) -> None
This method registers a username and password combination with the Credentials Manager.

The *username* argument is the username that will be used in the Authentication header and digest calculation.

The *password* argument is the password that will be used in the Authentication header digest calculation.

The *server* argument is used to determine the correct credentials when challenged for authentication. If *server* is left as ``None``, the credentials may be selected with any server.

The *realm* argument is used to determine the correct credentials when challenged for authentication. If *realm* is left as ``None``, the credentials may be selected with any realm.

The *user* argument is used to determine the correct credentials when challenged for authentication. If *user* is left as ``None``, the credentials may be selected with any user. The *user* argument is the user in the SIP URI, **not** the username used in authentication.

**get**\ (server: str, realm: str, user: str) -> Dict[str, str]
Looks for credentials that match the server, realm, and user in that order. If no matchng credentials are found, this will return anonymous credentials as a server MAY accept them per `RFC 3261 Section 22.1 <https://www.rfc-editor.org/rfc/rfc3261.html#section-22.1>`_.
78 changes: 51 additions & 27 deletions docs/Examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ Here we will go over a few basic phone setups.
Setup
*****

PyVoIP uses :ref:`VoIPPhone` child class to initiate phone calls. In the example below, our ringing function is named ``Call.ringing``.
PyVoIP uses a :ref:`VoIPPhone` class to receive and initiate phone calls. The settings for our phone are passed via the :ref:`VoIPPhoneParameter` dataclass. When a call is received, a new instance of a :ref:`VoIPCall` is initialized. You can overwrite this class in initialization of VoIPPhone.

We are also importing :ref:`VoIPPhone` and :ref:`InvalidStateError<invalidstateerror>`. VoIPPhone is the main class for our `softphone <https://en.wikipedia.org/wiki/Softphone>`_. An InvalidStateError is thrown when you try to perform an impossible command. For example, denying the call when the phone is already answered, answering when it's already answered, etc.
In this example, we are importing :ref:`CredentialsManager`, :ref:`VoIPPhone`, :ref:`VoIPPhoneParameter`, :ref:`VoIPCall`, and :ref:`InvalidStateError<InvalidStateError>`. :ref:`CredentialsManager` stores and retreives passwords for authentication with registrars. :ref:`VoIPPhone` is the main class for our `softphone <https://en.wikipedia.org/wiki/Softphone>`_. :ref:`VoIPPhoneParameter` is the settings for our :ref:`VoIPPhone`. :ref:`VoIPCall` will be used to create our custom answering class. An :ref:`InvalidStateError<InvalidStateError>` is thrown when you try to perform an impossible command. For example, denying the call when the phone is already answered, answering when it's already answered, etc.

The following will create a phone that answers and automatically hangs up:

.. code-block:: python

from pyVoIP.VoIP import VoIPPhone, VoIPCall, InvalidStateError
from pyVoIP.credentials import CredentialsManager
from pyVoIP.VoIP.call import VoIPCall
from pyVoIP.VoIP.error import InvalidStateError
from pyVoIP.VoIP.phone import VoIPPhone, VoIPPhoneParamter

class Call(VoIPCall):

Expand All @@ -26,19 +29,25 @@ The following will create a phone that answers and automatically hangs up:
pass

if __name__ == "__main__":
phone = VoIPPhone(<SIP server IP>, <SIP server port>, <SIP server username>, <SIP server password>, bind_ip=<Your computer's local IP>, callClass=Call)
cm = CredentialsManager()
cm.add(<SIP server username>, <SIP server password>)
params = VoIPPhoneParamter(<SIP server IP>, <SIP server port>, <SIP server user>, cm, bind_ip=<Your computers local IP>, call_class=Call)
phone = VoIPPhone(params)
phone.start()
input('Press enter to disable the phone')
phone.stop()

Announcement Board
******************

Let's say you want to make a phone that when you call it, it plays an announcement message, then hangs up. We can accomplish this with the builtin libraries `wave <https://docs.python.org/3/library/wave.html>`_, `audioop <https://docs.python.org/3/library/audioop.html>`_, `time <https://docs.python.org/3/library/time.html>`_, and by importing :ref:`CallState<callstate>`.
Let's say you want to make a phone that when you call it, it plays an announcement message, then hangs up. We can accomplish this with the builtin libraries `wave <https://docs.python.org/3/library/wave.html>`_, `audioop <https://docs.python.org/3/library/audioop.html>`_, `time <https://docs.python.org/3/library/time.html>`_, and by importing :ref:`CallState<callstate>`.

.. code-block:: python

from pyVoIP.VoIP import VoIPPhone, VoIPCall, InvalidStateError, CallState
from pyVoIP.credentials import CredentialsManager
from pyVoIP.VoIP.call import VoIPCall
from pyVoIP.VoIP.error import InvalidStateError
from pyVoIP.VoIP.phone import VoIPPhone, VoIPPhoneParamter
import time
import wave

Expand All @@ -65,12 +74,15 @@ Let's say you want to make a phone that when you call it, it plays an announceme
call.hangup()

if __name__ == "__main__":
phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, bind_ip=<Your computers local IP>, callClass=Call)
cm = CredentialsManager()
cm.add(<SIP server username>, <SIP server password>)
params = VoIPPhoneParamter(<SIP server IP>, <SIP server port>, <SIP server user>, cm, bind_ip=<Your computer's local IP>, call_class=Call)
phone = VoIPPhone(params)
phone.start()
input('Press enter to disable the phone')
phone.stop()

Something important to note is our wait function. We are currently using:
Something important to note is our wait function. We are currently using:

.. code-block:: python

Expand All @@ -79,18 +91,21 @@ Something important to note is our wait function. We are currently using:
while time.time() <= stop and call.state == CallState.ANSWERED:
time.sleep(0.1)

This could be replaced with ``time.sleep(frames / 8000)``. However, doing so will not cause the thread to automatically close if the user hangs up, or if ``VoIPPhone().stop()`` is called; using the while loop method will fix this issue. The ``time.sleep(0.1)`` inside the while loop is also important. Supplementing ``time.sleep(0.1)`` for ``pass`` will cause your CPU to ramp up while running the loop, making the RTP (audio being sent out and received) lag. This can make the voice audibly slow or choppy.
This could be replaced with ``time.sleep(frames / 8000)``. However, doing so will not cause the thread to automatically close if the user hangs up, or if ``VoIPPhone().stop()`` is called. Using the while loop method will fix this issue. The ``time.sleep(0.1)`` inside the while loop is also important. Supplementing ``time.sleep(0.1)`` for ``pass`` will cause your CPU to ramp up while running the loop, making the RTP (audio being sent out and received) lag. This can make the voice audibly slow or choppy.

*Note: Audio must be 8 bit, 8000Hz, and Mono/1 channel. You can accomplish this in a free program called* `Audacity <https://www.audacityteam.org/>`_. *To make an audio recording Mono, go to Tracks > Mix > Mix Stereo Down to Mono. To make an audio recording 8000 Hz, go to Tracks > Resample... and select 8000, then ensure that your 'Project Rate' in the bottom left is also set to 8000. To make an audio recording 8 bit, go to File > Export > Export as WAV, then change 'Save as type:' to 'Other uncompressed files', then set 'Header:' to 'WAV (Microsoft)', then set the 'Encoding:' to 'Unsigned 8-bit PCM'*
**Important Note:** *Audio must be 8 bit, 8000Hz, and Mono/1 channel. You can accomplish this in a free program called* `Audacity <https://www.audacityteam.org/>`_. *To make an audio recording Mono, go to Tracks > Mix > Mix Stereo Down to Mono. To make an audio recording 8000 Hz, go to Tracks > Resample... and select 8000, then ensure that your 'Project Rate' in the bottom left is also set to 8000. To make an audio recording 8 bit, go to File > Export > Export as WAV, then change 'Save as type:' to 'Other uncompressed files', then set 'Header:' to 'WAV (Microsoft)', then set the 'Encoding:' to 'Unsigned 8-bit PCM'*

IVR/Phone Menus
****************

We can use the following code to create `IVR Menus <https://en.wikipedia.org/wiki/Interactive_voice_response>`_. Currently, we cannot make 'breaking' IVR menus. Breaking IVR menus in this context means, a user selecting an option mid-prompt will cancel the prompt, and start the next action. Support for breaking IVR's will be made in the future. For now, here is the code for a non-breaking IVR:
We can use the following code to create `IVR Menus <https://en.wikipedia.org/wiki/Interactive_voice_response>`_. Currently, we cannot make 'breaking' IVR menus. Breaking IVR menus in this context means, a user selecting an option mid-prompt will cancel the prompt, and start the next action. Support for breaking IVR's will be made in the future. For now, here is the code for a non-breaking IVR:

.. code-block:: python

from pyVoIP.VoIP import VoIPPhone, VoIPCall, InvalidStateError, CallState
from pyVoIP.credentials import CredentialsManager
from pyVoIP.VoIP.call import VoIPCall
from pyVoIP.VoIP.error import InvalidStateError
from pyVoIP.VoIP.phone import VoIPPhone, VoIPPhoneParamter
import time
import wave

Expand All @@ -109,56 +124,65 @@ We can use the following code to create `IVR Menus <https://en.wikipedia.org/wik
while call.state == CallState.ANSWERED:
dtmf = call.get_dtmf()
if dtmf == "1":
# Do something
call.hangup()
if call.transfer("sales") # Transfer to same registrar
return
elif dtmf == "2":
# Do something else
call.hangup()
if call.transfer(uri="<100@different_regisrar.com>")
return
time.sleep(0.1)
except InvalidStateError:
pass
except:
call.hangup()

if __name__ == '__main__':
phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, bind_ip=<Your computers local IP>, callClass=Call)
cm = CredentialsManager()
cm.add(<SIP server username>, <SIP server password>)
params = VoIPPhoneParamter(<SIP server IP>, <SIP server port>, <SIP server user>, cm, bind_ip=<Your computer's local IP>, call_class=Call)
phone = VoIPPhone(params)
phone.start()
input('Press enter to disable the phone')
phone.stop()

Please note that ``get_dtmf()`` is actually ``get_dtmf(length=1)``, and as it is technically an ``io.StringBuffer()``, it will return ``""`` instead of ``None``. This may be important if you wanted an 'if anything else, do that' clause. Lastly, VoIPCall stores all DTMF keys pressed since the call was established; meaning, users can press any key they want before the prompt even finishes, or may press a wrong key before the prompt even starts.
Please note that ``get_dtmf()`` is actually ``get_dtmf(length=1)``, and as it is technically an ``io.StringBuffer()``, it will return ``""`` instead of ``None``. This may be important if you wanted an 'if anything else, do that' clause. Lastly, VoIPCall stores all DTMF keys pressed since the call was established; meaning, users can press any key they want before the prompt even finishes, or may press a wrong key before the prompt even starts.

Call state handling for outgoing calls
**************************************
Call State Handling
*******************

We can use the following code to handle various states for the outgoing calls:
We can use the following code to handle various states for calls:

.. code-block:: python

from pyVoIP.VoIP import VoIPPhone, VoIPCall, InvalidStateError, CallState
from pyVoIP.credentials import CredentialsManager
from pyVoIP.VoIP.call import VoIPCall
from pyVoIP.VoIP.error import InvalidStateError
from pyVoIP.VoIP.phone import VoIPPhone, VoIPPhoneParamter
import time
import wave

class Call(VoIPCall):

def progress(self, request):
print('Progress')
super().progress(request)
print('Progress')
super().progress(request)

def busy(self, request):
print('Call ended - callee is busy')
super().progress(request)
super().busy(request)

def answered(self, request):
print('Answered')
print('Answered')
super().answered()

def bye(self):
print('Bye')
super().bye()

if __name__ == '__main__':
phone = VoIPPhone(<SIP Server IP>, <SIP Server Port>, <SIP Server Username>, <SIP Server Password>, bind_ip=<Your computers local IP>, callClass=Call)
cm = CredentialsManager()
cm.add(<SIP server username>, <SIP server password>)
params = VoIPPhoneParamter(<SIP server IP>, <SIP server port>, <SIP server user>, cm, bind_ip=<Your computer's local IP>, call_class=Call)
phone = VoIPPhone(params)
phone.start()
phone.call(<Phone Number>)
input('Press enter to disable the phone\n')
Expand Down
42 changes: 40 additions & 2 deletions docs/Globals.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
Globals
#######

Global Variables
########
****************

There are a few global variables that may assist you if you're having problems with the library.

pyVoIP.\ **DEBUG** = False
If set to true, pyVoIP will print debug messages that may be useful if you need to open a GitHub issue. Otherwise, does nothing.
If set to true, pyVoIP will print debug messages that may be useful if you need to troubleshoot or open a GitHub issue.

pyVoIP.\ **TRANSMIT_DELAY_REDUCTION** = 0.0
The higher this variable is, the more often RTP packets are sent. This *should* only ever need to be 0.0. However, when testing on Windows, there has sometimes been jittering, setting this to 0.75 fixed this in testing, but you may need to tinker with this number on a per-system basis.

pyVoIP.\ **ALLOW_BASIC_AUTH** = False
Controls whether Basic authentication (:RFC:`7617`) is allowed for SIP authentication. Basic authentication is deprecated as it will send your password in plain-text, likely in the clear (unencrypted) as well. As such this is disabled be default.

pyVoIP.\ **ALLOW_MD5_AUTH** = True
MD5 Digest authentication is deprecated per `RFC 8760 Section 3 <https://tools.ietf.org/html/rfc8760#section-3>`_ as it a weak hash. However, it is still used often so it is enabled by default.

pyVoIP.\ **REGISTER_FAILURE_THRESHOLD** = 3
If registration fails this many times, VoIPPhone's status will be set to FAILED and the phone will stop.

pyVoIP.\ **ALLOW_TLS_FALLBACK** = False
If this is set to True TLS will fall back to TCP if the TLS handshake fails. This is off by default, as it would be irresponsible to have a security feature disabled by default.

This feature is currently not implemented.

pyVoIP.\ **TLS_CHECK_HOSTNAME** = True
Is used to create SSLContexts. See Python's documentation on `check_hostname <https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname>`_ for more details.

You should use the :ref:`set_tls_security <set_tls_security>` function to change this variable.

pyVoIP.\ **TLS_VERIFY_MODE** = True
Is used to create SSLContexts. See Python's documentation on `verify_mode <https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode>`_ for more details.

You should use the :ref:`set_tls_security <set_tls_security>` function to change this variable.

pyVoIP.\ **SIP_STATE_DB_LOCATION** = ":memory:"
This variable allows you to save the SIP message state database to a file instead of storing it in memory which is the default. This is useful for debugging, however pyVoIP does not delete the database afterwards which will cause an Exception upon restarting pyVoIP. For this reason, we recommend you do not change this variable in production.

Global Functions
****************

.. _set_tls_security:

pyVoIP.\ **set_tls_security**\ (verify_mode: `VerifyMode <https://docs.python.org/3/library/ssl.html?highlight=ssl#ssl.VerifyMode>`_) -> None
This method ensures that TLS_CHECK_HOSTNAME and TLS_VERIFY_MODE are set correctly depending on the TLS certificate verification settings you want to use.
Loading