PageRenderTime 178ms CodeModel.GetById 39ms RepoModel.GetById 6ms app.codeStats 2ms

/SessionController.py

https://github.com/wilane/blink-cocoa
Python | 2077 lines | 1971 code | 95 blank | 11 comment | 153 complexity | 82b2ed3e08dc1b3d16cf4478ae9b142e MD5 | raw file
  1. # Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
  2. #
  3. import cjson
  4. import hashlib
  5. import re
  6. import time
  7. import socket
  8. import urllib
  9. import urlparse
  10. import uuid
  11. from itertools import chain
  12. from application.notification import IObserver, NotificationCenter
  13. from application.python import Null
  14. from application.python.types import Singleton
  15. from datetime import datetime, timedelta
  16. from dateutil.tz import tzlocal
  17. from sipsimple.account import Account, AccountManager, BonjourAccount
  18. from sipsimple.application import SIPApplication
  19. from sipsimple.configuration.settings import SIPSimpleSettings
  20. from sipsimple.core import SIPURI, ToHeader, SIPCoreError
  21. from sipsimple.lookup import DNSLookup
  22. from sipsimple.session import Session, IllegalStateError, IllegalDirectionError
  23. from sipsimple.util import TimestampedNotificationData, Timestamp
  24. from sipsimple.threading.green import run_in_green_thread
  25. from zope.interface import implements
  26. from AppKit import *
  27. from Foundation import *
  28. from AlertPanel import AlertPanel
  29. from AudioController import AudioController
  30. from AccountSettings import AccountSettings
  31. from BlinkLogger import BlinkLogger
  32. from ChatController import ChatController
  33. from DesktopSharingController import DesktopSharingController, DesktopSharingServerController, DesktopSharingViewerController
  34. from FileTransferController import FileTransferController
  35. from FileTransferSession import OutgoingPushFileTransferHandler
  36. from HistoryManager import ChatHistory, SessionHistory
  37. from MediaStream import *
  38. from SessionRinger import Ringer
  39. from SessionInfoController import SessionInfoController
  40. from SIPManager import SIPManager
  41. from VideoController import VideoController
  42. from interfaces.itunes import MusicApplications
  43. from util import *
  44. SessionIdentifierSerial = 0
  45. OUTBOUND_AUDIO_CALLS = 0
  46. StreamHandlerForType = {
  47. "chat" : ChatController,
  48. "audio" : AudioController,
  49. # "video" : VideoController,
  50. "video" : ChatController,
  51. "file-transfer" : FileTransferController,
  52. "desktop-sharing" : DesktopSharingController,
  53. "desktop-server" : DesktopSharingServerController,
  54. "desktop-viewer" : DesktopSharingViewerController
  55. }
  56. class SessionControllersManager(object):
  57. __metaclass__ = Singleton
  58. implements(IObserver)
  59. def __init__(self):
  60. self.notification_center = NotificationCenter()
  61. self.notification_center.add_observer(self, name='AudioStreamGotDTMF')
  62. self.notification_center.add_observer(self, name='BlinkSessionDidEnd')
  63. self.notification_center.add_observer(self, name='BlinkSessionDidFail')
  64. self.notification_center.add_observer(self, name='CFGSettingsObjectDidChange')
  65. self.notification_center.add_observer(self, name='SIPAccountDidActivate')
  66. self.notification_center.add_observer(self, name='SIPAccountDidDeactivate')
  67. self.notification_center.add_observer(self, name='SIPApplicationWillEnd')
  68. self.notification_center.add_observer(self, name='SIPSessionNewIncoming')
  69. self.notification_center.add_observer(self, name='SIPSessionNewOutgoing')
  70. self.notification_center.add_observer(self, name='SIPSessionDidStart')
  71. self.notification_center.add_observer(self, name='SIPSessionDidFail')
  72. self.notification_center.add_observer(self, name='SIPSessionDidEnd')
  73. self.notification_center.add_observer(self, name='SIPSessionGotProposal')
  74. self.notification_center.add_observer(self, name='SIPSessionGotRejectProposal')
  75. self.notification_center.add_observer(self, name='SystemWillSleep')
  76. self.notification_center.add_observer(self, name='MediaStreamDidInitialize')
  77. self.notification_center.add_observer(self, name='MediaStreamDidEnd')
  78. self.notification_center.add_observer(self, name='MediaStreamDidFail')
  79. self.last_calls_connections = {}
  80. self.last_calls_connections_authRequestCount = {}
  81. self.sessionControllers = []
  82. self.ringer = Ringer(self)
  83. self.incomingSessions = set()
  84. self.activeAudioStreams = set()
  85. self.pause_music = True
  86. @allocate_autorelease_pool
  87. @run_in_gui_thread
  88. def handle_notification(self, notification):
  89. handler = getattr(self, '_NH_%s' % notification.name, Null)
  90. handler(notification.sender, notification.data)
  91. def _NH_SIPApplicationDidStart(self, sender, data):
  92. self.ringer.start()
  93. self.ringer.update_ringtones()
  94. settings = SIPSimpleSettings()
  95. self.pause_music = settings.audio.pause_music if settings.audio.pause_music and NSApp.delegate().applicationName != 'Blink Lite' else False
  96. def _NH_SIPApplicationWillEnd(self, sender, data):
  97. self.ringer.stop()
  98. def _NH_SIPSessionDidFail(self, session, data):
  99. self.incomingSessions.discard(session)
  100. if self.pause_music:
  101. self.incomingSessions.discard(session)
  102. if not self.activeAudioStreams and not self.incomingSessions:
  103. music_applications = MusicApplications()
  104. music_applications.resume()
  105. def _NH_SIPSessionDidStart(self, session, data):
  106. self.incomingSessions.discard(session)
  107. if self.pause_music:
  108. if all(stream.type != 'audio' for stream in data.streams):
  109. if not self.activeAudioStreams and not self.incomingSessions:
  110. music_applications = MusicApplications()
  111. music_applications.resume()
  112. if session.direction == 'incoming':
  113. if session.account is not BonjourAccount() and session.account.web_alert.show_alert_page_after_connect:
  114. self.show_web_alert_page(session)
  115. def _NH_SIPSessionDidEnd(self, session, data):
  116. if self.pause_music:
  117. self.incomingSessions.discard(session)
  118. if not self.activeAudioStreams and not self.incomingSessions:
  119. music_applications = MusicApplications()
  120. music_applications.resume()
  121. def _NH_SIPSessionGotProposal(self, session, data):
  122. if self.pause_music:
  123. if any(stream.type == 'audio' for stream in data.streams):
  124. music_applications = MusicApplications()
  125. music_applications.pause()
  126. def _NH_SIPSessionGotRejectProposal(self, session, data):
  127. if self.pause_music:
  128. if any(stream.type == 'audio' for stream in data.streams):
  129. if not self.activeAudioStreams and not self.incomingSessions:
  130. music_applications = MusicApplications()
  131. music_applications.resume()
  132. def _NH_MediaStreamDidInitialize(self, stream, data):
  133. if stream.type == 'audio':
  134. self.activeAudioStreams.add(stream)
  135. def _NH_MediaStreamDidEnd(self, stream, data):
  136. if self.pause_music:
  137. if stream.type == "audio":
  138. self.activeAudioStreams.discard(stream)
  139. # TODO: check if session has other streams and if yes, resume itunes
  140. # in case of session ends, resume is handled by the Session Controller
  141. if not self.activeAudioStreams and not self.incomingSessions:
  142. music_applications = MusicApplications()
  143. music_applications.resume()
  144. def _NH_MediaStreamDidFail(self, stream, data):
  145. if self.pause_music:
  146. if stream.type == "audio":
  147. self.activeAudioStreams.discard(stream)
  148. if not self.activeAudioStreams and not self.incomingSessions:
  149. music_applications = MusicApplications()
  150. music_applications.resume()
  151. @run_in_gui_thread
  152. def _NH_SIPSessionNewIncoming(self, session, data):
  153. streams = [stream for stream in data.streams if self.isProposedMediaTypeSupported([stream])]
  154. stream_type_list = list(set(stream.type for stream in streams))
  155. if not streams:
  156. BlinkLogger().log_info(u"Rejecting session for unsupported media type")
  157. session.reject(488, 'Incompatible media')
  158. return
  159. # if call waiting is disabled and we have audio calls reject with busy
  160. hasAudio = any(sess.hasStreamOfType("audio") for sess in self.sessionControllers)
  161. if 'audio' in stream_type_list and hasAudio and session.account is not BonjourAccount() and session.account.audio.call_waiting is False:
  162. BlinkLogger().log_info(u"Refusing audio call from %s because we are busy and call waiting is disabled" % format_identity_to_string(session.remote_identity))
  163. session.reject(486, 'Busy Here')
  164. return
  165. if 'audio' in stream_type_list and session.account is not BonjourAccount() and session.account.audio.do_not_disturb:
  166. BlinkLogger().log_info(u"Refusing audio call from %s because do not disturb is enabled" % format_identity_to_string(session.remote_identity))
  167. session.reject(session.account.sip.do_not_disturb_code, 'Do Not Disturb')
  168. return
  169. if 'audio' in stream_type_list and session.account is not BonjourAccount() and session.account.audio.reject_anonymous and session.remote_identity.uri.user.lower() in ('anonymous', 'unknown', 'unavailable'):
  170. BlinkLogger().log_info(u"Rejecting audio call from anonymous caller")
  171. session.reject(403, 'Anonymous Not Acceptable')
  172. return
  173. # at this stage call is allowed and will alert the user
  174. self.incomingSessions.add(session)
  175. if self.pause_music:
  176. music_applications = MusicApplications()
  177. music_applications.pause()
  178. self.ringer.add_incoming(session, streams)
  179. session.blink_supported_streams = streams
  180. settings = SIPSimpleSettings()
  181. stream_type_list = list(set(stream.type for stream in streams))
  182. if NSApp.delegate().contactsWindowController.hasContactMatchingURI(session.remote_identity.uri):
  183. if settings.chat.auto_accept and stream_type_list == ['chat']:
  184. BlinkLogger().log_info(u"Automatically accepting chat session from %s" % format_identity_to_string(session.remote_identity))
  185. self.startIncomingSession(session, streams)
  186. return
  187. elif settings.file_transfer.auto_accept and stream_type_list == ['file-transfer']:
  188. BlinkLogger().log_info(u"Automatically accepting file transfer from %s" % format_identity_to_string(session.remote_identity))
  189. self.startIncomingSession(session, streams)
  190. return
  191. elif session.account is BonjourAccount() and stream_type_list == ['chat']:
  192. BlinkLogger().log_info(u"Automatically accepting Bonjour chat session from %s" % format_identity_to_string(session.remote_identity))
  193. self.startIncomingSession(session, streams)
  194. return
  195. if stream_type_list == ['file-transfer'] and streams[0].file_selector.name.decode("utf8").startswith('xscreencapture'):
  196. BlinkLogger().log_info(u"Automatically accepting screenshot from %s" % format_identity_to_string(session.remote_identity))
  197. self.startIncomingSession(session, streams)
  198. return
  199. try:
  200. session.send_ring_indication()
  201. except IllegalStateError, e:
  202. BlinkLogger().log_info(u"IllegalStateError: %s" % e)
  203. else:
  204. if settings.answering_machine.enabled and settings.answering_machine.answer_delay == 0:
  205. self.startIncomingSession(session, [s for s in streams if s.type=='audio'], answeringMachine=True)
  206. else:
  207. sessionController = self.addControllerWithSession_(session)
  208. if not NSApp.delegate().contactsWindowController.alertPanel:
  209. NSApp.delegate().contactsWindowController.alertPanel = AlertPanel.alloc().init()
  210. NSApp.delegate().contactsWindowController.alertPanel.addIncomingSession(session)
  211. NSApp.delegate().contactsWindowController.alertPanel.show()
  212. if session.account is not BonjourAccount() and not session.account.web_alert.show_alert_page_after_connect:
  213. self.show_web_alert_page(session)
  214. @run_in_gui_thread
  215. def _NH_SIPSessionNewOutgoing(self, session, data):
  216. self.ringer.add_outgoing(session, data.streams)
  217. if session.transfer_info is not None:
  218. # This Session was created as a result of a transfer
  219. self.addControllerWithSessionTransfer_(session)
  220. def _NH_AudioStreamGotDTMF(self, sender, data):
  221. key = data.digit
  222. filename = 'dtmf_%s_tone.wav' % {'*': 'star', '#': 'pound'}.get(key, key)
  223. wave_player = WavePlayer(SIPApplication.voice_audio_mixer, Resources.get(filename))
  224. self.notification_center.add_observer(self, sender=wave_player)
  225. SIPApplication.voice_audio_bridge.add(wave_player)
  226. wave_player.start()
  227. def _NH_WavePlayerDidFail(self, sender, data):
  228. self.notification_center.remove_observer(self, sender=sender)
  229. def _NH_WavePlayerDidEnd(self, sender, data):
  230. self.notification_center.remove_observer(self, sender=sender)
  231. @run_in_gui_thread
  232. def _NH_BlinkSessionDidEnd(self, session_controller, data):
  233. if session_controller.session.direction == "incoming":
  234. self.log_incoming_session_ended(session_controller, data)
  235. else:
  236. self.log_outgoing_session_ended(session_controller, data)
  237. @run_in_gui_thread
  238. def _NH_BlinkSessionDidFail(self, session_controller, data):
  239. if data.direction == "outgoing":
  240. if data.code == 487:
  241. self.log_outgoing_session_cancelled(session_controller, data)
  242. else:
  243. self.log_outgoing_session_failed(session_controller, data)
  244. elif data.direction == "incoming":
  245. session = session_controller.session
  246. if data.code == 487 and data.failure_reason == 'Call completed elsewhere':
  247. self.log_incoming_session_answered_elsewhere(session_controller, data)
  248. else:
  249. self.log_incoming_session_missed(session_controller, data)
  250. if data.code == 487 and data.failure_reason == 'Call completed elsewhere':
  251. pass
  252. elif data.streams == ['file-transfer']:
  253. pass
  254. else:
  255. session_controller.log_info(u"Missed incoming session from %s" % format_identity_to_string(session.remote_identity))
  256. if 'audio' in data.streams:
  257. NSApp.delegate().noteMissedCall()
  258. growl_data = TimestampedNotificationData()
  259. growl_data.caller = format_identity_to_string(session.remote_identity, check_contact=True, format='compact')
  260. growl_data.timestamp = data.timestamp
  261. growl_data.streams = ",".join(data.streams)
  262. growl_data.account = session.account.id.username + '@' + session.account.id.domain
  263. self.notification_center.post_notification("GrowlMissedCall", sender=self, data=growl_data)
  264. def _NH_SIPAccountDidActivate(self, account, data):
  265. if account is not BonjourAccount():
  266. self.get_last_calls(account)
  267. def _NH_SIPAccountDidDeactivate(self, account, data):
  268. if account is not BonjourAccount():
  269. self.close_last_call_connection(account)
  270. def _NH_CFGSettingsObjectDidChange(self, account, data):
  271. if 'audio.pause_music' in data.modified:
  272. settings = SIPSimpleSettings()
  273. self.pause_music = settings.audio.pause_music if settings.audio.pause_music and NSApp.delegate().applicationName != 'Blink Lite' else False
  274. def addControllerWithSession_(self, session):
  275. sessionController = SessionController.alloc().initWithSession_(session)
  276. self.sessionControllers.append(sessionController)
  277. return sessionController
  278. def addControllerWithAccount_target_displayName_(self, account, target, display_name):
  279. sessionController = SessionController.alloc().initWithAccount_target_displayName_(account, target, display_name)
  280. self.sessionControllers.append(sessionController)
  281. return sessionController
  282. def addControllerWithSessionTransfer_(self, session):
  283. sessionController = SessionController.alloc().initWithSessionTransfer_(session)
  284. self.sessionControllers.append(sessionController)
  285. return sessionController
  286. def removeController(self, controller):
  287. try:
  288. self.sessionControllers.remove(controller)
  289. except ValueError:
  290. pass
  291. def send_files_to_contact(self, account, contact_uri, filenames):
  292. if not self.isMediaTypeSupported('file-transfer'):
  293. return
  294. target_uri = normalize_sip_uri_for_outgoing_session(contact_uri, AccountManager().default_account)
  295. for file in filenames:
  296. try:
  297. xfer = OutgoingPushFileTransferHandler(account, target_uri, file)
  298. xfer.start()
  299. except Exception, exc:
  300. BlinkLogger().log_error(u"Error while attempting to transfer file %s: %s" % (file, exc))
  301. def startIncomingSession(self, session, streams, answeringMachine=False):
  302. try:
  303. session_controller = (controller for controller in self.sessionControllers if controller.session == session).next()
  304. except StopIteration:
  305. session_controller = self.addControllerWithSession_(session)
  306. session_controller.setAnsweringMachineMode_(answeringMachine)
  307. session_controller.handleIncomingStreams(streams, False)
  308. def isScreenSharingEnabled(self):
  309. try:
  310. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  311. s.connect(('127.0.0.1', 5900))
  312. s.close()
  313. return True
  314. except socket.error, msg:
  315. s.close()
  316. return False
  317. def isProposedMediaTypeSupported(self, streams):
  318. settings = SIPSimpleSettings()
  319. stream_type_list = list(set(stream.type for stream in streams))
  320. if 'desktop-sharing' in stream_type_list:
  321. ds = [s for s in streams if s.type == "desktop-sharing"]
  322. if ds and ds[0].handler.type != "active":
  323. if settings.desktop_sharing.disabled:
  324. BlinkLogger().log_info(u"Screen Sharing is disabled in Blink Preferences")
  325. return False
  326. if not self.isScreenSharingEnabled():
  327. BlinkLogger().log_info(u"Screen Sharing is disabled in System Preferences")
  328. return False
  329. if settings.file_transfer.disabled and 'file-transfer' in stream_type_list:
  330. BlinkLogger().log_info(u"File Transfers are disabled")
  331. return False
  332. if settings.chat.disabled and 'chat' in stream_type_list:
  333. BlinkLogger().log_info(u"Chat sessions are disabled")
  334. return False
  335. if 'video' in stream_type_list:
  336. # TODO: enable Video -adi
  337. return False
  338. return True
  339. def isMediaTypeSupported(self, type):
  340. settings = SIPSimpleSettings()
  341. if type == 'desktop-server':
  342. if settings.desktop_sharing.disabled:
  343. return False
  344. if not self.isScreenSharingEnabled():
  345. return False
  346. if settings.file_transfer.disabled and type == 'file-transfer':
  347. BlinkLogger().log_info(u"File Transfers are disabled")
  348. return False
  349. if settings.chat.disabled and type == 'chat':
  350. BlinkLogger().log_info(u"Chat sessions are disabled")
  351. return False
  352. if type == 'video':
  353. # TODO: enable Video -adi
  354. return False
  355. return True
  356. def log_incoming_session_missed(self, controller, data):
  357. account = controller.account
  358. if account is BonjourAccount():
  359. return
  360. id=str(uuid.uuid1())
  361. media_types = ",".join(data.streams)
  362. participants = ",".join(data.participants)
  363. local_uri = format_identity_to_string(account)
  364. remote_uri = format_identity_to_string(controller.target_uri)
  365. focus = "1" if data.focus else "0"
  366. failure_reason = ''
  367. duration = 0
  368. call_id = data.call_id if data.call_id is not None else ''
  369. from_tag = data.from_tag if data.from_tag is not None else ''
  370. to_tag = data.to_tag if data.to_tag is not None else ''
  371. self.add_to_history(id, media_types, 'incoming', 'missed', failure_reason, data.timestamp, data.timestamp, duration, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  372. if 'audio' in data.streams:
  373. message = '<h3>Missed Incoming Audio Call</h3>'
  374. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  375. media_type = 'missed-call'
  376. direction = 'incoming'
  377. status = 'delivered'
  378. cpim_from = data.target_uri
  379. cpim_to = local_uri
  380. timestamp = str(Timestamp(datetime.now(tzlocal())))
  381. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  382. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  383. def log_incoming_session_ended(self, controller, data):
  384. account = controller.account
  385. session = controller.session
  386. if account is BonjourAccount():
  387. return
  388. id=str(uuid.uuid1())
  389. media_types = ",".join(data.streams)
  390. participants = ",".join(data.participants)
  391. local_uri = format_identity_to_string(account)
  392. remote_uri = format_identity_to_string(controller.target_uri)
  393. focus = "1" if data.focus else "0"
  394. failure_reason = ''
  395. if session.start_time is None and session.end_time is not None:
  396. # Session could have ended before it was completely started
  397. session.start_time = session.end_time
  398. duration = session.end_time - session.start_time
  399. call_id = data.call_id if data.call_id is not None else ''
  400. from_tag = data.from_tag if data.from_tag is not None else ''
  401. to_tag = data.to_tag if data.to_tag is not None else ''
  402. self.add_to_history(id, media_types, 'incoming', 'completed', failure_reason, session.start_time, session.end_time, duration.seconds, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  403. if 'audio' in data.streams:
  404. duration = self.get_printed_duration(session.start_time, session.end_time)
  405. message = '<h3>Incoming Audio Call</h3>'
  406. message += '<p>Call duration: %s' % duration
  407. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  408. media_type = 'audio'
  409. direction = 'incoming'
  410. status = 'delivered'
  411. cpim_from = data.target_uri
  412. cpim_to = format_identity_to_string(account)
  413. timestamp = str(Timestamp(datetime.now(tzlocal())))
  414. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  415. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  416. def log_incoming_session_answered_elsewhere(self, controller, data):
  417. account = controller.account
  418. if account is BonjourAccount():
  419. return
  420. id=str(uuid.uuid1())
  421. media_types = ",".join(data.streams)
  422. participants = ",".join(data.participants)
  423. local_uri = format_identity_to_string(account)
  424. remote_uri = format_identity_to_string(controller.target_uri)
  425. focus = "1" if data.focus else "0"
  426. failure_reason = 'Answered elsewhere'
  427. call_id = data.call_id if data.call_id is not None else ''
  428. from_tag = data.from_tag if data.from_tag is not None else ''
  429. to_tag = data.to_tag if data.to_tag is not None else ''
  430. self.add_to_history(id, media_types, 'incoming', 'completed', failure_reason, data.timestamp, data.timestamp, 0, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  431. if 'audio' in data.streams:
  432. message= '<h3>Incoming Audio Call</h3>'
  433. message += '<p>The call has been answered elsewhere'
  434. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  435. media_type = 'audio'
  436. local_uri = local_uri
  437. remote_uri = remote_uri
  438. direction = 'incoming'
  439. status = 'delivered'
  440. cpim_from = data.target_uri
  441. cpim_to = local_uri
  442. timestamp = str(Timestamp(datetime.now(tzlocal())))
  443. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  444. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  445. def log_outgoing_session_failed(self, controller, data):
  446. account = controller.account
  447. if account is BonjourAccount():
  448. return
  449. id=str(uuid.uuid1())
  450. media_types = ",".join(data.streams)
  451. participants = ",".join(data.participants)
  452. focus = "1" if data.focus else "0"
  453. local_uri = format_identity_to_string(account)
  454. remote_uri = format_identity_to_string(controller.target_uri)
  455. failure_reason = '%s (%s)' % (data.reason or data.failure_reason, data.code)
  456. call_id = data.call_id if data.call_id is not None else ''
  457. from_tag = data.from_tag if data.from_tag is not None else ''
  458. to_tag = data.to_tag if data.to_tag is not None else ''
  459. self.add_to_history(id, media_types, 'outgoing', 'failed', failure_reason, data.timestamp, data.timestamp, 0, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  460. if 'audio' in data.streams:
  461. message = '<h3>Failed Outgoing Audio Call</h3>'
  462. message += '<p>Reason: %s (%s)' % (data.reason or data.failure_reason, data.code)
  463. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  464. media_type = 'audio'
  465. local_uri = local_uri
  466. remote_uri = remote_uri
  467. direction = 'incoming'
  468. status = 'delivered'
  469. cpim_from = data.target_uri
  470. cpim_to = local_uri
  471. timestamp = str(Timestamp(datetime.now(tzlocal())))
  472. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  473. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  474. def log_outgoing_session_cancelled(self, controller, data):
  475. account = controller.account
  476. if account is BonjourAccount():
  477. return
  478. id=str(uuid.uuid1())
  479. media_types = ",".join(data.streams)
  480. participants = ",".join(data.participants)
  481. focus = "1" if data.focus else "0"
  482. local_uri = format_identity_to_string(account)
  483. remote_uri = format_identity_to_string(controller.target_uri)
  484. failure_reason = ''
  485. call_id = data.call_id if data.call_id is not None else ''
  486. from_tag = data.from_tag if data.from_tag is not None else ''
  487. to_tag = data.to_tag if data.to_tag is not None else ''
  488. self.add_to_history(id, media_types, 'outgoing', 'cancelled', failure_reason, data.timestamp, data.timestamp, 0, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  489. if 'audio' in data.streams:
  490. message= '<h3>Cancelled Outgoing Audio Call</h3>'
  491. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  492. media_type = 'audio'
  493. direction = 'incoming'
  494. status = 'delivered'
  495. cpim_from = data.target_uri
  496. cpim_to = local_uri
  497. timestamp = str(Timestamp(datetime.now(tzlocal())))
  498. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  499. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  500. def log_outgoing_session_ended(self, controller, data):
  501. account = controller.account
  502. session = controller.session
  503. if account is BonjourAccount():
  504. return
  505. id=str(uuid.uuid1())
  506. media_types = ",".join(data.streams)
  507. participants = ",".join(data.participants)
  508. focus = "1" if data.focus else "0"
  509. local_uri = format_identity_to_string(account)
  510. remote_uri = format_identity_to_string(controller.target_uri)
  511. direction = 'incoming'
  512. status = 'delivered'
  513. failure_reason = ''
  514. call_id = data.call_id if data.call_id is not None else ''
  515. from_tag = data.from_tag if data.from_tag is not None else ''
  516. to_tag = data.to_tag if data.to_tag is not None else ''
  517. if session.start_time is None and session.end_time is not None:
  518. # Session could have ended before it was completely started
  519. session.start_time = session.end_time
  520. duration = session.end_time - session.start_time
  521. self.add_to_history(id, media_types, 'outgoing', 'completed', failure_reason, session.start_time, session.end_time, duration.seconds, local_uri, data.target_uri, focus, participants, call_id, from_tag, to_tag)
  522. if 'audio' in data.streams:
  523. duration = self.get_printed_duration(session.start_time, session.end_time)
  524. message= '<h3>Outgoing Audio Call</h3>'
  525. message += '<p>Call duration: %s' % duration
  526. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  527. media_type = 'audio'
  528. cpim_from = data.target_uri
  529. cpim_to = local_uri
  530. timestamp = str(Timestamp(datetime.now(tzlocal())))
  531. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=True)
  532. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(controller.target_uri), local_party=local_uri if account is not BonjourAccount() else 'bonjour', check_contact=True))
  533. def get_printed_duration(self, start_time, end_time):
  534. duration = end_time - start_time
  535. if (duration.days > 0 or duration.seconds > 0):
  536. duration_print = ""
  537. if duration.days > 0 or duration.seconds > 3600:
  538. duration_print += "%i hours, " % (duration.days*24 + duration.seconds/3600)
  539. seconds = duration.seconds % 3600
  540. duration_print += "%02i:%02i" % (seconds/60, seconds%60)
  541. else:
  542. duration_print = "00:00"
  543. return duration_print
  544. def add_to_history(self, id, media_types, direction, status, failure_reason, start_time, end_time, duration, local_uri, remote_uri, remote_focus, participants, call_id, from_tag, to_tag):
  545. SessionHistory().add_entry(id, media_types, direction, status, failure_reason, start_time, end_time, duration, local_uri, remote_uri, remote_focus, participants, call_id, from_tag, to_tag)
  546. def add_to_chat_history(self, id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, skip_replication=False):
  547. ChatHistory().add_message(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, "html", "0", status, skip_replication=skip_replication)
  548. def updateGetCallsTimer_(self, timer):
  549. try:
  550. key = (account for account in self.last_calls_connections.keys() if self.last_calls_connections[account]['timer'] == timer).next()
  551. except StopIteration:
  552. return
  553. else:
  554. try:
  555. connection = self.last_calls_connections[key]['connection']
  556. nsurl = NSURL.URLWithString_(self.last_calls_connections[key]['url'])
  557. except KeyError:
  558. pass
  559. else:
  560. if connection:
  561. connection.cancel()
  562. request = NSURLRequest.requestWithURL_cachePolicy_timeoutInterval_(nsurl, NSURLRequestReloadIgnoringLocalAndRemoteCacheData, 15)
  563. connection = NSURLConnection.alloc().initWithRequest_delegate_(request, self)
  564. self.last_calls_connections[key]['data'] = ''
  565. self.last_calls_connections[key]['authRequestCount'] = 0
  566. self.last_calls_connections[key]['connection'] = connection
  567. # NSURLConnection delegate method
  568. def connection_didReceiveData_(self, connection, data):
  569. try:
  570. key = (account for account in self.last_calls_connections.keys() if self.last_calls_connections[account]['connection'] == connection).next()
  571. except StopIteration:
  572. pass
  573. else:
  574. try:
  575. account = AccountManager().get_account(key)
  576. except KeyError:
  577. pass
  578. else:
  579. self.last_calls_connections[key]['data'] = self.last_calls_connections[key]['data'] + str(data)
  580. def connectionDidFinishLoading_(self, connection):
  581. try:
  582. key = (account for account in self.last_calls_connections.keys() if self.last_calls_connections[account]['connection'] == connection).next()
  583. except StopIteration:
  584. pass
  585. else:
  586. BlinkLogger().log_debug(u"Calls history for %s retrieved from %s" % (key, self.last_calls_connections[key]['url']))
  587. try:
  588. account = AccountManager().get_account(key)
  589. except KeyError:
  590. pass
  591. else:
  592. try:
  593. calls = cjson.decode(self.last_calls_connections[key]['data'])
  594. except (TypeError, cjson.DecodeError):
  595. BlinkLogger().log_debug(u"Failed to parse calls history for %s from %s" % (key, self.last_calls_connections[key]['url']))
  596. else:
  597. self.syncServerHistoryWithLocalHistory(account, calls)
  598. # NSURLConnection delegate method
  599. def connection_didFailWithError_(self, connection, error):
  600. try:
  601. key = (account for account in self.last_calls_connections.keys() if self.last_calls_connections[account]['connection'] == connection).next()
  602. except StopIteration:
  603. return
  604. BlinkLogger().log_debug(u"Failed to retrieve calls history for %s from %s" % (key, self.last_calls_connections[key]['url']))
  605. @run_in_green_thread
  606. def syncServerHistoryWithLocalHistory(self, account, calls):
  607. growl_notifications = {}
  608. try:
  609. for call in calls['received']:
  610. direction = 'incoming'
  611. local_entry = SessionHistory().get_entries(direction=direction, count=1, call_id=call['sessionId'], from_tag=call['fromTag'])
  612. if not len(local_entry):
  613. id=str(uuid.uuid1())
  614. participants = ""
  615. focus = "0"
  616. local_uri = str(account.id)
  617. try:
  618. remote_uri = call['remoteParty']
  619. start_time = datetime.strptime(call['startTime'], "%Y-%m-%d %H:%M:%S")
  620. end_time = datetime.strptime(call['stopTime'], "%Y-%m-%d %H:%M:%S")
  621. status = call['status']
  622. duration = call['duration']
  623. call_id = call['sessionId']
  624. from_tag = call['fromTag']
  625. to_tag = call['toTag']
  626. media_types = ", ".join(call['media']) or 'audio'
  627. except KeyError:
  628. continue
  629. success = 'completed' if duration > 0 else 'missed'
  630. BlinkLogger().log_debug(u"Adding incoming %s call at %s from %s from server history" % (success, start_time, remote_uri))
  631. self.add_to_history(id, media_types, direction, success, status, start_time, end_time, duration, local_uri, remote_uri, focus, participants, call_id, from_tag, to_tag)
  632. if 'audio' in call['media']:
  633. direction = 'incoming'
  634. status = 'delivered'
  635. cpim_from = remote_uri
  636. cpim_to = local_uri
  637. timestamp = str(Timestamp(datetime.now(tzlocal())))
  638. if success == 'missed':
  639. message = '<h3>Missed Incoming Audio Call</h3>'
  640. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  641. media_type = 'missed-call'
  642. else:
  643. duration = self.get_printed_duration(start_time, end_time)
  644. message = '<h3>Incoming Audio Call</h3>'
  645. message += '<p>The call has been answered elsewhere'
  646. message += '<p>Call duration: %s' % duration
  647. #message += '<h4>Technicall Information</h4><table class=table_session_info><tr><td class=td_session_info>Call Id</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>From Tag</td><td class=td_session_info>%s</td></tr><tr><td class=td_session_info>To Tag</td><td class=td_session_info>%s</td></tr></table>' % (call_id, from_tag, to_tag)
  648. media_type = 'audio'
  649. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, time=start_time, skip_replication=True)
  650. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction=direction, history_entry=False, remote_party=remote_uri, local_party=local_uri, check_contact=True))
  651. if 'audio' in call['media'] and success == 'missed' and remote_uri not in growl_notifications.keys():
  652. now = datetime(*time.localtime()[:6])
  653. elapsed = now - start_time
  654. elapsed_hours = elapsed.seconds / (60*60)
  655. if elapsed_hours < 48:
  656. growl_data = TimestampedNotificationData()
  657. try:
  658. uri = SIPURI.parse('sip:'+str(remote_uri))
  659. except Exception:
  660. pass
  661. else:
  662. growl_data.caller = format_identity_to_string(uri, check_contact=True, format='compact')
  663. growl_data.timestamp = start_time
  664. growl_data.streams = media_types
  665. growl_data.account = str(account.id)
  666. self.notification_center.post_notification("GrowlMissedCall", sender=self, data=growl_data)
  667. growl_notifications[remote_uri] = True
  668. except (KeyError, TypeError):
  669. pass
  670. try:
  671. for call in calls['placed']:
  672. direction = 'outgoing'
  673. local_entry = SessionHistory().get_entries(direction=direction, count=1, call_id=call['sessionId'], from_tag=call['fromTag'])
  674. if not len(local_entry):
  675. id=str(uuid.uuid1())
  676. participants = ""
  677. focus = "0"
  678. local_uri = str(account.id)
  679. try:
  680. remote_uri = call['remoteParty']
  681. start_time = datetime.strptime(call['startTime'], "%Y-%m-%d %H:%M:%S")
  682. end_time = datetime.strptime(call['stopTime'], "%Y-%m-%d %H:%M:%S")
  683. status = call['status']
  684. duration = call['duration']
  685. call_id = call['sessionId']
  686. from_tag = call['fromTag']
  687. to_tag = call['toTag']
  688. media_types = ", ".join(call['media']) or 'audio'
  689. except KeyError:
  690. continue
  691. if duration > 0:
  692. success = 'completed'
  693. else:
  694. if status == "487":
  695. success = 'cancelled'
  696. else:
  697. success = 'failed'
  698. BlinkLogger().log_debug(u"Adding outgoing %s call at %s to %s from server history" % (success, start_time, remote_uri))
  699. self.add_to_history(id, media_types, direction, success, status, start_time, end_time, duration, local_uri, remote_uri, focus, participants, call_id, from_tag, to_tag)
  700. if 'audio' in call['media']:
  701. local_uri = local_uri
  702. remote_uri = remote_uri
  703. direction = 'incoming'
  704. status = 'delivered'
  705. cpim_from = remote_uri
  706. cpim_to = local_uri
  707. timestamp = str(Timestamp(datetime.now(tzlocal())))
  708. media_type = 'audio'
  709. if success == 'failed':
  710. message = '<h3>Failed Outgoing Audio Call</h3>'
  711. message += '<p>Reason: %s' % status
  712. elif success == 'cancelled':
  713. message= '<h3>Cancelled Outgoing Audio Call</h3>'
  714. else:
  715. duration = self.get_printed_duration(start_time, end_time)
  716. message= '<h3>Outgoing Audio Call</h3>'
  717. message += '<p>Call duration: %s' % duration
  718. self.add_to_chat_history(id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status, time=start_time, skip_replication=True)
  719. NotificationCenter().post_notification('AudioCallLoggedToHistory', sender=self, data=TimestampedNotificationData(direction=direction, history_entry=False, remote_party=remote_uri, local_party=local_uri, check_contact=True))
  720. except (KeyError, TypeError):
  721. pass
  722. # NSURLConnection delegate method
  723. def connection_didReceiveAuthenticationChallenge_(self, connection, challenge):
  724. try:
  725. key = (account for account in self.last_calls_connections.keys() if self.last_calls_connections[account]['connection'] == connection).next()
  726. except StopIteration:
  727. pass
  728. else:
  729. try:
  730. account = AccountManager().get_account(key)
  731. except KeyError:
  732. pass
  733. else:
  734. try:
  735. self.last_calls_connections[key]['authRequestCount'] += 1
  736. except KeyError:
  737. self.last_calls_connections[key]['authRequestCount'] = 1
  738. if self.last_calls_connections[key]['authRequestCount'] < 2:
  739. credential = NSURLCredential.credentialWithUser_password_persistence_(account.id, account.server.web_password or account.auth.password, NSURLCredentialPersistenceNone)
  740. challenge.sender().useCredential_forAuthenticationChallenge_(credential, challenge)
  741. @run_in_gui_thread
  742. def show_web_alert_page(self, session):
  743. # open web page with caller information
  744. if NSApp.delegate().applicationName == 'Blink Lite':
  745. return
  746. try:
  747. session_controller = (controller for controller in self.sessionControllers if controller.session == session).next()
  748. except StopIteration:
  749. return
  750. if session.account is not BonjourAccount() and session.account.web_alert.alert_url:
  751. url = unicode(session.account.web_alert.alert_url)
  752. replace_caller = urllib.urlencode({'x:': '%s@%s' % (session.remote_identity.uri.user, session.remote_identity.uri.host)})
  753. caller_key = replace_caller[5:]
  754. url = url.replace('$caller_party', caller_key)
  755. replace_username = urllib.urlencode({'x:': '%s' % session.remote_identity.uri.user})
  756. url = url.replace('$caller_username', replace_username[5:])
  757. replace_account = urllib.urlencode({'x:': '%s' % session.account.id})
  758. url = url.replace('$called_party', replace_account[5:])
  759. settings = SIPSimpleSettings()
  760. if settings.gui.use_default_web_browser_for_alerts:
  761. session_controller.log_info(u"Opening HTTP URL in default browser %s"% url)
  762. NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
  763. else:
  764. session_controller.log_info(u"Opening HTTP URL %s"% url)
  765. if not SIPManager()._delegate.accountSettingsPanels.has_key(caller_key):
  766. SIPManager()._delegate.accountSettingsPanels[caller_key] = AccountSettings.createWithOwner_(self)
  767. SIPManager()._delegate.accountSettingsPanels[caller_key].showIncomingCall(session, url)
  768. @run_in_gui_thread
  769. def get_last_calls(self, account):
  770. if not account.server.settings_url:
  771. return
  772. query_string = "action=get_history"
  773. url = urlparse.urlunparse(account.server.settings_url[:4] + (query_string,) + account.server.settings_url[5:])
  774. nsurl = NSURL.URLWithString_(url)
  775. request = NSURLRequest.requestWithURL_cachePolicy_timeoutInterval_(nsurl, NSURLRequestReloadIgnoringLocalAndRemoteCacheData, 15)
  776. connection = NSURLConnection.alloc().initWithRequest_delegate_(request, self)
  777. timer = NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(300, self, "updateGetCallsTimer:", None, True)
  778. NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSRunLoopCommonModes)
  779. NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSEventTrackingRunLoopMode)
  780. self.last_calls_connections[account.id] = { 'connection': connection,
  781. 'authRequestCount': 0,
  782. 'timer': timer,
  783. 'url': url,
  784. 'data': ''
  785. }
  786. self.updateGetCallsTimer_(None)
  787. @run_in_gui_thread
  788. def close_last_call_connection(self, account):
  789. try:
  790. connection = self.last_calls_connections[account.id]['connection']
  791. except KeyError:
  792. pass
  793. else:
  794. if connection:
  795. connection.cancel()
  796. try:
  797. timer = self.last_calls_connections[account.id]['timer']
  798. if timer and timer.isValid():
  799. timer.invalidate()
  800. timer = None
  801. del self.last_calls_connections[account.id]
  802. except KeyError:
  803. pass
  804. class SessionController(NSObject):
  805. implements(IObserver)
  806. session = None
  807. state = STATE_IDLE
  808. routes = None
  809. target_uri = None
  810. remoteParty = None
  811. endingBy = None
  812. answeringMachineMode = False
  813. failureReason = None
  814. inProposal = False
  815. proposalOriginator = None
  816. waitingForITunes = False
  817. streamHandlers = None
  818. chatPrintView = None
  819. collaboration_form_id = None
  820. remote_conference_has_audio = False
  821. transfer_window = None
  822. outbound_audio_calls = 0
  823. valid_dtmf = re.compile(r"^[0-9*#,]+$")
  824. pending_chat_messages = {}
  825. info_panel = None
  826. call_id = None
  827. from_tag = None
  828. to_tag = None
  829. dealloc_timer = None
  830. @property
  831. def sessionControllersManager(self):
  832. return NSApp.delegate().contactsWindowController.sessionControllersManager
  833. def initWithAccount_target_displayName_(self, account, target_uri, display_name):
  834. global SessionIdentifierSerial
  835. assert isinstance(target_uri, SIPURI)
  836. self = super(SessionController, self).init()
  837. BlinkLogger().log_debug(u"Creating %s" % self)
  838. self.contactDisplayName = display_name
  839. self.remoteParty = display_name or format_identity_to_string(target_uri, format='compact')
  840. self.remotePartyObject = target_uri
  841. self.account = account
  842. self.target_uri = target_uri
  843. self.postdial_string = None
  844. self.remoteSIPAddress = format_identity_to_string(target_uri)
  845. SessionIdentifierSerial += 1
  846. self.identifier = SessionIdentifierSerial
  847. self.streamHandlers = []
  848. self.notification_center = NotificationCenter()
  849. self.notification_center.add_observer(self, name='SystemWillSleep')
  850. self.notification_center.add_observer(self, sender=self)
  851. self.cancelledStream = None
  852. self.remote_focus = False
  853. self.conference_info = None
  854. self.invited_participants = []
  855. self.nickname = None
  856. self.conference_shared_files = []
  857. self.pending_removal_participants = set()
  858. self.failed_to_join_participants = {}
  859. self.mustShowDrawer = True
  860. self.open_chat_window_only = False
  861. self.try_next_hop = False
  862. # used for accounting
  863. self.streams_log = []
  864. self.participants_log = set()
  865. self.remote_focus_log = False
  866. return self
  867. def initWithSession_(self, session):
  868. global SessionIdentifierSerial
  869. self = super(SessionController, self).init()
  870. BlinkLogger().log_debug(u"Creating %s" % self)
  871. self.contactDisplayName = None
  872. self.remoteParty = format_identity_to_string(session.remote_identity, format='compact')
  873. self.remotePartyObject = session.remote_identity
  874. self.account = session.account
  875. self.session = session
  876. self.target_uri = SIPURI.new(session.remote_identity.uri if session.account is not BonjourAccount() else session._invitation.remote_contact_header.uri)
  877. self.postdial_string = None
  878. self.remoteSIPAddress = format_identity_to_string(self.target_uri)
  879. self.streamHandlers = []
  880. SessionIdentifierSerial += 1
  881. self.identifier = SessionIdentifierSerial
  882. self.notification_center = NotificationCenter()
  883. self.notification_center.add_observer(self, name='SystemWillSleep')
  884. self.notification_center.add_observer(self, sender=self)
  885. self.notification_center.add_observer(self, sender=self.session)
  886. self.cancelledStream = None
  887. self.remote_focus = False
  888. self.conference_info = None
  889. self.invited_participants = []
  890. self.nickname = None
  891. self.conference_shared_files = []
  892. self.pending_removal_participants = set()
  893. self.failed_to_join_participants = {}
  894. self.mustShowDrawer = True
  895. self.open_chat_window_only = False
  896. self.try_next_hop = False
  897. self.call_id = session._invitation.call_id
  898. self.initInfoPanel()
  899. # used for accounting
  900. self.streams_log = [stream.type for stream in session.proposed_streams or []]
  901. self.participants_log = set()
  902. self.remote_focus_log = False
  903. self.log_info(u'Invite from: "%s" <%s> with %s' % (session.remote_identity.display_name, session.remote_identity.uri, ", ".join(self.streams_log)))
  904. return self
  905. def initWithSessionTransfer_(self, session):
  906. global SessionIdentifierSerial
  907. self = super(SessionController, self).init()
  908. BlinkLogger().log_debug(u"Creating %s" % self)
  909. self.contactDisplayName = None
  910. self.remoteParty = format_identity_to_string(session.remote_identity, format='compact')
  911. self.remotePartyObject = session.remote_identity
  912. self.account = session.account
  913. self.session = session
  914. self.target_uri = SIPURI.new(session.remote_identity.uri)
  915. self.postdial_string = None
  916. self.remoteSIPAddress = format_identity_to_string(self.target_uri)
  917. self.streamHandlers = []
  918. SessionIdentifierSerial += 1
  919. self.identifier = SessionIdentifierSerial
  920. self.notification_center = NotificationCenter()
  921. self.notification_center.add_observer(self, name='SystemWillSleep')
  922. self.notification_center.add_observer(self, sender=self)
  923. self.notification_center.add_observer(self, sender=self.session)
  924. self.cancelledStream = None
  925. self.remote_focus = False
  926. self.conference_info = None
  927. self.invited_participants = []
  928. self.nickname = None
  929. self.conference_shared_files = []
  930. self.pending_removal_participants = set()
  931. self.failed_to_join_participants = {}
  932. self.mustShowDrawer = True
  933. self.open_chat_window_only = False
  934. self.try_next_hop = False
  935. self.initInfoPanel()
  936. # used for accounting
  937. self.streams_log = [stream.type for stream in session.proposed_streams or []]
  938. self.participants_log = set()
  939. self.remote_focus_log = False
  940. for stream in session.proposed_streams:
  941. if self.sessionControllersManager.isMediaTypeSupported(stream.type) and not self.hasStreamOfType(stream.type):
  942. handlerClass = StreamHandlerForType[stream.type]
  943. stream_controller = handlerClass(self, stream)
  944. self.streamHandlers.append(stream_controller)
  945. stream_controller.startOutgoing(False)
  946. return self
  947. def startDeallocTimer(self):
  948. self.notification_center.remove_observer(self, sender=self)
  949. self.notification_center.remove_observer(self, name='SystemWillSleep')
  950. self.destroyInfoPanel()
  951. if self.dealloc_timer is None:
  952. self.dealloc_timer = NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(10.0, self, "deallocTimer:", None, True)
  953. NSRunLoop.currentRunLoop().addTimer_forMode_(self.dealloc_timer, NSRunLoopCommonModes)
  954. NSRunLoop.currentRunLoop().addTimer_forMode_(self.dealloc_timer, NSEventTrackingRunLoopMode)
  955. def deallocTimer_(self, timer):
  956. self.resetSession()
  957. if self.chatPrintView is None:
  958. self.dealloc_timer.invalidate()
  959. self.dealloc_timer = None
  960. def dealloc(self):
  961. BlinkLogger().log_info(u"Disposing %s" % self)
  962. self.notification_center = None
  963. super(SessionController, self).dealloc()
  964. def log_info(self, text):
  965. BlinkLogger().log_info(u"[Session %d with %s] %s" % (self.identifier, self.remoteSIPAddress, text))
  966. def isActive(self):
  967. return self.state in (STATE_CONNECTED, STATE_CONNECTING, STATE_DNS_LOOKUP)
  968. def canProposeMediaStreamChanges(self):
  969. # TODO: hold is not handled by this, how to deal with it
  970. return not self.inProposal
  971. def canCancelProposal(self):
  972. if self.session is None:
  973. return False
  974. if self.session.state in ('cancelling_proposal', 'received_proposal', 'accepting_proposal', 'rejecting_proposal', 'accepting', 'incoming'):
  975. return False
  976. return True
  977. def acceptIncomingProposal(self, streams):
  978. self.handleIncomingStreams(streams, True)
  979. self.session.accept_proposal(streams)
  980. def handleIncomingStreams(self, streams, is_update=False):
  981. try:
  982. # give priority to chat stream so that we do not open audio drawer for composite streams
  983. sorted_streams = sorted(streams, key=lambda stream: 0 if stream.type=='chat' else 1)
  984. handled_types = set()
  985. for stream in sorted_streams:
  986. if self.sessionControllersManager.isMediaTypeSupported(stream.type):
  987. if stream.type in handled_types:
  988. self.log_info(u"Stream type %s has already been handled" % stream.type)
  989. continue
  990. controller = self.streamHandlerOfType(stream.type)
  991. if controller is None:
  992. handled_types.add(stream.type)
  993. handler = StreamHandlerForType.get(stream.type, None)
  994. controller = handler(self, stream)
  995. self.streamHandlers.append(controller)
  996. if stream.type not in self.streams_log:
  997. self.streams_log.append(stream.type)
  998. else:
  999. controller.stream = stream
  1000. if self.answeringMachineMode and stream.type == "audio":
  1001. controller.startIncoming(is_update=is_update, is_answering_machine=True)
  1002. else:
  1003. controller.startIncoming(is_update=is_update)
  1004. else:
  1005. self.log_info(u"Unknown incoming Stream type: %s (%s)" % (stream, stream.type))
  1006. raise TypeError("Unsupported stream type %s" % stream.type)
  1007. if not is_update:
  1008. self.session.accept(streams)
  1009. except Exception, exc:
  1010. # if there was some exception, reject the session
  1011. if is_update:
  1012. self.log_info(u"Error initializing additional streams: %s" % exc)
  1013. else:
  1014. self.log_info(u"Error initializing incoming session, rejecting it: %s" % exc)
  1015. try:
  1016. self.session.reject(500)
  1017. except IllegalDirectionError:
  1018. pass
  1019. log_data = TimestampedNotificationData(direction='incoming', target_uri=format_identity_to_string(self.target_uri, check_contact=True), timestamp=datetime.now(), code=500, originator='local', reason='Session already terminated', failure_reason=exc, streams=self.streams_log, focus=self.remote_focus_log, participants=self.participants_log, call_id=self.call_id, from_tag='', to_tag='')
  1020. self.notification_center.post_notification("BlinkSessionDidFail", sender=self, data=log_data)
  1021. def setAnsweringMachineMode_(self, flag):
  1022. self.answeringMachineMode = flag
  1023. def hasStreamOfType(self, stype):
  1024. return any(s for s in self.streamHandlers if s.stream and s.stream.type==stype)
  1025. def streamHandlerOfType(self, stype):
  1026. try:
  1027. return (s for s in self.streamHandlers if s.stream and s.stream.type==stype).next()
  1028. except StopIteration:
  1029. return None
  1030. def streamHandlerForStream(self, stream):
  1031. try:
  1032. return (s for s in self.streamHandlers if s.stream==stream).next()
  1033. except StopIteration:
  1034. return None
  1035. def end(self):
  1036. if self.state in (STATE_DNS_FAILED, STATE_DNS_LOOKUP):
  1037. return
  1038. if self.session:
  1039. self.session.end()
  1040. def endStream(self, streamHandler):
  1041. if self.session:
  1042. if streamHandler.stream.type=="audio" and self.hasStreamOfType("desktop-sharing") and len(self.streamHandlers)==2:
  1043. # if session is desktop-sharing end it
  1044. self.end()
  1045. return True
  1046. elif self.session.streams is not None and self.streamHandlers == [streamHandler]:
  1047. # session established, streamHandler is the only stream
  1048. self.log_info("Ending session with %s stream"% streamHandler.stream.type)
  1049. # end the whole session
  1050. self.end()
  1051. return True
  1052. elif len(self.streamHandlers) > 1 and self.session.streams and streamHandler.stream in self.session.streams:
  1053. # session established, streamHandler is one of many streams
  1054. if self.canProposeMediaStreamChanges():
  1055. self.log_info("Removing %s stream" % streamHandler.stream.type)
  1056. try:
  1057. self.session.remove_stream(streamHandler.stream)
  1058. self.notification_center.post_notification("BlinkSentRemoveProposal", sender=self, data=TimestampedNotificationData())
  1059. return True
  1060. except IllegalStateError, e:
  1061. self.log_info("IllegalStateError: %s" % e)
  1062. return False
  1063. else:
  1064. self.log_info("Media Stream proposal is already in progress")
  1065. return False
  1066. elif not self.streamHandlers and streamHandler.stream is None: # 3
  1067. # session established, streamHandler is being proposed but not yet established
  1068. self.log_info("Ending session with not-estabslihed %s stream"% streamHandler.stream.type)
  1069. self.end()
  1070. return True
  1071. else:
  1072. # session not yet established
  1073. if self.session.streams is None:
  1074. self.end()
  1075. return True
  1076. return False
  1077. def cancelProposal(self, stream):
  1078. if self.session:
  1079. if self.canCancelProposal():
  1080. self.log_info("Cancelling proposal")
  1081. self.cancelledStream = stream
  1082. try:
  1083. self.session.cancel_proposal()
  1084. self.notification_center.post_notification("BlinkWillCancelProposal", sender=self.session, data=TimestampedNotificationData())
  1085. except IllegalStateError, e:
  1086. self.log_info("IllegalStateError: %s" % e)
  1087. else:
  1088. self.log_info("Cancelling proposal is already in progress")
  1089. @property
  1090. def ended(self):
  1091. return self.state in (STATE_FINISHED, STATE_FAILED, STATE_DNS_FAILED)
  1092. def removeStreamHandler(self, streamHandler):
  1093. try:
  1094. self.streamHandlers.remove(streamHandler)
  1095. except ValueError:
  1096. return
  1097. # notify Chat Window controller to update the toolbar buttons
  1098. self.notification_center.post_notification("BlinkStreamHandlersChanged", sender=self, data=TimestampedNotificationData())
  1099. @allocate_autorelease_pool
  1100. @run_in_gui_thread
  1101. def changeSessionState(self, newstate, fail_reason=None):
  1102. self.state = newstate
  1103. # Below it makes a copy of the list because sessionChangedState can have the side effect of removing the handler from the list.
  1104. # This is very bad behavior and should be fixed. -Dan
  1105. for handler in self.streamHandlers[:]:
  1106. handler.sessionStateChanged(newstate, fail_reason)
  1107. self.notification_center.post_notification("BlinkSessionChangedState", sender=self, data=TimestampedNotificationData(state=newstate, reason=fail_reason))
  1108. def resetSession(self):
  1109. self.state = STATE_IDLE
  1110. self.session = None
  1111. self.endingBy = None
  1112. self.failureReason = None
  1113. self.cancelledStream = None
  1114. self.remote_focus = False
  1115. self.remote_focus_log = False
  1116. self.conference_info = None
  1117. self.invited_participants = []
  1118. self.nickname = None
  1119. self.conference_shared_files = []
  1120. self.pending_removal_participants = set()
  1121. self.failed_to_join_participants = {}
  1122. self.participants_log = set()
  1123. self.streams_log = []
  1124. self.remote_conference_has_audio = False
  1125. self.open_chat_window_only = False
  1126. self.destroyInfoPanel()
  1127. NSApp.delegate().contactsWindowController.updatePresenceStatus()
  1128. call_id = None
  1129. from_tag = None
  1130. to_tag = None
  1131. SessionControllersManager().removeController(self)
  1132. def initInfoPanel(self):
  1133. if self.info_panel is None:
  1134. self.info_panel = SessionInfoController(self)
  1135. self.info_panel_was_visible = False
  1136. self.info_panel_last_frame = False
  1137. def destroyInfoPanel(self):
  1138. if self.info_panel is not None:
  1139. self.info_panel.close()
  1140. self.info_panel = None
  1141. def lookup_destination(self, target_uri):
  1142. self.changeSessionState(STATE_DNS_LOOKUP)
  1143. lookup = DNSLookup()
  1144. self.notification_center.add_observer(self, sender=lookup)
  1145. settings = SIPSimpleSettings()
  1146. if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None:
  1147. uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port,
  1148. parameters={'transport': self.account.sip.outbound_proxy.transport})
  1149. self.log_info(u"Starting DNS lookup for %s through proxy %s" % (target_uri.host, uri))
  1150. elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy:
  1151. uri = SIPURI(host=self.account.id.domain)
  1152. self.log_info(u"Starting DNS lookup for %s via proxy of account %s" % (target_uri.host, self.account.id))
  1153. else:
  1154. uri = target_uri
  1155. self.log_info(u"Starting DNS lookup for %s" % target_uri.host)
  1156. lookup.lookup_sip_proxy(uri, settings.sip.transport_list)
  1157. def startCompositeSessionWithStreamsOfTypes(self, stype_tuple):
  1158. if self.state in (STATE_FINISHED, STATE_DNS_FAILED, STATE_FAILED):
  1159. self.resetSession()
  1160. self.initInfoPanel()
  1161. new_session = False
  1162. add_streams = []
  1163. if self.session is None:
  1164. self.session = Session(self.account)
  1165. if not self.try_next_hop:
  1166. self.routes = None
  1167. self.failureReason = None
  1168. new_session = True
  1169. for stype in stype_tuple:
  1170. if type(stype) == tuple:
  1171. stype, kwargs = stype
  1172. else:
  1173. kwargs = {}
  1174. if stype not in self.streams_log:
  1175. self.streams_log.append(stype)
  1176. if not self.hasStreamOfType(stype):
  1177. stream = None
  1178. if self.sessionControllersManager.isMediaTypeSupported(stype):
  1179. handlerClass = StreamHandlerForType[stype]
  1180. stream = handlerClass.createStream(self.account)
  1181. if not stream:
  1182. self.log_info("Cancelled session")
  1183. return False
  1184. streamController = handlerClass(self, stream)
  1185. self.streamHandlers.append(streamController)
  1186. if stype == 'chat':
  1187. if (len(stype_tuple) == 1 and self.open_chat_window_only) or (not new_session and not self.canProposeMediaStreamChanges()):
  1188. # just show the window and wait for user to type before starting the outgoing session
  1189. streamController.openChatWindow()
  1190. else:
  1191. # starts outgoing chat session
  1192. streamController.startOutgoing(not new_session, **kwargs)
  1193. else:
  1194. streamController.startOutgoing(not new_session, **kwargs)
  1195. if not new_session:
  1196. # there is already a session, add audio stream to it
  1197. add_streams.append(streamController.stream)
  1198. else:
  1199. self.log_info("Stream already exists: %s"%self.streamHandlers)
  1200. if stype == 'chat':
  1201. streamController = self.streamHandlerOfType('chat')
  1202. if streamController.status == STREAM_IDLE and len(stype_tuple) == 1:
  1203. # starts outgoing chat session
  1204. if self.streamHandlers == [streamController]:
  1205. new_session = True
  1206. else:
  1207. add_streams.append(streamController.stream)
  1208. streamController.startOutgoing(not new_session, **kwargs)
  1209. if new_session:
  1210. if not self.open_chat_window_only:
  1211. # starts outgoing session
  1212. if self.routes and self.try_next_hop:
  1213. self.connectSession()
  1214. if self.info_panel_was_visible:
  1215. self.info_panel.show()
  1216. self.info_panel.window.setFrame_display_animate_(self.info_panel_last_frame, True, True)
  1217. else:
  1218. self.lookup_destination(self.target_uri)
  1219. outdev = SIPSimpleSettings().audio.output_device
  1220. indev = SIPSimpleSettings().audio.input_device
  1221. if outdev == u"system_default":
  1222. outdev = u"System Default"
  1223. if indev == u"system_default":
  1224. indev = u"System Default"
  1225. if any(streamHandler.stream.type=='audio' for streamHandler in self.streamHandlers):
  1226. self.log_info(u"Selected audio input/output devices: %s/%s" % (indev, outdev))
  1227. global OUTBOUND_AUDIO_CALLS
  1228. OUTBOUND_AUDIO_CALLS += 1
  1229. self.outbound_audio_calls = OUTBOUND_AUDIO_CALLS
  1230. if self.sessionControllersManager.pause_music:
  1231. if any(streamHandler.stream.type=='audio' for streamHandler in self.streamHandlers):
  1232. self.waitingForITunes = True
  1233. music_applications = MusicApplications()
  1234. music_applications.pause()
  1235. self.notification_center.add_observer(self, sender=music_applications)
  1236. else:
  1237. self.waitingForITunes = False
  1238. else:
  1239. if self.canProposeMediaStreamChanges():
  1240. self.inProposal = True
  1241. for stream in add_streams:
  1242. self.log_info("Proposing %s stream" % stream.type)
  1243. try:
  1244. self.session.add_stream(stream)
  1245. self.notification_center.post_notification("BlinkSentAddProposal", sender=self, data=TimestampedNotificationData())
  1246. except IllegalStateError, e:
  1247. self.log_info("IllegalStateError: %s" % e)
  1248. log_data = TimestampedNotificationData(timestamp=datetime.now(), failure_reason=e)
  1249. self.notification_center.post_notification("BlinkProposalDidFail", sender=self, data=log_data)
  1250. return False
  1251. else:
  1252. self.log_info("A stream proposal is already in progress")
  1253. return False
  1254. self.open_chat_window_only = False
  1255. return True
  1256. def startSessionWithStreamOfType(self, stype, kwargs={}): # pyobjc doesn't like **kwargs
  1257. return self.startCompositeSessionWithStreamsOfTypes(((stype, kwargs), ))
  1258. def startAudioSession(self):
  1259. return self.startSessionWithStreamOfType("audio")
  1260. def startChatSession(self):
  1261. return self.startSessionWithStreamOfType("chat")
  1262. def offerFileTransfer(self, file_path, content_type=None):
  1263. return self.startSessionWithStreamOfType("chat", {"file_path":file_path, "content_type":content_type})
  1264. def addAudioToSession(self):
  1265. if not self.hasStreamOfType("audio"):
  1266. self.startSessionWithStreamOfType("audio")
  1267. def removeAudioFromSession(self):
  1268. if self.hasStreamOfType("audio"):
  1269. audioStream = self.streamHandlerOfType("audio")
  1270. self.endStream(audioStream)
  1271. def addChatToSession(self):
  1272. if not self.hasStreamOfType("chat"):
  1273. self.startSessionWithStreamOfType("chat")
  1274. else:
  1275. self.startSessionWithStreamOfType("chat")
  1276. def removeChatFromSession(self):
  1277. if self.hasStreamOfType("chat"):
  1278. chatStream = self.streamHandlerOfType("chat")
  1279. self.endStream(chatStream)
  1280. def addVideoToSession(self):
  1281. if not self.hasStreamOfType("video"):
  1282. self.startSessionWithStreamOfType("video")
  1283. def removeVideoFromSession(self):
  1284. if self.hasStreamOfType("video"):
  1285. videoStream = self.streamHandlerOfType("video")
  1286. self.endStream(videoStream)
  1287. def addMyDesktopToSession(self):
  1288. if not self.hasStreamOfType("desktop-sharing"):
  1289. self.startSessionWithStreamOfType("desktop-server")
  1290. def addRemoteDesktopToSession(self):
  1291. if not self.hasStreamOfType("desktop-sharing"):
  1292. self.startSessionWithStreamOfType("desktop-viewer")
  1293. def removeDesktopFromSession(self):
  1294. if self.hasStreamOfType("desktop-sharing"):
  1295. desktopStream = self.streamHandlerOfType("desktop-sharing")
  1296. self.endStream(desktopStream)
  1297. def getTitle(self):
  1298. return format_identity_to_string(self.remotePartyObject, format='full')
  1299. def getTitleFull(self):
  1300. if self.contactDisplayName and self.contactDisplayName != 'None' and not self.contactDisplayName.startswith('sip:') and not self.contactDisplayName.startswith('sips:'):
  1301. return "%s <%s>" % (self.contactDisplayName, format_identity_to_string(self.remotePartyObject, format='aor'))
  1302. else:
  1303. return self.getTitle()
  1304. def getTitleShort(self):
  1305. if self.contactDisplayName and self.contactDisplayName != 'None' and not self.contactDisplayName.startswith('sip:') and not self.contactDisplayName.startswith('sips:'):
  1306. return self.contactDisplayName
  1307. else:
  1308. return format_identity_to_string(self.remotePartyObject, format='compact')
  1309. @allocate_autorelease_pool
  1310. @run_in_gui_thread
  1311. def setRoutesFailed(self, msg):
  1312. self.log_info("DNS lookup for SIP routes failed: '%s'"%msg)
  1313. log_data = TimestampedNotificationData(direction='outgoing', target_uri=format_identity_to_string(self.target_uri, check_contact=True), timestamp=datetime.now(), code=478, originator='local', reason='DNS Lookup Failed', failure_reason='DNS Lookup Failed', streams=self.streams_log, focus=self.remote_focus_log, participants=self.participants_log, call_id='', from_tag='', to_tag='')
  1314. self.notification_center.post_notification("BlinkSessionDidFail", sender=self, data=log_data)
  1315. self.changeSessionState(STATE_DNS_FAILED, 'DNS Lookup Failed')
  1316. @allocate_autorelease_pool
  1317. @run_in_gui_thread
  1318. def setRoutesResolved(self, routes):
  1319. self.routes = routes
  1320. if not self.waitingForITunes:
  1321. self.connectSession()
  1322. def connectSession(self):
  1323. if self.dealloc_timer is not None and self.dealloc_timer.isValid():
  1324. self.dealloc_timer.invalidate()
  1325. self.dealloc_timer = None
  1326. self.log_info('Starting outgoing session to %s' % format_identity_to_string(self.target_uri, format='compact'))
  1327. streams = [s.stream for s in self.streamHandlers]
  1328. target_uri = SIPURI.new(self.target_uri)
  1329. if self.account is not BonjourAccount() and self.account.pstn.dtmf_delimiter and self.account.pstn.dtmf_delimiter in target_uri.user:
  1330. hash_parts = target_uri.user.partition(self.account.pstn.dtmf_delimiter)
  1331. if self.valid_dtmf.match(hash_parts[2]):
  1332. target_uri.user = hash_parts[0]
  1333. self.postdial_string = hash_parts[2]
  1334. self.notification_center.add_observer(self, sender=self.session)
  1335. self.session.connect(ToHeader(target_uri), self.routes, streams)
  1336. self.changeSessionState(STATE_CONNECTING)
  1337. self.log_info("Connecting session to %s" % self.routes[0])
  1338. self.notification_center.post_notification("BlinkSessionWillStart", sender=self, data=TimestampedNotificationData())
  1339. def transferSession(self, target, replaced_session_controller=None):
  1340. if self.session:
  1341. target_uri = str(target)
  1342. if '@' not in target_uri:
  1343. target_uri = target_uri + '@' + self.account.id.domain
  1344. if not target_uri.startswith(('sip:', 'sips:')):
  1345. target_uri = 'sip:' + target_uri
  1346. try:
  1347. target_uri = SIPURI.parse(target_uri)
  1348. except SIPCoreError:
  1349. self.log_info("Bogus SIP URI for transfer %s" % target_uri)
  1350. else:
  1351. self.session.transfer(target_uri, replaced_session_controller.session if replaced_session_controller is not None else None)
  1352. self.log_info("Outgoing transfer request to %s" % sip_prefix_pattern.sub("", str(target_uri)))
  1353. def _acceptTransfer(self):
  1354. self.log_info("Transfer request accepted by user")
  1355. try:
  1356. self.session.accept_transfer()
  1357. except IllegalDirectionError:
  1358. pass
  1359. self.transfer_window = None
  1360. def _rejectTransfer(self):
  1361. self.log_info("Transfer request rejected by user")
  1362. try:
  1363. self.session.reject_transfer()
  1364. except IllegalDirectionError:
  1365. pass
  1366. self.transfer_window = None
  1367. def reject(self, code, reason):
  1368. try:
  1369. self.session.reject(code, reason)
  1370. except IllegalStateError:
  1371. pass
  1372. @allocate_autorelease_pool
  1373. @run_in_gui_thread
  1374. def handle_notification(self, notification):
  1375. handler = getattr(self, '_NH_%s' % notification.name, Null)
  1376. handler(notification.sender, notification.data)
  1377. def _NH_DNSLookupDidFail(self, lookup, data):
  1378. self.notification_center.remove_observer(self, sender=lookup)
  1379. message = u"DNS lookup of SIP proxies for %s failed: %s" % (unicode(self.target_uri.host), data.error)
  1380. self.setRoutesFailed(message)
  1381. lookup = None
  1382. def _NH_DNSLookupDidSucceed(self, lookup, data):
  1383. self.notification_center.remove_observer(self, sender=lookup)
  1384. result_text = ', '.join(('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in data.result))
  1385. self.log_info(u"DNS lookup for %s succeeded: %s" % (self.target_uri.host, result_text))
  1386. routes = data.result
  1387. if not routes:
  1388. self.setRoutesFailed("No routes found to SIP Proxy")
  1389. else:
  1390. self.setRoutesResolved(routes)
  1391. lookup = None
  1392. def _NH_SystemWillSleep(self, sender, data):
  1393. self.end()
  1394. def _NH_MusicPauseDidExecute(self, sender, data):
  1395. if not self.waitingForITunes:
  1396. return
  1397. self.notification_center.remove_observer(self, sender=sender)
  1398. self.waitingForITunes = False
  1399. if self.routes:
  1400. self.connectSession()
  1401. def _NH_SIPSessionGotRingIndication(self, sender, data):
  1402. for sc in self.streamHandlers:
  1403. sc.sessionRinging()
  1404. self.notification_center.post_notification("BlinkSessionGotRingIndication", sender=self, data=TimestampedNotificationData())
  1405. def _NH_SIPSessionWillStart(self, sender, data):
  1406. self.log_info("Session will start")
  1407. def _NH_SIPSessionDidStart(self, sender, data):
  1408. self.remoteParty = format_identity_to_string(self.session.remote_identity)
  1409. if self.session.remote_focus:
  1410. self.remote_focus = True
  1411. self.remote_focus_log = True
  1412. else:
  1413. # Remove any invited participants as the remote party does not support conferencing
  1414. self.invited_participants = []
  1415. self.conference_shared_files = []
  1416. self.mustShowDrawer = True
  1417. self.changeSessionState(STATE_CONNECTED)
  1418. self.log_info("Session started")
  1419. for contact in self.invited_participants:
  1420. self.session.conference.add_participant(contact.uri)
  1421. def numerify(num):
  1422. try:
  1423. int(num)
  1424. except ValueError:
  1425. return num
  1426. else:
  1427. return chr(65+int(num))
  1428. # generate a unique id for the collaboration editor without digits, they don't work for some cloudy reason
  1429. # The only common identifier for both parties is the SIP call id, though it may still fail if a B2BUA is in the path -adi
  1430. hash = hashlib.sha1()
  1431. id = '%s' % (self.remoteSIPAddress) if self.remote_focus else self.session._invitation.call_id
  1432. hash.update(id)
  1433. self.collaboration_form_id = ''.join(numerify(c) for c in hash.hexdigest())
  1434. self.notification_center.post_notification("BlinkSessionDidStart", sender=self, data=TimestampedNotificationData())
  1435. def _NH_SIPSessionWillEnd(self, sender, data):
  1436. self.log_info("Session will end %sly"%data.originator)
  1437. self.endingBy = data.originator
  1438. if self.transfer_window is not None:
  1439. self.transfer_window.close()
  1440. self.transfer_window = None
  1441. def _NH_SIPSessionDidFail(self, sender, data):
  1442. try:
  1443. self.call_id = sender._invitation.call_id
  1444. except AttributeError:
  1445. self.call_id = ''
  1446. try:
  1447. self.to_tag = sender._invitation.to_header.parameters['tag']
  1448. except KeyError, AttributeError:
  1449. self.to_tag = ''
  1450. try:
  1451. self.from_tag = sender._invitation.from_header.parameters['tag']
  1452. except KeyError, AttributeError:
  1453. self.from_tag = ''
  1454. if data.failure_reason == 'Unknown error 61':
  1455. status = u"Connection refused"
  1456. self.failureReason = data.failure_reason
  1457. elif data.failure_reason != 'user request':
  1458. status = u"%s" % data.failure_reason
  1459. self.failureReason = data.failure_reason
  1460. elif data.reason:
  1461. status = u"%s" % data.reason
  1462. self.failureReason = data.reason
  1463. else:
  1464. status = u"Session Failed"
  1465. self.failureReason = "failed"
  1466. self.log_info("Session cancelled" if data.code == 487 else "Session failed: %s, %s (%s)" % (data.reason, data.failure_reason, data.code))
  1467. log_data = TimestampedNotificationData(originator=data.originator, direction=sender.direction, target_uri=format_identity_to_string(self.target_uri, check_contact=True), timestamp=data.timestamp, code=data.code, reason=data.reason, failure_reason=self.failureReason, streams=self.streams_log, focus=self.remote_focus_log, participants=self.participants_log, call_id=self.call_id, from_tag=self.from_tag, to_tag=self.to_tag)
  1468. self.notification_center.post_notification("BlinkSessionDidFail", sender=self, data=log_data)
  1469. self.changeSessionState(STATE_FAILED, status)
  1470. if self.info_panel is not None and self.info_panel.window.isVisible():
  1471. self.info_panel_was_visible = True
  1472. self.info_panel_last_frame = self.info_panel.window.frame()
  1473. oldSession = self.session
  1474. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=TimestampedNotificationData())
  1475. self.notification_center.remove_observer(self, sender=sender)
  1476. # redirect
  1477. if data.code in (301, 302) and data.redirect_identities:
  1478. redirect_to = data.redirect_identities[0].uri
  1479. ret = NSRunAlertPanel("Redirect Call",
  1480. "The remote party has redirected his calls to %s@%s.\nWould you like to call this address?" % (redirect_to.user, redirect_to.host),
  1481. "Call", "Cancel", None)
  1482. if ret == NSAlertDefaultReturn:
  1483. target_uri = SIPURI.new(redirect_to)
  1484. self.remotePartyObject = target_uri
  1485. self.target_uri = target_uri
  1486. self.remoteSIPAddress = format_identity_to_string(target_uri)
  1487. if len(oldSession.proposed_streams) == 1:
  1488. self.startSessionWithStreamOfType(oldSession.proposed_streams[0].type)
  1489. else:
  1490. self.startCompositeSessionWithStreamsOfTypes([s.type for s in oldSession.proposed_streams])
  1491. # local timeout while we have an alternative route
  1492. elif data.code == 408 and data.originator == 'local' and len(self.routes) > 1:
  1493. self.log_info('Trying alternative route')
  1494. self.routes.pop(0)
  1495. self.try_next_hop = True
  1496. if len(oldSession.proposed_streams) == 1:
  1497. self.startSessionWithStreamOfType(oldSession.proposed_streams[0].type)
  1498. else:
  1499. self.startCompositeSessionWithStreamsOfTypes([s.type for s in oldSession.proposed_streams])
  1500. def _NH_SIPSessionNewOutgoing(self, session, data):
  1501. self.log_info(u"Proposed media: %s" % ','.join([s.type for s in data.streams]))
  1502. def _NH_SIPSessionDidEnd(self, sender, data):
  1503. self.call_id = sender._invitation.call_id
  1504. try:
  1505. self.to_tag = sender._invitation.to_header.parameters['tag']
  1506. except KeyError:
  1507. self.to_tag= ''
  1508. try:
  1509. self.from_tag = sender._invitation.from_header.parameters['tag']
  1510. except KeyError:
  1511. self.from_tag = ''
  1512. self.log_info("Session ended")
  1513. self.changeSessionState(STATE_FINISHED, data.originator)
  1514. log_data = TimestampedNotificationData(target_uri=format_identity_to_string(self.target_uri, check_contact=True), streams=self.streams_log, focus=self.remote_focus_log, participants=self.participants_log, call_id=self.call_id, from_tag=self.from_tag, to_tag=self.to_tag)
  1515. self.notification_center.post_notification("BlinkSessionDidEnd", sender=self, data=log_data)
  1516. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=TimestampedNotificationData())
  1517. self.notification_center.post_notification("BlinkSessionDidProcessTransaction", sender=self, data=TimestampedNotificationData())
  1518. self.notification_center.remove_observer(self, sender=sender)
  1519. def _NH_SIPSessionGotProvisionalResponse(self, sender, data):
  1520. self.log_info("Got provisional response %s: %s" %(data.code, data.reason))
  1521. if data.code != 180:
  1522. log_data = TimestampedNotificationData(timestamp=datetime.now(), reason=data.reason, code=data.code)
  1523. self.notification_center.post_notification("BlinkSessionGotProvisionalResponse", sender=self, data=log_data)
  1524. def _NH_SIPSessionGotProposal(self, session, data):
  1525. self.inProposal = True
  1526. self.proposalOriginator = 'remote'
  1527. if data.originator != "local":
  1528. stream_names = ', '.join(stream.type for stream in data.streams)
  1529. self.log_info(u"Received %s proposal" % stream_names)
  1530. streams = data.streams
  1531. settings = SIPSimpleSettings()
  1532. stream_type_list = list(set(stream.type for stream in streams))
  1533. if not self.sessionControllersManager.isProposedMediaTypeSupported(streams):
  1534. self.log_info(u"Unsupported media type, proposal rejected")
  1535. session.reject_proposal()
  1536. return
  1537. if stream_type_list == ['chat'] and 'audio' in (s.type for s in session.streams):
  1538. self.log_info(u"Automatically accepting chat for established audio session from %s" % format_identity_to_string(session.remote_identity))
  1539. self.acceptIncomingProposal(streams)
  1540. return
  1541. if session.account is BonjourAccount():
  1542. if stream_type_list == ['chat']:
  1543. self.log_info(u"Automatically accepting Bonjour chat session from %s" % format_identity_to_string(session.remote_identity))
  1544. self.acceptIncomingProposal(streams)
  1545. return
  1546. elif 'audio' in stream_type_list and session.account.audio.auto_accept:
  1547. session_manager = SessionManager()
  1548. have_audio_call = any(s for s in session_manager.sessions if s is not session and s.streams and 'audio' in (stream.type for stream in s.streams))
  1549. if not have_audio_call:
  1550. accepted_streams = [s for s in streams if s.type in ("audio", "chat")]
  1551. self.log_info(u"Automatically accepting Bonjour audio and chat session from %s" % format_identity_to_string(session.remote_identity))
  1552. self.acceptIncomingProposal(accepted_streams)
  1553. return
  1554. if NSApp.delegate().contactsWindowController.hasContactMatchingURI(session.remote_identity.uri):
  1555. if settings.chat.auto_accept and stream_type_list == ['chat']:
  1556. self.log_info(u"Automatically accepting chat session from %s" % format_identity_to_string(session.remote_identity))
  1557. self.acceptIncomingProposal(streams)
  1558. return
  1559. elif settings.file_transfer.auto_accept and stream_type_list == ['file-transfer']:
  1560. self.log_info(u"Automatically accepting file transfer from %s" % format_identity_to_string(session.remote_identity))
  1561. self.acceptIncomingProposal(streams)
  1562. return
  1563. try:
  1564. session.send_ring_indication()
  1565. except IllegalStateError, e:
  1566. self.log_info(u"IllegalStateError: %s" % e)
  1567. return
  1568. else:
  1569. if not NSApp.delegate().contactsWindowController.alertPanel:
  1570. NSApp.delegate().contactsWindowController.alertPanel = AlertPanel.alloc().init()
  1571. NSApp.delegate().contactsWindowController.alertPanel.addIncomingStreamProposal(session, streams)
  1572. NSApp.delegate().contactsWindowController.alertPanel.show()
  1573. # needed to temporarily disable the Chat Window toolbar buttons
  1574. self.notification_center.post_notification("BlinkGotProposal", sender=self, data=TimestampedNotificationData())
  1575. def _NH_SIPSessionGotRejectProposal(self, sender, data):
  1576. self.inProposal = False
  1577. self.proposalOriginator = None
  1578. self.log_info("Proposal cancelled" if data.code == 487 else "Proposal was rejected: %s (%s)"%(data.reason, data.code))
  1579. log_data = TimestampedNotificationData(timestamp=datetime.now(), reason=data.reason, code=data.code)
  1580. self.notification_center.post_notification("BlinkProposalGotRejected", sender=self, data=log_data)
  1581. if data.streams:
  1582. for stream in data.streams:
  1583. if stream == self.cancelledStream:
  1584. self.cancelledStream = None
  1585. if stream.type == "chat":
  1586. self.log_info("Removing chat stream")
  1587. handler = self.streamHandlerForStream(stream)
  1588. if handler:
  1589. handler.changeStatus(STREAM_FAILED, data.reason)
  1590. elif stream.type == "audio":
  1591. self.log_info("Removing audio stream")
  1592. handler = self.streamHandlerForStream(stream)
  1593. if handler:
  1594. handler.changeStatus(STREAM_FAILED, data.reason)
  1595. elif stream.type == "desktop-sharing":
  1596. self.log_info("Removing desktop sharing stream")
  1597. handler = self.streamHandlerForStream(stream)
  1598. if handler:
  1599. handler.changeStatus(STREAM_FAILED, data.reason)
  1600. else:
  1601. self.log_info("Got reject proposal for unhandled stream type: %r" % stream)
  1602. # notify Chat Window controller to update the toolbar buttons
  1603. self.notification_center.post_notification("BlinkStreamHandlersChanged", sender=self, data=TimestampedNotificationData())
  1604. def _NH_SIPSessionGotAcceptProposal(self, sender, data):
  1605. self.inProposal = False
  1606. self.proposalOriginator = None
  1607. self.log_info("Proposal accepted")
  1608. if data.streams:
  1609. for stream in data.streams:
  1610. handler = self.streamHandlerForStream(stream)
  1611. if not handler and self.cancelledStream == stream:
  1612. self.log_info("Cancelled proposal for %s was accepted by remote, removing stream" % stream)
  1613. try:
  1614. self.session.remove_stream(stream)
  1615. self.cancelledStream = None
  1616. except IllegalStateError, e:
  1617. self.log_info("IllegalStateError: %s" % e)
  1618. # notify by Chat Window controller to update the toolbar buttons
  1619. self.notification_center.post_notification("BlinkStreamHandlersChanged", sender=self, data=TimestampedNotificationData())
  1620. def _NH_SIPSessionHadProposalFailure(self, sender, data):
  1621. self.inProposal = False
  1622. self.proposalOriginator = None
  1623. self.log_info("Proposal failure: %s" % data.failure_reason)
  1624. log_data = TimestampedNotificationData(timestamp=datetime.now(), failure_reason=data.failure_reason)
  1625. self.notification_center.post_notification("BlinkProposalDidFail", sender=self, data=log_data)
  1626. if data.streams:
  1627. for stream in data.streams:
  1628. if stream == self.cancelledStream:
  1629. self.cancelledStream = None
  1630. self.log_info("Removing %s stream" % stream.type)
  1631. handler = self.streamHandlerForStream(stream)
  1632. if handler:
  1633. handler.changeStatus(STREAM_FAILED, data.failure_reason)
  1634. # notify Chat Window controller to update the toolbar buttons
  1635. self.notification_center.post_notification("BlinkStreamHandlersChanged", sender=self, data=TimestampedNotificationData())
  1636. def _NH_SIPInvitationChangedState(self, sender, data):
  1637. self.notification_center.post_notification("BlinkInvitationChangedState", sender=self, data=data)
  1638. def _NH_SIPSessionDidProcessTransaction(self, sender, data):
  1639. self.notification_center.post_notification("BlinkSessionDidProcessTransaction", sender=self, data=data)
  1640. def _NH_SIPSessionDidRenegotiateStreams(self, sender, data):
  1641. if data.action == 'remove' and not sender.streams:
  1642. self.log_info("There are no streams anymore, ending the session")
  1643. self.end()
  1644. self.notification_center.post_notification("BlinkDidRenegotiateStreams", sender=self, data=data)
  1645. def _NH_SIPSessionGotConferenceInfo(self, sender, data):
  1646. self.log_info(u"Received conference-info update")
  1647. self.pending_removal_participants = set()
  1648. self.failed_to_join_participants = {}
  1649. self.conference_shared_files = []
  1650. self.conference_info = data.conference_info
  1651. remote_conference_has_audio = any(media.media_type == 'audio' for media in chain(*chain(*(user for user in self.conference_info.users))))
  1652. if remote_conference_has_audio and not self.remote_conference_has_audio:
  1653. self.notification_center.post_notification("ConferenceHasAddedAudio", sender=self)
  1654. self.remote_conference_has_audio = remote_conference_has_audio
  1655. for user in data.conference_info.users:
  1656. uri = sip_prefix_pattern.sub("", str(user.entity))
  1657. # save uri for accounting purposes
  1658. if uri != self.account.id:
  1659. self.participants_log.add(uri)
  1660. # remove invited participants that joined the conference
  1661. try:
  1662. contact = (contact for contact in self.invited_participants if uri == contact.uri).next()
  1663. except StopIteration:
  1664. pass
  1665. else:
  1666. self.invited_participants.remove(contact)
  1667. if data.conference_info.conference_description.resources is not None and data.conference_info.conference_description.resources.files is not None:
  1668. for file in data.conference_info.conference_description.resources.files:
  1669. self.conference_shared_files.append(file)
  1670. # notify controllers who need conference information
  1671. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=data)
  1672. def _NH_SIPConferenceDidAddParticipant(self, sender, data):
  1673. self.log_info(u"Added participant to conference: %s" % data.participant)
  1674. uri = sip_prefix_pattern.sub("", str(data.participant))
  1675. try:
  1676. contact = (contact for contact in self.invited_participants if uri == contact.uri).next()
  1677. except StopIteration:
  1678. pass
  1679. else:
  1680. self.invited_participants.remove(contact)
  1681. # notify controllers who need conference information
  1682. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=TimestampedNotificationData())
  1683. def _NH_SIPConferenceDidNotAddParticipant(self, sender, data):
  1684. self.log_info(u"Failed to add participant %s to conference: %s %s" % (data.participant, data.code, data.reason))
  1685. uri = sip_prefix_pattern.sub("", str(data.participant))
  1686. try:
  1687. contact = (contact for contact in self.invited_participants if uri == contact.uri).next()
  1688. except StopIteration:
  1689. pass
  1690. else:
  1691. contact.setDetail('%s (%s)' % (data.reason, data.code))
  1692. self.failed_to_join_participants[uri]=time.time()
  1693. if data.code >= 400 or data.code == 0:
  1694. contact.setDetail('Invite Failed: %s (%s)' % (data.reason, data.code))
  1695. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=TimestampedNotificationData())
  1696. def _NH_SIPConferenceGotAddParticipantProgress(self, sender, data):
  1697. uri = sip_prefix_pattern.sub("", str(data.participant))
  1698. try:
  1699. contact = (contact for contact in self.invited_participants if uri == contact.uri).next()
  1700. except StopIteration:
  1701. pass
  1702. else:
  1703. if data.code == 100:
  1704. contact.setDetail('Connecting...')
  1705. elif data.code in (180, 183):
  1706. contact.setDetail('Ringing...')
  1707. elif data.code == 200:
  1708. contact.setDetail('Invitation accepted')
  1709. elif data.code < 400:
  1710. contact.setDetail('%s (%s)' % (data.reason, data.code))
  1711. # notify controllers who need conference information
  1712. self.notification_center.post_notification("BlinkConferenceGotUpdate", sender=self, data=TimestampedNotificationData())
  1713. def _NH_SIPSessionTransferNewIncoming(self, sender, data):
  1714. target = "%s@%s" % (data.transfer_destination.user, data.transfer_destination.host)
  1715. self.log_info(u'Incoming transfer request to %s' % target)
  1716. self.notification_center.post_notification("BlinkSessionTransferNewIncoming", sender=self, data=data)
  1717. if self.account.audio.auto_transfer:
  1718. self.log_info(u'Auto-accepting transfer request')
  1719. sender.accept_transfer()
  1720. else:
  1721. self.transfer_window = CallTransferWindowController(self, target)
  1722. self.transfer_window.show()
  1723. def _NH_SIPSessionTransferNewOutgoing(self, sender, data):
  1724. target = "%s@%s" % (data.transfer_destination.user, data.transfer_destination.host)
  1725. self.notification_center.post_notification("BlinkSessionTransferNewOutgoing", sender=self, data=data)
  1726. def _NH_SIPSessionTransferDidStart(self, sender, data):
  1727. self.log_info(u'Transfer started')
  1728. self.notification_center.post_notification("BlinkSessionTransferDidStart", sender=self, data=data)
  1729. def _NH_SIPSessionTransferDidEnd(self, sender, data):
  1730. self.log_info(u'Transfer succeeded')
  1731. self.notification_center.post_notification("BlinkSessionTransferDidEnd", sender=self, data=data)
  1732. def _NH_SIPSessionTransferDidFail(self, sender, data):
  1733. self.log_info(u'Transfer failed %s: %s' % (data.code, data.reason))
  1734. self.notification_center.post_notification("BlinkSessionTransferDidFail", sender=self, data=data)
  1735. def _NH_SIPSessionTransferGotProgress(self, sender, data):
  1736. self.log_info(u'Transfer got progress %s: %s' % (data.code, data.reason))
  1737. self.notification_center.post_notification("BlinkSessionTransferGotProgress", sender=self, data=data)
  1738. def _NH_BlinkChatWindowWasClosed(self, sender, data):
  1739. self.startDeallocTimer()
  1740. def _NH_BlinkSessionDidFail(self, sender, data):
  1741. self.startDeallocTimer()
  1742. def _NH_BlinkSessionDidEnd(self, sender, data):
  1743. self.startDeallocTimer()
  1744. class CallTransferWindowController(NSObject):
  1745. window = objc.IBOutlet()
  1746. label = objc.IBOutlet()
  1747. def __new__(cls, *args, **kwargs):
  1748. return cls.alloc().init()
  1749. def __init__(self, session_controller, target):
  1750. NSBundle.loadNibNamed_owner_("CallTransferWindow", self)
  1751. self.session_controller = session_controller
  1752. self.label.setStringValue_("Remote party would like to transfer you to %s\nWould you like to proceed and call this address?" % target)
  1753. @objc.IBAction
  1754. def callButtonClicked_(self, sender):
  1755. self.session_controller._acceptTransfer()
  1756. self.close()
  1757. @objc.IBAction
  1758. def cancelButtonClicked_(self, sender):
  1759. self.session_controller._rejectTransfer()
  1760. self.close()
  1761. def show(self):
  1762. self.window.makeKeyAndOrderFront_(None)
  1763. def close(self):
  1764. self.session_controller = None
  1765. self.window.close()