]> git.ipfire.org Git - ipfire.org.git/commitdiff
Integrate talk.ipfire.org with Asterisk
authorMichael Tremer <michael.tremer@ipfire.org>
Wed, 15 Mar 2017 17:31:17 +0000 (17:31 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 15 Mar 2017 17:32:36 +0000 (17:32 +0000)
The webapp will now connect to Asterisk to find out about
any open channels, etc.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
templates/talk/diagnosis.html
templates/talk/index.html
templates/talk/modules/contact.html [new file with mode: 0644]
templates/talk/modules/ongoing-calls.html
webapp/__init__.py
webapp/backend/asterisk.py [new file with mode: 0644]
webapp/backend/talk.py
webapp/backend/util.py
webapp/handlers_talk.py
webapp/ui_modules.py

index a609e7bae0c20da7a2ec15a3dafabea8d54fef06..052f789316cf87425be8e6b76ba725d61c87b4b1 100644 (file)
@@ -18,6 +18,8 @@
        </section>
 
        {% if current_user.is_admin() %}
+               {% module TalkOngoingCalls() %}
+
                <section id="lines">
                        {% module TalkLines(show_account=True) %}
                </section>
index 51f4cc90547b39229fd476f6aa852ade9b698a94..e2bed372abb331bcd8b0d1ddb7903bfccb8b7dd8 100644 (file)
                </div>
        {% end %}
 
-       {% if current_user and current_user.is_admin() %}
-               {% module TalkOngoingCalls() %}
-       {% else %}
-               {% module TalkOngoingCalls(current_user) %}
-       {% end %}
+       {% module TalkOngoingCalls(current_user) %}
 
        {% module TalkCallLog(current_user) %}
 {% end block %}
diff --git a/templates/talk/modules/contact.html b/templates/talk/modules/contact.html
new file mode 100644 (file)
index 0000000..84b1b6e
--- /dev/null
@@ -0,0 +1,32 @@
+{% if account %}
+       <i class="fa fa-user"></i>
+       <a href="/phonebook/{{ account.uid }}">{{ account.name }}</a>
+       <span class="text-muted">({{ number }})</span>
+
+{% elif number == "900" %}
+       {{ _("Conference Service") }}
+
+{% elif number in ("901", "902", "903", "904", "905", "906", "907", "908", "909") %}
+       {{ _("Conference Room #%s") % number[2] }}
+
+{% elif number in ("980", "981", "982", "983", "984", "985", "986", "987", "988", "989") %}
+       {{ _("Parked Calls Extension") }} <span class="text-muted">({{ number }})</span>
+
+{% elif number == "990" %}
+       {{ _("Voicemail") }}
+
+{% elif number == "991" %}
+       {{ _("Echo Test") }}
+
+{% elif number == "992" %}
+       {{ _("Music") }}
+
+{% elif name and number %}
+       {{ name }} <span class="text-muted">({{ number }})</span>
+
+{% elif number %}
+       {{ number }}
+
+{% else %}
+       <span class="text-muted">{{ _("Unknown") }}</span>
+{% end %}
index 59b468328318e8e179034e4c56f275a32483a12d..d57f6331dc1b1090442bc7fdd19e4dce5306a373 100644 (file)
@@ -1,41 +1,38 @@
-<h3>{{ _("Ongoing Calls") }}</h3>
+{% if channels %}
+       <h3>{{ _("Ongoing Calls") }}</h3>
 
-<table class="table table-hover table-striped">
-       <thead>
-               <tr>
-                       <th>{{ _("Time Started") }}</th>
-                       <th>{{ _("Caller") }}</th>
-                       <th></th>
-                       <th>{{ _("Called") }}</th>
-               </tr>
-       </thead>
-       <tbody>
-               {% for call in calls %}
+       <table class="table table-hover table-striped">
+               <thead>
                        <tr>
-                               <td>{{ locale.format_date(call.time) }}</td>
-                               <td>
-                                       {% if call.caller_account %}
-                                               <a href="/phonebook/{{ call.caller_account.uid }}">{{ call.caller_account.name }}</a>
-                                               <span class="text-muted">({{ call.caller }})</span>
-                                       {% else %}
-                                               {{ call.caller }}
-                                       {% end %}
-                               </td>
-                               <td>
-                                       <span class="glyphicon glyphicon-arrow-right text-success"></span>
-                               </td>
-                               <td>
-                                       {% if call.called_account %}
-                                               <a href="/phonebook/{{ call.called_account.uid }}">{{ call.called_account.name }}</a>
-                                               <span class="text-muted">({{ call.called }})</span>
-                                       {% elif call.called_conference %}
-                                               <a href="/conferences#{{ call.called_conference.no }}">{{ call.called_conference.name }}</a>
-                                               <span class="text-muted">({{ call.called }})</span>
-                                       {% else %}
-                                               {{ call.called }}
-                                       {% end %}
-                               </td>
+                               <th>{{ _("Caller") }}</th>
+                               <th></th>
+                               <th>{{ _("Called") }}</th>
+                               <th>{{ _("Codec") }}</th>
+                               <th class="ar">{{ _("Duration") }}</th>
                        </tr>
-               {% end %}
-       </tbody>
-</table>
+               </thead>
+               <tbody>
+                       {% for c in channels %}
+                               <tr>
+                                       <td>
+                                               {% module TalkContact(c.caller, name=c.caller_name) %}
+                                       </td>
+
+                                       <td>
+                                               <span class="glyphicon glyphicon-arrow-right text-success"></span>
+                                       </td>
+
+                                       <td>
+                                               {% module TalkContact(c.callee) %}
+                                       </td>
+
+                                       <td>
+                                               {{ c.format }}
+                                       </td>
+
+                                       <td class="ar">{{ format_time(c.duration) }}</td>
+                               </tr>
+                       {% end %}
+               </tbody>
+       </table>
+{% end %}
index a6d380baf082e8a06ef7d216b8c527acc8b74ade..b6db358b12f458929d69b4ca6bbf75e5649d0ef6 100644 (file)
@@ -61,6 +61,7 @@ class Application(tornado.web.Application):
                                "FireinfoDeviceTable"     : FireinfoDeviceTableModule,
                                "FireinfoDeviceAndGroupsTable" : FireinfoDeviceAndGroupsTableModule,
                                "FireinfoGeoTable"        : FireinfoGeoTableModule,
+                               "TalkContact"          : TalkContactModule,
                                "TalkCallLog"          : TalkCallLogModule,
                                "TalkLines"            : TalkLinesModule,
                                "TalkOngoingCalls"     : TalkOngoingCallsModule,
diff --git a/webapp/backend/asterisk.py b/webapp/backend/asterisk.py
new file mode 100644 (file)
index 0000000..782a77a
--- /dev/null
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+
+import logging
+import socket
+import time
+
+log = logging.getLogger("asterisk")
+
+class AsteriskManager(object):
+        def __init__(self, address, port=5038, username=None, password=None, timeout=10):
+                self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                self.socket.settimeout(timeout)
+                self.socket.connect((address, port))
+
+                self.conn = self.socket.makefile("rw", 0)
+
+                self._authenticate(username, password)
+
+        def _authenticate(self, username, password):
+                banner = self.conn.readline()
+
+                if not banner.startswith("Asterisk Call Manager"):
+                        raise RuntimeError("Did not connect to an Asterisk here")
+
+                self._send_action("Login", {
+                        "Username" : username,
+                        "Secret"   : password,
+                        "Events"   : "off",
+                })
+
+        def _make_action_id(self):
+                return "%s" % time.time()
+
+        def _send_action(self, action, parameters={}):
+                self._send("Action", action)
+
+                action_id = self._make_action_id()
+                self._send("ActionID", action_id)
+
+                for k, v in parameters.items():
+                        self._send(k, v)
+
+                return self._submit()
+
+        def _send(self, key, value):
+                line = "%s: %s" % (key, value)
+                log.debug("S: %s" % line)
+
+                self.conn.write("%s\r\n" % line)
+
+        def _recv(self):
+                line = self.conn.readline()
+                line = line.rstrip()
+
+                log.debug("R: %s" % line)
+
+                return line
+
+        def _recv_response(self):
+                response = {}
+
+                while True:
+                        # Read one line
+                        line = self._recv()
+
+                        # An empty line signals end of response
+                        if not line:
+                                break
+
+                        k, sep, v = line.partition(": ")
+                        response[k] = v
+
+                return response
+
+        def _submit(self):
+                # End command
+                self.conn.write("\r\n")
+
+                # Read response
+                res = self._recv_response()
+
+                if res["Response"] == "Error":
+                        raise Exception(res["Message"])
+
+                if res.get("EventList") == "start":
+                    events = []
+
+                    while True:
+                            event = self._recv_response()
+
+                            # This is the end of the list
+                            if event.get("EventList") == "Complete":
+                                    break
+
+                            events.append(event)
+
+                    # Return event list
+                    return events
+
+                return res
+
+        def ping(self):
+                """
+                        Sends a ping to asterisk and expects pong
+                """
+                res = self._send_action("Ping")
+
+                return res["Ping"] == "Pong"
+
+        def call(self, caller, callee, caller_id=None, timeout=30000):
+                res = self._send_action("Originate", {
+                        "Channel"  : caller,
+                        "Exten"    : callee,
+                        "Context"  : "from-cli",
+                        "Priority" : 1,
+                        "Timeout"  : timeout,
+                        "CallerID" : caller_id or callee,
+                })
+
+                return res
+
+        def list_channels(self):
+                channels = []
+
+                for c in self._send_action("CoreShowChannels"):
+                        channel = Channel(self, c.get("Channel"))
+                        channels.append(channel)
+
+                return sorted(channels)
+
+        def _mailbox_status(self, mailbox):
+                return self._send_action("MailboxStatus", { "Mailbox" : "%s@default" % mailbox })
+
+        def messages_waiting(self, mailbox):
+                status = self._mailbox_status(mailbox)
+
+                # Get messages waiting
+                waiting = status.get("Waiting", 0)
+
+                return int(waiting)
+
+        def list_peers(self):
+                peers = []
+                
+                for p in self._send_action("SIPPeers"):
+                        peer = Peer(self, p.get("ObjectName"))
+                        peers.append(peer)
+
+                return sorted(peers)
+
+        def list_registry(self):
+                print self._send_action("SIPShowRegistry")
+
+
+class Channel(object):
+        def __init__(self, manager, channel_id):
+                self.manager = manager
+                self.id = channel_id
+
+                self.status = self.manager._send_action("Status", { "Channel" : self.id })[0]
+
+        def __eq__(self, other):
+                return self.id == other.id
+
+        def __lt__(self, other):
+                # Longest first
+                return not self.duration < other.duration
+
+        def __repr__(self):
+                return "<%s %s>" % (self.__class__.__name__, self.id)
+
+        def hangup(self):
+                res = self.manager._send_action("Hangup", { "Channel" : self.id })
+
+                return res["Response"] == "Success"
+
+        @property
+        def type(self):
+                return self.status.get("Type")
+
+        @property
+        def application(self):
+                return self.status.get("Application")
+
+        @property
+        def duration(self):
+                seconds = self.status.get("Seconds", None)
+
+                if seconds is None:
+                        return
+
+                return int(seconds)
+
+        @property
+        def format(self):
+                return "%s/%s" % (
+                        self.status.get("Readformat"),
+                        self.status.get("Writeformat"),
+                )
+
+        @staticmethod
+        def _format_number(number):
+            # Replace 00 by +
+            if number and number.startswith("00"):
+                    return "+%s" % number[2:]
+
+            return number
+
+        @property
+        def caller(self):
+                num = self.status.get("CallerIDNum")
+
+                return self._format_number(num)
+
+        @property
+        def caller_name(self):
+                name = self.status.get("CallerIDName")
+
+                if name in ("", "<unknown>"):
+                        return None
+
+                return name
+
+        @property
+        def callee(self):
+                num = self.status.get("DNID")
+
+                return self._format_number(num)
+
+
+class Peer(object):
+        def __init__(self, manager, peer_id):
+                self.manager = manager
+                self.id = peer_id
+
+                self.data = self.manager._send_action("SIPShowPeer", { "Peer" : self.id })
+
+        def __repr__(self):
+                return "<%s %s>" % (self.__class__.__name__, self.id)
+
+        def __eq__(self, other):
+                return self.id == other.id
+
+        def __lt__(self, other):
+                return self.id < other.id
index cad51d984e96942791ffd321f7d13d999fad598b..d4e43f98f5d1c6579df52b278f2eb45f5bc22ea7 100644 (file)
@@ -2,7 +2,8 @@
 
 import re
 
-import database
+from . import asterisk
+from . import database
 
 from misc import Object
 
@@ -19,6 +20,15 @@ class Talk(Object):
        def db(self):
                return self._db
 
+        def connect_to_asterisk(self):
+                hostname = self.settings.get("asterisk_hostname")
+                username = self.settings.get("asterisk_username")
+                password = self.settings.get("asterisk_password")
+
+                return asterisk.AsteriskManager(
+                        hostname, username=username, password=password
+                )
+
        def get_phonebook(self, account=None):
                accounts = []
                for a in self.accounts.list():
@@ -137,6 +147,18 @@ class Talk(Object):
 
                return self._process_cdr(res)
 
+        def get_channels(self, account=None):
+                a = self.connect_to_asterisk()
+
+                channels = []
+                for c in a.list_channels():
+                        if account and not account.sip_id in (c.caller, c.callee):
+                                continue
+
+                        channels.append(c)
+
+                return sorted(channels)
+
        def get_ongoing_calls(self, account=None, sip_id=None):
                if account and sip_id is None:
                        sip_id = account.sip_id
index bd404ec707761414d92323aabc01197fcb287642..ee9dd60f9882e79ab27fe45b0abd29a654a2ab5c 100644 (file)
@@ -12,7 +12,7 @@ def format_size(s):
 
        return "%.0f%s" % (s, units[i])
 
-def format_time(s):
+def format_time(s, shorter=True):
        #_ = handler.locale.translate
        _ = lambda x: x
 
index 27602e48a5568626bddfc9da891c65f086216717..efb0307e2afe42d52e440806d06a1a10d46c7c82 100644 (file)
@@ -10,13 +10,8 @@ class TalkIndexHandler(BaseHandler):
                call_log = self.talk.get_call_log(self.current_user, limit=6)
                favourite_contacts = self.talk.get_favourite_contacts(self.current_user)
 
-               if self.current_user.is_admin():
-                       ongoing_calls = self.talk.get_ongoing_calls()
-               else:
-                       ongoing_calls = self.talk.get_ongoing_calls(self.current_user)
-
                self.render("talk/index.html", call_log=call_log,
-                       favourite_contacts=favourite_contacts, ongoing_calls=ongoing_calls)
+                       favourite_contacts=favourite_contacts)
 
 
 class TalkPhonebookHandler(BaseHandler):
