ABPresence

A PyObjC Example without documentation

Sources

ABPersonDisplayNameAdditions.py

import AddressBook
import Cocoa
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_(f"{lastName} {firstName}")
            else:
                return Cocoa.NSString.stringWithString_(f"{firstName} {lastName}")

        if hasLastName:
            return lastName

        return firstName

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

PeopleDataSource.py

import AddressBook
import Cocoa
import InstantMessage
import objc
from ServiceWatcher import kAddressBookPersonStatusChanged, kStatusImagesChanged


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 status 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 _ 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 == "image":
            status = self._imPersonStatus[row]
            return Cocoa.NSImage.imageNamed_(
                InstantMessage.IMService.imageNameForStatus_(status)
            )

        elif identifier == "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 AddressBook  # noqa: F401
import Cocoa
import InstantMessage

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

import ABPersonDisplayNameAdditions  # noqa: F401
import PeopleDataSource  # noqa: F401
import ServiceWatcher  # noqa: F401
from PyObjCTools import AppHelper


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",
    ],
)