1"""
2FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
3Copyright (C) 2005-2014, Anthony Minessale II <anthm@freeswitch.org>
4
5Version: MPL 1.1
6
7The contents of this file are subject to the Mozilla Public License Version
81.1 (the "License"); you may not use this file except in compliance with
9the License. You may obtain a copy of the License at
10http://www.mozilla.org/MPL/
11
12Software distributed under the License is distributed on an "AS IS" basis,
13WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
14for the specific language governing rights and limitations under the
15License.
16
17The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
18
19The Initial Developer of the Original Code is
20Anthony Minessale II <anthm@freeswitch.org>
21Portions created by the Initial Developer are Copyright (C)
22the Initial Developer. All Rights Reserved.
23
24Contributor(s): Traun Leyden <tleyden@branchcut.com>
25"""
26
27import sys
28
29from twisted.internet import reactor, defer
30from twisted.protocols.basic import LineReceiver
31from twisted.internet.protocol import Protocol, ClientFactory
32from twisted.python import failure
33import time, re
34from time import strftime
35from Queue import Queue
36from freepy import request
37import freepy.globals
38from freepy.globals import debug
39
40"""
41freepy library -- connect to freeswitch mod_socket_event via python/twisted
42
43All commands currently use api instead of bgapi.  For the networking model
44used (twisted), this seems to work well and is simpler.
45
46"""
47
48DEBUG_ON = "see globals.py to turn on debugging"
49
50class FreepyDispatcher(LineReceiver):
51
52    def __init__(self, conncb, discocb=None):
53        self.delimiter='\n' # parent class uses this
54        self.conncb=conncb
55        self.discocb=discocb
56        self.requestq = Queue() # queue of pending requests
57        self.active_request = None # the current active (de-queued) request
58
59    def connectionMade(self):
60        debug("FREEPY: Connection made")
61        self.conncb(self)
62
63    def connectionLost(self, reason):
64        if self.discocb:
65            self.discocb(reason)
66        debug("connectionLost: %s" % reason)
67
68    def login(self, passwd):
69        """
70        send login request
71        """
72        msg = "auth %s" % passwd
73        req = request.LoginRequest()
74        self.requestq.put(req)
75        self.transport.write("%s\n\n" % msg)
76        debug(">> %s" % msg)
77        return req.getDeferred()
78
79    def _sendCommand(self, command, bgapi):
80        """
81        there is a lot of duplication in this object, and as many
82        methods as possible should be changed to use this method
83        rather than repeating the code
84        """
85        command = ("bgapi %s" if bgapi else "api %s") % command
86        req = (request.BgDialoutRequest if bgapi else
87               request.DialoutRequest)()
88        self.requestq.put(req)
89        self.transport.write("%s\n\n" % command)
90        debug(">> %s" % command)
91        return req.getDeferred()
92
93    def confdialout(self, conf_name, sofia_url, bgapi=True):
94        """
95        Instruct conference to join a particular user via dialout
96        @param conf_name - the name of the conference (arbitrary)
97        @param party2dial - a freeswitch sofia url, eg, sofia/mydomain.com/foo@bar.com
98        @return - a deferred that will be called back with a string like:
99                  Reply-Text: +OK Job-UUID: 4d410a8e-2409-11dc-99bf-a5e17fab9c65
100
101
102        """
103        if bgapi == True:
104            msg = "bgapi conference %s dial %s" % (conf_name,
105                                                   sofia_url)
106            req = request.BgDialoutRequest()
107        else:
108            msg = "api conference %s dial %s" % (conf_name,
109                                                 sofia_url)
110            req = request.DialoutRequest()
111        self.requestq.put(req)
112        self.transport.write("%s\n\n" % msg)
113        debug(">> %s" % msg)
114        return req.getDeferred()
115
116    def originate(self, party2dial, dest_ext_app, bgapi=True):
117
118        if bgapi == True:
119            msg = "bgapi originate %s %s" % (party2dial,
120                                             dest_ext_app)
121            req = request.BgDialoutRequest()
122        else:
123            msg = "api originate %s %s" % (party2dial,
124                                           dest_ext_app)
125            req = request.DialoutRequest()
126        self.requestq.put(req)
127        self.transport.write("%s\n\n" % msg)
128        debug(">> %s" % msg)
129        return req.getDeferred()
130
131    def listconf(self, conf_name):
132        """
133        List users in a conf
134        @param conf_name - the name of the conference (arbitrary)
135        @return - a deferred that will be called back with an array
136                  of models.ConfMember instances
137        """
138        msg = "api conference %s list" % (conf_name)
139        req = request.ListConfRequest()
140        self.requestq.put(req)
141        self.transport.write("%s\n\n" % msg)
142        debug(">> %s" % msg)
143        return req.getDeferred()
144
145
146    def confkick(self, member_id, conf_name, bgapi=False):
147        """
148        Kick member_id from conf
149        conf_name - name of conf
150        member_id - member id of user to kick, eg, "7"
151        returns - a deferred that will be called back with a result
152                  like:
153
154                  TODO: add this
155        """
156        if bgapi == True:
157            msg = "bgapi conference %s kick %s" % (conf_name, member_id)
158            req = request.BgConfKickRequest()
159        else:
160            msg = "api conference %s kick %s" % (conf_name, member_id)
161            req = request.ConfKickRequest()
162        self.requestq.put(req)
163        self.transport.write("%s\n\n" % msg)
164        debug(">> %s" % msg)
165        return req.getDeferred()
166
167    def confdtmf(self, member_id, conf_name, dtmf, bgapi=False):
168        """
169        Send dtmf to member_id or to all members
170        conf_name - name of conf
171        member_id - member id of user to kick, eg, "7"
172        dtmf - a single dtmf or a string of dtms, eg "1" or "123"
173        returns - a deferred that will be called back with a result
174                  like:
175
176                  TODO: add this
177        """
178        msg = "conference %s dtmf %s %s" % \
179              (conf_name, member_id, dtmf)
180        return self._sendCommand(msg, bgapi)
181
182    def confsay(self, conf_name, text2speak, bgapi=False):
183        """
184        Speak text all members
185        conf_name - name of conf
186        dtmf - text to speak
187        returns - a deferred that will be called back with a result
188                  like:
189
190                  TODO: add this
191        """
192        msg = "conference %s say %s" % (conf_name, text2speak)
193        return self._sendCommand(msg, bgapi)
194
195    def confplay(self, conf_name, snd_url, bgapi=False):
196        """
197        Play a file to all members
198        conf_name - name of conf
199        dtmf - text to speak
200        returns - a deferred that will be called back with a result
201                  like:
202
203                  TODO: add this
204        """
205        msg = "conference %s play %s" % (conf_name, snd_url)
206        return self._sendCommand(msg, bgapi)
207
208    def confstop(self, conf_name, bgapi=False):
209        """
210        Stop playback of all sound files
211        conf_name - name of conf
212        returns - a deferred that will be called back with a result
213                  like:
214
215                  TODO: add this
216        """
217        msg = "conference %s stop" % (conf_name)
218        return self._sendCommand(msg, bgapi)
219
220    def showchannels(self, bgapi=False):
221        """
222        Get a list of all live channels on switch
223
224        returns - a deferred that will be called back with a result
225
226        <result row_count="2">
227          <row row_id="1">
228            <uuid>21524b8c-6d19-11dc-9380-357de4a7a612</uuid>
229            <created>2007-09-27 11:46:01</created>
230            <name>sofia/test/4761</name>
231            <state>CS_LOOPBACK</state>
232            <cid_name>FreeSWITCH</cid_name>
233            <cid_num>0000000000</cid_num>
234            <ip_addr></ip_addr>
235            <dest>outgoing2endpoint-6207463</dest>
236            <application>echo</application>
237            <application_data></application_data>
238            <read_codec>PCMU</read_codec>
239            <read_rate>8000</read_rate>
240            <write_codec>PCMU</write_codec>
241            <write_rate>8000</write_rate>
242          </row>
243          ...
244        </result>
245        """
246        msg = "show channels as xml"
247        return self._sendCommand(msg, bgapi)
248
249    def sofia_status_profile(self, profile_name, bgapi=False):
250        msg = "sofia status profile %s as xml" % (profile_name)
251        return self._sendCommand(msg, bgapi)
252
253    def sofia_profile_restart(self, sofia_profile_name, bgapi = False):
254
255        msg = "sofia profile %s restart" % (sofia_profile_name)
256        return self._sendCommand(msg, bgapi)
257
258    def killchan(self, uuid, bgapi = False):
259        return self._sendCommand("uuid_kill %s" % (uuid), bgapi)
260
261    def broadcast(self, uuid, path, legs, bgapi = False):
262        msg = "uuid_broadcast %s %s %s" % (uuid, path, legs)
263        return self._sendCommand(msg, bgapi)
264
265    def transfer(self, uuid, dest_ext, legs, bgapi = False):
266        """
267        transfer <uuid> [-bleg|-both] <dest-exten>
268        """
269        msg = "uuid_transfer %s %s %s" % (uuid, legs, dest_ext)
270        return self._sendCommand(msg, bgapi)
271
272
273    def lineReceived(self, line):
274        debug("<< %s" % line)
275        if not self.active_request:
276
277            # if no active request pending, we ignore
278            # blank lines
279            if not line.strip():
280                return
281
282            # if no active request, dequeue a new one
283            if self.requestq.empty():
284                # we are receiving non-empty data from fs without an
285                # active request pending.  that means that
286                # there is a bug in the protocol handler
287                # (or possibly in fs)
288                raise Exception("Received line: %s w/ no pending requests" % line)
289            self.active_request = self.requestq.get()
290
291        # tell the request to process the line, and tell us
292        # if its finished or not.  if its finished, we remove it
293        # as the active request so that a new active request will
294        # be de-queued.
295        finished = self.active_request.process(line)
296        if finished == True:
297            self.active_request = None
298
299