Simplify the "status message" contact/account property into one getter method. Correct error -1728 from AS on contact's status messages. Fixes #13460.
The totally undocumented -1728 error appears to be caused by runtime type disagreeing with event type. "status message" is apparently assumed to only have 1 code, so specifying one for contacts and one for accounts was confusing it when it was going to fetch it.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <Adium/AIContactControllerProtocol.h>
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIContentControllerProtocol.h>
20 #import <Adium/AIStatusControllerProtocol.h>
21 #import <Adium/AIContentMessage.h>
22 #import <Adium/AIListContact.h>
23 #import <Adium/AIContactList.h>
24 #import <Adium/AIListGroup.h>
25 #import <Adium/AIMetaContact.h>
26 #import <Adium/AIService.h>
27 #import <Adium/AIUserIcons.h>
28 #import <Adium/ESFileTransfer.h>
29 #import <Adium/AIStatus.h>
30 #import <Adium/AIHTMLDecoder.h>
32 #import <AIUtilities/AIMutableOwnerArray.h>
33 #import <AIUtilities/AIMutableStringAdditions.h>
35 #import <AvailabilityMacros.h>
37 #import "AIAddressBookController.h"
39 #define KEY_BASE_WRITING_DIRECTION @"Base Writing Direction"
40 #define PREF_GROUP_WRITING_DIRECTION @"Writing Direction"
42 #define CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY 15
44 @interface AIListObject ()
45 - (void)setContainingObject:(AIListObject <AIContainingObject> *)inGroup;
48 @interface AIListContact ()
49 @property (readwrite, nonatomic, assign) AIMetaContact *metaContact;
50 - (void) remoteGroupingChanged;
53 @implementation AIListContact
55 //Init with an account
56 - (id)initWithUID:(NSString *)inUID account:(AIAccount *)inAccount service:(AIService *)inService
58 if ((self = [self initWithUID:inUID service:inService])) {
59 account = [inAccount retain];
66 - (id)initWithUID:(NSString *)inUID service:(AIService *)inService
68 if ((self = [super initWithUID:inUID service:inService])) {
70 m_remoteGroupNames = [[NSMutableSet alloc] initWithCapacity:1];
71 internalUniqueObjectID = nil;
79 [account release]; account = nil;
80 [m_remoteGroupNames release]; m_remoteGroupNames = nil;
81 [internalUniqueObjectID release]; internalUniqueObjectID = nil;
86 //The account that owns this contact
90 * @brief Set the UID of this contact
92 * The UID for an AIListContact generally shouldn't change... if the contact is actually renamed serverside, however,
93 * it is useful to change the UID without having to change everything else associated with it.
95 - (void)setUID:(NSString *)inUID
98 [UID release]; UID = [inUID retain];
99 [internalObjectID release]; internalObjectID = nil;
100 [internalUniqueObjectID release]; internalUniqueObjectID = nil;
104 //An object ID generated by Adium that is completely unique to this contact. This ID is generated from the service ID,
105 //UID, and account UID. Adium will not allow multiple contacts with the same internalUniqueObjectID to be created.
106 - (NSString *)internalUniqueObjectID
108 if (!internalUniqueObjectID) {
109 internalUniqueObjectID = [[AIListContact internalUniqueObjectIDForService:self.service
111 UID:self.UID] retain];
113 return internalUniqueObjectID;
116 //Generate a unique object ID for the passed object
117 + (NSString *)internalUniqueObjectIDForService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
119 return [NSString stringWithFormat:@"%@.%@.%@", inService.serviceClass, inAccount.UID, inUID];
123 //Remote Grouping ------------------------------------------------------------------------------------------------------
124 #pragma mark Remote Grouping
126 - (NSSet *) remoteGroupNames
128 return [[m_remoteGroupNames copy] autorelease];
131 - (void) setRemoteGroupNames:(NSSet *)inGroupNames
133 NSParameterAssert(inGroupNames != nil);
134 [m_remoteGroupNames setSet:inGroupNames];
135 [self remoteGroupingChanged];
138 - (void) addRemoteGroupName:(NSString *)inName
140 NSParameterAssert(inName != nil);
141 if ([m_remoteGroupNames containsObject:inName])
144 [m_remoteGroupNames addObject:inName];
145 [self remoteGroupingChanged];
148 - (void) removeRemoteGroupName:(NSString *)inName
150 NSParameterAssert(inName != nil);
151 if (![m_remoteGroupNames containsObject:inName])
154 [m_remoteGroupNames removeObject:inName];
155 [self remoteGroupingChanged];
158 - (NSUInteger) countOfRemoteGroupNames
160 return m_remoteGroupNames.count;
163 - (NSSet *)remoteGroups
165 NSMutableSet *groups = [NSMutableSet set];
166 for (NSString *remoteGroup in m_remoteGroupNames) {
167 [groups addObject:[adium.contactController groupWithUID:remoteGroup]];
172 - (void) remoteGroupingChanged
174 NSUInteger remoteGroupCount = m_remoteGroupNames.count;
175 if (remoteGroupCount == 0)
176 [AIUserIcons flushCacheForObject:self];
178 [self restoreGrouping];
180 if (self.isStranger != (remoteGroupCount == 0)) {
181 [self setValue:[NSNumber numberWithBool:remoteGroupCount > 0]
182 forProperty:@"NotAStranger"
184 [self notifyOfChangedPropertiesSilently:YES];
188 //An AIListContact normally groups based on its remoteGroupNames (if it is not within a metaContact).
189 //Restore this grouping.
190 - (void)restoreGrouping
192 if (self.metaContact) {
193 [self.metaContact updateRemoteGroupingOfContact:self];
197 //Create a group for the contact even if contact list groups aren't on,
198 //otherwise requests for all the contact list groups will return nothing
199 NSMutableSet *groups = [NSMutableSet set];
200 for (NSString *remoteGroupName in m_remoteGroupNames) {
201 AIListGroup *localGroup = [adium.contactController groupWithUID:remoteGroupName];
203 if (!adium.contactController.useContactListGroups)
204 localGroup = adium.contactController.contactList;
205 else if (adium.contactController.useOfflineGroup && !self.online && !self.alwaysVisible)
206 localGroup = adium.contactController.offlineGroup;
208 [groups addObject:localGroup];
210 [adium.contactController _moveContactLocally:self fromGroups:self.groups toGroups:groups];
215 * @brief Display name
217 * Display name, drawing first from any externally-provided display name, then falling back to
220 * A listContact attempts to have the same displayName as its containing contact (potentially its metaContact).
221 * If it is not in a metaContact, its display name is returned by super.displayName
223 - (NSString *)displayName
225 AIMetaContact *meta = self.metaContact;
227 NSString *displayName = meta ? meta.displayName : super.displayName;
229 //If a display name was found, return it; otherwise, return the formattedUID
230 return displayName ? displayName : self.formattedUID;
234 * @brief Own display name
236 * Returns the display name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
237 * infinite recursion by its displayName calling our displayName calling its displayName and so on.
239 - (NSString *)ownDisplayName
241 return super.displayName;
245 * @brief This contact's serverside display name, which is generally specificed by the contact remotely
247 * @result The serverside display name, or nil if none is set
249 - (NSString *)serversideDisplayName
251 return [self valueForProperty:@"Server Display Name"];
254 - (void)setServersideAlias:(NSString *)alias
255 silently:(BOOL)silent
258 BOOL displayNameChanges = NO;
260 AILogWithSignature(@"%@ received alias %@", self, alias);
262 //This is the server display name. Set it as such.
263 if (![alias isEqualToString:[self valueForProperty:@"Server Display Name"]]) {
264 //Set the server display name property as the full display name
266 forProperty:@"Server Display Name"
272 NSMutableString *cleanedAlias;
274 //Remove any newlines, since we won't want them anywhere below
275 cleanedAlias = [alias mutableCopy];
276 [cleanedAlias convertNewlinesToSlashes];
278 AIMutableOwnerArray *displayNameArray = [self displayArrayForKey:@"Display Name"];
279 NSString *oldDisplayName = [displayNameArray objectValue];
281 //If the mutableOwnerArray's current value isn't identical to this alias, we should set it
282 if (![[displayNameArray objectWithOwner:self.account] isEqualToString:cleanedAlias]) {
283 [displayNameArray setObject:cleanedAlias
284 withOwner:self.account
285 priorityLevel:Low_Priority];
287 //If this causes the object value to change, we need to request a manual update of the display name
288 if (oldDisplayName != [displayNameArray objectValue]) {
289 displayNameChanges = YES;
295 [self notifyOfChangedPropertiesSilently:silent];
298 if (displayNameChanges) {
299 //Request an alias change
300 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ApplyDisplayName
302 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
306 [cleanedAlias release];
310 * @brief The way this object's name should be spoken
312 * If not found, the display name is returned.
314 - (NSString *)phoneticName
316 AIMetaContact *meta = self.metaContact;
317 NSString *phoneticName;
319 phoneticName = meta ? meta.phoneticName : super.phoneticName;;
321 //If a display name was found, return it; otherwise, return the formattedUID
322 return phoneticName ? phoneticName : self.displayName;
326 * @brief Own phonetic name
328 * Returns the phonetic name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
329 * infinite recursion by its phoneticName calling our phoneticName calling its phoneticName and so on.
331 - (NSString *)ownPhoneticName
333 return super.phoneticName;
336 #pragma mark Properties
341 - (void)setOnline:(BOOL)online notify:(NotifyTiming)notify silently:(BOOL)silent
343 if (online != self.online) {
344 [self setValue:[NSNumber numberWithBool:online]
345 forProperty:@"Online"
349 [self setValue:[NSNumber numberWithBool:YES]
350 forProperty:(online ? @"Signed On" : @"Signed Off")
353 forProperty:(online ? @"Signed Off" : @"Signed On")
356 forProperty:(online ? @"Signed On" : @"Signed Off")
357 afterDelay:CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY];
361 if (notify == NotifyNow) {
362 [self notifyOfChangedPropertiesSilently:silent];
367 [self.account removePropertyValuesFromContact:self
374 * @brief Set the sign on date
376 - (void)setSignonDate:(NSDate *)signonDate notify:(NotifyTiming)notify
378 [self setValue:signonDate
379 forProperty:@"Signon Date"
383 * @brief Date this contact signed on, if available
385 - (NSDate *)signonDate
387 return [self valueForProperty:@"Signon Date"];
391 * @brief Set the idle state
393 * @param isIdle YES if the contact is idle
394 * @param idleSinceDate The date this contact went idle. Only relevant if isIdle is YES
395 * @param notify The NotifyTiming
397 - (void)setIdle:(BOOL)isIdle sinceDate:(NSDate *)idleSinceDate notify:(NotifyTiming)notify
401 [self setValue:idleSinceDate
402 forProperty:@"IdleSince"
405 //No idleSinceDate means we are Idle but don't know how long, so set to -1
406 [self setValue:[NSNumber numberWithInt:-1]
412 forProperty:@"IdleSince"
419 /* @"Idle", for a contact with an IdleSince date, will be changing every minute. @"IsIdle" provides observers a way
420 * to perform an action when the contact becomes/comes back from idle, regardless of whether an IdleSince is available,
421 * without having to do that action every minute for other contacts.
423 [self setValue:[NSNumber numberWithBool:isIdle]
424 forProperty:@"IsIdle"
428 if (notify == NotifyNow) {
429 [self notifyOfChangedPropertiesSilently:NO];
433 - (void)setServersideIconData:(NSData *)iconData notify:(NotifyTiming)notify
435 [AIUserIcons setServersideIconData:iconData forObject:self notify:notify];
439 * @brief Set the warning level
441 * @param warningLevel The warning level, an integer between 0 and 100
442 * @param notify The NotifyTiming
444 - (void)setWarningLevel:(int)warningLevel notify:(NotifyTiming)notify
446 if (warningLevel != self.warningLevel) {
447 [self setValue:[NSNumber numberWithInt:warningLevel]
448 forProperty:@"Warning"
454 * @brief Warning level
456 * @result The warning level, an integer between 0 and 100
458 - (NSInteger)warningLevel
460 return [self integerValueForProperty:@"Warning"];
464 * @brief Set the profile array
466 - (void)setProfileArray:(NSArray *)array notify:(NotifyTiming)notify
469 forProperty:@"ProfileArray"
474 * @brief The profile array
476 - (NSArray *)profileArray
478 return [self valueForProperty:@"ProfileArray"];
482 * @brief Set the profile
484 - (void)setProfile:(NSAttributedString *)profile notify:(NotifyTiming)notify
486 [self setValue:profile
487 forProperty:@"TextProfile"
494 - (NSAttributedString *)profile
496 return [self valueForProperty:@"TextProfile"];
500 * @brief Is this contact a stranger?
502 * A listContact is a stranger if it has a nil remoteGroupName
506 return ![self boolValueForProperty:@"NotAStranger"];
510 * @brief If this contact intentionally on the contact list?
512 - (BOOL)isIntentionallyNotAStranger
514 return !self.isStranger && [self.account isContactIntentionallyListed:self];
518 * @brief Is this object connected via a mobile device?
522 return [self boolValueForProperty:@"IsMobile"];
526 * @brief Set if this contact is mobile
528 - (void)setIsMobile:(BOOL)isMobile notify:(NotifyTiming)notify
530 [self setValue:[NSNumber numberWithBool:isMobile]
531 forProperty:@"IsMobile"
536 * @brief Is this contact blocked?
538 * @result A boolean indicating if the contact is blocked or not
542 return [self boolValueForProperty:KEY_IS_BLOCKED];
545 - (void)setIsBlocked:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
547 [self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypeDeny];
550 - (void)setIsAllowed:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
552 [self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypePermit];
556 * @brief Set if this contact is on the privacy list
558 - (void)setIsOnPrivacyList:(BOOL)shouldBeBlocked updateList:(BOOL)addToPrivacyLists privacyType:(AIPrivacyType)privType
560 if (addToPrivacyLists) { //caller of this method wants to actually block or unblock the contact, rather than just update the property
562 if (![self.account conformsToProtocol:@protocol(AIAccount_Privacy)]) {
563 NSLog(@"Privacy is not supported on contacts for the account: %@", self.account);
567 id<AIAccount_Privacy> contactAccount = (id<AIAccount_Privacy>)self.account;
569 BOOL isBlocked = [[contactAccount listObjectsOnPrivacyList:privType] containsObject:self];
571 if (shouldBeBlocked == isBlocked)
577 result = [contactAccount addListObject:self toPrivacyList:privType];
579 result = [contactAccount removeListObject:self fromPrivacyList:privType];
581 //Don't update the property if we didn't change anything
586 [self setValue:[NSNumber numberWithBool:((privType == AIPrivacyTypeDeny) == shouldBeBlocked)]
587 forProperty:KEY_IS_BLOCKED
591 - (AIEncryptedChatPreference)encryptedChatPreferences {
592 AIEncryptedChatPreference pref = EncryptedChat_Default;
594 //Get the contact's preference (or metacontact's)
595 NSNumber *prefNumber = [self.parentContact preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
597 //If that turned up nothing, check all the groups it's in
598 if (!prefNumber || [prefNumber integerValue] == EncryptedChat_Default) {
599 for (AIListGroup *group in self.parentContact.groups)
601 if ((prefNumber = [group preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION]))
606 //If that turned up nothing, check global prefs
608 prefNumber = [adium.preferenceController preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
610 //If no contact preference or the contact is set to use the default, use the account preference
611 if (!prefNumber || ([prefNumber integerValue] == EncryptedChat_Default)) {
612 prefNumber = [self.account preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE
613 group:GROUP_ENCRYPTION];
617 pref = [prefNumber integerValue];
622 - (void)setAlwaysVisible:(BOOL)inVisible
624 [super setAlwaysVisible:inVisible];
626 [self restoreGrouping];
629 - (BOOL)alwaysVisible
631 if (self.metaContact) {
632 return self.metaContact.alwaysVisible;
635 return [super alwaysVisible];
641 * @brief Determine the status message to be displayed in the contact list
643 * Look at the contact's status message.
644 * Failing that, look for a statusName, which might be something like "DND" or "Free for Chat"
645 * and look up the localized description of it.
647 - (NSAttributedString *)contactListStatusMessage
649 NSAttributedString *contactListStatusMessage = self.statusMessage;
651 if (!contactListStatusMessage) {
652 NSString *statusName = self.statusName;
655 NSString *descriptionOfStatus = [adium.statusController localizedDescriptionForStatusName:statusName
656 statusType:self.statusType];
658 if (descriptionOfStatus)
659 contactListStatusMessage = [[[NSAttributedString alloc] initWithString:descriptionOfStatus] autorelease];
663 return contactListStatusMessage;
667 * @brief Are sounds for this contact muted?
669 - (BOOL)soundsAreMuted
671 return [self.account.statusState mutesSound];
677 * @brief This object's parent AIListContact
679 * The parent AIListContact is the appropriate place to apply preferences specific to this contact so that such
680 * preferences are also applied to other AIListContacts in the same meta contact, if necessary.
682 * @result Either this contact or some more-encompassing contact which ultimately contains it.
684 - (AIListContact *)parentContact
686 return self.metaContact ?: self;
689 - (BOOL)containsObject:(AIListObject*)object
694 - (NSSet *) containingObjects {
696 return [NSSet setWithObject:metaContact];
697 return super.containingObjects;
701 * @brief Can this object be part of a metacontact?
703 - (BOOL)canJoinMetaContacts
708 - (AIMetaContact *)metaContact
713 - (void) setMetaContact:(AIMetaContact *)meta
716 [m_groups removeAllObjects];
719 - (BOOL) existsServerside
724 - (void)removeFromGroup:(AIListObject <AIContainingObject> *)group
726 if (self.account.online) {
727 if (group == adium.contactController.contactList
728 || group == adium.contactController.offlineGroup) {
729 [self.account removeContacts:[NSArray arrayWithObject:self]
730 fromGroups:[self.remoteGroups allObjects]];
732 [self.account removeContacts:[NSArray arrayWithObject:self]
733 fromGroups:[NSArray arrayWithObject:group]];
738 #pragma mark Equality
740 - (BOOL)isEqual:(id)anObject
742 return ([anObject isMemberOfClass:[self class]] &&
743 [[(AIListContact *)anObject internalUniqueObjectID] isEqualToString:[self internalUniqueObjectID]]);
746 //AppleScript ----------------------------------------------------------------------------------------------------------
747 #pragma mark AppleScript
749 - (id)sendScriptCommand:(NSScriptCommand *)command {
750 NSDictionary *evaluatedArguments = [command evaluatedArguments];
751 NSString *message = [evaluatedArguments objectForKey:@"message"];
752 AIAccount *targetAccount = [evaluatedArguments objectForKey:@"account"];
753 NSString *filePath = [evaluatedArguments objectForKey:@"filePath"];
755 AIListContact *targetMessagingContact = self;
756 AIListContact *targetFileTransferContact = nil;
759 if (self.account != account)
760 targetMessagingContact = [adium.contactController contactWithService:self.service account:account UID:self.UID];
762 targetFileTransferContact = targetMessagingContact;
765 //Send any message we were told to send
766 if (message && [message length]) {
768 BOOL autoreply = [[evaluatedArguments objectForKey:@"autoreply"] boolValue];
770 //Make sure we know where we are sending the message - if we don't have a target yet, find the best contact for
771 //sending CONTENT_MESSAGE_TYPE.
772 if (!targetMessagingContact) {
773 //Get the target contact. This could be the same contact, an identical contact on another account,
774 //or a subcontact (if we're talking about a metaContact, for example)
775 targetMessagingContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
776 forListContact:self];
777 targetAccount = targetMessagingContact.account;
780 if (targetMessagingContact) {
781 chat = [adium.chatController openChatWithContact:targetMessagingContact
782 onPreferredAccount:NO];
784 //Take the string and turn it into an attributed string (in case we were passed HTML)
785 NSAttributedString *attributedMessage = [AIHTMLDecoder decodeHTML:message];
786 AIContentMessage *messageContent;
787 messageContent = [AIContentMessage messageInChat:chat
788 withSource:targetAccount
789 destination:targetMessagingContact
791 message:attributedMessage
792 autoreply:autoreply];
794 [adium.contentController sendContentObject:messageContent];
796 AILogWithSignature(@"No contact available to receive a message to %@", self);
800 //Send any file we were told to send
801 if (filePath && [filePath length]) {
802 //Make sure we know where we are sending the file - if we don't have a target yet, find the best contact for
803 //sending CONTENT_FILE_TRANSFER_TYPE.
804 if (!targetFileTransferContact) {
805 //Get the target contact. This could be the same contact, an identical contact on another account,
806 //or a subcontact (if we're talking about a metaContact, for example)
807 targetFileTransferContact = [adium.contactController preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
808 forListContact:self];
811 if (targetFileTransferContact) {
812 [adium.fileTransferController sendFile:filePath toListContact:targetFileTransferContact];
814 AILogWithSignature(@"No contact available to receive files to %@", self);
822 //Writing Direction ----------------------------------------------------------------------------------------------------------
823 #pragma mark Writing Direction
825 - (NSWritingDirection)defaultBaseWritingDirection
827 static NSWritingDirection defaultBaseWritingDirection;
828 static BOOL determinedDefaultBaseWritingDirection = NO;
830 if (!determinedDefaultBaseWritingDirection) {
831 /* Use the default writing direction of the language of the user's locale (and not the language
832 * of the active localization). By that, we assume most users are mostly talking to their local friends.
834 NSString *lang = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
835 defaultBaseWritingDirection = [NSParagraphStyle defaultWritingDirectionForLanguage:lang];
836 determinedDefaultBaseWritingDirection = YES;
839 return defaultBaseWritingDirection;
842 - (NSWritingDirection)baseWritingDirection {
843 NSNumber *dir = [self preferenceForKey:KEY_BASE_WRITING_DIRECTION group:PREF_GROUP_WRITING_DIRECTION];
845 return (dir ? [dir intValue] : [self defaultBaseWritingDirection]);
848 - (void)setBaseWritingDirection:(NSWritingDirection)direction {
849 [self setPreference:[NSNumber numberWithInt:direction]
850 forKey:KEY_BASE_WRITING_DIRECTION
851 group:PREF_GROUP_WRITING_DIRECTION];
854 #pragma mark Address Book
855 - (ABPerson *)addressBookPerson
857 return [AIAddressBookController personForListObject:self.parentContact];
859 - (void)setAddressBookPerson:(ABPerson *)inPerson
861 [self.parentContact setPreference:[inPerson uniqueId]
862 forKey:KEY_AB_UNIQUE_ID
863 group:PREF_GROUP_ADDRESSBOOK];
866 #pragma mark Applescript
868 - (NSScriptObjectSpecifier *)objectSpecifier
870 NSScriptObjectSpecifier *containerRef = self.account.objectSpecifier;
871 return [[[NSNameSpecifier allocWithZone:[self zone]]
872 initWithContainerClassDescription:[containerRef keyClassDescription]
873 containerSpecifier:containerRef key:@"contacts" name:self.UID] autorelease];
877 - (NSArray *)groupsAsArray
879 return self.groups.allObjects;
882 - (BOOL)scriptingBlocked
884 return [self isBlocked];
886 - (void)setScriptingBlocked:(BOOL)b
888 [self setIsBlocked:b updateList:YES];
891 @dynamic containingObject;