ABPresence

Simple example that uses the InstantMessage framework to show the connection status of persons in your address book.

Sources

ABPersonDisplayNameAdditions.py

import Cocoa
import AddressBook
import objc

class ABPerson (objc.Category(AddressBook.ABPerson)):
    # Pull first and last name, organization, and record flags
    # If the entry is a company, display the organization name instead
    def displayName(self):
        firstName = self.valueForProperty_(AddressBook.kABFirstNameProperty)
        lastName = self.valueForProperty_(AddressBook.kABLastNameProperty)
        companyName = self.valueForProperty_(AddressBook.kABOrganizationProperty)
        flags = self.valueForProperty_(AddressBook.kABPersonFlags)
        if flags is None:
            flags = 0

        if (flags & AddressBook.kABShowAsMask) == AddressBook.kABShowAsCompany:
            if len(companyName):
                return companyName;

        lastNameFirst = (flags & AddressBook.kABNameOrderingMask) == AddressBook.kABLastNameFirst
        hasFirstName = firstName is not None
        hasLastName = lastName is not None

        if hasLastName and hasFirstName:
            if lastNameFirst:
                return Cocoa.NSString.stringWithString_("%s %s"%(lastName, firstName))
            else:
                return Cocoa.NSString.stringWithString_("%s %s"%(firstName, lastName))

        if hasLastName:
            return lastName

        return firstName

    def compareDisplayNames_(self, person):
        return self.displayName().localizedCaseInsensitiveCompare_(
                person.displayName())

PeopleDataSource.py

import Cocoa
import AddressBook
import InstantMessage
import objc

from ServiceWatcher import kStatusImagesChanged, kAddressBookPersonStatusChanged

class PeopleDataSource (Cocoa.NSObject):
    _abPeople = objc.ivar()
    _imPersonStatus = objc.ivar() # Parallel array to abPeople
    _table = objc.IBOutlet()

    def awakeFromNib(self):
        self._imPersonStatus = Cocoa.NSMutableArray.alloc().init()

        # We don't need to query the staus of everyone, we will wait for
        # notifications of their status to arrive, so we just set them all up
        # as offline.
        self.setupABPeople()

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, b'abDatabaseChangedExternallyNotification:',
                AddressBook.kABDatabaseChangedExternallyNotification, None)

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, b'addressBookPersonStatusChanged:',
                kAddressBookPersonStatusChanged, None)

        Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, b'statusImagesChanged:',
                kStatusImagesChanged, None)

    # This dumps all the status information and rebuilds the array against
    # the current _abPeople
    # Fairly expensive, so this is only done when necessary
    def rebuildStatusInformation(self):
        # Now scan through all the people, adding their status to the status
        # cache array
        for i, person in enumerate(self._abPeople):
            # Let's assume they're offline to start
            bestStatus = InstantMessage.IMPersonStatusOffline

            for service in InstantMessage.IMService.allServices():
                screenNames = service.screenNamesForPerson_(person)

                for screenName in screenNames:
                    dictionary = service.infoForScreenName_(
                            screenName)
                    status = dictionary.get(InstantMessage.IMPersonStatusKey)
                    if status is not None:
                        thisStatus = status
                        if InstantMessage.IMComparePersonStatus(bestStatus, thisStatus) != Cocoa.NSOrderedAscending:
                            bestStatus = thisStatus;

            self._imPersonStatus[i] = bestStatus

        self._table.reloadData()

    # Rebuild status information for a given person, much faster than a full
    # rebuild
    def rebuildStatusInformationForPerson_(self, forPerson):
        for i, person in enumerate(self._abPeople):
            if person is forPerson:
                bestStatus = InstantMessage.IMPersonStatusOffline

                # Scan through all the services, taking the 'best' status we
                # can find
                for service in InstantMessage.IMService.allServices():
                    screenNames = service.screenNamesForPerson_(person)

                    # Ask for the status on each of their screen names
                    for screenName in screenNames:
                        dictionary = service.infoForScreenName_(screenName)
                        status = dictionary.get(InstantMessage.IMPersonStatusKey)
                        if status is not None:
                            thisStatus = status
                            if InstantMessage.IMComparePersonStatus(bestStatus, thisStatus) != Cocoa.NSOrderedAscending:
                                bestStatus = thisStatus

                self._imPersonStatus[i] = bestStatus
                self._table.reloadData()
                break

    # Sets up all our internal data
    def setupABPeople(self):
        # Keep around a copy of all the people in the AB now
        self._abPeople = AddressBook.ABAddressBook.sharedAddressBook().people().mutableCopy()

        # Sort them by display name
        self._abPeople.sortUsingSelector_('compareDisplayNames:')

        # Assume everyone is offline.
        self._imPersonStatus.removeAllObjects()
        offlineNumber =  InstantMessage.IMPersonStatusOffline
        for i in range(len(self._abPeople)):
            self._imPersonStatus.append(offlineNumber)

    # This will do a full flush of people in our AB Cache, along with
    # rebuilding their status */
    def reloadABPeople(self):
        self.setupABPeople()

        # Now recache all the status info, this will spawn a reload of the table
        self.rebuildStatusInformation()

    def numberOfRowsInTableView_(self, tableView):
        if self._abPeople is None:
            return 0
        return len(self._abPeople)

    def tableView_objectValueForTableColumn_row_(self, tableView, tableColumn, row):
        identifier = tableColumn.identifier()
        if identifier == u"image":
            status = self._imPersonStatus[row]
            return Cocoa.NSImage.imageNamed_(InstantMessage.IMService.imageNameForStatus_(status))

        elif identifier == u"name":
            return self._abPeople[row].displayName()

        return None


    # Posted from ServiceWatcher
    # The object of this notification is an ABPerson who's status has
    # Changed
    def addressBookPersonStatusChanged_(self, notification):
        self.rebuildStatusInformationForPerson_(notification.object())

    # Posted from ServiceWatcher
    # We should reload the tableview, because the user has changed the
    # status images that iChat is using.
    def statusImagesChanged_(self, notification):
        self._table.reloadData()

    # If the AB database changes, force a reload of everyone
    # We could look in the notification to catch differential updates, but
    # for now this is fine.
    def abDatabaseChangedExternallyNotification_(self, notification):
        self.reloadABPeople()

