Skip to content

Commit

Permalink
Major refactor for AMI v2 and new bridging.
Browse files Browse the repository at this point in the history
- Upgrade event handling to work with AMI v2 (Asterisk 12+).
- Upgrade internal structure to work with new bridging system (no more
masquerades!).
- Refactor outside API to pass all channel data to reporters, not just
CallerID data.
- Refactor internal structure to better distinguish between Asterisk
internals and usage opinions.
  • Loading branch information
Grendel7 committed Jul 26, 2018
1 parent edc7908 commit 3f72639
Show file tree
Hide file tree
Showing 116 changed files with 10,402 additions and 18,772 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tests/.coverage
build/
tests/report/
cover/
htmlcov/

.coverage
coverage.xml
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
- Luit
- JorisE
- HansAdema
- hafkensite
30 changes: 21 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
# Changelog

## 0.5.0 - AMI v2

- Upgrade Cacofonisk to work with AMI v2 (Asterisk 12+). **WARNING**: This
change is not backwards compatible. If you use Asterisk <11, you can keep using
version 0.4.0.
- Rename ChannelManager to EventHandler and decouple Asterisk channel tracking
from (opinionated) call tracking.
- Reporters now get immutable copies of Channels rather than CallerId objects,
so it's possible to analyze more channel attributes in the reporter.
- `trace_msg` and `trace_ami` have been removed in favor of proper logging
facilities.

## 0.4.0 - ConnectAB

- Add support for calls where Asterisk calls and connects both parties.

## 0.3.0 - Fixed Destinations

- Calls to external phone numbers (rather than just phone accounts) are now
- Calls to external phone numbers (rather than just phone accounts) are now
tracked correctly.
- The `on_transfer` hook was split to `on_warm_transfer` (for attended
transfers) and `on_cold_transfer` (for blind and blonde transfers), with
- The `on_transfer` hook was split to `on_warm_transfer` (for attended
transfers) and `on_cold_transfer` (for blind and blonde transfers), with
different method signatures.
- The `on_b_dial` events for a single call have been merged. If multiple
destinations start to ring for a single call, one `on_b_dial` event will be
- The `on_b_dial` events for a single call have been merged. If multiple
destinations start to ring for a single call, one `on_b_dial` event will be
triggered with a list of CallerId objects.
- The `on_pickup` event was removed. You can compare the data from the
- The `on_pickup` event was removed. You can compare the data from the
`on_b_dial` call with the `on_up` call to see whether the callee's phone rang.

## 0.2.2 - Cancelled Calls

- Fix issue where calls which were hung up by the caller before being answered
- Fix issue where calls which were hung up by the caller before being answered
were not tracked correctly.

## 0.2.1 - Call Confirmation
Expand All @@ -30,12 +42,12 @@ before the call was patched through (like pressing a button).

## 0.2.0 - Queues

