blob: aa5efbf75a60eb4123688057f56491f6f0e99f59 [file] [log] [blame]
Benny Prijono8b9f0082009-08-23 14:26:37 +00001# $Id$
2#
3# SIP Conference Bot
4#
5# Copyright (C) 2008-2009 Teluu Inc. (http://www.teluu.com)
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20#
21import pjsua as pj
22import string
23import sys
24
25CFG_FILE = "config"
26
27INFO = 1
28TRACE = 2
29
30# Call callback. This would just forward the event to the Member class
31class CallCb(pj.CallCallback):
32 def __init__(self, member, call=None):
33 pj.CallCallback.__init__(self, call)
34 self.member = member
35
36 def on_state(self):
37 self.member.on_call_state(self.call)
38
39 def on_media_state(self):
40 self.member.on_call_media_state(self.call)
41
42 def on_dtmf_digit(self, digits):
43 self.member.on_call_dtmf_digit(self.call, digits)
44
45 def on_transfer_request(self, dst, code):
46 return self.member.on_call_transfer_request(self.call, dst, code)
47
48 def on_transfer_status(self, code, reason, final, cont):
49 return self.member.on_call_transfer_status(self.call, code, reason, final, cont)
50
51 def on_replace_request(self, code, reason):
52 return self.member.on_call_replace_request(self.call, code, reason)
53
54 def on_replaced(self, new_call):
55 self.member.on_call_replaced(self.call, new_call)
56
57 def on_typing(self, is_typing):
58 self.member.on_typing(is_typing, call=self.call)
59
60 def on_pager(self, mime_type, body):
61 self.member.on_pager(mime_type, body, call=self.call)
62
63 def on_pager_status(self, body, im_id, code, reason):
64 self.member.on_pager_status(body, im_id, code, reason, call=self.call)
65
66# Buddy callback. This would just forward the event to Member class
67class BuddyCb(pj.BuddyCallback):
68 def __init__(self, member, buddy=None):
69 pj.BuddyCallback.__init__(self, buddy)
70 self.member = member
71
72 def on_pager(self, mime_type, body):
73 self.member.on_pager(mime_type, body, buddy=self.buddy)
74
75 def on_pager_status(self, body, im_id, code, reason):
76 self.member.on_pager_status(body, im_id, code, reason, buddy=self.buddy)
77
78 def on_state(self):
79 self.member.on_pres_state(self.buddy)
80
81 def on_typing(self, is_typing):
82 self.member.on_typing(is_typing, buddy=self.buddy)
83
Benny Prijono0079ac32009-08-24 11:56:13 +000084
85
86
87##############################################################################
88#
89#
Benny Prijono8b9f0082009-08-23 14:26:37 +000090# This class represents individual room member (either/both chat and voice conf)
Benny Prijono0079ac32009-08-24 11:56:13 +000091#
92#
Benny Prijono8b9f0082009-08-23 14:26:37 +000093class Member:
94 def __init__(self, bot, uri):
95 self.uri = uri
96 self.bot = bot
97 self.call = None
98 self.buddy = None
99 self.bi = pj.BuddyInfo()
100 self.in_chat = False
101 self.in_voice = False
102 self.im_error = False
Benny Prijono0079ac32009-08-24 11:56:13 +0000103 self.html = False
Benny Prijono8b9f0082009-08-23 14:26:37 +0000104
105 def __str__(self):
106 str = string.ljust(self.uri, 30) + " -- "
107 if self.buddy:
108 bi = self.buddy.info()
109 str = str + bi.online_text
110 else:
111 str = str + "Offline"
112 str = str + " ["
113 if (self.in_voice):
Benny Prijono0079ac32009-08-24 11:56:13 +0000114 str = str + " voice"
Benny Prijono8b9f0082009-08-23 14:26:37 +0000115 if (self.in_chat):
Benny Prijono0079ac32009-08-24 11:56:13 +0000116 str = str + " chat"
117 if (self.html):
118 str = str + " html"
119 else:
120 str = str + " plain"
121
Benny Prijono8b9f0082009-08-23 14:26:37 +0000122 if (self.im_error):
Benny Prijono0079ac32009-08-24 11:56:13 +0000123 str = str + " im_error"
Benny Prijono8b9f0082009-08-23 14:26:37 +0000124 str = str + "]"
125 return str
126
127 def join_call(self, call):
128 if self.call:
129 self.call.hangup(603, "You have been disconnected for making another call")
130 self.call = call
131 call.set_callback(CallCb(self, call))
132 msg = "%(uri)s is attempting to join the voice conference" % \
133 {'uri': self.uri}
134 self.bot.DEBUG(msg + "\n", INFO)
135 self.bot.broadcast_pager(None, msg)
136
137 def join_chat(self):
138 if not self.buddy:
139 self.bot.DEBUG(self.uri + " joining chatroom...\n", INFO)
140 self.buddy = self.bot.acc.add_buddy(self.uri)
141 self.buddy.set_callback(BuddyCb(self, self.buddy))
142 self.buddy.subscribe()
143 else:
144 self.bot.DEBUG(self.uri + " already in chatroom, resubscribing..\n", INFO)
145 self.buddy.subscribe()
146
147 def send_pager(self, body, mime="text/plain"):
148 self.bot.DEBUG("send_pager() to " + self.uri)
149 if self.in_chat and not self.im_error and self.buddy:
Benny Prijono0079ac32009-08-24 11:56:13 +0000150 if self.html:
151 #This will make us receive html!
152 #mime = "text/html"
153 body = body.replace("<", "&lt;")
154 body = body.replace(">", "&gt;")
155 body = body.replace('"', "&quot;")
156 body = body.replace("\n", "<BR>\n")
Benny Prijono8b9f0082009-08-23 14:26:37 +0000157 self.buddy.send_pager(body, content_type=mime)
158 self.bot.DEBUG("..sent\n")
159 else:
160 self.bot.DEBUG("..not sent!\n")
161
162 def on_call_state(self, call):
163 ci = call.info()
164 if ci.state==pj.CallState.DISCONNECTED:
165 if self.in_voice:
166 msg = "%(uri)s has left the voice conference (%(1)d/%(2)s)" % \
167 {'uri': self.uri, '1': ci.last_code, '2': ci.last_reason}
168 self.bot.DEBUG(msg + "\n", INFO)
169 self.bot.broadcast_pager(None, msg)
170 self.in_voice = False
171 self.call = None
172 self.bot.on_member_left(self)
173 elif ci.state==pj.CallState.CONFIRMED:
174 msg = "%(uri)s has joined the voice conference" % \
175 {'uri': self.uri}
176 self.bot.DEBUG(msg + "\n", INFO)
177 self.bot.broadcast_pager(None, msg)
178
179 def on_call_media_state(self, call):
180 self.bot.DEBUG("Member.on_call_media_state\n")
181 ci = call.info()
182 if ci.conf_slot!=-1:
183 if not self.in_voice:
184 msg = self.uri + " call media is active"
185 self.bot.broadcast_pager(None, msg)
186 self.in_voice = True
187 self.bot.add_to_voice_conf(self)
188 else:
189 if self.in_voice:
190 msg = self.uri + " call media is inactive"
191 self.bot.broadcast_pager(None, msg)
192 self.in_voice = False
193
194 def on_call_dtmf_digit(self, call, digits):
195 msg = "%(uri)s sent DTMF digits %(dig)s" % \
196 {'uri': self.uri, 'dig': digits}
197 self.bot.broadcast_pager(None, msg)
198
199 def on_call_transfer_request(self, call, dst, code):
200 msg = "%(uri)s is transfering the call to %(dst)s" % \
201 {'uri': self.uri, 'dst': dst}
202 self.bot.broadcast_pager(None, msg)
203 return 202
204
205 def on_call_transfer_status(self, call, code, reason, final, cont):
206 msg = "%(uri)s call transfer status is %(code)d/%(res)s" % \
207 {'uri': self.uri, 'code': code, 'res': reason}
208 self.bot.broadcast_pager(None, msg)
209 return True
210
211 def on_call_replace_request(self, call, code, reason):
212 msg = "%(uri)s is requesting call replace" % \
213 {'uri': self.uri}
214 self.bot.broadcast_pager(None, msg)
215 return (code, reason)
216
217 def on_call_replaced(self, call, new_call):
218 msg = "%(uri)s call is replaced" % \
219 {'uri': self.uri}
220 self.bot.broadcast_pager(None, msg)
221
222 def on_pres_state(self, buddy):
223 old_bi = self.bi
224 self.bi = buddy.info()
225 msg = "%(uri)s status is %(st)s" % \
226 {'uri': self.uri, 'st': self.bi.online_text}
227 self.bot.DEBUG(msg + "\n", INFO)
228 self.bot.broadcast_pager(self, msg)
229
230 if self.bi.sub_state==pj.SubscriptionState.ACTIVE:
231 if not self.in_chat:
232 self.in_chat = True
233 buddy.send_pager("Welcome to chatroom")
234 self.bot.broadcast_pager(self, self.uri + " has joined the chat room")
235 else:
236 self.in_chat = True
237 elif self.bi.sub_state==pj.SubscriptionState.NULL or \
238 self.bi.sub_state==pj.SubscriptionState.TERMINATED or \
239 self.bi.sub_state==pj.SubscriptionState.UNKNOWN:
240 self.buddy.delete()
241 self.buddy = None
242 if self.in_chat:
243 self.in_chat = False
244 self.bot.broadcast_pager(self, self.uri + " has left the chat room")
245 else:
246 self.in_chat = False
247 self.bot.on_member_left(self)
248
249 def on_typing(self, is_typing, call=None, buddy=None):
250 if is_typing:
251 msg = self.uri + " is typing..."
252 else:
253 msg = self.uri + " has stopped typing"
254 self.bot.broadcast_pager(self, msg)
255
256 def on_pager(self, mime_type, body, call=None, buddy=None):
257 if not self.bot.handle_cmd(self, None, body):
258 msg = self.uri + ": " + body
259 self.bot.broadcast_pager(self, msg, mime_type)
260
261 def on_pager_status(self, body, im_id, code, reason, call=None, buddy=None):
262 self.im_error = (code/100 != 2)
263
Benny Prijono0079ac32009-08-24 11:56:13 +0000264
265
266##############################################################################
267#
268#
Benny Prijono8b9f0082009-08-23 14:26:37 +0000269# The Bot instance (singleton)
Benny Prijono0079ac32009-08-24 11:56:13 +0000270#
271#
Benny Prijono8b9f0082009-08-23 14:26:37 +0000272class Bot(pj.AccountCallback):
273 def __init__(self):
274 pj.AccountCallback.__init__(self, None)
275 self.lib = pj.Lib()
276 self.acc = None
277 self.calls = []
278 self.members = {}
Benny Prijono0079ac32009-08-24 11:56:13 +0000279 self.cfg = None
Benny Prijono8b9f0082009-08-23 14:26:37 +0000280
281 def DEBUG(self, msg, level=TRACE):
282 print msg,
283
284 def helpstring(self):
285 return """
286--h[elp] Display this help screen
287--j[oin] Join the chat room
Benny Prijono0079ac32009-08-24 11:56:13 +0000288--html on|off Set to receive HTML or plain text
Benny Prijono8b9f0082009-08-23 14:26:37 +0000289
290Participant commands:
291--s[how] Show confbot settings
292--leave Leave the chatroom
293--l[ist] List all members
294
295Admin commands:
296--a[dmin] <CMD> Where <CMD> are:
297 list List the admins
298 add <URI> Add URI as admin
299 del <URI> Remove URI as admin
300 rr Reregister account to server
301 call <URI> Make call to the URI and add to voice conf
302 dc <URI> Disconnect call to URI
303 hold <URI> Hold call with that URI
304 update <URI> Send UPDATE to call with that URI
305 reinvite <URI> Send re-INVITE to call with that URI
306"""
307
308 def listmembers(self):
309 msg = ""
310 for uri, m in self.members.iteritems():
Benny Prijono0079ac32009-08-24 11:56:13 +0000311 msg = msg + str(m) + "\n"
Benny Prijono8b9f0082009-08-23 14:26:37 +0000312 return msg
313
314 def showsettings(self):
315 ai = self.acc.info()
316 msg = """
317ConfBot status and settings:
318 URI: %(uri)s
319 Status: %(pres)s
320 Reg Status: %(reg_st)d
321 Reg Reason: %(reg_res)s
322""" % {'uri': ai.uri, 'pres': ai.online_text, \
323 'reg_st': ai.reg_status, 'reg_res': ai.reg_reason}
324 return msg
325
326 def main(self, cfg_file):
327 try:
Benny Prijono0079ac32009-08-24 11:56:13 +0000328 cfg = self.cfg = __import__(cfg_file)
Benny Prijono8b9f0082009-08-23 14:26:37 +0000329
330 self.lib.init(ua_cfg=cfg.ua_cfg, log_cfg=cfg.log_cfg, media_cfg=cfg.media_cfg)
331 self.lib.set_null_snd_dev()
332
333 transport = None
334 if cfg.udp_cfg:
335 transport = self.lib.create_transport(pj.TransportType.UDP, cfg.udp_cfg)
336 if cfg.tcp_cfg:
337 t = self.lib.create_transport(pj.TransportType.TCP, cfg.tcp_cfg)
338 if not transport:
339 transport = t
340
341 self.lib.start()
342
343 if cfg.acc_cfg:
344 self.DEBUG("Creating account %(uri)s..\n" % {'uri': cfg.acc_cfg.id}, INFO)
345 self.acc = self.lib.create_account(cfg.acc_cfg, cb=self)
346 else:
347 self.DEBUG("Creating account for %(t)s..\n" % \
348 {'t': transport.info().description}, INFO)
349 self.acc = self.lib.create_account_for_transport(transport, cb=self)
350
351 self.acc.set_basic_status(True)
352
353 # Wait for ENTER before quitting
354 print "Press q to quit or --help/--h for help"
355 while True:
356 input = sys.stdin.readline().strip(" \t\r\n")
357 if not self.handle_cmd(None, None, input):
358 if input=="q":
359 break
360
361 self.lib.destroy()
362 self.lib = None
363
364 except pj.Error, e:
365 print "Exception: " + str(e)
366 if self.lib:
367 self.lib.destroy()
368 self.lib = None
369
370 def broadcast_pager(self, exclude_member, body, mime_type="text/plain"):
371 self.DEBUG("Broadcast: " + body + "\n")
372 for uri, m in self.members.iteritems():
373 if m != exclude_member:
374 m.send_pager(body, mime_type)
375
376 def add_to_voice_conf(self, member):
377 if not member.call:
378 return
379 src_ci = member.call.info()
380 self.DEBUG("bot.add_to_voice_conf\n")
381 for uri, m in self.members.iteritems():
382 if m==member:
383 continue
384 if not m.call:
385 continue
386 dst_ci = m.call.info()
387 if dst_ci.media_state==pj.MediaState.ACTIVE and dst_ci.conf_slot!=-1:
388 self.lib.conf_connect(src_ci.conf_slot, dst_ci.conf_slot)
389 self.lib.conf_connect(dst_ci.conf_slot, src_ci.conf_slot)
390
391 def on_member_left(self, member):
392 if not member.call and not member.buddy:
393 del self.members[member.uri]
394 del member
395
Benny Prijono0079ac32009-08-24 11:56:13 +0000396 def handle_admin_cmd(self, member, body):
397 if member and self.cfg.admins and not member.uri in self.cfg.admins:
398 member.send_pager("You are not admin")
399 return
400 args = body.split()
401 msg = ""
402
403 if len(args)==1:
404 args.append(" ")
405
406 if args[1]=="list":
407 if not self.cfg.admins:
408 msg = "Everyone is admin!"
409 else:
410 msg = str(self.cfg.admins)
411 elif args[1]=="add":
412 if len(args)!=3:
413 msg = "Usage: add <URI>"
414 else:
415 self.cfg.admins.append(args[2])
416 msg = args[2] + " added as admin"
417 elif args[1]=="del":
418 if len(args)!=3:
419 msg = "Usage: del <URI>"
420 elif args[2] not in self.cfg.admins:
421 msg = args[2] + " is not admin"
422 else:
423 self.cfg.admins.remove(args[2])
424 msg = args[2] + " has been removed from admins"
425 elif args[1]=="rr":
426 msg = "Reregistering.."
427 self.acc.set_registration(True)
428 elif args[1]=="call":
429 if len(args)!=3:
430 msg = "Usage: call <URI>"
431 else:
432 uri = args[2]
433 try:
434 call = self.acc.make_call(uri)
435 except pj.Error, e:
436 msg = "Error: " + str(e)
437 call = None
438
439 if call:
440 if not uri in self.members:
441 m = Member(self, uri)
442 self.members[m.uri] = m
443 else:
444 m = self.members[uri]
445 msg = "Adding " + m.uri + " to voice conference.."
446 m.join_call(call)
447 elif args[1]=="dc" or args[1]=="hold" or args[1]=="update" or args[1]=="reinvite":
448 if len(args)!=3:
449 msg = "Usage: " + args[1] + " <URI>"
450 else:
451 uri = args[2]
452 if not uri in self.members:
453 msg = "Member not found/URI doesn't match (note: case matters!)"
454 else:
455 m = self.members[uri]
456 if m.call:
457 if args[1]=="dc":
458 msg = "Disconnecting.."
459 m.call.hangup(603, "You're disconnected by admin")
460 elif args[1]=="hold":
461 msg = "Holding the call"
462 m.call.hold()
463 elif args[1]=="update":
464 msg = "Sending UPDATE"
465 m.call.update()
466 elif args[1]=="reinvite":
467 msg = "Sending re-INVITE"
468 m.call.reinvite()
469 else:
470 msg = "He is not in call"
471 else:
472 msg = "Unknown admin command " + body
473
474 #print "msg is '%(msg)s'" % {'msg': msg}
475
476 if True:
477 if member:
478 member.send_pager(msg)
479 else:
480 print msg
481
Benny Prijono8b9f0082009-08-23 14:26:37 +0000482 def handle_cmd(self, member, from_uri, body):
483 body = body.strip(" \t\r\n")
484 msg = ""
485 handled = True
Benny Prijono0079ac32009-08-24 11:56:13 +0000486 if body=="--l" or body=="--list":
Benny Prijono8b9f0082009-08-23 14:26:37 +0000487 msg = self.listmembers()
488 if msg=="":
489 msg = "Nobody is here"
Benny Prijono0079ac32009-08-24 11:56:13 +0000490 elif body[0:3]=="--s":
Benny Prijono8b9f0082009-08-23 14:26:37 +0000491 msg = self.showsettings()
Benny Prijono0079ac32009-08-24 11:56:13 +0000492 elif body[0:6]=="--html" and member:
493 if body[8:11]=="off":
494 member.html = False
495 else:
496 member.html = True
497 elif body=="--h" or body=="--help":
Benny Prijono8b9f0082009-08-23 14:26:37 +0000498 msg = self.helpstring()
499 elif body=="--leave":
500 if not member or not member.buddy:
501 msg = "You are not in chatroom"
502 else:
503 member.buddy.unsubscribe()
Benny Prijono0079ac32009-08-24 11:56:13 +0000504 elif body[0:3]=="--j":
Benny Prijono8b9f0082009-08-23 14:26:37 +0000505 if not from_uri in self.members:
506 m = Member(self, from_uri)
507 self.members[m.uri] = m
508 self.DEBUG("Adding " + m.uri + " to chatroom\n")
509 m.join_chat()
510 else:
511 m = self.members[from_uri]
512 self.DEBUG("Adding " + m.uri + " to chatroom\n")
513 m.join_chat()
Benny Prijono0079ac32009-08-24 11:56:13 +0000514 elif body[0:3]=="--a":
515 self.handle_admin_cmd(member, body)
516 handled = True
Benny Prijono8b9f0082009-08-23 14:26:37 +0000517 else:
518 handled = False
519
520 if msg:
521 if member:
522 member.send_pager(msg)
523 elif from_uri:
524 self.acc.send_pager(from_uri, msg);
525 else:
526 print msg
527 return handled
528
529 def on_incoming_call(self, call):
530 self.DEBUG("on_incoming_call from %(uri)s\n" % {'uri': call.info().remote_uri}, INFO)
531 ci = call.info()
532 if not ci.remote_uri in self.members:
533 m = Member(self, ci.remote_uri)
534 self.members[m.uri] = m
535 m.join_call(call)
536 else:
537 m = self.members[ci.remote_uri]
538 m.join_call(call)
539 call.answer(200)
540
541 def on_incoming_subscribe(self, buddy, from_uri, contact_uri, pres_obj):
542 self.DEBUG("on_incoming_subscribe from %(uri)s\n" % from_uri, INFO)
543 return (200, 'OK')
544
545 def on_reg_state(self):
546 ai = self.acc.info()
547 self.DEBUG("Registration state: %(code)d/%(reason)s\n" % \
548 {'code': ai.reg_status, 'reason': ai.reg_reason}, INFO)
Benny Prijono0079ac32009-08-24 11:56:13 +0000549 if ai.reg_status/100==2 and ai.reg_expires > 0:
550 self.acc.set_basic_status(True)
Benny Prijono8b9f0082009-08-23 14:26:37 +0000551
552 def on_pager(self, from_uri, contact, mime_type, body):
553 body = body.strip(" \t\r\n")
554 if not self.handle_cmd(None, from_uri, body):
555 self.acc.send_pager(from_uri, "You have not joined the chat room. Type '--join' to join or '--help' for the help")
556
557 def on_pager_status(self, to_uri, body, im_id, code, reason):
558 pass
559
560 def on_typing(self, from_uri, contact, is_typing):
561 pass
562
563
Benny Prijono0079ac32009-08-24 11:56:13 +0000564
565
566##############################################################################
567#
Benny Prijono8b9f0082009-08-23 14:26:37 +0000568#
569# main()
570#
Benny Prijono0079ac32009-08-24 11:56:13 +0000571#
Benny Prijono8b9f0082009-08-23 14:26:37 +0000572if __name__ == "__main__":
573 bot = Bot()
574 bot.main(CFG_FILE)
575