class SMSViewController(NSObject): implements(IObserver) chatViewController = objc.IBOutlet() splitView = objc.IBOutlet() smileyButton = objc.IBOutlet() outputContainer = objc.IBOutlet() addContactView = objc.IBOutlet() addContactLabel = objc.IBOutlet() showHistoryEntries = 50 remoteTypingTimer = None enableIsComposing = False account = None target_uri = None routes = None queue = None queued_serial = 0 def initWithAccount_target_name_(self, account, target, display_name): self = super(SMSViewController, self).init() if self: self.notification_center = NotificationCenter() self.account = account self.target_uri = target self.display_name = display_name self.queue = [] self.messages = {} self.history = ChatHistory() self.local_uri = '%s@%s' % (account.id.username, account.id.domain) self.remote_uri = '%s@%s' % (self.target_uri.user, self.target_uri.host) NSBundle.loadNibNamed_owner_("SMSView", self) self.chatViewController.setContentFile_( NSBundle.mainBundle().pathForResource_ofType_( "ChatView", "html")) self.chatViewController.setAccount_(self.account) self.chatViewController.resetRenderedMessages() self.chatViewController.inputText.unregisterDraggedTypes() self.chatViewController.inputText.setMaxLength_(MAX_MESSAGE_LENGTH) self.splitView.setText_("%i chars left" % MAX_MESSAGE_LENGTH) return self def dealloc(self): if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() super(SMSViewController, self).dealloc() def awakeFromNib(self): # setup smiley popup smileys = SmileyManager().get_smiley_list() menu = self.smileyButton.menu() while menu.numberOfItems() > 0: menu.removeItemAtIndex_(0) bigText = NSAttributedString.alloc().initWithString_attributes_( " ", NSDictionary.dictionaryWithObject_forKey_( NSFont.systemFontOfSize_(16), NSFontAttributeName)) for text, file in smileys: image = NSImage.alloc().initWithContentsOfFile_(file) if not image: print "Can't load %s" % file continue image.setScalesWhenResized_(True) image.setSize_(NSMakeSize(16, 16)) atext = bigText.mutableCopy() atext.appendAttributedString_( NSAttributedString.alloc().initWithString_(text)) item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( text, "insertSmiley:", "") menu.addItem_(item) item.setTarget_(self) item.setAttributedTitle_(atext) item.setRepresentedObject_( NSAttributedString.alloc().initWithString_(text)) item.setImage_(image) def isOutputFrameVisible(self): return True def log_info(self, text): BlinkLogger().log_info(u"[Message to %s] %s" % (self.remote_uri, text)) @objc.IBAction def addContactPanelClicked_(self, sender): if sender.tag() == 1: NSApp.delegate().contactsWindowController.addContact( self.target_uri) self.addContactView.removeFromSuperview() frame = self.chatViewController.outputView.frame() frame.origin.y = 0 frame.size = self.outputContainer.frame().size self.chatViewController.outputView.setFrame_(frame) def insertSmiley_(self, sender): smiley = sender.representedObject() self.chatViewController.appendAttributedString_(smiley) def matchesTargetAccount(self, target, account): that_contact = NSApp.delegate( ).contactsWindowController.getContactMatchingURI(target) this_contact = NSApp.delegate( ).contactsWindowController.getContactMatchingURI(self.target_uri) return (self.target_uri == target or (this_contact and that_contact and this_contact == that_contact)) and self.account == account def gotMessage(self, sender, message, is_html=False, state=None, timestamp=None): self.enableIsComposing = True icon = NSApp.delegate().contactsWindowController.iconPathForURI( format_identity_to_string(sender)) timestamp = timestamp or Timestamp(datetime.datetime.now(tzlocal())) hash = hashlib.sha1() hash.update(message.encode('utf-8') + str(timestamp) + str(sender)) msgid = hash.hexdigest() self.chatViewController.showMessage(msgid, 'incoming', format_identity_to_string(sender), icon, message, timestamp, is_html=is_html, state="delivered") self.notification_center.post_notification( 'ChatViewControllerDidDisplayMessage', sender=self, data=TimestampedNotificationData( direction='incoming', history_entry=False, remote_party=format_identity_to_string(sender), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour', check_contact=True)) # save to history message = MessageInfo(msgid, direction='incoming', sender=sender, recipient=self.account, timestamp=timestamp, text=message, content_type="html" if is_html else "text", status="delivered") self.add_to_history(message) def remoteBecameIdle_(self, timer): window = timer.userInfo() if window: window.noteView_isComposing_(self, False) if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None def gotIsComposing(self, window, state, refresh, last_active): self.enableIsComposing = True flag = state == "active" if flag: if refresh is None: refresh = 120 if last_active is not None and ( last_active - datetime.datetime.now(tzlocal()) > datetime.timedelta(seconds=refresh)): # message is old, discard it return if self.remoteTypingTimer: # if we don't get any indications in the request refresh, then we assume remote to be idle self.remoteTypingTimer.setFireDate_( NSDate.dateWithTimeIntervalSinceNow_(refresh)) else: self.remoteTypingTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( refresh, self, "remoteBecameIdle:", window, False) else: if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None window.noteView_isComposing_(self, flag) @allocate_autorelease_pool @run_in_gui_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification.sender, notification.data) def _NH_DNSLookupDidFail(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) message = u"DNS lookup of SIP proxies for %s failed: %s" % (unicode( self.target_uri.host), data.error) self.setRoutesFailed(message) def _NH_DNSLookupDidSucceed(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) result_text = ', '.join( ('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in data.result)) self.log_info(u"DNS lookup for %s succeeded: %s" % (self.target_uri.host, result_text)) routes = data.result if not routes: self.setRoutesFailed("No routes found to SIP Proxy") else: self.setRoutesResolved(routes) def _NH_SIPMessageDidSucceed(self, sender, data): BlinkLogger().log_info(u"SMS message delivery suceeded") self.composeReplicationMessage(sender, data.code) message = self.messages.pop(str(sender)) if message.content_type != "application/im-iscomposing+xml": if data.code == 202: self.chatViewController.markMessage(message.msgid, MSG_STATE_DEFERRED) message.status = 'deferred' else: self.chatViewController.markMessage(message.msgid, MSG_STATE_DELIVERED) message.status = 'delivered' self.add_to_history(message) self.notification_center.remove_observer(self, sender=sender) def _NH_SIPMessageDidFail(self, sender, data): BlinkLogger().log_info(u"SMS message delivery failed: %s" % data.reason) self.composeReplicationMessage(sender, data.code) message = self.messages.pop(str(sender)) if message.content_type != "application/im-iscomposing+xml": self.chatViewController.markMessage(message.msgid, MSG_STATE_FAILED) message.status = 'failed' self.add_to_history(message) self.notification_center.remove_observer(self, sender=sender) @run_in_green_thread def add_to_history(self, message): # writes the record to the sql database cpim_to = format_identity_to_string( message.recipient) if message.recipient else '' cpim_from = format_identity_to_string( message.sender) if message.sender else '' cpim_timestamp = str(message.timestamp) content_type = "html" if "html" in message.content_type else "text" self.history.add_message(message.msgid, 'sms', self.local_uri, self.remote_uri, message.direction, cpim_from, cpim_to, cpim_timestamp, message.text, content_type, "0", message.status) def composeReplicationMessage(self, sent_message, response_code): if isinstance(self.account, Account): settings = SIPSimpleSettings() if settings.chat.sms_replication: contact = NSApp.delegate( ).contactsWindowController.getContactMatchingURI( self.target_uri) msg = CPIMMessage( sent_message.body.decode('utf-8'), sent_message.content_type, sender=CPIMIdentity(self.account.uri, self.account.display_name), recipients=[ CPIMIdentity(self.target_uri, contact.display_name if contact else None) ]) self.sendReplicationMessage(response_code, str(msg), content_type='message/cpim') @run_in_green_thread def sendReplicationMessage(self, response_code, text, content_type="message/cpim", timestamp=None): timestamp = timestamp or datetime.datetime.now(tzlocal()) # Lookup routes if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={ 'transport': self.account.sip.outbound_proxy.transport }) else: uri = SIPURI(host=self.account.id.domain) lookup = DNSLookup() settings = SIPSimpleSettings() try: routes = lookup.lookup_sip_proxy( uri, settings.sip.transport_list).wait() except DNSLookupError: pass else: utf8_encode = content_type not in ( 'application/im-iscomposing+xml', 'message/cpim') extra_headers = [ Header("X-Offline-Storage", "no"), Header("X-Replication-Code", str(response_code)), Header("X-Replication-Timestamp", str(Timestamp(datetime.datetime.now()))) ] message_request = Message( FromHeader(self.account.uri, self.account.display_name), ToHeader(self.account.uri), RouteHeader(routes[0].get_uri()), content_type, text.encode('utf-8') if utf8_encode else text, credentials=self.account.credentials, extra_headers=extra_headers) message_request.send( 15 if content_type != "application/im-iscomposing+xml" else 5) @allocate_autorelease_pool @run_in_gui_thread def setRoutesResolved(self, routes): self.routes = routes for msgid, text, content_type in self.queue: self._sendMessage(msgid, text, content_type) self.queue = [] @allocate_autorelease_pool @run_in_gui_thread def setRoutesFailed(self, msg): BlinkLogger().log_error(u"DNS Lookup failed: %s" % msg) self.chatViewController.showSystemMessage( "Cannot send SMS message to %s\n%s" % (self.target_uri, msg)) for msgid, text, content_type in self.queue: message = self.messages.pop(msgid) if content_type not in ('application/im-iscomposing+xml', 'message/cpim'): message.status = 'failed' self.add_to_history(message) self.queue = [] def _sendMessage(self, msgid, text, content_type="text/plain"): utf8_encode = content_type not in ('application/im-iscomposing+xml', 'message/cpim') message_request = Message( FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target_uri), RouteHeader(self.routes[0].get_uri()), content_type, text.encode('utf-8') if utf8_encode else text, credentials=self.account.credentials) self.notification_center.add_observer(self, sender=message_request) message_request.send( 15 if content_type != "application/im-iscomposing+xml" else 5) id = str(message_request) if content_type != "application/im-iscomposing+xml": BlinkLogger().log_info(u"Sent %s SMS message to %s" % (content_type, self.target_uri)) self.enableIsComposing = True message = self.messages.pop(msgid) message.status = 'sent' else: message = MessageInfo(id, content_type=content_type) self.messages[id] = message return message def lookup_destination(self, target_uri): assert isinstance(target_uri, SIPURI) lookup = DNSLookup() self.notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={ 'transport': self.account.sip.outbound_proxy.transport }) self.log_info(u"Starting DNS lookup for %s through proxy %s" % (target_uri.host, uri)) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) self.log_info( u"Starting DNS lookup for %s via proxy of account %s" % (target_uri.host, self.account.id)) else: uri = target_uri self.log_info(u"Starting DNS lookup for %s" % target_uri.host) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def sendMessage(self, text, content_type="text/plain"): self.lookup_destination(self.target_uri) timestamp = Timestamp(datetime.datetime.now(tzlocal())) hash = hashlib.sha1() hash.update(text.encode("utf-8") + str(timestamp)) msgid = hash.hexdigest() if content_type != "application/im-iscomposing+xml": icon = NSApp.delegate().contactsWindowController.iconPathForSelf() self.chatViewController.showMessage(msgid, 'outgoing', None, icon, text, timestamp, state="sent") recipient = CPIMIdentity(self.target_uri, self.display_name) self.messages[msgid] = MessageInfo(msgid, sender=self.account, recipient=recipient, timestamp=timestamp, content_type=content_type, text=text, status="queued") self.queue.append((msgid, text, content_type)) def textView_doCommandBySelector_(self, textView, selector): if selector == "insertNewline:" and self.chatViewController.inputText == textView: text = unicode(textView.string()) textView.setString_("") textView.didChangeText() if text: self.sendMessage(text) self.chatViewController.resetTyping() recipient = CPIMIdentity(self.target_uri, self.display_name) self.notification_center.post_notification( 'ChatViewControllerDidDisplayMessage', sender=self, data=TimestampedNotificationData( direction='outgoing', history_entry=False, remote_party=format_identity_to_string(recipient), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour', check_contact=True)) return True return False def textDidChange_(self, notif): chars_left = MAX_MESSAGE_LENGTH - self.chatViewController.inputText.textStorage( ).length() self.splitView.setText_("%i chars left" % chars_left) def getContentView(self): return self.chatViewController.view def chatView_becameIdle_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage( state=State("idle"), refresh=Refresh(60), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatView_becameActive_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage( state=State("active"), refresh=Refresh(60), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatViewDidLoad_(self, chatView): self.replay_history() @run_in_green_thread def replay_history(self): results = self.history.get_messages(local_uri=self.local_uri, remote_uri=self.remote_uri, media_type='sms', count=self.showHistoryEntries) messages = [row for row in reversed(results)] self.render_history_messages(messages) @allocate_autorelease_pool @run_in_gui_thread def render_history_messages(self, messages): for message in messages: if message.direction == 'outgoing': icon = NSApp.delegate( ).contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string( message.cpim_from)[0] icon = NSApp.delegate( ).contactsWindowController.iconPathForURI(sender_uri) timestamp = Timestamp.parse(message.cpim_timestamp) is_html = False if message.content_type == 'text' else True self.chatViewController.showMessage(message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True) def webviewFinishedLoading_(self, notification): self.document = self.outputView.mainFrameDocument() self.finishedLoading = True for script in self.messageQueue: self.outputView.stringByEvaluatingJavaScriptFromString_(script) self.messageQueue = [] if hasattr(self.delegate, "chatViewDidLoad_"): self.delegate.chatViewDidLoad_(self) def webView_decidePolicyForNavigationAction_request_frame_decisionListener_( self, webView, info, request, frame, listener): # intercept link clicks so that they are opened in Safari theURL = info[WebActionOriginalURLKey] if theURL.scheme() == "file": listener.use() else: listener.ignore() NSWorkspace.sharedWorkspace().openURL_(theURL)
class SMSViewController(NSObject): implements(IObserver) chatViewController = objc.IBOutlet() splitView = objc.IBOutlet() smileyButton = objc.IBOutlet() outputContainer = objc.IBOutlet() addContactView = objc.IBOutlet() addContactLabel = objc.IBOutlet() showHistoryEntries = 50 remoteTypingTimer = None enableIsComposing = False account = None target_uri = None routes = None queue = None queued_serial = 0 def initWithAccount_target_name_(self, account, target, display_name): self = super(SMSViewController, self).init() if self: self.notification_center = NotificationCenter() self.account = account self.target_uri = target self.display_name = display_name self.queue = [] self.messages = {} self.history=ChatHistory() self.local_uri = '%s@%s' % (account.id.username, account.id.domain) self.remote_uri = '%s@%s' % (self.target_uri.user, self.target_uri.host) NSBundle.loadNibNamed_owner_("SMSView", self) self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) self.chatViewController.setAccount_(self.account) self.chatViewController.resetRenderedMessages() self.chatViewController.inputText.unregisterDraggedTypes() self.chatViewController.inputText.setMaxLength_(MAX_MESSAGE_LENGTH) self.splitView.setText_("%i chars left" % MAX_MESSAGE_LENGTH) return self def dealloc(self): if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() super(SMSViewController, self).dealloc() def awakeFromNib(self): # setup smiley popup smileys = SmileyManager().get_smiley_list() menu = self.smileyButton.menu() while menu.numberOfItems() > 0: menu.removeItemAtIndex_(0) bigText = NSAttributedString.alloc().initWithString_attributes_(" ", NSDictionary.dictionaryWithObject_forKey_(NSFont.systemFontOfSize_(16), NSFontAttributeName)) for text, file in smileys: image = NSImage.alloc().initWithContentsOfFile_(file) if not image: print "Can't load %s" % file continue image.setScalesWhenResized_(True) image.setSize_(NSMakeSize(16, 16)) atext = bigText.mutableCopy() atext.appendAttributedString_(NSAttributedString.alloc().initWithString_(text)) item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(text, "insertSmiley:", "") menu.addItem_(item) item.setTarget_(self) item.setAttributedTitle_(atext) item.setRepresentedObject_(NSAttributedString.alloc().initWithString_(text)) item.setImage_(image) def isOutputFrameVisible(self): return True def log_info(self, text): BlinkLogger().log_info(u"[Message to %s] %s" % (self.remote_uri, text)) @objc.IBAction def addContactPanelClicked_(self, sender): if sender.tag() == 1: NSApp.delegate().contactsWindowController.addContact(self.target_uri) self.addContactView.removeFromSuperview() frame = self.chatViewController.outputView.frame() frame.origin.y = 0 frame.size = self.outputContainer.frame().size self.chatViewController.outputView.setFrame_(frame) def insertSmiley_(self, sender): smiley = sender.representedObject() self.chatViewController.appendAttributedString_(smiley) def matchesTargetAccount(self, target, account): that_contact = NSApp.delegate().contactsWindowController.getContactMatchingURI(target) this_contact = NSApp.delegate().contactsWindowController.getContactMatchingURI(self.target_uri) return (self.target_uri==target or (this_contact and that_contact and this_contact==that_contact)) and self.account==account def gotMessage(self, sender, message, is_html=False, state=None, timestamp=None): self.enableIsComposing = True icon = NSApp.delegate().contactsWindowController.iconPathForURI(format_identity_to_string(sender)) timestamp = timestamp or Timestamp(datetime.datetime.now(tzlocal())) hash = hashlib.sha1() hash.update(message.encode('utf-8')+str(timestamp)+str(sender)) msgid = hash.hexdigest() self.chatViewController.showMessage(msgid, 'incoming', format_identity_to_string(sender), icon, message, timestamp, is_html=is_html, state="delivered") self.notification_center.post_notification('ChatViewControllerDidDisplayMessage', sender=self, data=TimestampedNotificationData(direction='incoming', history_entry=False, remote_party=format_identity_to_string(sender), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour', check_contact=True)) # save to history message = MessageInfo(msgid, direction='incoming', sender=sender, recipient=self.account, timestamp=timestamp, text=message, content_type="html" if is_html else "text", status="delivered") self.add_to_history(message) def remoteBecameIdle_(self, timer): window = timer.userInfo() if window: window.noteView_isComposing_(self, False) if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None def gotIsComposing(self, window, state, refresh, last_active): self.enableIsComposing = True flag = state == "active" if flag: if refresh is None: refresh = 120 if last_active is not None and (last_active - datetime.datetime.now(tzlocal()) > datetime.timedelta(seconds=refresh)): # message is old, discard it return if self.remoteTypingTimer: # if we don't get any indications in the request refresh, then we assume remote to be idle self.remoteTypingTimer.setFireDate_(NSDate.dateWithTimeIntervalSinceNow_(refresh)) else: self.remoteTypingTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(refresh, self, "remoteBecameIdle:", window, False) else: if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None window.noteView_isComposing_(self, flag) @allocate_autorelease_pool @run_in_gui_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification.sender, notification.data) def _NH_DNSLookupDidFail(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) message = u"DNS lookup of SIP proxies for %s failed: %s" % (unicode(self.target_uri.host), data.error) self.setRoutesFailed(message) def _NH_DNSLookupDidSucceed(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) result_text = ', '.join(('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in data.result)) self.log_info(u"DNS lookup for %s succeeded: %s" % (self.target_uri.host, result_text)) routes = data.result if not routes: self.setRoutesFailed("No routes found to SIP Proxy") else: self.setRoutesResolved(routes) def _NH_SIPMessageDidSucceed(self, sender, data): BlinkLogger().log_info(u"SMS message delivery suceeded") self.composeReplicationMessage(sender, data.code) message = self.messages.pop(str(sender)) if message.content_type != "application/im-iscomposing+xml": if data.code == 202: self.chatViewController.markMessage(message.msgid, MSG_STATE_DEFERRED) message.status='deferred' else: self.chatViewController.markMessage(message.msgid, MSG_STATE_DELIVERED) message.status='delivered' self.add_to_history(message) self.notification_center.remove_observer(self, sender=sender) def _NH_SIPMessageDidFail(self, sender, data): BlinkLogger().log_info(u"SMS message delivery failed: %s" % data.reason) self.composeReplicationMessage(sender, data.code) message = self.messages.pop(str(sender)) if message.content_type != "application/im-iscomposing+xml": self.chatViewController.markMessage(message.msgid, MSG_STATE_FAILED) message.status='failed' self.add_to_history(message) self.notification_center.remove_observer(self, sender=sender) @run_in_green_thread def add_to_history(self, message): # writes the record to the sql database cpim_to = format_identity_to_string(message.recipient) if message.recipient else '' cpim_from = format_identity_to_string(message.sender) if message.sender else '' cpim_timestamp = str(message.timestamp) content_type="html" if "html" in message.content_type else "text" self.history.add_message(message.msgid, 'sms', self.local_uri, self.remote_uri, message.direction, cpim_from, cpim_to, cpim_timestamp, message.text, content_type, "0", message.status) def composeReplicationMessage(self, sent_message, response_code): if isinstance(self.account, Account): settings = SIPSimpleSettings() if settings.chat.sms_replication: contact = NSApp.delegate().contactsWindowController.getContactMatchingURI(self.target_uri) msg = CPIMMessage(sent_message.body.decode('utf-8'), sent_message.content_type, sender=CPIMIdentity(self.account.uri, self.account.display_name), recipients=[CPIMIdentity(self.target_uri, contact.display_name if contact else None)]) self.sendReplicationMessage(response_code, str(msg), content_type='message/cpim') @run_in_green_thread def sendReplicationMessage(self, response_code, text, content_type="message/cpim", timestamp=None): timestamp = timestamp or datetime.datetime.now(tzlocal()) # Lookup routes if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = SIPURI(host=self.account.id.domain) lookup = DNSLookup() settings = SIPSimpleSettings() try: routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait() except DNSLookupError: pass else: utf8_encode = content_type not in ('application/im-iscomposing+xml', 'message/cpim') extra_headers = [Header("X-Offline-Storage", "no"), Header("X-Replication-Code", str(response_code)), Header("X-Replication-Timestamp", str(Timestamp(datetime.datetime.now())))] message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.account.uri), RouteHeader(routes[0].get_uri()), content_type, text.encode('utf-8') if utf8_encode else text, credentials=self.account.credentials, extra_headers=extra_headers) message_request.send(15 if content_type != "application/im-iscomposing+xml" else 5) @allocate_autorelease_pool @run_in_gui_thread def setRoutesResolved(self, routes): self.routes = routes for msgid, text, content_type in self.queue: self._sendMessage(msgid, text, content_type) self.queue = [] @allocate_autorelease_pool @run_in_gui_thread def setRoutesFailed(self, msg): BlinkLogger().log_error(u"DNS Lookup failed: %s" % msg) self.chatViewController.showSystemMessage("Cannot send SMS message to %s\n%s" % (self.target_uri, msg)) for msgid, text, content_type in self.queue: message = self.messages.pop(msgid) if content_type not in ('application/im-iscomposing+xml', 'message/cpim'): message.status='failed' self.add_to_history(message) self.queue = [] def _sendMessage(self, msgid, text, content_type="text/plain"): utf8_encode = content_type not in ('application/im-iscomposing+xml', 'message/cpim') message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target_uri), RouteHeader(self.routes[0].get_uri()), content_type, text.encode('utf-8') if utf8_encode else text, credentials=self.account.credentials) self.notification_center.add_observer(self, sender=message_request) message_request.send(15 if content_type!="application/im-iscomposing+xml" else 5) id=str(message_request) if content_type != "application/im-iscomposing+xml": BlinkLogger().log_info(u"Sent %s SMS message to %s" % (content_type, self.target_uri)) self.enableIsComposing = True message = self.messages.pop(msgid) message.status='sent' else: message = MessageInfo(id, content_type=content_type) self.messages[id] = message return message def lookup_destination(self, target_uri): assert isinstance(target_uri, SIPURI) lookup = DNSLookup() self.notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) self.log_info(u"Starting DNS lookup for %s through proxy %s" % (target_uri.host, uri)) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) self.log_info(u"Starting DNS lookup for %s via proxy of account %s" % (target_uri.host, self.account.id)) else: uri = target_uri self.log_info(u"Starting DNS lookup for %s" % target_uri.host) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) def sendMessage(self, text, content_type="text/plain"): self.lookup_destination(self.target_uri) timestamp = Timestamp(datetime.datetime.now(tzlocal())) hash = hashlib.sha1() hash.update(text.encode("utf-8")+str(timestamp)) msgid = hash.hexdigest() if content_type != "application/im-iscomposing+xml": icon = NSApp.delegate().contactsWindowController.iconPathForSelf() self.chatViewController.showMessage(msgid, 'outgoing', None, icon, text, timestamp, state="sent") recipient=CPIMIdentity(self.target_uri, self.display_name) self.messages[msgid] = MessageInfo(msgid, sender=self.account, recipient=recipient, timestamp=timestamp, content_type=content_type, text=text, status="queued") self.queue.append((msgid, text, content_type)) def textView_doCommandBySelector_(self, textView, selector): if selector == "insertNewline:" and self.chatViewController.inputText == textView: text = unicode(textView.string()) textView.setString_("") textView.didChangeText() if text: self.sendMessage(text) self.chatViewController.resetTyping() recipient=CPIMIdentity(self.target_uri, self.display_name) self.notification_center.post_notification('ChatViewControllerDidDisplayMessage', sender=self, data=TimestampedNotificationData(direction='outgoing', history_entry=False, remote_party=format_identity_to_string(recipient), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour', check_contact=True)) return True return False def textDidChange_(self, notif): chars_left = MAX_MESSAGE_LENGTH - self.chatViewController.inputText.textStorage().length() self.splitView.setText_("%i chars left" % chars_left) def getContentView(self): return self.chatViewController.view def chatView_becameIdle_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage(state=State("idle"), refresh=Refresh(60), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatView_becameActive_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage(state=State("active"), refresh=Refresh(60), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatViewDidLoad_(self, chatView): self.replay_history() @run_in_green_thread def replay_history(self): results = self.history.get_messages(local_uri=self.local_uri, remote_uri=self.remote_uri, media_type='sms', count=self.showHistoryEntries) messages = [row for row in reversed(results)] self.render_history_messages(messages) @allocate_autorelease_pool @run_in_gui_thread def render_history_messages(self, messages): for message in messages: if message.direction == 'outgoing': icon = NSApp.delegate().contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string(message.cpim_from)[0] icon = NSApp.delegate().contactsWindowController.iconPathForURI(sender_uri) timestamp=Timestamp.parse(message.cpim_timestamp) is_html = False if message.content_type == 'text' else True self.chatViewController.showMessage(message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True) def webviewFinishedLoading_(self, notification): self.document = self.outputView.mainFrameDocument() self.finishedLoading = True for script in self.messageQueue: self.outputView.stringByEvaluatingJavaScriptFromString_(script) self.messageQueue = [] if hasattr(self.delegate, "chatViewDidLoad_"): self.delegate.chatViewDidLoad_(self) def webView_decidePolicyForNavigationAction_request_frame_decisionListener_(self, webView, info, request, frame, listener): # intercept link clicks so that they are opened in Safari theURL = info[WebActionOriginalURLKey] if theURL.scheme() == "file": listener.use() else: listener.ignore() NSWorkspace.sharedWorkspace().openURL_(theURL)
class SMSViewController(NSObject): chatViewController = objc.IBOutlet() splitView = objc.IBOutlet() smileyButton = objc.IBOutlet() outputContainer = objc.IBOutlet() addContactView = objc.IBOutlet() addContactLabel = objc.IBOutlet() zoom_period_label = '' showHistoryEntries = 50 remoteTypingTimer = None enableIsComposing = False handle_scrolling = True scrollingTimer = None scrolling_back = False message_count_from_history = 0 contact = None account = None target_uri = None routes = None queue = None queued_serial = 0 windowController = None last_route = None def initWithAccount_target_name_(self, account, target, display_name): self = objc.super(SMSViewController, self).init() if self: self.session_id = str(uuid.uuid1()) self.notification_center = NotificationCenter() self.account = account self.target_uri = target self.display_name = display_name self.messages = {} self.encryption = OTREncryption(self) self.message_queue = EventQueue(self._send_message) self.history = ChatHistory() self.local_uri = '%s@%s' % (account.id.username, account.id.domain) self.remote_uri = '%s@%s' % (self.target_uri.user.decode(), self.target_uri.host.decode()) self.contact = NSApp.delegate( ).contactsWindowController.getFirstContactFromAllContactsGroupMatchingURI( self.remote_uri) NSBundle.loadNibNamed_owner_("SMSView", self) self.chatViewController.setContentFile_( NSBundle.mainBundle().pathForResource_ofType_( "ChatView", "html")) self.chatViewController.setAccount_(self.account) self.chatViewController.resetRenderedMessages() self.chatViewController.inputText.unregisterDraggedTypes() self.chatViewController.inputText.setMaxLength_(MAX_MESSAGE_LENGTH) self.splitView.setText_( NSLocalizedString("%i chars left", "Label") % MAX_MESSAGE_LENGTH) self.enableIsComposing = True self.log_info('Using local account %s' % self.local_uri) self.notification_center.add_observer( self, name='ChatStreamOTREncryptionStateChanged') self.started = False return self def dealloc(self): if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.chatViewController.close() objc.super(SMSViewController, self).dealloc() def awakeFromNib(self): # setup smiley popup smileys = SmileyManager().get_smiley_list() menu = self.smileyButton.menu() while menu.numberOfItems() > 0: menu.removeItemAtIndex_(0) bigText = NSAttributedString.alloc().initWithString_attributes_( " ", NSDictionary.dictionaryWithObject_forKey_( NSFont.systemFontOfSize_(16), NSFontAttributeName)) for text, file in smileys: image = NSImage.alloc().initWithContentsOfFile_(file) if not image: print("Can't load %s" % file) continue image.setScalesWhenResized_(True) image.setSize_(NSMakeSize(16, 16)) atext = bigText.mutableCopy() atext.appendAttributedString_( NSAttributedString.alloc().initWithString_(text)) item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( text, "insertSmiley:", "") menu.addItem_(item) item.setTarget_(self) item.setAttributedTitle_(atext) item.setRepresentedObject_( NSAttributedString.alloc().initWithString_(text)) item.setImage_(image) @objc.python_method def revalidateToolbar(self): pass @objc.python_method def isOutputFrameVisible(self): return True @objc.python_method def log_info(self, text): BlinkLogger().log_info("[SMS with %s] %s" % (self.remote_uri, text)) @objc.IBAction def addContactPanelClicked_(self, sender): if sender.tag() == 1: NSApp.delegate().contactsWindowController.addContact( uris=[(self.target_uri, 'sip')]) self.addContactView.removeFromSuperview() frame = self.chatViewController.outputView.frame() frame.origin.y = 0 frame.size = self.outputContainer.frame().size self.chatViewController.outputView.setFrame_(frame) @objc.python_method def insertSmiley_(self, sender): smiley = sender.representedObject() self.chatViewController.appendAttributedString_(smiley) @objc.python_method def matchesTargetAccount(self, target, account): that_contact = NSApp.delegate( ).contactsWindowController.getFirstContactMatchingURI(target) this_contact = NSApp.delegate( ).contactsWindowController.getFirstContactMatchingURI(self.target_uri) return (self.target_uri == target or (this_contact and that_contact and this_contact == that_contact)) and self.account == account @objc.python_method def gotMessage(self, sender, call_id, content, content_type, is_replication_message=False, timestamp=None, window=None): is_html = content_type == 'text/html' encrypted = False try: content = self.encryption.otr_session.handle_input( content, content_type) except IgnoreMessage: return None except UnencryptedMessage: encrypted = False encryption_active = True except EncryptedMessageError as e: self.log_info('OTP encrypted message error: %s' % str(e)) return None except OTRError as e: self.log_info('OTP error: %s' % str(e)) return None else: encrypted = encryption_active = self.encryption.active content = content.decode() if isinstance(content, bytes) else content if content.startswith('?OTR:'): self.log_info('Dropped OTR message that could not be decoded') return None self.enableIsComposing = True icon = NSApp.delegate().contactsWindowController.iconPathForURI( format_identity_to_string(sender)) timestamp = timestamp or ISOTimestamp.now() hash = hashlib.sha1() hash.update((content + str(timestamp) + str(sender)).encode('utf-8')) id = hash.hexdigest() encryption = '' if encrypted: encryption = 'verified' if self.encryption.verified else 'unverified' if not is_replication_message and not window.isKeyWindow(): nc_body = html2txt(content) if is_html else content nc_title = NSLocalizedString("SMS Message Received", "Label") nc_subtitle = format_identity_to_string(sender, format='full') NSApp.delegate().gui_notify(nc_title, nc_body, nc_subtitle) self.log_info("Incoming message %s received" % call_id) self.chatViewController.showMessage(call_id, id, 'incoming', format_identity_to_string(sender), icon, content, timestamp, is_html=is_html, state="delivered", media_type='sms', encryption=encryption) self.notification_center.post_notification( 'ChatViewControllerDidDisplayMessage', sender=self, data=NotificationData( direction='incoming', history_entry=False, remote_party=format_identity_to_string(sender), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour.local', check_contact=True)) # save to history if not is_replication_message or (is_replication_message and self.local_uri == self.account.id): message = MessageInfo(id, call_id=call_id, direction='incoming', sender=sender, recipient=self.account, timestamp=timestamp, content=content, content_type=content_type, status="delivered", encryption=encryption) self.add_to_history(message) def remoteBecameIdle_(self, timer): window = timer.userInfo() if window: window.noteView_isComposing_(self, False) if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None @objc.python_method def gotIsComposing(self, window, state, refresh, last_active): self.enableIsComposing = True flag = state == "active" if flag: if refresh is None: refresh = 120 if last_active is not None and ( last_active - ISOTimestamp.now() > datetime.timedelta(seconds=refresh)): # message is old, discard it return if self.remoteTypingTimer: # if we don't get any indications in the request refresh, then we assume remote to be idle self.remoteTypingTimer.setFireDate_( NSDate.dateWithTimeIntervalSinceNow_(refresh)) else: self.remoteTypingTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( refresh, self, "remoteBecameIdle:", window, False) else: if self.remoteTypingTimer: self.remoteTypingTimer.invalidate() self.remoteTypingTimer = None window.noteView_isComposing_(self, flag) @objc.python_method @run_in_gui_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification.sender, notification.data) @objc.python_method def inject_otr_message(self, data): if not self.encryption.active: self.log_info('Negotiating OTR encryption...') messageObject = OTRInternalMessage(data) self.sendMessage(messageObject) @objc.python_method def _NH_DNSLookupDidFail(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) message = "DNS lookup of SIP proxies for %s failed: %s" % (str( self.target_uri.host), data.error) self.setRoutesFailed(message) @objc.python_method def _NH_DNSLookupDidSucceed(self, lookup, data): self.notification_center.remove_observer(self, sender=lookup) result_text = ', '.join( ('%s:%s (%s)' % (result.address, result.port, result.transport.upper()) for result in data.result)) self.log_info("DNS lookup for %s succeeded: %s" % (self.target_uri.host.decode(), result_text)) routes = data.result if not routes: self.setRoutesFailed("No routes found to SIP Proxy") else: self.setRoutesResolved(routes) @objc.python_method def _NH_ChatStreamOTREncryptionStateChanged(self, stream, data): if data.new_state is OTRState.Encrypted: local_fingerprint = stream.encryption.key_fingerprint remote_fingerprint = stream.encryption.peer_fingerprint self.log_info("Chat encryption activated using OTR protocol") self.log_info("OTR local fingerprint %s" % local_fingerprint) self.log_info("OTR remote fingerprint %s" % remote_fingerprint) self.chatViewController.showSystemMessage("0", "Encryption enabled", ISOTimestamp.now()) self.showSystemMessage("Encryption enabled", ISOTimestamp.now()) elif data.new_state is OTRState.Finished: self.log_info("Chat encryption deactivated") self.chatViewController.showSystemMessage("0", "Encryption deactivated", ISOTimestamp.now(), is_error=True) elif data.new_state is OTRState.Plaintext: self.log_info("Chat encryption deactivated") self.chatViewController.showSystemMessage("0", "Encryption deactivated", ISOTimestamp.now(), is_error=True) @objc.python_method def _NH_SIPMessageDidSucceed(self, sender, data): try: message = self.messages.pop(str(sender)) except KeyError: pass else: call_id = data.headers['Call-ID'].body self.composeReplicationMessage(message, data.code) if message.content_type != "application/im-iscomposing+xml": self.log_info("Outgoing %s message %s delivered" % (message.content_type, call_id)) if data.code == 202: self.chatViewController.markMessage( message.id, MSG_STATE_DEFERRED) message.status = 'deferred' else: self.chatViewController.markMessage( message.id, MSG_STATE_DELIVERED) message.status = 'delivered' message.call_id = call_id self.add_to_history(message) self.notification_center.remove_observer(self, sender=sender) @objc.python_method def _NH_SIPMessageDidFail(self, sender, data): try: message = self.messages.pop(str(sender)) except KeyError: pass else: if data.code == 408: self.last_route = None call_id = message.call_id self.composeReplicationMessage(message, data.code) if message.content_type != "application/im-iscomposing+xml": self.chatViewController.markMessage(message.id, MSG_STATE_FAILED) message.status = 'failed' self.add_to_history(message) self.log_info("Outgoing message %s delivery failed: %s" % (call_id.decode(), data.reason)) self.notification_center.remove_observer(self, sender=sender) @objc.python_method def add_to_history(self, message): # writes the record to the sql database cpim_to = format_identity_to_string( message.recipient) if message.recipient else '' cpim_from = format_identity_to_string( message.sender) if message.sender else '' cpim_timestamp = str(message.timestamp) content_type = "html" if "html" in message.content_type else "text" self.history.add_message(message.id, 'sms', self.local_uri, self.remote_uri, message.direction, cpim_from, cpim_to, cpim_timestamp, message.content, content_type, "0", message.status, call_id=message.call_id) @objc.python_method def composeReplicationMessage(self, sent_message, response_code): if sent_message.content_type == "application/im-iscomposing+xml": return if isinstance(self.account, Account): if not self.account.sms.disable_replication: contact = NSApp.delegate( ).contactsWindowController.getFirstContactMatchingURI( self.target_uri) msg = CPIMPayload( sent_message.content, sent_message.content_type, charset='utf-8', sender=ChatIdentity(self.account.uri, self.account.display_name), recipients=[ ChatIdentity(self.target_uri, contact.name if contact else None) ]) self.sendReplicationMessage(response_code, msg.encode()[0], content_type='message/cpim') @objc.python_method @run_in_green_thread def sendReplicationMessage(self, response_code, content, content_type="message/cpim", timestamp=None): timestamp = timestamp or ISOTimestamp.now() # Lookup routes if self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={ 'transport': self.account.sip.outbound_proxy.transport }) else: uri = SIPURI(host=self.account.id.domain) route = None if self.last_route is None: lookup = DNSLookup() settings = SIPSimpleSettings() try: routes = lookup.lookup_sip_proxy( uri, settings.sip.transport_list).wait() except DNSLookupError: pass else: route = routes[0] else: route = self.last_route if route: extra_headers = [ Header("X-Offline-Storage", "no"), Header("X-Replication-Code", str(response_code)), Header("X-Replication-Timestamp", str(ISOTimestamp.now())) ] message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.account.uri), RouteHeader(route.uri), content_type, content, credentials=self.account.credentials, extra_headers=extra_headers) message_request.send( 15 if content_type != "application/im-iscomposing+xml" else 5) @objc.python_method @run_in_gui_thread def setRoutesResolved(self, routes): self.routes = routes self.last_route = self.routes[0] self.log_info('Proceed using route: %s' % self.last_route) if not self.started: self.message_queue.start() if not self.encryption.active and not self.started: self.encryption.start() self.started = True @objc.python_method @run_in_gui_thread def setRoutesFailed(self, reason): self.message_queue.stop() self.started = False for msgObject in self.message_queue: id = msgObject.id try: message = self.messages.pop(id) except KeyError: pass else: if content_type not in ('application/im-iscomposing+xml', 'message/cpim'): self.chatViewController.markMessage( message.id, MSG_STATE_FAILED) message.status = 'failed' self.add_to_history(message) log_text = NSLocalizedString("Routing failure: %s", "Label") % msg self.chatViewController.showSystemMessage( '0', reason, ISOTimestamp.now(), True) self.log_info(log_text) @objc.python_method def _send_message(self, message): if (not self.last_route): self.log_info('No route found') return message.timestamp = ISOTimestamp.now() if not isinstance(message, OTRInternalMessage): try: content = self.encryption.otr_session.handle_output( message.content, message.content_type) except OTRError as e: if 'has ended the private conversation' in str(e): self.log_info( 'Encryption has been disabled by remote party, please resend the message again' ) self.encryption.stop() else: self.log_info('Failed to encrypt outgoing message: %s' % str(e)) return timeout = 5 if message.content_type != "application/im-iscomposing+xml": self.enableIsComposing = True timeout = 15 message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target_uri), RouteHeader(self.last_route.uri), message.content_type, message.content, credentials=self.account.credentials) self.notification_center.add_observer(self, sender=message_request) message_request.send(timeout) message.status = 'sent' message.call_id = message_request._request.call_id.decode() if not isinstance(message, OTRInternalMessage): if self.encryption.active: self.log_info( 'Sending encrypted %s message %s to %s' % (message.content_type, message.id, self.last_route.uri)) else: self.log_info( 'Sending %s message %s to %s' % (message.content_type, message.id, self.last_route.uri)) id = str(message_request) self.messages[id] = message @objc.python_method def lookup_destination(self, target_uri): assert isinstance(target_uri, SIPURI) lookup = DNSLookup() self.notification_center.add_observer(self, sender=lookup) settings = SIPSimpleSettings() if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={ 'transport': self.account.sip.outbound_proxy.transport }) self.log_info("Starting DNS lookup for %s through proxy %s" % (target_uri.host.decode(), uri)) elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) self.log_info( "Starting DNS lookup for %s via proxy of account %s" % (target_uri.host.decode(), self.account.id)) else: uri = target_uri self.log_info("Starting DNS lookup for %s" % target_uri.host.decode()) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) @objc.python_method def sendMessage(self, content, content_type="text/plain"): # entry point for sending messages, they will be added to self.message_queue if content_type != "application/im-iscomposing+xml": icon = NSApp.delegate().contactsWindowController.iconPathForSelf() if not isinstance(content, OTRInternalMessage): timestamp = ISOTimestamp.now() hash = hashlib.sha1() content = content.decode() if isinstance(content, bytes) else content hash.update((content + str(timestamp)).encode("utf-8")) id = hash.hexdigest() call_id = '' encryption = '' if self.encryption.active: encryption = 'verified' if self.encryption.verified else 'unverified' self.chatViewController.showMessage(call_id, id, 'outgoing', None, icon, content, timestamp, state="sent", media_type='sms', encryption=encryption) recipient = ChatIdentity(self.target_uri, self.display_name) mInfo = MessageInfo(id, sender=self.account, recipient=recipient, timestamp=timestamp, content_type=content_type, content=content, status="queued", encryption=encryption) self.messages[id] = mInfo self.message_queue.put(mInfo) else: self.message_queue.put(content) # Async DNS lookup if host is None or host.default_ip is None: self.setRoutesFailed( NSLocalizedString("No Internet connection", "Label")) return if self.last_route is None: self.lookup_destination(self.target_uri) else: self.setRoutesResolved([self.last_route]) def textView_doCommandBySelector_(self, textView, selector): if selector == "insertNewline:" and self.chatViewController.inputText == textView: content = str(textView.string()) textView.setString_("") textView.didChangeText() if content: self.sendMessage(content) self.chatViewController.resetTyping() recipient = ChatIdentity(self.target_uri, self.display_name) self.notification_center.post_notification( 'ChatViewControllerDidDisplayMessage', sender=self, data=NotificationData( direction='outgoing', history_entry=False, remote_party=format_identity_to_string(recipient), local_party=format_identity_to_string(self.account) if self.account is not BonjourAccount() else 'bonjour.local', check_contact=True)) return True return False def textDidChange_(self, notif): chars_left = MAX_MESSAGE_LENGTH - self.chatViewController.inputText.textStorage( ).length() self.splitView.setText_( NSLocalizedString("%i chars left", "Label") % chars_left) @objc.python_method def getContentView(self): return self.chatViewController.view def chatView_becameIdle_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage( state=State("idle"), refresh=Refresh(60), last_active=LastActive(last_active or ISOTimestamp.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatView_becameActive_(self, chatView, last_active): if self.enableIsComposing: content = IsComposingMessage( state=State("active"), refresh=Refresh(60), last_active=LastActive(last_active or ISOTimestamp.now()), content_type=ContentType('text')).toxml() self.sendMessage(content, IsComposingDocument.content_type) def chatViewDidLoad_(self, chatView): self.replay_history() @objc.python_method def scroll_back_in_time(self): self.chatViewController.clear() self.chatViewController.resetRenderedMessages() self.replay_history() @objc.python_method @run_in_green_thread def replay_history(self): blink_contact = NSApp.delegate( ).contactsWindowController.getFirstContactMatchingURI(self.target_uri) if not blink_contact: remote_uris = self.remote_uri else: remote_uris = list( str(uri.uri) for uri in blink_contact.uris if '@' in uri.uri) zoom_factor = self.chatViewController.scrolling_zoom_factor if zoom_factor: period_array = { 1: datetime.datetime.now() - datetime.timedelta(days=2), 2: datetime.datetime.now() - datetime.timedelta(days=7), 3: datetime.datetime.now() - datetime.timedelta(days=31), 4: datetime.datetime.now() - datetime.timedelta(days=90), 5: datetime.datetime.now() - datetime.timedelta(days=180), 6: datetime.datetime.now() - datetime.timedelta(days=365), 7: datetime.datetime.now() - datetime.timedelta(days=3650) } after_date = period_array[zoom_factor].strftime("%Y-%m-%d") if zoom_factor == 1: self.zoom_period_label = NSLocalizedString( "Displaying messages from last day", "Label") elif zoom_factor == 2: self.zoom_period_label = NSLocalizedString( "Displaying messages from last week", "Label") elif zoom_factor == 3: self.zoom_period_label = NSLocalizedString( "Displaying messages from last month", "Label") elif zoom_factor == 4: self.zoom_period_label = NSLocalizedString( "Displaying messages from last three months", "Label") elif zoom_factor == 5: self.zoom_period_label = NSLocalizedString( "Displaying messages from last six months", "Label") elif zoom_factor == 6: self.zoom_period_label = NSLocalizedString( "Displaying messages from last year", "Label") elif zoom_factor == 7: self.zoom_period_label = NSLocalizedString( "Displaying all messages", "Label") self.chatViewController.setHandleScrolling_(False) results = self.history.get_messages( remote_uri=remote_uris, media_type=('chat', 'sms'), after_date=after_date, count=10000, search_text=self.chatViewController.search_text) else: results = self.history.get_messages( remote_uri=remote_uris, media_type=('chat', 'sms'), count=self.showHistoryEntries, search_text=self.chatViewController.search_text) messages = [row for row in reversed(results)] self.render_history_messages(messages) @objc.python_method @run_in_gui_thread def render_history_messages(self, messages): if self.chatViewController.scrolling_zoom_factor: if not self.message_count_from_history: self.message_count_from_history = len(messages) self.chatViewController.lastMessagesLabel.setStringValue_( self.zoom_period_label) else: if self.message_count_from_history == len(messages): self.chatViewController.setHandleScrolling_(False) self.chatViewController.lastMessagesLabel.setStringValue_( NSLocalizedString( "%s. There are no previous messages.", "Label") % self.zoom_period_label) self.chatViewController.setHandleScrolling_(False) else: self.chatViewController.lastMessagesLabel.setStringValue_( self.zoom_period_label) else: self.message_count_from_history = len(messages) if len(messages): self.chatViewController.lastMessagesLabel.setStringValue_( NSLocalizedString("Scroll up for going back in time", "Label")) else: self.chatViewController.setHandleScrolling_(False) self.chatViewController.lastMessagesLabel.setStringValue_( NSLocalizedString("There are no previous messages", "Label")) if len(messages): message = messages[0] delta = datetime.date.today() - message.date if not self.chatViewController.scrolling_zoom_factor: if delta.days <= 2: self.chatViewController.scrolling_zoom_factor = 1 elif delta.days <= 7: self.chatViewController.scrolling_zoom_factor = 2 elif delta.days <= 31: self.chatViewController.scrolling_zoom_factor = 3 elif delta.days <= 90: self.chatViewController.scrolling_zoom_factor = 4 elif delta.days <= 180: self.chatViewController.scrolling_zoom_factor = 5 elif delta.days <= 365: self.chatViewController.scrolling_zoom_factor = 6 elif delta.days <= 3650: self.chatViewController.scrolling_zoom_factor = 7 call_id = None seen_sms = {} last_media_type = 'sms' last_chat_timestamp = None for message in messages: if message.status == 'failed': continue if message.sip_callid != '' and message.media_type == 'sms': try: seen = seen_sms[message.sip_callid] except KeyError: seen_sms[message.sip_callid] = True else: continue if message.direction == 'outgoing': icon = NSApp.delegate( ).contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string( message.cpim_from)[0] icon = NSApp.delegate( ).contactsWindowController.iconPathForURI(sender_uri) timestamp = ISOTimestamp(message.cpim_timestamp) is_html = False if message.content_type == 'text' else True #if call_id is not None and call_id != message.sip_callid and message.media_type == 'chat': # self.chatViewController.showSystemMessage(message.sip_callid, 'Chat session established', timestamp, False) #if message.media_type == 'sms' and last_media_type == 'chat': # self.chatViewController.showSystemMessage(message.sip_callid, 'Short messages', timestamp, False) self.chatViewController.showMessage(message.sip_callid, message.id, message.direction, message.cpim_from, icon, message.body, timestamp, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True, media_type=message.media_type, encryption=message.encryption) call_id = message.sip_callid last_media_type = 'chat' if message.media_type == 'chat' else 'sms' if message.media_type == 'chat': last_chat_timestamp = timestamp self.chatViewController.loadingProgressIndicator.stopAnimation_(None) self.chatViewController.loadingTextIndicator.setStringValue_("") def webviewFinishedLoading_(self, notification): self.document = self.outputView.mainFrameDocument() self.finishedLoading = True for script in self.messageQueue: self.outputView.stringByEvaluatingJavaScriptFromString_(script) self.messageQueue = [] if hasattr(self.delegate, "chatViewDidLoad_"): self.delegate.chatViewDidLoad_(self) def webView_decidePolicyForNavigationAction_request_frame_decisionListener_( self, webView, info, request, frame, listener): # intercept link clicks so that they are opened in Safari theURL = info[WebActionOriginalURLKey] if theURL.scheme() == "file": listener.use() else: listener.ignore() NSWorkspace.sharedWorkspace().openURL_(theURL)