- Calls passed through the Queue app are now tracked correctly (requires the
- Calls passed through the Queue app are now tracked correctly (requires the
`eventwhencalled` flag to be enabled).

## 0.1.0 - Ups and Downs

- Major refactor of the existing code.
- Add `on_up` and `on_hangup` events.
- Add `call_id` to uniquely identify a call (based on the source channel's
- Add `call_id` to uniquely identify a call (based on the source channel's
UniqueId, just like Asterisk's LinkedId).
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 Devhouse Spindle
Copyright (c) 2018 Devhouse Spindle

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
75 changes: 39 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Cacofonisk

Cacofonisk is a framework that connects to the Asterisk PBX, listens to events
on the Asterisk Management Interface (AMI) and tracks the status of calls
Cacofonisk is a framework that connects to the Asterisk PBX, listens to events
on the Asterisk Management Interface (AMI) and tracks the status of calls
currently in progress in Asterisk.

Cacofonisk takes a stream of AMI events as input and uses these to keep track
of the channels currently active in Asterisk and how they are related. When
Cacofonisk takes a stream of AMI events as input and uses these to keep track
of the channels currently active in Asterisk and how they are related. When
something interesting happens to one of the channels, it will call a method on
a call state Reporter with interesting information about the call, like who is
a call state Reporter with interesting information about the call, like who is
in the call, and a unique identifier.

This data can then be used to send webhooks regarding a call, to notify a
This data can then be used to send webhooks regarding a call, to notify a
person who is being called, or to log calls being performed.

## Status

This product is actively being developed and used at VoIPGRID.
Expand All @@ -23,6 +23,7 @@ This product is actively being developed and used at VoIPGRID.

- Python >= 3.4
- Panoramisk 1.x
- Asterisk >= 12

### Installation

Expand All @@ -42,54 +43,56 @@ $ python3 setup.py install

To run Cacofonisk, you will need two things: a Runner and a Reporter.

A Runner is a class which is responsible for passing AMI events to the Cacofonisk. Two runners are included: an AmiRunner (which connects to the Asterisk Management Interface) and a FileRunner (which imports AMI events from a JSON file).
A Runner is a class which is responsible for passing AMI events to the
Cacofonisk. Two runners are included: an AmiRunner (which connects to the
Asterisk Management Interface) and a FileRunner (which imports AMI events from
a JSON file).

A Reporter is a class which takes the interesting data from Cacofonisk and does awesome things with it. Two reports have been included: a DebugReporter (which just dumps the data to stdout) and a JsonReporter (which creates JSON files for the FileRunner).
A Reporter is a class which takes the interesting data from Cacofonisk and does
awesome things with it. You can find various Reporters in the `examples`
folder.

To create your own reporter, you can extend the BaseReport class and implement your own event handlers, like so:
To create your own reporter, you can extend the BaseReporter class and
implement your own event handlers, like so:

```python
from cacofonisk import AmiRunner, BaseReporter


class ReportAllTheThings(BaseReporter):

def on_b_dial(self, call_id, caller, to_number, targets):
callee_codes = [target.code for target in targets]
caller_number = caller.number
print("{} is now ringing {} on number {}".format(
caller_number, ', '.join(callee_codes), to_number,
def on_b_dial(self, caller, targets):
target_channels = [target.name for target in targets]
caller_number = caller.caller_id.num
print("{} is now calling {}".format(
caller_number, ', '.join(target_channels),
))

def on_up(self, call_id, caller, to_number, callee):
callee_account_code = callee.code
caller_number = caller.number
print("{} is now in conversation with {}".format(caller_number, callee_account_code))

def on_warm_transfer(self, call_id, merged_id, redirector, caller, destination):
print('{} is now calling with {} (was calling {})'.format(caller, destination, redirector))

def on_cold_transfer(self, call_id, merged_id, redirector, caller, to_number, targets):
print('{} tried to transfer the call from {} to number {} (ringing {})'.format(
redirector, caller, to_number, ', '.join(targets),
))
def on_up(self, caller, target):
target_number = target.caller_id.num
caller_number = caller.caller_id.num
print("{} is now in conversation with {}".format(caller_number, target_number))

def on_hangup(self, caller, reason):
caller_number = caller.caller_id.num
print("{} is no longer calling (reason: {})".format(caller_number, reason))

def on_hangup(self, call_id, caller, to_number, reason):
print("{} is no longer calling number {} (reason: {})".format(caller, to_number, reason))


reporter = ReportAllTheThings()
runner = AmiRunner([
{'host': '127.0.0.1', 'username': 'cacofonisk', 'password': 'bard', 'port': 5038},
], reporter)
runner = AmiRunner(['tcp://username:[email protected]:5038'], reporter)
runner.run()
```

This reporter can then be passed to a Runner of your choice to process AMI events.
This reporter can then be passed to a Runner of your choice to process AMI
events.

For more information about the parameters of the reporter, please see the docs in BaseReporter.
For more information about the parameters of the reporter, please see the docs
in BaseReporter.

You can also listen for [UserEvents](https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+Application_UserEvent) using the `on_user_event` function. This can be used to pass additional data from Asterisk to your Cacofonisk application.
You can also listen for
[UserEvents](https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+Application_UserEvent)
using the `on_user_event` function. This can be used to pass additional data
from Asterisk to your Cacofonisk application.

#### Running the tests

Expand Down
8 changes: 2 additions & 6 deletions cacofonisk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
from .handlers import EventHandler
from .reporters import BaseReporter, LoggingReporter
from .runners.ami_runner import AmiRunner
from .runners.file_runner import FileRunner

from .reporters.base_reporter import BaseReporter
from .reporters.debug_reporter import DebugReporter
from .reporters.json_reporter import JsonReporter

from .channel import ChannelManager
60 changes: 60 additions & 0 deletions cacofonisk/bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
class MissingBridgeUniqueid(KeyError):
pass


class Bridge(object):
"""
The Bridge object represents a Bridge in Asterisk.
With Asterisk 12+, Asterisk creates bridges and puts channels into them to
make audio flow between the channels. This class is a Python representation
of such bridges.
"""

def __init__(self, event):
"""
Create a new bridge object.
Args:
event (Event): A BridgeCreate event.
"""
self.uniqueid = event['BridgeUniqueid']
self.type = event['BridgeType']
self.technology = event['BridgeTechnology']
self.creator = event['BridgeCreator']
self.video_source_mode = event['BridgeVideoSourceMode']

self.peers = set()

def __len__(self):
"""
Get the number of channels in this bridge.
Returns:
int: The number of channels in this bridge.
"""
return len(self.peers)

def __repr__(self):
"""
Get a textual representation of this bridge.
Returns:
str: A representation of this bridge.
"""
return '<Bridge(id={self.uniqueid},peers={peers})>'.format(
self=self,
peers=','.join([chan.name for chan in self.peers]),
)


class BridgeDict(dict):
"""
A dict which raises a MissingBridgeUniqueid exception if a key is missing.
"""

def __getitem__(self, item):
try:
return super(BridgeDict, self).__getitem__(item)
except KeyError:
raise MissingBridgeUniqueid(item)
50 changes: 22 additions & 28 deletions cacofonisk/callerid.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,43 @@
"""
CallerId holds information about one end of a call.
The users of this application are interested in tuples of caller ID
number, caller ID name and sometimes an account ID (accountcode), and
it's privacy settings, both for call initiators and for call recipients.
"""
from collections import namedtuple


class CallerId(namedtuple('CallerIdBase', 'code name number is_public')):
class CallerId(namedtuple('CallerIdBase', 'name num')):
"""
An immutable CallerId class.
CallerId holds immutable information about one end of a call.
The users of this application are interested in tuples of caller ID number,
caller ID name and sometimes an account ID (accountcode), and it's privacy
settings, both for call initiators and for call recipients.
Usage::
caller = CallerId(name='My name', number='+311234567', is_public=True)
caller = caller.replace(code=123456789)
"""
def __new__(cls, code=0, name='', number=None, is_public=None):
return super().__new__(cls, code, name, number, is_public)

def __new__(cls, name='', num=''):
if name == '<unknown>':
name = ''

if num == '<unknown>':
num = ''

return super().__new__(cls, name, str(num))

def replace(self, **kwargs):
"""
Return a new CallerId instance replacing specified fields with
new values.
Create a copy of this CallerId with specified changes.
Args:
**kwargs: One or more of code, name, number, is_public.
Returns:
CallerId: A new instance with replaced values.
"""
# The method already exists on the namedtuple. We simply make it
# public.
return self._replace(**kwargs)
if 'name' in kwargs and kwargs['name'] == '<unknown>':
kwargs['name'] = ''

def _is_public_tag(self):
if self.is_public is None:
return ''
elif self.is_public:
return ';pub'
else:
return ';priv'

def __str__(self):
return '"{}" <{}{};code={}>'.format(
self.name.replace('\\', '\\\\').replace('"', '\\"'),
self.number, self._is_public_tag(), self.code)
if 'number' in kwargs and kwargs['number'] == '<unknown>':
kwargs['number'] = ''

return self._replace(**kwargs)
Loading

0 comments on commit 3f72639

Please sign in to comment.