# ABPresence

- `Download example`

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