From: Michael Tremer Date: Wed, 15 Mar 2017 17:31:17 +0000 (+0000) Subject: Integrate talk.ipfire.org with Asterisk X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5ac74b02acb671ba1b32af78deb1a3d8eb891473;p=ipfire.org.git Integrate talk.ipfire.org with Asterisk The webapp will now connect to Asterisk to find out about any open channels, etc. Signed-off-by: Michael Tremer --- diff --git a/templates/talk/diagnosis.html b/templates/talk/diagnosis.html index a609e7ba..052f7893 100644 --- a/templates/talk/diagnosis.html +++ b/templates/talk/diagnosis.html @@ -18,6 +18,8 @@ {% if current_user.is_admin() %} + {% module TalkOngoingCalls() %} +
{% module TalkLines(show_account=True) %}
diff --git a/templates/talk/index.html b/templates/talk/index.html index 51f4cc90..e2bed372 100644 --- a/templates/talk/index.html +++ b/templates/talk/index.html @@ -44,11 +44,7 @@ {% 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 index 00000000..84b1b6e6 --- /dev/null +++ b/templates/talk/modules/contact.html @@ -0,0 +1,32 @@ +{% if account %} + + {{ account.name }} + ({{ number }}) + +{% 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") }} ({{ number }}) + +{% elif number == "990" %} + {{ _("Voicemail") }} + +{% elif number == "991" %} + {{ _("Echo Test") }} + +{% elif number == "992" %} + {{ _("Music") }} + +{% elif name and number %} + {{ name }} ({{ number }}) + +{% elif number %} + {{ number }} + +{% else %} + {{ _("Unknown") }} +{% end %} diff --git a/templates/talk/modules/ongoing-calls.html b/templates/talk/modules/ongoing-calls.html index 59b46832..d57f6331 100644 --- a/templates/talk/modules/ongoing-calls.html +++ b/templates/talk/modules/ongoing-calls.html @@ -1,41 +1,38 @@ -

{{ _("Ongoing Calls") }}

+{% if channels %} +

{{ _("Ongoing Calls") }}

- - - - - - - - - - - {% for call in calls %} +
{{ _("Time Started") }}{{ _("Caller") }}{{ _("Called") }}
+ - - - - + + + + + - {% end %} - -
{{ locale.format_date(call.time) }} - {% if call.caller_account %} - {{ call.caller_account.name }} - ({{ call.caller }}) - {% else %} - {{ call.caller }} - {% end %} - - - - {% if call.called_account %} - {{ call.called_account.name }} - ({{ call.called }}) - {% elif call.called_conference %} - {{ call.called_conference.name }} - ({{ call.called }}) - {% else %} - {{ call.called }} - {% end %} - {{ _("Caller") }}{{ _("Called") }}{{ _("Codec") }}{{ _("Duration") }}
+ + + {% for c in channels %} + + + {% module TalkContact(c.caller, name=c.caller_name) %} + + + + + + + + {% module TalkContact(c.callee) %} + + + + {{ c.format }} + + + {{ format_time(c.duration) }} + + {% end %} + + +{% end %} diff --git a/webapp/__init__.py b/webapp/__init__.py index a6d380ba..b6db358b 100644 --- a/webapp/__init__.py +++ b/webapp/__init__.py @@ -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 index 00000000..782a77a9 --- /dev/null +++ b/webapp/backend/asterisk.py @@ -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 ("", ""): + 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 diff --git a/webapp/backend/talk.py b/webapp/backend/talk.py index cad51d98..d4e43f98 100644 --- a/webapp/backend/talk.py +++ b/webapp/backend/talk.py @@ -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 diff --git a/webapp/backend/util.py b/webapp/backend/util.py index bd404ec7..ee9dd60f 100644 --- a/webapp/backend/util.py +++ b/webapp/backend/util.py @@ -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 diff --git a/webapp/handlers_talk.py b/webapp/handlers_talk.py index 27602e48..efb0307e 100644 --- a/webapp/handlers_talk.py +++ b/webapp/handlers_talk.py @@ -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): diff --git a/webapp/ui_modules.py b/webapp/ui_modules.py index d70d1324..f72f65ab 100644 --- a/webapp/ui_modules.py +++ b/webapp/ui_modules.py @@ -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):