index d70d1324cfba96ec65437d0cb631a7235e994032..f72f65abcd77ba7202843dcf66b31f0e0bb2f683 100644 (file)
@@ -328,6 +328,14 @@ class ProgressBarModule(UIModule):
                        colour=colour, value=value)
 
 
+class TalkContactModule(UIModule):
+        def render(self, number, name=None):
+                account = self.accounts.get_by_sip_id(number)
+
+                return self.render_string("talk/modules/contact.html",
+                        account=account, number=number, name=name)
+
+
 class TalkCallLogModule(UIModule):
        def render(self, account=None, viewer=None):
                if (account is None or not self.current_user == account) \
@@ -356,18 +364,15 @@ class TalkLinesModule(UIModule):
 
 
 class TalkOngoingCallsModule(UIModule):
-       def render(self, account=None):
+       def render(self, account=None, debug=False):
                if (account is None or not self.current_user == account) \
                                and not self.current_user.is_admin():
                        raise RuntimeException("Insufficient permissions")
 
-               calls = self.talk.get_ongoing_calls(account)
-
-               if calls:
-                       return self.render_string("talk/modules/ongoing-calls.html",
-                               calls=calls)
+               channels = self.talk.get_channels()
 
-               return ""
+               return self.render_string("talk/modules/ongoing-calls.html",
+                       account=account, channels=channels, debug=debug)
 
 
 class TrackerPeerListModule(UIModule):