ServiceWatcher.py

import Cocoa
import InstantMessage
import AddressBook
import objc

kAddressBookPersonStatusChanged = "AddressBookPersonStatusChanged"
kStatusImagesChanged = "StatusImagesChanged"

class ServiceWatcher (Cocoa.NSObject):
    def startMonitoring(self):
        nCenter = InstantMessage.IMService.notificationCenter()
        if nCenter is None:
            return None

        nCenter.addObserver_selector_name_object_(
                self, b'imPersonStatusChangedNotification:',
                InstantMessage.IMPersonStatusChangedNotification, None)

        nCenter.addObserver_selector_name_object_(
                self, b'imStatusImagesChangedAppearanceNotification:',
                InstantMessage.IMStatusImagesChangedAppearanceNotification, None)

    def stopMonitoring(self):
        nCenter = InstantMessage.IMService.notificationCenter()
        nCenter.removeObserver_(self)

    def awakeFromNib(self):
        self.startMonitoring()

    # Received from IMService's custom notification center. Posted when a
    # different user (screenName) logs in, logs off, goes away,
    # and so on. This notification is for the IMService object. The user
    # information dictionary will always contain an
    # IMPersonScreenNameKey and an IMPersonStatusKey, and no others.
    def imPersonStatusChangedNotification_(self, notification):
        service = notification.object()
        userInfo = notification.userInfo()
        screenName = userInfo[InstantMessage.IMPersonScreenNameKey]
        abPersons = service.peopleWithScreenName_(screenName)

        center = Cocoa.NSNotificationCenter.defaultCenter()
        for person in abPersons:
            center.postNotificationName_object_(
                    kAddressBookPersonStatusChanged, person)

    # Received from IMService's custom notification center. Posted when the
    # user changes their preferred images for displaying status.
    # This notification is relevant to no particular object. The user
    # information dictionary will not contain keys. Clients that display
    # status information graphically (using the green/yellow/red dots) should
    # call <tt>imageURLForStatus:</tt> to get the new image.
    # See "Class Methods" for IMService in this document.
    def imStatusImagesChangedAppearanceNotification_(self, notification):
        Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_(
                kStatusImagesChanged, self)

main.py

from PyObjCTools import AppHelper
import objc; objc.setVerbose(True)

import ABPersonDisplayNameAdditions
import PeopleDataSource
import ServiceWatcher


AppHelper.runEventLoop()

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

setup(
    name="PyABPresence",
    app=["main.py"],
    data_files=["English.lproj"],
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
        "pyobjc-framework-InstantMessage",
    ]

)

Resources