def __init__(self): if self: BlinkLogger().log_debug('Starting History Viewer') NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkHistoryViewerContact('Any Address', name='All Contacts') self.bonjour_contact = BlinkHistoryViewerContact('bonjour.local', name='Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.notification_center.add_observer(self, name='BlinkConferenceContactPresenceHasChanged') self.notification_center.add_observer(self, name='BlinkShouldTerminate') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_(NSLocalizedString("Type text and press Enter", "Placeholder text")) self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) self.chatViewController.setHandleScrolling_(False) self.entriesView.setShouldCloseWithWindow_(False) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.chat_history = ChatHistory() self.session_history = SessionHistory() self.setPeriod(1) self.selectedTableView = self.contactTable
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 __init__(self): if self: NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkContact( 'bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer( self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer( self, name='AudioCallLoggedToHistory') self.notification_center.add_observer( self, name='BlinkContactsHaveChanged') self.notification_center.add_observer( self, name='BlinkTableViewSelectionChaged') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_( "Type text and press Enter") self.chatViewController.setContentFile_( NSBundle.mainBundle().pathForResource_ofType_( "ChatView", "html")) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_( c, True) col.setSortDescriptorPrototype_(descriptor) self.history = ChatHistory() tag = self.afterDate.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None else: self.before_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None self.refreshViewer() self.selectedTableView = self.contactTable
def add_to_history(self): FileTransferHistory().add_transfer( transfer_id=self.ft_info.transfer_id, direction=self.ft_info.direction, local_uri=self.ft_info.local_uri, remote_uri=self.ft_info.remote_uri, file_path=self.ft_info.file_path, bytes_transfered=self.ft_info.bytes_transfered, file_size=self.ft_info.file_size or 0, status=self.ft_info.status) message = "<h3>%s File Transfer</h3>" % self.ft_info.direction.capitalize( ) message += "<p>%s (%s)" % (self.ft_info.file_path, format_size(self.ft_info.file_size or 0)) media_type = 'file-transfer' local_uri = self.ft_info.local_uri remote_uri = self.ft_info.remote_uri direction = self.ft_info.direction status = 'delivered' if self.ft_info.status == 'completed' else 'failed' cpim_from = self.ft_info.remote_uri cpim_to = self.ft_info.remote_uri timestamp = str(ISOTimestamp.now()) ChatHistory().add_message(self.ft_info.transfer_id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, "html", "0", status)
def __init__(self): if self: NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkContact('bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_("Type text and press Enter") self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.history = ChatHistory() tag = self.afterDate.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None else: self.before_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None self.refreshViewer() self.selectedTableView = self.contactTable
def __init__(self): if self: BlinkLogger().log_debug('Starting History Viewer') NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkHistoryViewerContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkHistoryViewerContact('bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.notification_center.add_observer(self, name='BlinkConferenceContactPresenceHasChanged') self.notification_center.add_observer(self, name='BlinkShouldTerminate') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_(NSLocalizedString("Type text and press Enter", "Placeholder text")) self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) self.chatViewController.setHandleScrolling_(False) self.entriesView.setShouldCloseWithWindow_(False) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.chat_history = ChatHistory() self.session_history = SessionHistory() self.setPeriod(1) self.selectedTableView = self.contactTable
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 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 add_to_history(self, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status): try: controller = next((controller for controller in NSApp.delegate().contactsWindowController.sessionControllersManager.sessionControllers if controller.session == self.session)) except StopIteration: history_id = str(uuid.uuid1()) else: history_id = controller.history_id ChatHistory().add_message(history_id, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, "html", "0", status)
class HistoryViewer(NSWindowController): implements(IObserver) chatViewController = objc.IBOutlet() indexTable = objc.IBOutlet() contactTable = objc.IBOutlet() toolbar = objc.IBOutlet() entriesView = objc.IBOutlet() period = objc.IBOutlet() searchText = objc.IBOutlet() searchMedia = objc.IBOutlet() searchContactBox = objc.IBOutlet() paginationButton = objc.IBOutlet() foundMessagesLabel = objc.IBOutlet() queryDatabaseLabel = objc.IBOutlet() busyIndicator = objc.IBOutlet() contactMenu = objc.IBOutlet() # viewer sections contacts = [] dayly_entries = NSMutableArray.array() messages = [] # database handler history = None # search filters start = 0 search_text = None search_uris = None search_local = None search_media = None after_date = None before_date = None refresh_contacts_counter = 1 daily_order_fields = {'date': 'DESC', 'local_uri': 'ASC', 'remote_uri': 'ASC'} media_type_array = {0: None, 1: 'audio', 2: 'chat', 3: 'sms', 4: 'file-transfer', 5: 'audio-recording', 6: 'availability', 7: 'voicemail', 8: 'missed-call'} period_array = {0: None, 1: datetime.datetime.now()-datetime.timedelta(days=1), 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), -1: datetime.datetime.now()-datetime.timedelta(days=1), -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) } def format_media_type(self, media_type): if media_type == 'sms': return NSLocalizedString("Instant Messages", "Column title") elif media_type == 'chat': return NSLocalizedString("Chat Sessions", "Column title") elif media_type == 'audio': return NSLocalizedString("Audio Calls", "Column title") elif media_type == 'file-transfer': return NSLocalizedString("File Transfers", "Column title") else: return media_type.title() def __new__(cls, *args, **kwargs): return cls.alloc().init() def __init__(self): if self: BlinkLogger().log_debug('Starting History Viewer') NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkHistoryViewerContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkHistoryViewerContact('bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.notification_center.add_observer(self, name='BlinkConferenceContactPresenceHasChanged') self.notification_center.add_observer(self, name='BlinkShouldTerminate') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_(NSLocalizedString("Type text and press Enter", "Placeholder text")) self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) self.chatViewController.setHandleScrolling_(False) self.entriesView.setShouldCloseWithWindow_(False) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.chat_history = ChatHistory() self.session_history = SessionHistory() self.setPeriod(1) self.selectedTableView = self.contactTable def setPeriod(self, days): if days <= -365: tag = -6 elif days <= -180: tag = -5 elif days <= -90: tag = -4 elif days <= -31: tag = -3 elif days <= -7: tag = -2 elif days <= -1: tag = -1 elif days <= 1: tag = 1 elif days <= 7: tag = 2 elif days <= 31: tag = 3 elif days <= 90: tag = 4 elif days <= 180: tag = 5 elif days <= 365: tag = 6 else: tag = 0 if tag == 0: self.before_date = None self.after_date = None elif tag < 0: try: date = self.period_array[tag] except KeyError: date = None self.before_date = date self.after_date = None else: try: date = self.period_array[tag] except KeyError: date = None self.after_date = date self.before_date = None self.period.selectItemWithTag_(tag) def awakeFromNib(self): NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, "contactSelectionChanged:", NSTableViewSelectionDidChangeNotification, self.contactTable) timer = NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(0.3, self, "refreshContactsTimer:", None, True) NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSModalPanelRunLoopMode) NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSDefaultRunLoopMode) self.contactTable.setDoubleAction_("doubleClick:") def close_(self, sender): self.window().close() @allocate_autorelease_pool @run_in_gui_thread def refreshViewer(self): self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @run_in_green_thread def delete_messages(self, local_uri=None, remote_uri=None, date=None, after_date=None, before_date=None, media_type=None): self.chat_history.delete_messages(local_uri=local_uri, remote_uri=remote_uri, date=date, after_date=after_date, before_date=before_date, media_type=media_type) self.session_history.delete_entries(local_uri=local_uri, remote_uri=remote_uri, after_date=after_date, before_date=before_date) self.search_text = None self.search_uris = None self.search_local = None self.refreshViewer() @run_in_green_thread def refreshContacts(self): if self.chat_history: self.updateBusyIndicator(True) remote_uri = self.search_uris if self.search_uris else None media_type = self.search_media if self.search_media else None search_text = self.search_text if self.search_text else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.chat_history.get_contacts(remote_uri=remote_uri, media_type=media_type, search_text=search_text, after_date=after_date, before_date=before_date) self.renderContacts(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderContacts(self, results): index = 0 found_uris = [] for item in self.contacts: item.destroy() getFirstContactMatchingURI = NSApp.delegate().contactsWindowController.getFirstContactMatchingURI self.contacts = [self.all_contacts, self.bonjour_contact] if self.search_uris: for uri in self.search_uris: found_contact = getFirstContactMatchingURI(uri, exact_match=True) if found_contact: contact_exist = False for contact_uri in found_contact.uris: if contact_uri.uri in found_uris: contact_exist = True break if contact_exist: continue contact = BlinkHistoryViewerContact(found_contact.uri, name=found_contact.name, icon=found_contact.icon) for contact_uri in found_contact.uris: found_uris.append(contact_uri.uri) self.contacts.append(contact) if isinstance(found_contact, BlinkPresenceContact): contact.setPresenceContact_(found_contact) else: if uri in found_uris: continue found_uris.append(uri) contact = BlinkHistoryViewerContact(unicode(uri), name=unicode(uri)) try: index = self.contacts.index(contact) except ValueError: pass if results: for row in results: found_contact = getFirstContactMatchingURI(row[0], exact_match=True) if found_contact: contact_exist = False for contact_uri in found_contact.uris: if contact_uri.uri in found_uris: contact_exist = True break if contact_exist: continue contact = BlinkHistoryViewerContact(found_contact.uri, name=found_contact.name, icon=found_contact.icon, presence_contact=found_contact if isinstance(found_contact, BlinkPresenceContact) else None) for contact_uri in found_contact.uris: found_uris.append(contact_uri.uri) else: if row[0] in found_uris: continue found_uris.append(row[0]) contact = BlinkHistoryViewerContact(unicode(row[0]), name=unicode(row[0])) self.contacts.append(contact) self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(index), False) self.contactTable.scrollRowToVisible_(index) self.updateContactsColumnHeader() @run_in_green_thread def refreshDailyEntries(self, order_text=None): if self.chat_history: self.resetDailyEntries() self.updateBusyIndicator(True) search_text = self.search_text if self.search_text else None remote_uri = self.search_uris if self.search_uris else None local_uri = self.search_local if self.search_local else None media_type = self.search_media if self.search_media else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.chat_history.get_daily_entries(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, search_text=search_text, order_text=order_text, after_date=after_date, before_date=before_date) self.renderDailyEntries(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def resetDailyEntries(self): self.dayly_entries = NSMutableArray.array() self.indexTable.reloadData() @allocate_autorelease_pool @run_in_gui_thread def renderDailyEntries(self, results): getFirstContactMatchingURI = NSApp.delegate().contactsWindowController.getFirstContactMatchingURI self.dayly_entries = NSMutableArray.array() for result in results: contact = getFirstContactMatchingURI(result[2], exact_match=True) if contact: remote_uri = '%s <%s>' % (contact.name, result[2]) else: remote_uri = result[2] entry = NSDictionary.dictionaryWithObjectsAndKeys_(result[1], "local_uri", remote_uri, "remote_uri", result[2], "remote_uri_sql", result[0], 'date', result[3], 'type') self.dayly_entries.addObject_(entry) self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() if self.search_uris and not self.dayly_entries: self.contactTable.deselectAll_(True) @run_in_green_thread @allocate_autorelease_pool def refreshMessages(self, count=SQL_LIMIT, remote_uri=None, local_uri=None, media_type=None, date=None, after_date=None, before_date=None): self.updateBusyIndicator(True) if self.chat_history: search_text = self.search_text if self.search_text else None if not remote_uri: remote_uri = self.search_uris if self.search_uris else None if not local_uri: local_uri = self.search_local if self.search_local else None if not media_type: media_type = self.search_media if self.search_media else None if not after_date: after_date = self.after_date if self.after_date else None if not before_date: before_date = self.before_date if self.before_date else None results = self.chat_history.get_messages(count=count, local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date, search_text=search_text, after_date=after_date, before_date=before_date) # cache message for pagination self.messages=[] for e in reversed(results): self.messages.append(e) # reset pagination to the last page start = len(self.messages) - len(self.messages)%MAX_MESSAGES_PER_PAGE if len(self.messages) > MAX_MESSAGES_PER_PAGE else 0 self.renderMessages(start) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderMessages(self, start=None): self.chatViewController.clear() self.chatViewController.resetRenderedMessages() start_from = start or self.start end = start_from + MAX_MESSAGES_PER_PAGE if start_from + MAX_MESSAGES_PER_PAGE < len(self.messages) else len(self.messages) for row in self.messages[start_from:end]: self.renderMessage(row) self.paginationButton.setEnabled_forSegment_(True if len(self.messages)>MAX_MESSAGES_PER_PAGE and start_from > MAX_MESSAGES_PER_PAGE else False, 0) self.paginationButton.setEnabled_forSegment_(True if start_from else False, 1) self.paginationButton.setEnabled_forSegment_(True if start_from+MAX_MESSAGES_PER_PAGE+1 < len(self.messages) else False, 2) self.paginationButton.setEnabled_forSegment_(True if len(self.messages)>MAX_MESSAGES_PER_PAGE and len(self.messages) - start_from > 2*MAX_MESSAGES_PER_PAGE else False, 3) text = u'No entry found' if len(self.messages): if len(self.messages) == 1: text = NSLocalizedString("Displaying 1 entry", "Text label") elif MAX_MESSAGES_PER_PAGE > len(self.messages): text = NSLocalizedString("Displaying %d entries" % end, "Text label") else: l = len(self.messages) text = NSLocalizedString("Displaying %d to "% start_from + 1, "Text label") + end + NSLocalizedString("out of %d entries" % l, "Text label") self.foundMessagesLabel.setStringValue_(text) @allocate_autorelease_pool @run_in_gui_thread def renderMessage(self, message): if message.direction == 'outgoing': icon = NSApp.delegate().contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string(message.cpim_from)[0] # TODO: How to render the icons from Address Book? Especially in sandbox mode we do not have access to other folders icon = NSApp.delegate().contactsWindowController.iconPathForURI(sender_uri) try: timestamp=ISOTimestamp(message.cpim_timestamp) except Exception: pass else: is_html = False if message.content_type == 'text' else True private = True if message.private == "1" else False self.chatViewController.showMessage(message.sip_callid, message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, is_private=private, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True, media_type=message.media_type, encryption=message.encryption if message.media_type == 'chat' else None) @objc.IBAction def paginateResults_(self, sender): if sender.selectedSegment() == 0: self.start = 0 elif sender.selectedSegment() == 1: next_start = self.start - MAX_MESSAGES_PER_PAGE self.start = next_start if next_start >= 0 else self.start elif sender.selectedSegment() == 2: next_start = self.start + MAX_MESSAGES_PER_PAGE self.start = next_start if next_start < len(self.messages)-1 else self.start elif sender.selectedSegment() == 3: self.start = len(self.messages) - len(self.messages)%MAX_MESSAGES_PER_PAGE if len(self.messages) > MAX_MESSAGES_PER_PAGE else 0 self.renderMessages() def tableView_deleteRow_(self, table, row): pass def tableView_sortDescriptorsDidChange_(self, table, odescr): self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() @objc.IBAction def printDocument_(self, sender): printInfo = NSPrintInfo.sharedPrintInfo() printInfo.setTopMargin_(30) printInfo.setBottomMargin_(30) printInfo.setLeftMargin_(10) printInfo.setRightMargin_(10) printInfo.setOrientation_(NSPortraitOrientation) printInfo.setHorizontallyCentered_(True) printInfo.setVerticallyCentered_(False) printInfo.setHorizontalPagination_(NSFitPagination) printInfo.setVerticalPagination_(NSFitPagination) NSPrintInfo.setSharedPrintInfo_(printInfo) # print the content of the web view self.entriesView.mainFrame().frameView().documentView().print_(self) @objc.IBAction def search_(self, sender): if self.chat_history: self.search_text = unicode(sender.stringValue()).lower() self.refreshContacts() row = self.indexTable.selectedRow() if row > 0: self.refreshDailyEntries() self.refreshMessages(local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) else: row = self.contactTable.selectedRow() if row > 0: self.refreshMessages() self.refreshDailyEntries() else: self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def searchContacts_(self, sender): text = unicode(self.searchContactBox.stringValue().strip()) contacts = [contact for contact in self.contacts[2:] if text in contact] if text else self.contacts[2:] self.contacts = [self.all_contacts, self.bonjour_contact] + contacts self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) self.updateContactsColumnHeader() if not text: self.refreshContacts() def updateContactsColumnHeader(self): found_contacts = len(self.contacts)-2 if found_contacts == 1: title = NSLocalizedString("1 Contact Found", "Column title") elif found_contacts > 1: title = NSLocalizedString("%d Contacts Found" % found_contacts, "Column title") else: title = NSLocalizedString("Contacts", "Column title") self.contactTable.tableColumnWithIdentifier_('contacts').headerCell().setStringValue_(title) def tableViewSelectionDidChange_(self, notification): if self.chat_history: if notification.object() == self.contactTable: row = self.contactTable.selectedRow() if row < 0: return elif row == 0: self.search_local = None self.search_uris = None self.searchContactBox.setStringValue_('') self.refreshContacts() elif row == 1: self.search_local = 'bonjour' self.search_uris = None elif row > 1: self.search_local = None if self.contacts[row].presence_contact is not None: self.search_uris = list(unicode(contact_uri.uri) for contact_uri in self.contacts[row].presence_contact.uris) self.chatViewController.expandSmileys = not self.contacts[row].presence_contact.contact.disable_smileys self.chatViewController.toggleSmileys(self.chatViewController.expandSmileys) try: item = (item for item in self.toolbar.visibleItems() if item.itemIdentifier() == 'smileys').next() except StopIteration: pass else: item.setImage_(NSImage.imageNamed_("smiley_on" if self.chatViewController.expandSmileys else "smiley_off")) else: self.search_uris = (self.contacts[row].uri,) self.refreshDailyEntries() self.refreshMessages() else: row = self.indexTable.selectedRow() if row >= 0: self.refreshMessages(remote_uri=(self.dayly_entries[row].objectForKey_("remote_uri_sql"),), local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) def numberOfRowsInTableView_(self, table): if table == self.indexTable: return self.dayly_entries.count() elif table == self.contactTable: return len(self.contacts) return 0 def tableView_objectValueForTableColumn_row_(self, table, column, row): if table == self.indexTable: ident = column.identifier() if ident == 'type': return self.format_media_type(self.dayly_entries[row].objectForKey_(ident)) try: return unicode(self.dayly_entries[row].objectForKey_(ident)) except IndexError: return None elif table == self.contactTable: try: if type(self.contacts[row]) in (str, unicode): return self.contacts[row] else: return self.contacts[row].name except IndexError: return None return None def tableView_willDisplayCell_forTableColumn_row_(self, table, cell, tableColumn, row): if table == self.contactTable: try: if row < len(self.contacts): if type(self.contacts[row]) in (str, unicode): cell.setContact_(None) else: cell.setContact_(self.contacts[row]) except: pass def showWindow_(self, sender): self.window().makeKeyAndOrderFront_(None) @run_in_gui_thread def filterByURIs(self, uris=(), media_type=None): self.search_text = None self.search_local = None if media_type != self.search_media: for tag in self.media_type_array.keys(): if self.media_type_array[tag] == media_type: self.searchMedia.selectItemAtIndex_(tag) self.search_media = media_type self.search_uris = uris self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByMediaChanged_(self, sender): tag = sender.selectedItem().tag() self.search_media = self.media_type_array[tag] self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByPeriodChanged_(self, sender): tag = sender.selectedItem().tag() if tag == 0: self.before_date = None self.after_date = None elif tag < 0: try: date = self.period_array[tag] except KeyError: date = None self.before_date = date self.after_date = None else: try: date = self.period_array[tag] except KeyError: date = None self.after_date = date self.before_date = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() def validateToolbarItem_(self, item): if item.itemIdentifier() == NSToolbarPrintItemIdentifier and not self.messages: return False return True def toolbarWillAddItem_(self, notification): item = notification.userInfo()["item"] if item.itemIdentifier() == NSToolbarPrintItemIdentifier: item.setToolTip_("Print Current Entries") item.setTarget_(self) item.setAutovalidates_(True) @objc.IBAction def userClickedToolbarItem_(self, sender): if sender.itemIdentifier() == 'smileys': self.chatViewController.expandSmileys = not self.chatViewController.expandSmileys sender.setImage_(NSImage.imageNamed_("smiley_on" if self.chatViewController.expandSmileys else "smiley_off")) self.chatViewController.toggleSmileys(self.chatViewController.expandSmileys) row = self.contactTable.selectedRow() if row and row > 1 and self.contacts[row].presence_contact is not None: self.contacts[row].presence_contact.contact.disable_smileys = not self.contacts[row].presence_contact.contact.disable_smileys self.contacts[row].presence_contact.contact.save() elif sender.itemIdentifier() == 'delete': if self.selectedTableView == self.contactTable: try: row = self.contactTable.selectedRow() self.showDeleteConfirmationDialog(row) except IndexError: pass elif self.selectedTableView == self.indexTable: try: row = self.indexTable.selectedRow() local_uri = self.dayly_entries[row].objectForKey_("local_uri") remote_uri = self.dayly_entries[row].objectForKey_("remote_uri") remote_uri_sql = self.dayly_entries[row].objectForKey_("remote_uri_sql") date = self.dayly_entries[row].objectForKey_("date") media_type = self.dayly_entries[row].objectForKey_("type") label = NSLocalizedString("Please confirm the deletion of %s history entries" % media_type, "Text label") + NSLocalizedString(" from %s" % remote_uri, "SIP Address label") + NSLocalizedString(" on %s. " % date, "Date label") + NSLocalizedString("This operation cannot be undone. ", "Text label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri=local_uri, remote_uri=remote_uri_sql, media_type=media_type, date=date) except IndexError: pass def showDeleteConfirmationDialog(self, row): media_print = self.search_media or NSLocalizedString("all", "Text label") tag = self.period.selectedItem().tag() period = '%s %s' % (NSLocalizedString(" newer than", "Date label") if tag < 4 else NSLocalizedString(" older than", "Date label"), self.period_array[tag].strftime("%Y-%m-%d")) if tag else '' if row == 0: label = NSLocalizedString("Please confirm the deletion of %s history entries" % media_print, "Text label") + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Text label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) elif row == 1: remote_uri=self.contacts[row].uri label = NSLocalizedString("Please confirm the deletion of %s Bonjour history entries" % media_print, "Text label") + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Text label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri='bonjour', media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) else: contact = self.contacts[row] if contact.presence_contact is not None: remote_uri = list(unicode(contact.uri) for contact in contact.presence_contact.uris) else: remote_uri = contact.uri label = NSLocalizedString("Please confirm the deletion of %s history entries" % media_print, "Text label") + NSLocalizedString(" from", "Text label") + " " + contact.name + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Text label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(remote_uri=remote_uri, media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) @allocate_autorelease_pool @run_in_gui_thread def updateBusyIndicator(self, busy=False): if busy: self.queryDatabaseLabel.setHidden_(False) self.busyIndicator.setHidden_(False) self.busyIndicator.setIndeterminate_(True) self.busyIndicator.setStyle_(NSProgressIndicatorSpinningStyle) self.busyIndicator.startAnimation_(None) else: self.busyIndicator.stopAnimation_(None) self.busyIndicator.setHidden_(True) self.queryDatabaseLabel.setHidden_(True) @allocate_autorelease_pool def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_BlinkShouldTerminate(self, notification): if self.window(): self.window().orderOut_(self) def _NH_ChatViewControllerDidDisplayMessage(self, notification): if notification.data.local_party != 'bonjour': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() def _NH_AudioCallLoggedToHistory(self, notification): if notification.data.local_party != 'bonjour': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() def _NH_BlinkContactsHaveChanged(self, notification): self.refresh_contacts_counter += 1 def refreshContactsTimer_(self, timer): if self.refresh_contacts_counter: self.refresh_contacts_counter = 0 self.refreshContacts() self.toolbar.validateVisibleItems() def _NH_BlinkTableViewSelectionChaged(self, notification): self.selectedTableView = notification.sender self.toolbar.validateVisibleItems() def _NH_BlinkConferenceContactPresenceHasChanged(self, notification): try: contact = (contact for contact in self.contacts[2:] if contact == notification.sender).next() except StopIteration: return else: try: idx = self.contacts.index(contact) self.contactTable.reloadDataForRowIndexes_columnIndexes_(NSIndexSet.indexSetWithIndex_(idx), NSIndexSet.indexSetWithIndex_(0)) except ValueError: pass def contactSelectionChanged_(self, notification): pass def menuWillOpen_(self, menu): if menu == self.contactMenu: self.contactMenu.itemWithTag_(2).setEnabled_(False) self.contactMenu.itemWithTag_(3).setEnabled_(False) self.contactMenu.itemWithTag_(4).setEnabled_(False) try: row = self.contactTable.selectedRow() except: return if row < 2: return try: contact = self.contacts[row] except IndexError: return contact_exists = bool(contact.presence_contact is not None) self.contactMenu.itemWithTag_(2).setEnabled_(not is_anonymous(contact.uri)) self.contactMenu.itemWithTag_(3).setEnabled_(not contact_exists and not is_anonymous(contact.uri)) self.contactMenu.itemWithTag_(4).setEnabled_(contact_exists) @objc.IBAction def doubleClick_(self, sender): row = self.contactTable.selectedRow() if row < 2: return try: contact = self.contacts[row] except IndexError: return NSApp.delegate().contactsWindowController.startSessionWithTarget(contact.uri) @objc.IBAction def userClickedContactMenu_(self, sender): row = self.contactTable.selectedRow() try: contact = self.contacts[row] except IndexError: return tag = sender.tag() if tag == 1: self.showDeleteConfirmationDialog(row) elif tag == 2: NSApp.delegate().contactsWindowController.searchBox.setStringValue_(contact.uri) NSApp.delegate().contactsWindowController.searchContacts() NSApp.delegate().contactsWindowController.window().makeFirstResponder_(NSApp.delegate().contactsWindowController.searchBox) NSApp.delegate().contactsWindowController.window().deminiaturize_(sender) NSApp.delegate().contactsWindowController.window().makeKeyWindow() elif tag == 3: NSApp.delegate().contactsWindowController.addContact(contact.uri, contact.name) elif tag == 4 and contact.presence_contact is not None: NSApp.delegate().contactsWindowController.model.editContact(contact.presence_contact) @objc.IBAction def userClickedActionsButton_(self, sender): point = sender.window().convertScreenToBase_(NSEvent.mouseLocation()) event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( NSLeftMouseUp, point, 0, NSDate.timeIntervalSinceReferenceDate(), sender.window().windowNumber(), sender.window().graphicsContext(), 0, 1, 0) NSMenu.popUpContextMenu_withEvent_forView_(self.contactMenu, event, sender)
def add_to_history(self, media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, status): ChatHistory().add_message(str(uuid.uuid1()), media_type, local_uri, remote_uri, direction, cpim_from, cpim_to, timestamp, message, "html", "0", status)
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 HistoryViewer(NSWindowController): chatViewController = objc.IBOutlet() indexTable = objc.IBOutlet() contactTable = objc.IBOutlet() toolbar = objc.IBOutlet() entriesView = objc.IBOutlet() period = objc.IBOutlet() searchText = objc.IBOutlet() searchMedia = objc.IBOutlet() searchContactBox = objc.IBOutlet() paginationButton = objc.IBOutlet() foundMessagesLabel = objc.IBOutlet() queryDatabaseLabel = objc.IBOutlet() busyIndicator = objc.IBOutlet() contactMenu = objc.IBOutlet() # viewer sections contacts = [] dayly_entries = NSMutableArray.array() messages = [] # database handler history = None # search filters start = 0 search_text = None search_uris = None search_local = None search_media = None after_date = None before_date = None refresh_contacts_counter = 1 contact_cache = {} display_name_cache = {} refresh_in_progress = False daily_order_fields = {'date': 'DESC', 'local_uri': 'ASC', 'remote_uri': 'ASC'} media_type_array = {0: None, 1: ('audio', 'video'), 2: ('chat', 'sms'), 3: 'file-transfer', 4: 'audio-recording', 5: 'availability', 6: 'voicemail', 7: 'video-recording'} period_array = {0: None, 1: datetime.datetime.now()-datetime.timedelta(days=1), 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), -1: datetime.datetime.now()-datetime.timedelta(days=1), -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) } @objc.python_method def format_media_type(self, media_type): if media_type == 'sms': media_type_formated = NSLocalizedString("Short Messages", "Label") elif media_type == 'chat': media_type_formated = NSLocalizedString("Chat Sessions", "Label") elif media_type == 'audio': media_type_formated = NSLocalizedString("Audio Calls", "Label") elif media_type == 'file-transfer': media_type_formated = NSLocalizedString("File Transfers", "Label") elif media_type == 'availability': media_type_formated = NSLocalizedString("Availability", "Label") elif media_type == 'missed-call': media_type_formated = NSLocalizedString("Missed Call", "Label") elif media_type == 'voicemail': media_type_formated = NSLocalizedString("Voicemail", "Label") else: media_type_formated = media_type.title() return media_type_formated def __new__(cls, *args, **kwargs): return cls.alloc().init() def __init__(self): if self: BlinkLogger().log_debug('Starting History Viewer') NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkHistoryViewerContact('Any Address', name='All Contacts') self.bonjour_contact = BlinkHistoryViewerContact('bonjour.local', name='Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.notification_center.add_observer(self, name='BlinkConferenceContactPresenceHasChanged') self.notification_center.add_observer(self, name='BlinkShouldTerminate') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_(NSLocalizedString("Type text and press Enter", "Placeholder text")) self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) self.chatViewController.setHandleScrolling_(False) self.entriesView.setShouldCloseWithWindow_(False) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.chat_history = ChatHistory() self.session_history = SessionHistory() self.setPeriod(1) self.selectedTableView = self.contactTable @objc.python_method def setPeriod(self, days): if days <= -365: tag = -6 elif days <= -180: tag = -5 elif days <= -90: tag = -4 elif days <= -31: tag = -3 elif days <= -7: tag = -2 elif days <= -1: tag = -1 elif days <= 1: tag = 1 elif days <= 7: tag = 2 elif days <= 31: tag = 3 elif days <= 90: tag = 4 elif days <= 180: tag = 5 elif days <= 365: tag = 6 else: tag = 0 if tag == 0: self.before_date = None self.after_date = None elif tag < 0: try: date = self.period_array[tag] except KeyError: date = None self.before_date = date self.after_date = None else: try: date = self.period_array[tag] except KeyError: date = None self.after_date = date self.before_date = None self.period.selectItemWithTag_(tag) def awakeFromNib(self): NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, "contactSelectionChanged:", NSTableViewSelectionDidChangeNotification, self.contactTable) timer = NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(1, self, "refreshContactsTimer:", None, True) NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSModalPanelRunLoopMode) NSRunLoop.currentRunLoop().addTimer_forMode_(timer, NSDefaultRunLoopMode) self.contactTable.setDoubleAction_("doubleClick:") def close_(self, sender): self.window().close() @objc.python_method @run_in_green_thread def delete_messages(self, local_uri=None, remote_uri=None, date=None, after_date=None, before_date=None, media_type=None): block_on(self.chat_history.delete_messages(local_uri=local_uri, remote_uri=remote_uri, date=date, after_date=after_date, before_date=before_date, media_type=media_type)) block_on(self.session_history.delete_entries(local_uri=local_uri, remote_uri=remote_uri, after_date=after_date, before_date=before_date)) self.search_text = None self.search_uris = None self.search_local = None self.refreshViewer() @objc.python_method def refreshViewer(self): self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.python_method @run_in_green_thread def refreshContacts(self): if self.refresh_in_progress: return self.refresh_in_progress = True self.refresh_contacts_counter = 0 if self.chat_history: self.updateBusyIndicator(True) remote_uri = self.search_uris if self.search_uris else None media_type = self.search_media if self.search_media else None search_text = self.search_text if self.search_text else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.chat_history.get_contacts(remote_uri=remote_uri, media_type=media_type, search_text=search_text, after_date=after_date, before_date=before_date) self.renderContacts(results) self.updateBusyIndicator(False) @objc.python_method @run_in_gui_thread def renderContacts(self, results): index = 0 found_uris = [] uris_without_display_name = [] for item in self.contacts: item.destroy() getFirstContactMatchingURI = NSApp.delegate().contactsWindowController.getFirstContactMatchingURI self.contacts = [self.all_contacts, self.bonjour_contact] if self.search_uris: for uri in self.search_uris: try: found_contact = self.contact_cache[uri] except KeyError: found_contact = getFirstContactMatchingURI(uri, exact_match=True) self.contact_cache[uri] = found_contact if found_contact: contact_exist = False for contact_uri in found_contact.uris: if contact_uri.uri in found_uris: contact_exist = True break if contact_exist: continue contact = BlinkHistoryViewerContact(found_contact.uri, name=found_contact.name, icon=found_contact.icon) for contact_uri in found_contact.uris: found_uris.append(contact_uri.uri) self.contacts.append(contact) if isinstance(found_contact, BlinkPresenceContact): contact.setPresenceContact_(found_contact) else: if uri in found_uris: continue found_uris.append(uri) contact = BlinkHistoryViewerContact(str(uri), name=str(uri)) try: index = self.contacts.index(contact) except ValueError: pass if results: for row in results: try: found_contact = self.contact_cache[row[0]] except KeyError: found_contact = getFirstContactMatchingURI(row[0], exact_match=True) self.contact_cache[row[0]] = found_contact if found_contact: contact_exist = False for contact_uri in found_contact.uris: if contact_uri.uri in found_uris: contact_exist = True break if contact_exist: continue contact = BlinkHistoryViewerContact(found_contact.uri, name=found_contact.name, icon=found_contact.icon, presence_contact=found_contact if isinstance(found_contact, BlinkPresenceContact) else None) for contact_uri in found_contact.uris: found_uris.append(contact_uri.uri) else: if row[0] in found_uris: continue found_uris.append(row[0]) try: display_name = self.display_name_cache[row[0]] except KeyError: display_name = str(row[0]) uris_without_display_name.append(row[0]) contact = BlinkHistoryViewerContact(str(row[0]), name=display_name) self.contacts.append(contact) self.update_display_names(uris_without_display_name) self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(index), False) self.contactTable.scrollRowToVisible_(index) self.updateContactsColumnHeader() self.refresh_in_progress = False @objc.python_method @run_in_green_thread def update_display_names(self, uris_without_display_name): results = self.session_history.get_display_names(uris_without_display_name) self.updateDisplayNames(results) @objc.python_method @run_in_gui_thread def updateDisplayNames(self, results): must_reload = False for result in results: self.display_name_cache[result[0]]=result[1] for contact in self.contacts: if contact.uri == result[0] and contact.name != result[1]: contact.name = result[1] must_reload = True if must_reload: self.contactTable.reloadData() must_reload = False for entry in self.dayly_entries: if entry['remote_uri_sql'] == entry['remote_uri']: try: display_name = self.display_name_cache[entry['remote_uri_sql']] except KeyError: pass else: entry['display_name'] = display_name entry['remote_uri'] = '%s <%s>' % (display_name, entry['remote_uri_sql']) if '@' in entry['remote_uri_sql'] else display_name must_reload = True self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() @objc.python_method @run_in_green_thread def refreshDailyEntries(self, order_text=None): if self.chat_history: self.resetDailyEntries() self.updateBusyIndicator(True) search_text = self.search_text if self.search_text else None remote_uri = self.search_uris if self.search_uris else None local_uri = self.search_local if self.search_local else None media_type = self.search_media if self.search_media else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.chat_history.get_daily_entries(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, search_text=search_text, order_text=order_text, after_date=after_date, before_date=before_date) self.renderDailyEntries(results) self.updateBusyIndicator(False) @objc.python_method @run_in_gui_thread def resetDailyEntries(self): self.dayly_entries = NSMutableArray.array() self.indexTable.reloadData() @objc.python_method @run_in_gui_thread def renderDailyEntries(self, results): getFirstContactMatchingURI = NSApp.delegate().contactsWindowController.getFirstContactMatchingURI self.dayly_entries = NSMutableArray.array() for result in results: display_name = None try: found_contact = self.contact_cache[result[2]] except KeyError: found_contact = getFirstContactMatchingURI(result[2], exact_match=True) self.contact_cache[result[2]] = found_contact if found_contact: display_name = found_contact.name remote_uri = '%s <%s>' % (display_name, result[2]) if '@' in result[2] else display_name else: try: display_name = self.display_name_cache[result[2]] except KeyError: remote_uri = result[2] else: remote_uri = '%s <%s>' % (display_name, result[2]) if '@' in result[2] else display_name entry = NSMutableDictionary.dictionaryWithObjectsAndKeys_(result[1], "local_uri", remote_uri, "remote_uri", result[2], "remote_uri_sql", result[0], 'date', result[3], 'type', display_name, "display_name") self.dayly_entries.addObject_(entry) self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() if self.search_uris and not self.dayly_entries: self.contactTable.deselectAll_(True) @objc.python_method @run_in_green_thread def refreshMessages(self, count=SQL_LIMIT, remote_uri=None, local_uri=None, media_type=None, date=None, after_date=None, before_date=None): if self.chat_history: self.updateBusyIndicator(True) search_text = self.search_text if self.search_text else None if not remote_uri: remote_uri = self.search_uris if self.search_uris else None if not local_uri: local_uri = self.search_local if self.search_local else None if not media_type: media_type = self.search_media if self.search_media else None if not after_date: after_date = self.after_date if self.after_date else None if not before_date: before_date = self.before_date if self.before_date else None results = self.chat_history.get_messages(count=count, local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date, search_text=search_text, after_date=after_date, before_date=before_date) self.renderMessages(results) self.updateBusyIndicator(False) @objc.python_method @run_in_gui_thread def renderMessages(self, messages=None): self.chatViewController.clear() self.chatViewController.resetRenderedMessages() self.chatViewController.last_sender = None if messages is not None: # new message list. cache for pagination and reset pagination to the last page self.messages = list(reversed(messages)) message_count = len(messages) start = message_count // MAX_MESSAGES_PER_PAGE * MAX_MESSAGES_PER_PAGE end = message_count else: message_count = len(self.messages) start = self.start end = min(start + MAX_MESSAGES_PER_PAGE, message_count) for row in self.messages[start:end]: self.renderMessage(row) self.paginationButton.setEnabled_forSegment_(start > MAX_MESSAGES_PER_PAGE, 0) self.paginationButton.setEnabled_forSegment_(start > 0, 1) self.paginationButton.setEnabled_forSegment_(start + MAX_MESSAGES_PER_PAGE + 1 < message_count, 2) self.paginationButton.setEnabled_forSegment_(start + MAX_MESSAGES_PER_PAGE * 2 < message_count, 3) if message_count == 0: text = NSLocalizedString("No entry found", "Label") elif message_count == 1: text = NSLocalizedString("Displaying 1 entry", "Label") elif message_count < MAX_MESSAGES_PER_PAGE: text = NSLocalizedString("Displaying {} entries".format(end), "Label") else: text = NSLocalizedString("Displaying {} to {} out of {} entries", "Label").format(start+1, end, message_count) self.foundMessagesLabel.setStringValue_(text) @objc.python_method @run_in_gui_thread def renderMessage(self, message): if message.direction == 'outgoing': icon = NSApp.delegate().contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string(message.cpim_from)[0] # TODO: How to render the icons from Address Book? Especially in sandbox mode we do not have access to other folders icon = NSApp.delegate().contactsWindowController.iconPathForURI(sender_uri) try: timestamp=ISOTimestamp(message.cpim_timestamp) except Exception: pass else: is_html = False if message.content_type == 'text' else True private = True if message.private == "1" else False self.chatViewController.showMessage(message.sip_callid, message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, is_private=private, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True, media_type=message.media_type, encryption=message.encryption if message.media_type == 'chat' else None) @objc.IBAction def paginateResults_(self, sender): if sender.selectedSegment() == 0: self.start = 0 elif sender.selectedSegment() == 1: next_start = self.start - MAX_MESSAGES_PER_PAGE self.start = next_start if next_start >= 0 else self.start elif sender.selectedSegment() == 2: next_start = self.start + MAX_MESSAGES_PER_PAGE self.start = next_start if next_start < len(self.messages)-1 else self.start elif sender.selectedSegment() == 3: self.start = len(self.messages) - len(self.messages)%MAX_MESSAGES_PER_PAGE if len(self.messages) > MAX_MESSAGES_PER_PAGE else 0 self.renderMessages() def tableView_deleteRow_(self, table, row): pass def tableView_sortDescriptorsDidChange_(self, table, odescr): self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() @objc.IBAction def printDocument_(self, sender): printInfo = NSPrintInfo.sharedPrintInfo() printInfo.setTopMargin_(30) printInfo.setBottomMargin_(30) printInfo.setLeftMargin_(10) printInfo.setRightMargin_(10) printInfo.setOrientation_(NSPortraitOrientation) printInfo.setHorizontallyCentered_(True) printInfo.setVerticallyCentered_(False) printInfo.setHorizontalPagination_(NSFitPagination) printInfo.setVerticalPagination_(NSFitPagination) NSPrintInfo.setSharedPrintInfo_(printInfo) # print the content of the web view self.entriesView.mainFrame().frameView().documentView().print_(self) @objc.IBAction def search_(self, sender): if self.chat_history: self.search_text = str(sender.stringValue()).lower() self.refreshContacts() row = self.indexTable.selectedRow() if row > 0: self.refreshDailyEntries() self.refreshMessages(local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) else: row = self.contactTable.selectedRow() if row > 0: self.refreshMessages() self.refreshDailyEntries() else: self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def searchContacts_(self, sender): text = str(self.searchContactBox.stringValue().strip()) contacts = [contact for contact in self.contacts[2:] if text in contact] if text else self.contacts[2:] self.contacts = [self.all_contacts, self.bonjour_contact] + contacts self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) self.updateContactsColumnHeader() if not text: self.refreshContacts() @objc.python_method def updateContactsColumnHeader(self): found_contacts = len(self.contacts)-2 if found_contacts == 1: title = NSLocalizedString("1 contact found", "Label") elif found_contacts > 1: title = NSLocalizedString("%d contacts found", "Label") % found_contacts else: title = NSLocalizedString("Contacts", "Label") self.contactTable.tableColumnWithIdentifier_('contacts').headerCell().setStringValue_(title) def tableViewSelectionDidChange_(self, notification): if self.chat_history: if notification.object() == self.contactTable: row = self.contactTable.selectedRow() if row < 0: return elif row == 0: self.search_local = None self.search_uris = None self.searchContactBox.setStringValue_('') self.refreshContacts() elif row == 1: self.search_local = 'bonjour.local' self.search_uris = None elif row > 1: self.search_local = None if self.contacts[row].presence_contact is not None: self.search_uris = list(str(contact_uri.uri) for contact_uri in self.contacts[row].presence_contact.uris) self.chatViewController.expandSmileys = not self.contacts[row].presence_contact.contact.disable_smileys self.chatViewController.toggleSmileys(self.chatViewController.expandSmileys) try: item = next((item for item in self.toolbar.visibleItems() if item.itemIdentifier() == 'smileys')) except StopIteration: pass else: item.setImage_(NSImage.imageNamed_("smiley_on" if self.chatViewController.expandSmileys else "smiley_off")) else: self.search_uris = (self.contacts[row].uri,) self.refreshDailyEntries() self.refreshMessages() else: row = self.indexTable.selectedRow() if row >= 0: self.refreshMessages(remote_uri=(self.dayly_entries[row].objectForKey_("remote_uri_sql"),), local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) def numberOfRowsInTableView_(self, table): if table == self.indexTable: return self.dayly_entries.count() elif table == self.contactTable: return len(self.contacts) return 0 def tableView_objectValueForTableColumn_row_(self, table, column, row): if table == self.indexTable: ident = column.identifier() if ident == 'type': return self.format_media_type(self.dayly_entries[row].objectForKey_(ident)) try: return str(self.dayly_entries[row].objectForKey_(ident)) except IndexError: return None elif table == self.contactTable: try: if type(self.contacts[row]) in (str, str): return self.contacts[row] else: return self.contacts[row].name except IndexError: return None return None def tableView_willDisplayCell_forTableColumn_row_(self, table, cell, tableColumn, row): if table == self.contactTable: try: if row < len(self.contacts): if type(self.contacts[row]) in (str, str): cell.setContact_(None) else: cell.setContact_(self.contacts[row]) except: pass def showWindow_(self, sender): self.window().makeKeyAndOrderFront_(None) @objc.python_method @run_in_gui_thread def filterByURIs(self, uris=(), media_type=None): self.search_text = None self.search_local = None if media_type != self.search_media: for tag in list(self.media_type_array.keys()): if self.media_type_array[tag] == media_type: self.searchMedia.selectItemAtIndex_(tag) self.search_media = media_type self.search_uris = uris self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByMediaChanged_(self, sender): tag = sender.selectedItem().tag() self.search_media = self.media_type_array[tag] self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByPeriodChanged_(self, sender): tag = sender.selectedItem().tag() if tag == 0: self.before_date = None self.after_date = None elif tag < 0: try: date = self.period_array[tag] except KeyError: date = None self.before_date = date self.after_date = None else: try: date = self.period_array[tag] except KeyError: date = None self.after_date = date self.before_date = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() def validateToolbarItem_(self, item): if item.itemIdentifier() == NSToolbarPrintItemIdentifier and not self.messages: return False return True def toolbarWillAddItem_(self, notification): item = notification.userInfo()["item"] if item.itemIdentifier() == NSToolbarPrintItemIdentifier: item.setToolTip_("Print Current Entries") item.setTarget_(self) item.setAutovalidates_(True) @objc.IBAction def userClickedToolbarItem_(self, sender): if sender.itemIdentifier() == 'smileys': self.chatViewController.expandSmileys = not self.chatViewController.expandSmileys sender.setImage_(NSImage.imageNamed_("smiley_on" if self.chatViewController.expandSmileys else "smiley_off")) self.chatViewController.toggleSmileys(self.chatViewController.expandSmileys) row = self.contactTable.selectedRow() if row and row > 1 and self.contacts[row].presence_contact is not None: self.contacts[row].presence_contact.contact.disable_smileys = not self.contacts[row].presence_contact.contact.disable_smileys self.contacts[row].presence_contact.contact.save() elif sender.itemIdentifier() == 'delete': if self.selectedTableView == self.contactTable: try: row = self.contactTable.selectedRow() self.showDeleteConfirmationDialog(row) except IndexError: pass elif self.selectedTableView == self.indexTable: try: row = self.indexTable.selectedRow() local_uri = self.dayly_entries[row].objectForKey_("local_uri") remote_uri = self.dayly_entries[row].objectForKey_("remote_uri") remote_uri_sql = self.dayly_entries[row].objectForKey_("remote_uri_sql") date = self.dayly_entries[row].objectForKey_("date") media_type = self.dayly_entries[row].objectForKey_("type") label = NSLocalizedString("Please confirm the deletion of %s history entries", "Label") % media_type + NSLocalizedString(" from %s", "SIP Address label") % remote_uri + NSLocalizedString(" on %s. ", "Date label") % date + NSLocalizedString("This operation cannot be undone. ", "Label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri=local_uri, remote_uri=remote_uri_sql, media_type=media_type, date=date) except IndexError: pass @objc.python_method def showDeleteConfirmationDialog(self, row): media_print = self.search_media or NSLocalizedString("all", "Label") tag = self.period.selectedItem().tag() period = '%s %s' % (NSLocalizedString(" newer than", "Date label") if tag < 4 else NSLocalizedString(" older than", "Date label"), self.period_array[tag].strftime("%Y-%m-%d")) if tag else '' if row == 0: label = NSLocalizedString("Please confirm the deletion of %s history entries", "Label") % media_print + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) elif row == 1: remote_uri=self.contacts[row].uri label = NSLocalizedString("Please confirm the deletion of %s Bonjour history entries", "Label") % media_print + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri='bonjour.local', media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) else: contact = self.contacts[row] if contact.presence_contact is not None: remote_uri = list(str(contact.uri) for contact in contact.presence_contact.uris) else: remote_uri = contact.uri label = NSLocalizedString("Please confirm the deletion of %s history entries", "Label") % media_print + NSLocalizedString(" from ", "Label") + contact.name + period + ". "+ NSLocalizedString("This operation cannot be undone. ", "Label") ret = NSRunAlertPanel(NSLocalizedString("Purge History Entries", "Window title"), label, NSLocalizedString("Confirm", "Button title"), NSLocalizedString("Cancel", "Button title"), None) if ret == NSAlertDefaultReturn: self.delete_messages(remote_uri=remote_uri, media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) @objc.python_method @run_in_gui_thread def updateBusyIndicator(self, busy=False): if busy: self.queryDatabaseLabel.setHidden_(False) self.busyIndicator.setHidden_(False) self.busyIndicator.setIndeterminate_(True) self.busyIndicator.setStyle_(NSProgressIndicatorSpinningStyle) self.busyIndicator.startAnimation_(None) else: self.busyIndicator.stopAnimation_(None) self.busyIndicator.setHidden_(True) self.queryDatabaseLabel.setHidden_(True) @objc.python_method @run_in_gui_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @objc.python_method def _NH_BlinkShouldTerminate(self, notification): if self.window(): self.window().orderOut_(self) @objc.python_method def _NH_ChatViewControllerDidDisplayMessage(self, notification): if notification.data.local_party != 'bonjour.local': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() @objc.python_method def _NH_AudioCallLoggedToHistory(self, notification): if notification.data.local_party != 'bonjour.local': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() @objc.python_method def _NH_BlinkContactsHaveChanged(self, notification): self.refresh_contacts_counter += 1 def refreshContactsTimer_(self, timer): if self.refresh_contacts_counter: self.refreshContacts() self.toolbar.validateVisibleItems() @objc.python_method def _NH_BlinkTableViewSelectionChaged(self, notification): self.selectedTableView = notification.sender self.toolbar.validateVisibleItems() @objc.python_method def _NH_BlinkConferenceContactPresenceHasChanged(self, notification): try: contact = next((contact for contact in self.contacts[2:] if contact == notification.sender)) except StopIteration: return else: try: idx = self.contacts.index(contact) self.contactTable.reloadDataForRowIndexes_columnIndexes_(NSIndexSet.indexSetWithIndex_(idx), NSIndexSet.indexSetWithIndex_(0)) except ValueError: pass def contactSelectionChanged_(self, notification): pass def menuWillOpen_(self, menu): if menu == self.contactMenu: self.contactMenu.itemWithTag_(2).setEnabled_(False) self.contactMenu.itemWithTag_(3).setEnabled_(False) self.contactMenu.itemWithTag_(4).setEnabled_(False) try: row = self.contactTable.selectedRow() except: return if row < 2: return try: contact = self.contacts[row] except IndexError: return contact_exists = bool(contact.presence_contact is not None) if '@' in contact.uri: self.contactMenu.itemWithTag_(2).setEnabled_(not is_anonymous(contact.uri)) self.contactMenu.itemWithTag_(3).setEnabled_(not contact_exists and not is_anonymous(contact.uri)) self.contactMenu.itemWithTag_(4).setEnabled_(contact_exists) else: bonjour_contact = NSApp.delegate().contactsWindowController.model.getBonjourContactMatchingDeviceId(contact.uri) self.contactMenu.itemWithTag_(2).setEnabled_(bool(bonjour_contact)) self.contactMenu.itemWithTag_(3).setEnabled_(False) self.contactMenu.itemWithTag_(4).setEnabled_(False) @objc.IBAction def doubleClick_(self, sender): row = self.contactTable.selectedRow() if row < 2: return try: contact = self.contacts[row] except IndexError: return if '@' in contact.uri: NSApp.delegate().contactsWindowController.startSessionWithTarget(contact.uri) else: bonjour_contact = NSApp.delegate().contactsWindowController.model.getBonjourContactMatchingDeviceId(contact.uri) if not bonjour_contact: BlinkLogger().log_info("Bonjour neighbour %s was not found on this network" % contact.name) message = NSLocalizedString("Bonjour neighbour %s was not found on this network. ", "label") % contact.name NSRunAlertPanel(NSLocalizedString("Error", "Window title"), message, NSLocalizedString("OK", "Button title"), None, None) return NSApp.delegate().contactsWindowController.startSessionWithTarget(bonjour_contact.uri) @objc.IBAction def userClickedContactMenu_(self, sender): row = self.contactTable.selectedRow() try: contact = self.contacts[row] except IndexError: return tag = sender.tag() if tag == 1: self.showDeleteConfirmationDialog(row) elif tag == 2: NSApp.delegate().contactsWindowController.searchBox.setStringValue_(contact.uri) NSApp.delegate().contactsWindowController.searchContacts() NSApp.delegate().contactsWindowController.window().makeFirstResponder_(NSApp.delegate().contactsWindowController.searchBox) NSApp.delegate().contactsWindowController.window().deminiaturize_(sender) NSApp.delegate().contactsWindowController.window().makeKeyWindow() elif tag == 3: NSApp.delegate().contactsWindowController.addContact(uris=[(contact.uri, 'sip')], name=contact.name) elif tag == 4 and contact.presence_contact is not None: NSApp.delegate().contactsWindowController.model.editContact(contact.presence_contact) @objc.IBAction def userClickedActionsButton_(self, sender): point = sender.window().convertScreenToBase_(NSEvent.mouseLocation()) event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( NSLeftMouseUp, point, 0, NSDate.timeIntervalSinceReferenceDate(), sender.window().windowNumber(), sender.window().graphicsContext(), 0, 1, 0) NSMenu.popUpContextMenu_withEvent_forView_(self.contactMenu, event, sender)
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)
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 HistoryViewer(NSWindowController): implements(IObserver) chatViewController = objc.IBOutlet() indexTable = objc.IBOutlet() contactTable = objc.IBOutlet() toolbar = objc.IBOutlet() entriesView = objc.IBOutlet() afterDate = objc.IBOutlet() searchText = objc.IBOutlet() searchMedia = objc.IBOutlet() searchContactBox = objc.IBOutlet() paginationButton = objc.IBOutlet() foundMessagesLabel = objc.IBOutlet() queryDatabaseLabel = objc.IBOutlet() busyIndicator = objc.IBOutlet() contactMenu = objc.IBOutlet() # viewer sections allContacts = [] contacts = [] dayly_entries = NSMutableArray.array() messages = [] # database handler history = None # search filters start = 0 search_text = None search_contact = None search_local = None search_media = None after_date = None before_date = None daily_order_fields = {'date': 'DESC', 'local_uri': 'ASC', 'remote_uri': 'ASC'} media_type_array = {0: None, 1: 'audio', 2: 'chat', 3: 'sms', 4: 'file-transfer', 5: 'audio-recording', 6: 'video-recording', 7: 'voicemail', 8: 'missed-call'} period_array = {0: None, 1: datetime.datetime.now()-datetime.timedelta(days=1), 2: datetime.datetime.now()-datetime.timedelta(days=7), 3: datetime.datetime.now()-datetime.timedelta(days=31), 4: datetime.datetime.now()-datetime.timedelta(days=31), 5: datetime.datetime.now()-datetime.timedelta(days=90), 6: datetime.datetime.now()-datetime.timedelta(days=180), 7: datetime.datetime.now()-datetime.timedelta(days=365) } def __new__(cls, *args, **kwargs): return cls.alloc().init() def __init__(self): if self: NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkContact('bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer(self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer(self, name='AudioCallLoggedToHistory') self.notification_center.add_observer(self, name='BlinkContactsHaveChanged') self.notification_center.add_observer(self, name='BlinkTableViewSelectionChaged') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_("Type text and press Enter") self.chatViewController.setContentFile_(NSBundle.mainBundle().pathForResource_ofType_("ChatView", "html")) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_(c, True) col.setSortDescriptorPrototype_(descriptor) self.history = ChatHistory() tag = self.afterDate.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None else: self.before_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None self.refreshViewer() self.selectedTableView = self.contactTable def awakeFromNib(self): NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, "contactSelectionChanged:", NSTableViewSelectionDidChangeNotification, self.contactTable) self.contactTable.setDoubleAction_("doubleClick:") def close_(self, sender): self.window().close() @allocate_autorelease_pool @run_in_gui_thread def refreshViewer(self): self.search_text = None self.search_contact = None self.search_local = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @run_in_green_thread def delete_messages(self, local_uri=None, remote_uri=None, date=None, after_date=None, before_date=None, media_type=None): self.history.delete_messages(local_uri=local_uri, remote_uri=remote_uri, date=date, after_date=after_date, before_date=before_date, media_type=media_type) self.refreshViewer() @run_in_green_thread def refreshContacts(self): if self.history: self.updateBusyIndicator(True) media_type = self.search_media if self.search_media else None search_text = self.search_text if self.search_text else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.history.get_contacts(media_type=media_type, search_text=search_text, after_date=after_date, before_date=before_date) self.renderContacts(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderContacts(self, results): getContactMatchingURI = NSApp.delegate().contactsWindowController.getContactMatchingURI self.contacts = [self.all_contacts, self.bonjour_contact] self.allContacts = [] for row in results: contact = getContactMatchingURI(row[0]) if contact: detail = contact.uri contact = BlinkContact(unicode(row[0]), name=contact.name, icon=contact.icon) else: detail = unicode(row[0]) contact = BlinkContact(unicode(row[0]), name=unicode(row[0])) contact.setDetail(detail) self.contacts.append(contact) self.allContacts.append(contact) real_contacts = len(self.contacts)-2 self.contactTable.reloadData() if self.search_contact: try: contact = (contact for contact in self.contacts if contact.uri == self.search_contact).next() except StopIteration: pass else: try: row = self.contacts.index(contact) self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(row), False) self.contactTable.scrollRowToVisible_(row) except: pass else: self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) self.contactTable.tableColumnWithIdentifier_('contacts').headerCell(). setStringValue_(u'%d Contacts'%real_contacts if real_contacts else u'Contacts') @run_in_green_thread def refreshDailyEntries(self, order_text=None): if self.history: self.resetDailyEntries() self.updateBusyIndicator(True) search_text = self.search_text if self.search_text else None remote_uri = self.search_contact if self.search_contact else None local_uri = self.search_local if self.search_local else None media_type = self.search_media if self.search_media else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.history.get_daily_entries(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, search_text=search_text, order_text=order_text, after_date=after_date, before_date=before_date) self.renderDailyEntries(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def resetDailyEntries(self): self.dayly_entries = NSMutableArray.array() self.indexTable.reloadData() @allocate_autorelease_pool @run_in_gui_thread def renderDailyEntries(self, results): getContactMatchingURI = NSApp.delegate().contactsWindowController.getContactMatchingURI self.dayly_entries = NSMutableArray.array() for result in results: contact = getContactMatchingURI(result[2]) if contact: remote_uri = '%s <%s>' % (contact.name, contact.uri) else: remote_uri = result[2] entry = NSDictionary.dictionaryWithObjectsAndKeys_(result[1], "local_uri", remote_uri, "remote_uri", result[2], "remote_uri_sql", result[0], 'date', result[3], 'type') self.dayly_entries.addObject_(entry) self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() if self.search_contact and not self.dayly_entries: self.contactTable.deselectAll_(True) @run_in_green_thread @allocate_autorelease_pool def refreshMessages(self, count=SQL_LIMIT, remote_uri=None, local_uri=None, media_type=None, date=None, after_date=None, before_date=None): self.updateBusyIndicator(True) if self.history: search_text = self.search_text if self.search_text else None if not remote_uri: remote_uri = self.search_contact if self.search_contact else None if not local_uri: local_uri = self.search_local if self.search_local else None if not media_type: media_type = self.search_media if self.search_media else None if not after_date: after_date = self.after_date if self.after_date else None if not before_date: before_date = self.before_date if self.before_date else None results = self.history.get_messages(count=count, local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date, search_text=search_text, after_date=after_date, before_date=before_date) # cache message for pagination self.messages=[] for e in reversed(results): self.messages.append(e) # reset pagination self.start=0 self.renderMessages() self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderMessages(self): self.chatViewController.clear() self.chatViewController.resetRenderedMessages() end = self.start + MAX_MESSAGES_PER_PAGE if self.start + MAX_MESSAGES_PER_PAGE < len(self.messages) else len(self.messages) for row in self.messages[self.start:end]: self.renderMessage(row) self.paginationButton.setEnabled_forSegment_(True if len(self.messages)>MAX_MESSAGES_PER_PAGE and self.start > MAX_MESSAGES_PER_PAGE else False, 0) self.paginationButton.setEnabled_forSegment_(True if self.start else False, 1) self.paginationButton.setEnabled_forSegment_(True if self.start+MAX_MESSAGES_PER_PAGE+1 < len(self.messages) else False, 2) self.paginationButton.setEnabled_forSegment_(True if len(self.messages)>MAX_MESSAGES_PER_PAGE and len(self.messages) - self.start > 2*MAX_MESSAGES_PER_PAGE else False, 3) text = u'No entry found' if len(self.messages): if len(self.messages) == 1: text = u'Displaying 1 entry' elif MAX_MESSAGES_PER_PAGE > len(self.messages): text = u'Displaying %d entries' % end else: text = u'Displaying %d to %d out of %d entries' % (self.start+1, end, len(self.messages)) self.foundMessagesLabel.setStringValue_(text) @allocate_autorelease_pool @run_in_gui_thread def renderMessage(self, message): if message.direction == 'outgoing': icon = NSApp.delegate().contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string(message.cpim_from)[0] # TODO: How to render the icons from Address Book? Especially in sandbox mode we do not have access to other folders icon = NSApp.delegate().contactsWindowController.iconPathForURI(sender_uri) try: timestamp=Timestamp.parse(message.cpim_timestamp) except ValueError: pass else: is_html = False if message.content_type == 'text' else True private = True if message.private == "1" else False self.chatViewController.showMessage(message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, is_private=private, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True) @objc.IBAction def paginateResults_(self, sender): if sender.selectedSegment() == 0: self.start = 0 elif sender.selectedSegment() == 1: next_start = self.start - MAX_MESSAGES_PER_PAGE self.start = next_start if next_start >= 0 else self.start elif sender.selectedSegment() == 2: next_start = self.start + MAX_MESSAGES_PER_PAGE self.start = next_start if next_start < len(self.messages)-1 else self.start elif sender.selectedSegment() == 3: self.start = len(self.messages) - len(self.messages)%MAX_MESSAGES_PER_PAGE if len(self.messages) > MAX_MESSAGES_PER_PAGE else 0 self.renderMessages() def tableView_deleteRow_(self, table, row): pass def tableView_sortDescriptorsDidChange_(self, table, odescr): self.dayly_entries.sortUsingDescriptors_(self.indexTable.sortDescriptors()) self.indexTable.reloadData() @objc.IBAction def printDocument_(self, sender): printInfo = NSPrintInfo.sharedPrintInfo() printInfo.setTopMargin_(30) printInfo.setBottomMargin_(30) printInfo.setLeftMargin_(10) printInfo.setRightMargin_(10) printInfo.setOrientation_(NSPortraitOrientation) printInfo.setHorizontallyCentered_(True) printInfo.setVerticallyCentered_(False) printInfo.setHorizontalPagination_(NSFitPagination) printInfo.setVerticalPagination_(NSFitPagination) NSPrintInfo.setSharedPrintInfo_(printInfo) # print the content of the web view self.entriesView.mainFrame().frameView().documentView().print_(self) @objc.IBAction def search_(self, sender): if self.history: self.search_text = unicode(sender.stringValue()).lower() self.refreshContacts() row = self.indexTable.selectedRow() if row > 0: self.refreshDailyEntries() self.refreshMessages(local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) else: row = self.contactTable.selectedRow() if row > 0: self.refreshMessages() self.refreshDailyEntries() else: self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def searchContacts_(self, sender): text = unicode(self.searchContactBox.stringValue().strip()) contacts = [contact for contact in self.allContacts if text in contact] if text else self.allContacts self.contacts = [self.all_contacts, self.bonjour_contact] + contacts self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_(NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) def tableViewSelectionDidChange_(self, notification): if self.history: if not notification or notification.object() == self.contactTable: row = self.contactTable.selectedRow() if row == 0: self.search_local = None self.search_contact = None elif row == 1: self.search_local = 'bonjour' self.search_contact = None elif row > 1: self.search_local = None self.search_contact = self.contacts[row].uri self.refreshDailyEntries() self.refreshMessages() else: row = self.indexTable.selectedRow() if row >= 0: self.refreshMessages(remote_uri=self.dayly_entries[row].objectForKey_("remote_uri_sql"), local_uri=self.dayly_entries[row].objectForKey_("local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) def numberOfRowsInTableView_(self, table): if table == self.indexTable: return self.dayly_entries.count() elif table == self.contactTable: return len(self.contacts) return 0 def tableView_objectValueForTableColumn_row_(self, table, column, row): if table == self.indexTable: ident = column.identifier() try: return unicode(self.dayly_entries[row].objectForKey_(ident)) except IndexError: return None elif table == self.contactTable: try: if type(self.contacts[row]) in (str, unicode): return self.contacts[row] else: return self.contacts[row].name except IndexError: return None return None def tableView_willDisplayCell_forTableColumn_row_(self, table, cell, tableColumn, row): if table == self.contactTable: try: if row < len(self.contacts): if type(self.contacts[row]) in (str, unicode): cell.setContact_(None) else: cell.setContact_(self.contacts[row]) except: pass def showWindow_(self, sender): self.window().makeKeyAndOrderFront_(None) @run_in_gui_thread def filterByContact(self, contact_uri, media_type=None): self.search_text = None self.search_local = None if media_type != self.search_media: for tag in self.media_type_array.keys(): if self.media_type_array[tag] == media_type: self.searchMedia.selectItemAtIndex_(tag) self.search_media = media_type self.search_contact = contact_uri self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByMediaChanged_(self, sender): tag = sender.selectedItem().tag() self.search_media = self.media_type_array[tag] self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByDateChanged_(self, sender): tag = sender.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None self.before_date = None else: self.before_date = self.period_array[tag].strftime("%Y-%m-%d") if self.period_array[tag] else None self.after_date = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() def validateToolbarItem_(self, item): if item.itemIdentifier() == NSToolbarPrintItemIdentifier and not self.messages: return False return True def toolbarWillAddItem_(self, notification): item = notification.userInfo()["item"] if item.itemIdentifier() == NSToolbarPrintItemIdentifier: item.setToolTip_("Print Current Entries") item.setTarget_(self) item.setAutovalidates_(True) @objc.IBAction def userClickedToolbarItem_(self, sender): if sender.itemIdentifier() == 'smileys': self.chatViewController.expandSmileys = not self.chatViewController.expandSmileys sender.setImage_(NSImage.imageNamed_("smiley_on" if self.chatViewController.expandSmileys else "smiley_off")) self.chatViewController.toggleSmileys(self.chatViewController.expandSmileys) elif sender.itemIdentifier() == 'delete': if self.selectedTableView == self.contactTable: try: row = self.contactTable.selectedRow() self.showDeleteConfirmationDialog(row) except IndexError: pass elif self.selectedTableView == self.indexTable: try: row = self.indexTable.selectedRow() local_uri = self.dayly_entries[row].objectForKey_("local_uri") remote_uri = self.dayly_entries[row].objectForKey_("remote_uri") date = self.dayly_entries[row].objectForKey_("date") media_type = self.dayly_entries[row].objectForKey_("type") ret = NSRunAlertPanel(u"Purge History Entries", u"Please confirm the deletion of %s history entries from %s on %s. This operation cannot be undone."%(media_type, remote_uri, date), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date) except IndexError: pass def showDeleteConfirmationDialog(self, row): media_print = self.search_media or 'All' tag = self.afterDate.selectedItem().tag() tag = self.afterDate.selectedItem().tag() period = '%s %s' % (' newer than ' if tag < 4 else ' older than ', self.period_array[tag].strftime("%Y-%m-%d")) if tag else '' if row == 0: ret = NSRunAlertPanel(u"Purge History Entries", u"Please confirm the deletion of %s history entries%s. This operation cannot be undone."%(media_print, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) elif row == 1: remote_uri=self.contacts[row].uri ret = NSRunAlertPanel(u"Purge History Entries", u"Please confirm the deletion of %s Bonjour history entries%s. This operation cannot be undone."%(media_print, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri='bonjour', media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) else: remote_uri=self.contacts[row].uri ret = NSRunAlertPanel(u"Purge History Entries", u"Please confirm the deletion of %s history entries from %s%s. This operation cannot be undone."%(media_print, remote_uri, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(remote_uri=remote_uri, media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) @allocate_autorelease_pool @run_in_gui_thread def updateBusyIndicator(self, busy=False): if busy: self.queryDatabaseLabel.setHidden_(False) self.busyIndicator.setHidden_(False) self.busyIndicator.setIndeterminate_(True) self.busyIndicator.setStyle_(NSProgressIndicatorSpinningStyle) self.busyIndicator.startAnimation_(None) else: self.busyIndicator.stopAnimation_(None) self.busyIndicator.setHidden_(True) self.queryDatabaseLabel.setHidden_(True) @allocate_autorelease_pool def handle_notification(self, notification): if notification.name in ("ChatViewControllerDidDisplayMessage", "AudioCallLoggedToHistory"): if notification.data.local_party != 'bonjour': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() elif notification.name == 'BlinkContactsHaveChanged': self.refreshContacts() self.toolbar.validateVisibleItems() elif notification.name == 'BlinkTableViewSelectionChaged': self.selectedTableView = notification.sender self.toolbar.validateVisibleItems() def contactSelectionChanged_(self, notification): hasContactMatchingURI = NSApp.delegate().contactsWindowController.hasContactMatchingURI try: row = self.contactTable.selectedRow() remote_uri=self.contacts[row].uri self.contactMenu.itemWithTag_(1).setEnabled_(False if hasContactMatchingURI(remote_uri) or row < 2 else True) self.contactMenu.itemWithTag_(3).setEnabled_(False if row < 2 else True) except: self.contactMenu.itemWithTag_(1).setEnabled_(False) def menuWillOpen_(self, menu): if menu == self.contactMenu: pass @objc.IBAction def doubleClick_(self, sender): try: row = self.contactTable.selectedRow() except: return if row < 2: return try: contact = self.contacts[row] except IndexError: return NSApp.delegate().contactsWindowController.startSessionWithSIPURI(contact.uri) @objc.IBAction def userClickedContactMenu_(self, sender): try: row = self.contactTable.selectedRow() except: return try: contact = self.contacts[row] except IndexError: return tag = sender.tag() if tag == 1: NSApp.delegate().contactsWindowController.addContact(contact.uri, contact.display_name) elif tag == 2: self.showDeleteConfirmationDialog(row) elif tag == 3: NSApp.delegate().contactsWindowController.searchBox.setStringValue_(contact.uri) NSApp.delegate().contactsWindowController.searchContacts() NSApp.delegate().contactsWindowController.window().makeFirstResponder_(NSApp.delegate().contactsWindowController.searchBox) NSApp.delegate().contactsWindowController.window().deminiaturize_(sender) NSApp.delegate().contactsWindowController.window().makeKeyWindow() @objc.IBAction def userClickedActionsButton_(self, sender): point = sender.convertPointToBase_(NSZeroPoint) point.x += 20 point.y -= 10 event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( NSLeftMouseUp, point, 0, NSDate.timeIntervalSinceReferenceDate(), sender.window().windowNumber(), sender.window().graphicsContext(), 0, 1, 0) NSMenu.popUpContextMenu_withEvent_forView_(self.contactMenu, event, sender)
class HistoryViewer(NSWindowController): implements(IObserver) chatViewController = objc.IBOutlet() indexTable = objc.IBOutlet() contactTable = objc.IBOutlet() toolbar = objc.IBOutlet() entriesView = objc.IBOutlet() afterDate = objc.IBOutlet() searchText = objc.IBOutlet() searchMedia = objc.IBOutlet() searchContactBox = objc.IBOutlet() paginationButton = objc.IBOutlet() foundMessagesLabel = objc.IBOutlet() queryDatabaseLabel = objc.IBOutlet() busyIndicator = objc.IBOutlet() contactMenu = objc.IBOutlet() # viewer sections allContacts = [] contacts = [] dayly_entries = NSMutableArray.array() messages = [] # database handler history = None # search filters start = 0 search_text = None search_contact = None search_local = None search_media = None after_date = None before_date = None daily_order_fields = { 'date': 'DESC', 'local_uri': 'ASC', 'remote_uri': 'ASC' } media_type_array = { 0: None, 1: 'audio', 2: 'chat', 3: 'sms', 4: 'file-transfer', 5: 'audio-recording', 6: 'video-recording', 7: 'voicemail', 8: 'missed-call' } period_array = { 0: None, 1: datetime.datetime.now() - datetime.timedelta(days=1), 2: datetime.datetime.now() - datetime.timedelta(days=7), 3: datetime.datetime.now() - datetime.timedelta(days=31), 4: datetime.datetime.now() - datetime.timedelta(days=31), 5: datetime.datetime.now() - datetime.timedelta(days=90), 6: datetime.datetime.now() - datetime.timedelta(days=180), 7: datetime.datetime.now() - datetime.timedelta(days=365) } def __new__(cls, *args, **kwargs): return cls.alloc().init() def __init__(self): if self: NSBundle.loadNibNamed_owner_("HistoryViewer", self) self.all_contacts = BlinkContact('Any Address', name=u'All Contacts') self.bonjour_contact = BlinkContact( 'bonjour', name=u'Bonjour Neighbours', icon=NSImage.imageNamed_("NSBonjour")) self.notification_center = NotificationCenter() self.notification_center.add_observer( self, name='ChatViewControllerDidDisplayMessage') self.notification_center.add_observer( self, name='AudioCallLoggedToHistory') self.notification_center.add_observer( self, name='BlinkContactsHaveChanged') self.notification_center.add_observer( self, name='BlinkTableViewSelectionChaged') self.searchText.cell().setSendsSearchStringImmediately_(True) self.searchText.cell().setPlaceholderString_( "Type text and press Enter") self.chatViewController.setContentFile_( NSBundle.mainBundle().pathForResource_ofType_( "ChatView", "html")) for c in ('remote_uri', 'local_uri', 'date', 'type'): col = self.indexTable.tableColumnWithIdentifier_(c) descriptor = NSSortDescriptor.alloc().initWithKey_ascending_( c, True) col.setSortDescriptorPrototype_(descriptor) self.history = ChatHistory() tag = self.afterDate.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None else: self.before_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None self.refreshViewer() self.selectedTableView = self.contactTable def awakeFromNib(self): NSNotificationCenter.defaultCenter().addObserver_selector_name_object_( self, "contactSelectionChanged:", NSTableViewSelectionDidChangeNotification, self.contactTable) self.contactTable.setDoubleAction_("doubleClick:") def close_(self, sender): self.window().close() @allocate_autorelease_pool @run_in_gui_thread def refreshViewer(self): self.search_text = None self.search_contact = None self.search_local = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @run_in_green_thread def delete_messages(self, local_uri=None, remote_uri=None, date=None, after_date=None, before_date=None, media_type=None): self.history.delete_messages(local_uri=local_uri, remote_uri=remote_uri, date=date, after_date=after_date, before_date=before_date, media_type=media_type) self.refreshViewer() @run_in_green_thread def refreshContacts(self): if self.history: self.updateBusyIndicator(True) media_type = self.search_media if self.search_media else None search_text = self.search_text if self.search_text else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.history.get_contacts(media_type=media_type, search_text=search_text, after_date=after_date, before_date=before_date) self.renderContacts(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderContacts(self, results): getContactMatchingURI = NSApp.delegate( ).contactsWindowController.getContactMatchingURI self.contacts = [self.all_contacts, self.bonjour_contact] self.allContacts = [] for row in results: contact = getContactMatchingURI(row[0]) if contact: detail = contact.uri contact = BlinkContact(unicode(row[0]), name=contact.name, icon=contact.icon) else: detail = unicode(row[0]) contact = BlinkContact(unicode(row[0]), name=unicode(row[0])) contact.setDetail(detail) self.contacts.append(contact) self.allContacts.append(contact) real_contacts = len(self.contacts) - 2 self.contactTable.reloadData() if self.search_contact: try: contact = (contact for contact in self.contacts if contact.uri == self.search_contact).next() except StopIteration: pass else: try: row = self.contacts.index(contact) self.contactTable.selectRowIndexes_byExtendingSelection_( NSIndexSet.indexSetWithIndex_(row), False) self.contactTable.scrollRowToVisible_(row) except: pass else: self.contactTable.selectRowIndexes_byExtendingSelection_( NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) self.contactTable.tableColumnWithIdentifier_('contacts').headerCell( ).setStringValue_(u'%d Contacts' % real_contacts if real_contacts else u'Contacts') @run_in_green_thread def refreshDailyEntries(self, order_text=None): if self.history: self.resetDailyEntries() self.updateBusyIndicator(True) search_text = self.search_text if self.search_text else None remote_uri = self.search_contact if self.search_contact else None local_uri = self.search_local if self.search_local else None media_type = self.search_media if self.search_media else None after_date = self.after_date if self.after_date else None before_date = self.before_date if self.before_date else None results = self.history.get_daily_entries(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, search_text=search_text, order_text=order_text, after_date=after_date, before_date=before_date) self.renderDailyEntries(results) self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def resetDailyEntries(self): self.dayly_entries = NSMutableArray.array() self.indexTable.reloadData() @allocate_autorelease_pool @run_in_gui_thread def renderDailyEntries(self, results): getContactMatchingURI = NSApp.delegate( ).contactsWindowController.getContactMatchingURI self.dayly_entries = NSMutableArray.array() for result in results: contact = getContactMatchingURI(result[2]) if contact: remote_uri = '%s <%s>' % (contact.name, contact.uri) else: remote_uri = result[2] entry = NSDictionary.dictionaryWithObjectsAndKeys_( result[1], "local_uri", remote_uri, "remote_uri", result[2], "remote_uri_sql", result[0], 'date', result[3], 'type') self.dayly_entries.addObject_(entry) self.dayly_entries.sortUsingDescriptors_( self.indexTable.sortDescriptors()) self.indexTable.reloadData() if self.search_contact and not self.dayly_entries: self.contactTable.deselectAll_(True) @run_in_green_thread @allocate_autorelease_pool def refreshMessages(self, count=SQL_LIMIT, remote_uri=None, local_uri=None, media_type=None, date=None, after_date=None, before_date=None): self.updateBusyIndicator(True) if self.history: search_text = self.search_text if self.search_text else None if not remote_uri: remote_uri = self.search_contact if self.search_contact else None if not local_uri: local_uri = self.search_local if self.search_local else None if not media_type: media_type = self.search_media if self.search_media else None if not after_date: after_date = self.after_date if self.after_date else None if not before_date: before_date = self.before_date if self.before_date else None results = self.history.get_messages(count=count, local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date, search_text=search_text, after_date=after_date, before_date=before_date) # cache message for pagination self.messages = [] for e in reversed(results): self.messages.append(e) # reset pagination self.start = 0 self.renderMessages() self.updateBusyIndicator(False) @allocate_autorelease_pool @run_in_gui_thread def renderMessages(self): self.chatViewController.clear() self.chatViewController.resetRenderedMessages() end = self.start + MAX_MESSAGES_PER_PAGE if self.start + MAX_MESSAGES_PER_PAGE < len( self.messages) else len(self.messages) for row in self.messages[self.start:end]: self.renderMessage(row) self.paginationButton.setEnabled_forSegment_( True if len(self.messages) > MAX_MESSAGES_PER_PAGE and self.start > MAX_MESSAGES_PER_PAGE else False, 0) self.paginationButton.setEnabled_forSegment_( True if self.start else False, 1) self.paginationButton.setEnabled_forSegment_( True if self.start + MAX_MESSAGES_PER_PAGE + 1 < len(self.messages) else False, 2) self.paginationButton.setEnabled_forSegment_( True if len(self.messages) > MAX_MESSAGES_PER_PAGE and len(self.messages) - self.start > 2 * MAX_MESSAGES_PER_PAGE else False, 3) text = u'No entry found' if len(self.messages): if len(self.messages) == 1: text = u'Displaying 1 entry' elif MAX_MESSAGES_PER_PAGE > len(self.messages): text = u'Displaying %d entries' % end else: text = u'Displaying %d to %d out of %d entries' % ( self.start + 1, end, len(self.messages)) self.foundMessagesLabel.setStringValue_(text) @allocate_autorelease_pool @run_in_gui_thread def renderMessage(self, message): if message.direction == 'outgoing': icon = NSApp.delegate().contactsWindowController.iconPathForSelf() else: sender_uri = sipuri_components_from_string(message.cpim_from)[0] # TODO: How to render the icons from Address Book? Especially in sandbox mode we do not have access to other folders icon = NSApp.delegate().contactsWindowController.iconPathForURI( sender_uri) try: timestamp = Timestamp.parse(message.cpim_timestamp) except ValueError: pass else: is_html = False if message.content_type == 'text' else True private = True if message.private == "1" else False self.chatViewController.showMessage(message.msgid, message.direction, message.cpim_from, icon, message.body, timestamp, is_private=private, recipient=message.cpim_to, state=message.status, is_html=is_html, history_entry=True) @objc.IBAction def paginateResults_(self, sender): if sender.selectedSegment() == 0: self.start = 0 elif sender.selectedSegment() == 1: next_start = self.start - MAX_MESSAGES_PER_PAGE self.start = next_start if next_start >= 0 else self.start elif sender.selectedSegment() == 2: next_start = self.start + MAX_MESSAGES_PER_PAGE self.start = next_start if next_start < len( self.messages) - 1 else self.start elif sender.selectedSegment() == 3: self.start = len(self.messages) - len( self.messages) % MAX_MESSAGES_PER_PAGE if len( self.messages) > MAX_MESSAGES_PER_PAGE else 0 self.renderMessages() def tableView_deleteRow_(self, table, row): pass def tableView_sortDescriptorsDidChange_(self, table, odescr): self.dayly_entries.sortUsingDescriptors_( self.indexTable.sortDescriptors()) self.indexTable.reloadData() @objc.IBAction def printDocument_(self, sender): printInfo = NSPrintInfo.sharedPrintInfo() printInfo.setTopMargin_(30) printInfo.setBottomMargin_(30) printInfo.setLeftMargin_(10) printInfo.setRightMargin_(10) printInfo.setOrientation_(NSPortraitOrientation) printInfo.setHorizontallyCentered_(True) printInfo.setVerticallyCentered_(False) printInfo.setHorizontalPagination_(NSFitPagination) printInfo.setVerticalPagination_(NSFitPagination) NSPrintInfo.setSharedPrintInfo_(printInfo) # print the content of the web view self.entriesView.mainFrame().frameView().documentView().print_(self) @objc.IBAction def search_(self, sender): if self.history: self.search_text = unicode(sender.stringValue()).lower() self.refreshContacts() row = self.indexTable.selectedRow() if row > 0: self.refreshDailyEntries() self.refreshMessages( local_uri=self.dayly_entries[row].objectForKey_( "local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_("type")) else: row = self.contactTable.selectedRow() if row > 0: self.refreshMessages() self.refreshDailyEntries() else: self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def searchContacts_(self, sender): text = unicode(self.searchContactBox.stringValue().strip()) contacts = [ contact for contact in self.allContacts if text in contact ] if text else self.allContacts self.contacts = [self.all_contacts, self.bonjour_contact] + contacts self.contactTable.reloadData() self.contactTable.selectRowIndexes_byExtendingSelection_( NSIndexSet.indexSetWithIndex_(0), False) self.contactTable.scrollRowToVisible_(0) def tableViewSelectionDidChange_(self, notification): if self.history: if not notification or notification.object() == self.contactTable: row = self.contactTable.selectedRow() if row == 0: self.search_local = None self.search_contact = None elif row == 1: self.search_local = 'bonjour' self.search_contact = None elif row > 1: self.search_local = None self.search_contact = self.contacts[row].uri self.refreshDailyEntries() self.refreshMessages() else: row = self.indexTable.selectedRow() if row >= 0: self.refreshMessages( remote_uri=self.dayly_entries[row].objectForKey_( "remote_uri_sql"), local_uri=self.dayly_entries[row].objectForKey_( "local_uri"), date=self.dayly_entries[row].objectForKey_("date"), media_type=self.dayly_entries[row].objectForKey_( "type")) def numberOfRowsInTableView_(self, table): if table == self.indexTable: return self.dayly_entries.count() elif table == self.contactTable: return len(self.contacts) return 0 def tableView_objectValueForTableColumn_row_(self, table, column, row): if table == self.indexTable: ident = column.identifier() try: return unicode(self.dayly_entries[row].objectForKey_(ident)) except IndexError: return None elif table == self.contactTable: try: if type(self.contacts[row]) in (str, unicode): return self.contacts[row] else: return self.contacts[row].name except IndexError: return None return None def tableView_willDisplayCell_forTableColumn_row_(self, table, cell, tableColumn, row): if table == self.contactTable: try: if row < len(self.contacts): if type(self.contacts[row]) in (str, unicode): cell.setContact_(None) else: cell.setContact_(self.contacts[row]) except: pass def showWindow_(self, sender): self.window().makeKeyAndOrderFront_(None) @run_in_gui_thread def filterByContact(self, contact_uri, media_type=None): self.search_text = None self.search_local = None if media_type != self.search_media: for tag in self.media_type_array.keys(): if self.media_type_array[tag] == media_type: self.searchMedia.selectItemAtIndex_(tag) self.search_media = media_type self.search_contact = contact_uri self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByMediaChanged_(self, sender): tag = sender.selectedItem().tag() self.search_media = self.media_type_array[tag] self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() @objc.IBAction def filterByDateChanged_(self, sender): tag = sender.selectedItem().tag() if tag < 4: self.after_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None self.before_date = None else: self.before_date = self.period_array[tag].strftime( "%Y-%m-%d") if self.period_array[tag] else None self.after_date = None self.refreshContacts() self.refreshDailyEntries() self.refreshMessages() def validateToolbarItem_(self, item): if item.itemIdentifier( ) == NSToolbarPrintItemIdentifier and not self.messages: return False return True def toolbarWillAddItem_(self, notification): item = notification.userInfo()["item"] if item.itemIdentifier() == NSToolbarPrintItemIdentifier: item.setToolTip_("Print Current Entries") item.setTarget_(self) item.setAutovalidates_(True) @objc.IBAction def userClickedToolbarItem_(self, sender): if sender.itemIdentifier() == 'smileys': self.chatViewController.expandSmileys = not self.chatViewController.expandSmileys sender.setImage_( NSImage.imageNamed_("smiley_on" if self.chatViewController. expandSmileys else "smiley_off")) self.chatViewController.toggleSmileys( self.chatViewController.expandSmileys) elif sender.itemIdentifier() == 'delete': if self.selectedTableView == self.contactTable: try: row = self.contactTable.selectedRow() self.showDeleteConfirmationDialog(row) except IndexError: pass elif self.selectedTableView == self.indexTable: try: row = self.indexTable.selectedRow() local_uri = self.dayly_entries[row].objectForKey_( "local_uri") remote_uri = self.dayly_entries[row].objectForKey_( "remote_uri") date = self.dayly_entries[row].objectForKey_("date") media_type = self.dayly_entries[row].objectForKey_("type") ret = NSRunAlertPanel( u"Purge History Entries", u"Please confirm the deletion of %s history entries from %s on %s. This operation cannot be undone." % (media_type, remote_uri, date), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri=local_uri, remote_uri=remote_uri, media_type=media_type, date=date) except IndexError: pass def showDeleteConfirmationDialog(self, row): media_print = self.search_media or 'All' tag = self.afterDate.selectedItem().tag() tag = self.afterDate.selectedItem().tag() period = '%s %s' % ( ' newer than ' if tag < 4 else ' older than ', self.period_array[tag].strftime("%Y-%m-%d")) if tag else '' if row == 0: ret = NSRunAlertPanel( u"Purge History Entries", u"Please confirm the deletion of %s history entries%s. This operation cannot be undone." % (media_print, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) elif row == 1: remote_uri = self.contacts[row].uri ret = NSRunAlertPanel( u"Purge History Entries", u"Please confirm the deletion of %s Bonjour history entries%s. This operation cannot be undone." % (media_print, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(local_uri='bonjour', media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) else: remote_uri = self.contacts[row].uri ret = NSRunAlertPanel( u"Purge History Entries", u"Please confirm the deletion of %s history entries from %s%s. This operation cannot be undone." % (media_print, remote_uri, period), u"Confirm", u"Cancel", None) if ret == NSAlertDefaultReturn: self.delete_messages(remote_uri=remote_uri, media_type=self.search_media, after_date=self.after_date, before_date=self.before_date) @allocate_autorelease_pool @run_in_gui_thread def updateBusyIndicator(self, busy=False): if busy: self.queryDatabaseLabel.setHidden_(False) self.busyIndicator.setHidden_(False) self.busyIndicator.setIndeterminate_(True) self.busyIndicator.setStyle_(NSProgressIndicatorSpinningStyle) self.busyIndicator.startAnimation_(None) else: self.busyIndicator.stopAnimation_(None) self.busyIndicator.setHidden_(True) self.queryDatabaseLabel.setHidden_(True) @allocate_autorelease_pool def handle_notification(self, notification): if notification.name in ("ChatViewControllerDidDisplayMessage", "AudioCallLoggedToHistory"): if notification.data.local_party != 'bonjour': exists = any(contact for contact in self.contacts if notification.data.remote_party == contact.uri) if not exists: self.refreshContacts() elif notification.name == 'BlinkContactsHaveChanged': self.refreshContacts() self.toolbar.validateVisibleItems() elif notification.name == 'BlinkTableViewSelectionChaged': self.selectedTableView = notification.sender self.toolbar.validateVisibleItems() def contactSelectionChanged_(self, notification): hasContactMatchingURI = NSApp.delegate( ).contactsWindowController.hasContactMatchingURI try: row = self.contactTable.selectedRow() remote_uri = self.contacts[row].uri self.contactMenu.itemWithTag_(1).setEnabled_( False if hasContactMatchingURI(remote_uri) or row < 2 else True ) self.contactMenu.itemWithTag_(3).setEnabled_( False if row < 2 else True) except: self.contactMenu.itemWithTag_(1).setEnabled_(False) def menuWillOpen_(self, menu): if menu == self.contactMenu: pass @objc.IBAction def doubleClick_(self, sender): try: row = self.contactTable.selectedRow() except: return if row < 2: return try: contact = self.contacts[row] except IndexError: return NSApp.delegate().contactsWindowController.startSessionWithSIPURI( contact.uri) @objc.IBAction def userClickedContactMenu_(self, sender): try: row = self.contactTable.selectedRow() except: return try: contact = self.contacts[row] except IndexError: return tag = sender.tag() if tag == 1: NSApp.delegate().contactsWindowController.addContact( contact.uri, contact.display_name) elif tag == 2: self.showDeleteConfirmationDialog(row) elif tag == 3: NSApp.delegate( ).contactsWindowController.searchBox.setStringValue_(contact.uri) NSApp.delegate().contactsWindowController.searchContacts() NSApp.delegate().contactsWindowController.window( ).makeFirstResponder_( NSApp.delegate().contactsWindowController.searchBox) NSApp.delegate().contactsWindowController.window().deminiaturize_( sender) NSApp.delegate().contactsWindowController.window().makeKeyWindow() @objc.IBAction def userClickedActionsButton_(self, sender): point = sender.convertPointToBase_(NSZeroPoint) point.x += 20 point.y -= 10 event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_( NSLeftMouseUp, point, 0, NSDate.timeIntervalSinceReferenceDate(), sender.window().windowNumber(), sender.window().graphicsContext(), 0, 1, 0) NSMenu.popUpContextMenu_withEvent_forView_(self.contactMenu, event, sender)