Display unhandled purple conversation writes in the next run loop. Fixes #13190.
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 "CBPurpleAccount.h"
19 #import "PurpleService.h"
21 #import <libpurple/notify.h>
22 #import <libpurple/cmds.h>
23 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
24 #import <Adium/AIAccount.h>
25 #import <Adium/AIChat.h>
26 #import <Adium/AIContentMessage.h>
27 #import <Adium/AIContentTopic.h>
28 #import <Adium/AIContentEvent.h>
29 #import <Adium/AIContentContext.h>
30 #import <Adium/AIContentNotification.h>
31 #import <Adium/AIHTMLDecoder.h>
32 #import <Adium/AIListContact.h>
33 #import <Adium/AIListGroup.h>
34 #import <Adium/AIListObject.h>
35 #import <Adium/AIMetaContact.h>
36 #import <Adium/AIService.h>
37 #import <Adium/AIServiceIcons.h>
38 #import <Adium/AIStatus.h>
39 #import <Adium/ESFileTransfer.h>
40 #import <Adium/AIWindowController.h>
41 #import <Adium/AIEmoticon.h>
42 #import <Adium/AIAccountControllerProtocol.h>
43 #import <Adium/AIChatControllerProtocol.h>
44 #import <Adium/AIContactControllerProtocol.h>
45 #import <Adium/AIContactObserverManager.h>
46 #import <Adium/AIContentControllerProtocol.h>
47 #import <Adium/AIInterfaceControllerProtocol.h>
48 #import <Adium/AIStatusControllerProtocol.h>
49 #import <AIUtilities/AIAttributedStringAdditions.h>
50 #import <AIUtilities/AIDictionaryAdditions.h>
51 #import <AIUtilities/AIMenuAdditions.h>
52 #import <AIUtilities/AIMutableOwnerArray.h>
53 #import <AIUtilities/AIStringAdditions.h>
54 #import <AIUtilities/AIApplicationAdditions.h>
55 #import <AIUtilities/AIObjectAdditions.h>
56 #import <AIUtilities/AIImageAdditions.h>
57 #import <AIUtilities/AIImageDrawingAdditions.h>
58 #import <AIUtilities/AIMutableStringAdditions.h>
59 #import <AIUtilities/AISystemNetworkDefaults.h>
60 #import <Adium/AdiumAuthorization.h>
62 #import "ESiTunesPlugin.h"
63 #import "AMPurpleTuneTooltip.h"
64 #import "adiumPurpleRequest.h"
65 #import "AIDualWindowInterfacePlugin.h"
68 #import "AIPurpleCertificateViewer.h"
71 #define NO_GROUP @"__NoGroup__"
73 #define PREF_GROUP_ALIASES @"Aliases" //Preference group to store aliases in
74 #define NEW_ACCOUNT_DISPLAY_TEXT AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
76 #define KEY_PRIVACY_OPTION @"Privacy Option"
78 @interface CBPurpleAccount ()
79 - (NSString *)_mapIncomingGroupName:(NSString *)name;
80 - (NSString *)_mapOutgoingGroupName:(NSString *)name;
81 - (void)setTypingFlagOfChat:(AIChat *)inChat to:(NSNumber *)typingState;
82 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
83 - (NSNumber *)shouldCheckMail;
84 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector;
85 - (void)continueConnectWithConfiguredPurpleAccount;
86 - (void)continueConnectWithConfiguredProxy;
87 - (void)continueRegisterWithConfiguredPurpleAccount;
88 - (void)promptForHostBeforeConnecting;
89 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation;
90 - (void)performAccountMenuAction:(NSMenuItem *)sender;
92 - (void)showServerCertificate;
95 @implementation CBPurpleAccount
97 static SLPurpleCocoaAdapter *purpleAdapter = nil;
99 // The PurpleAccount currently associated with this Adium account
100 - (PurpleAccount*)purpleAccount
102 //Create a purple account if one does not already exist
104 [self createNewPurpleAccount];
105 AILog(@"Created PurpleAccount 0x%x with UID %@, protocolPlugin %s", account, self.UID, [self protocolPlugin]);
111 - (SLPurpleCocoaAdapter *)purpleAdapter
113 if (!purpleAdapter) {
114 purpleAdapter = [[SLPurpleCocoaAdapter sharedInstance] retain];
116 return purpleAdapter;
119 // Subclasses must override this
120 - (const char*)protocolPlugin { return NULL; }
122 - (PurplePluginProtocolInfo *)protocolInfo
126 if ((prpl = purple_find_prpl(purple_account_get_protocol_id(account)))) {
127 return PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
133 // Contacts ------------------------------------------------------------------------------------------------
134 #pragma mark Contacts
135 - (void)newContact:(AIListContact *)theContact withName:(NSString *)inName
140 - (void)addContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName
142 //When a new contact is created, if we aren't already silent and delayed, set it a second to cover our initial
144 if (!silentAndDelayed) {
145 [self silenceAllContactUpdatesForInterval:2.0];
146 [[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
149 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
150 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
151 if (![contactName isEqualToString:theContact.formattedUID] && ![contactName isEqualToString:theContact.UID]) {
152 [theContact setValue:contactName
153 forProperty:@"FormattedUID"
157 if (groupName && [groupName isEqualToString:@PURPLE_ORPHANS_GROUP_NAME]) {
158 [theContact addRemoteGroupName:AILocalizedString(@"Orphans","Name for the orphans group")];
159 } else if (groupName && [groupName length] != 0) {
160 [theContact addRemoteGroupName:[self _mapIncomingGroupName:groupName]];
162 AILog(@"Got a nil group for %@",theContact);
165 [self gotGroupForContact:theContact];
168 - (void)removeContact:(AIListContact *)theContact fromGroupName:(NSString *)groupName
170 NSParameterAssert(groupName != nil); //is this always true?
171 NSParameterAssert(theContact != nil);
172 [theContact removeRemoteGroupName:[self _mapIncomingGroupName:groupName]];
176 * @brief Change the UID of a contact
178 * If we're just passed a formatted version of the current UID, don't change the UID but instead use the information
179 * as the FormattedUID. For example, we get sent this when an AIM contact's name formatting changes; we always want
180 * to use a lowercase and space-free version for the UID, however.
182 - (void)renameContact:(AIListContact *)theContact toUID:(NSString *)newUID
184 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
185 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
186 NSString *normalizedUID = [self.service normalizeUID:newUID removeIgnoredCharacters:YES];
188 if ([normalizedUID isEqualToString:theContact.UID]) {
189 [theContact setValue:newUID
190 forProperty:@"FormattedUID"
193 [theContact setUID:newUID];
197 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)purpleAlias
199 if (![[purpleAlias compactedString] isEqualToString:[theContact.UID compactedString]]) {
200 //Store this alias as the serverside display name so long as it isn't identical when unformatted to the UID
201 [theContact setServersideAlias:purpleAlias
202 silently:silentAndDelayed];
205 //If it's the same characters as the UID, apply it as a formatted UID
206 if (![purpleAlias isEqualToString:theContact.formattedUID] &&
207 ![purpleAlias isEqualToString:theContact.UID]) {
208 [theContact setFormattedUID:purpleAlias
212 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
217 - (void)updateContact:(AIListContact *)theContact forEvent:(NSNumber *)event
223 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
225 [theContact setOnline:YES
227 silently:silentAndDelayed];
229 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
233 - (void)updateSignoff:(AIListContact *)theContact withData:(void *)data
235 [theContact setOnline:NO
237 silently:silentAndDelayed];
239 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
243 - (void)updateSignonTime:(AIListContact *)theContact withData:(NSDate *)signonDate
245 [theContact setSignonDate:signonDate
249 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
253 * @brief Status name to use for a Purple buddy
255 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
261 * @brief Status message for a contact
263 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)buddy
265 PurplePresence *presence = purple_buddy_get_presence(buddy);
266 PurpleStatus *status = (presence ? purple_presence_get_active_status(presence) : NULL);
267 const char *message = (status ? purple_status_get_attr_string(status, "message") : NULL);
268 NSString *statusMessage = nil;
270 // Get the plugin's status message for this buddy if they don't have a status message
272 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
274 if (prpl_info && prpl_info->status_text) {
275 char *status_text = (prpl_info->status_text)(buddy);
277 // Don't display "Offline" as a status message.
278 if (status_text && strcmp(status_text, _("Offline")) != 0) {
279 statusMessage = [NSString stringWithUTF8String:status_text];
285 statusMessage = [NSString stringWithUTF8String:message];
288 return statusMessage ? [AIHTMLDecoder decodeHTML:statusMessage] : nil;
292 * @brief Update the status message and away state of the contact
294 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName statusMessage:(NSAttributedString *)statusMessage isMobile:(BOOL)isMobile
296 [theContact setStatusWithName:statusName
297 statusType:[statusTypeNumber integerValue]
299 [theContact setStatusMessage:statusMessage
301 [theContact setIsMobile:isMobile notify:NotifyLater];
304 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
308 - (void)updateWentIdle:(AIListContact *)theContact withData:(NSDate *)idleSinceDate
310 [theContact setIdle:YES sinceDate:idleSinceDate notify:NotifyLater];
313 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
315 - (void)updateIdleReturn:(AIListContact *)theContact withData:(void *)data
317 [theContact setIdle:NO
322 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
325 //Evil level (warning level)
326 - (void)updateEvil:(AIListContact *)theContact withData:(NSNumber *)evilNumber
328 [theContact setWarningLevel:[evilNumber integerValue]
332 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
336 - (void)clearIconForContact:(AIListContact *)theContact
338 [theContact setServersideIconData:nil
342 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
346 - (void)updateIcon:(AIListContact *)theContact withData:(NSData *)userIconData
348 [NSObject cancelPreviousPerformRequestsWithTarget:self
349 selector:@selector(clearIconForContact:)
352 [theContact setServersideIconData:userIconData
356 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
359 /* We may receive an empty icon update just before an actual change. We don't want to flicker through no-icon.
360 * We therefore cancel empty icon updates when we receive a new icon, and we do the actual clearing on a delay in case
361 * this is what is about to happen.
363 [self performSelector:@selector(clearIconForContact:)
364 withObject:theContact
369 - (NSString *)processedIncomingUserInfo:(NSString *)inString
371 NSMutableString *returnString = nil;
372 if ([inString rangeOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."].location != NSNotFound) {
373 returnString = [[inString mutableCopy] autorelease];
374 [returnString replaceOccurrencesOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."
375 withString:AILocalizedString(@"Adium could not find any information in the user's profile. This may not be a registered name.", "Message shown when a contact's profile can't be found")
376 options:NSLiteralSearch
377 range:NSMakeRange(0, [returnString length])];
380 return (returnString ? returnString : inString);
383 - (NSString *)webProfileStringForContact:(AIListContact *)contact
385 return [NSString stringWithFormat:NSLocalizedString(@"View %@'s %@ web profile", nil),
386 contact.formattedUID, [contact.service shortDescription]];
389 - (NSMutableArray *)arrayOfDictionariesFromPurpleNotifyUserInfo:(PurpleNotifyUserInfo *)user_info forContact:(AIListContact *)contact
392 NSMutableArray *array = [NSMutableArray array];
394 for (l = purple_notify_user_info_get_entries(user_info); l != NULL; l = l->next) {
395 PurpleNotifyUserInfoEntry *user_info_entry = l->data;
397 switch (purple_notify_user_info_entry_get_type(user_info_entry)) {
398 case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_HEADER:
399 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
400 [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)], KEY_KEY,
401 [NSNumber numberWithInteger:AIUserInfoSectionHeader], KEY_TYPE,
405 case PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK:
406 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
407 [NSNumber numberWithInteger:AIUserInfoSectionBreak], KEY_TYPE,
411 case PURPLE_NOTIFY_USER_INFO_ENTRY_PAIR:
413 if (purple_notify_user_info_entry_get_label(user_info_entry) && purple_notify_user_info_entry_get_value(user_info_entry)) {
414 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
415 [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)], KEY_KEY,
416 processPurpleImages([NSString stringWithUTF8String:purple_notify_user_info_entry_get_value(user_info_entry)], self), KEY_VALUE,
419 } else if (purple_notify_user_info_entry_get_label(user_info_entry)) {
420 [array addObject:[NSDictionary dictionaryWithObject:
421 [NSString stringWithUTF8String:purple_notify_user_info_entry_get_label(user_info_entry)]
423 } else if (purple_notify_user_info_entry_get_value(user_info_entry)) {
424 NSMutableString *value = [processPurpleImages([NSString stringWithUTF8String:purple_notify_user_info_entry_get_value(user_info_entry)],
426 [value replaceOccurrencesOfString:@"<br>" withString:@"<br/>" options:(NSCaseInsensitiveSearch | NSLiteralSearch)];
427 [value replaceOccurrencesOfString:@"<br />" withString:@"<br/>" options:(NSCaseInsensitiveSearch | NSLiteralSearch)];
428 [value replaceOccurrencesOfString:@"<B>" withString:@"<b>" options:NSLiteralSearch];
430 for (NSString *valuePair in [value componentsSeparatedByString:@"<br/><b>"]) {
431 NSRange firstStartBold = [valuePair rangeOfString:@"<b>"];
432 NSRange firstEndBold = [valuePair rangeOfString:@"</b>"];
434 if (firstEndBold.length > 0) {
435 // Chop off <b> from the beginning and :</b> from the end. The extra -1 is for the colon.
436 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
437 [valuePair substringWithRange:NSMakeRange(firstStartBold.length, firstEndBold.location-firstStartBold.length-1)], KEY_KEY,
438 [valuePair substringFromIndex:NSMaxRange(firstEndBold)], KEY_VALUE,
441 [array addObject:[NSDictionary dictionaryWithObject:valuePair
452 NSString *webProfileValue = [NSString stringWithFormat:@"%s</a>", _("View web profile")];
455 NSUInteger count = [array count];
456 for (i = 0; i < count; i++) {
457 NSDictionary *dict = [array objectAtIndex:i];
458 NSString *value = [dict objectForKey:KEY_VALUE];
460 [value rangeOfString:webProfileValue options:(NSBackwardsSearch | NSAnchoredSearch | NSLiteralSearch)].location != NSNotFound) {
461 NSMutableString *newValue = [[value mutableCopy] autorelease];
462 [newValue replaceOccurrencesOfString:webProfileValue
463 withString:[self webProfileStringForContact:contact]
464 options:(NSBackwardsSearch | NSAnchoredSearch | NSLiteralSearch)];
466 NSMutableDictionary *replacementDict = [dict mutableCopy];
467 [replacementDict setObject:newValue forKey:KEY_VALUE];
468 [array replaceObjectAtIndex:i withObject:replacementDict];
469 [replacementDict release];
471 /* There will only be 1 (at most) web profile link */
479 - (void)updateUserInfo:(AIListContact *)theContact withData:(PurpleNotifyUserInfo *)user_info
481 NSArray *profileContents = [self arrayOfDictionariesFromPurpleNotifyUserInfo:user_info forContact:theContact];
483 [theContact setProfileArray:profileContents
486 [self openInspectorForContactInfo:theContact];
489 [theContact notifyOfChangedPropertiesSilently:silentAndDelayed];
493 * @brief Open the info inspector when getting info
495 - (void)openInspectorForContactInfo:(AIListContact *)theContact
501 * @brief Purple removed a contact from the local blist
503 * This can happen in many situations:
504 * - For every contact on an account when the account signs off
505 * - For a contact as it is deleted by the user
506 * - For a contact as it is deleted by Purple (e.g. when Sametime refuses an addition because it is known to be invalid)
507 * - In the middle of the move process as a contact moves from one group to another
509 * We need not take any action; we'll be notified of changes by Purple as necessary.
511 - (void)removeContact:(AIListContact *)theContact
516 //To allow root level buddies on protocols which don't support them, we map any buddies in a group
517 //named after this account's UID to the root group. These functions handle the mapping. Group names should
518 //be filtered through incoming before being sent to Adium - and group names from Adium should be filtered through
519 //outgoing before being used.
520 - (NSString *)_mapIncomingGroupName:(NSString *)name
522 if (!name || ([[name compactedString] caseInsensitiveCompare:self.UID] == NSOrderedSame)) {
523 return ADIUM_ROOT_GROUP_NAME;
528 - (NSString *)_mapOutgoingGroupName:(NSString *)name
530 if ([[name compactedString] caseInsensitiveCompare:ADIUM_ROOT_GROUP_NAME] == NSOrderedSame) {
537 //Update the status of a contact (Request their profile)
538 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
541 AILogWithSignature(@"");
542 [purpleAdapter getInfoFor:inContact.UID onAccount:self];
545 - (void)requestAddContactWithUID:(NSString *)contactUID
547 [adium.contactController requestAddContactWithUID:contactUID
548 service:[self _serviceForUID:contactUID]
552 - (AIService *)_serviceForUID:(NSString *)contactUID
557 - (void)gotGroupForContact:(AIListContact *)listContact {};
560 * @brief Return the serverside icon for a contact
562 - (NSData *)serversideIconDataForContact:(AIListContact *)contact
567 if (self.purpleAccount &&
568 (buddy = purple_find_buddy(account, [contact.UID UTF8String]))) {
569 PurpleBuddyIcon *buddyIcon;
570 BOOL shouldUnref = NO;
572 /* First, try to get a current buddy icon from the PurpleBuddy */
573 buddyIcon = purple_buddy_get_icon(buddy);
575 /* Failing that, load one from the cache. We'll need to unreference the returned PurpleBuddyIcon
578 buddyIcon = purple_buddy_icons_find(account, [contact.UID UTF8String]);
583 const guchar *iconData;
586 iconData = purple_buddy_icon_get_data(buddyIcon, &len);
588 if (iconData && len) {
589 data = [NSData dataWithBytes:iconData length:len];
593 purple_buddy_icon_unref(buddyIcon);
597 AILogWithSignature(@"Could not get serverside icon data for %@. account is %p", contact, account);
604 * @brief Libpurple manages a contact icon cache; we don't need to duplicate it.
606 - (BOOL)managesOwnContactIconCache
611 /*********************/
612 /* AIAccount_Handles */
613 /*********************/
614 #pragma mark Contact List Editing
616 - (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
618 for (AIListGroup *group in groups) {
619 NSString *groupName = [self _mapOutgoingGroupName:group.UID];
621 for (AIListContact *object in objects) {
622 //Have the purple thread perform the serverside actions
623 [purpleAdapter removeUID:object.UID onAccount:self fromGroup:groupName];
625 //Remove it from Adium's list
626 [object removeRemoteGroupName:groupName];
631 - (void)addContact:(AIListContact *)contact toGroup:(AIListGroup *)group
633 NSString *groupName = [self _mapOutgoingGroupName:group.UID];
635 if(![group containsObject:contact]) {
636 AILogWithSignature(@"%@ adding %@ to %@", self, [self _UIDForAddingObject:contact], groupName);
638 NSString *alias = [contact.parentContact preferenceForKey:@"Alias"
639 group:PREF_GROUP_ALIASES];
641 [purpleAdapter addUID:[self _UIDForAddingObject:contact] onAccount:self toGroup:groupName withAlias:alias];
643 //Add it to Adium's list
644 [contact addRemoteGroupName:group.UID]; //Use the non-mapped group name locally
648 - (NSString *)_UIDForAddingObject:(AIListContact *)object
653 - (NSSet *)mappedGroupNamesFromGroups:(NSSet *)groups
655 NSMutableSet *mappedNames = [NSMutableSet set];
657 for (AIListGroup *group in groups) {
658 [mappedNames addObject:[self _mapOutgoingGroupName:group.UID]];
664 - (void)moveListObjects:(NSArray *)objects fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
666 NSSet *sourceMappedNames = [self mappedGroupNamesFromGroups:oldGroups];
667 NSSet *destinationMappedNames = [self mappedGroupNamesFromGroups:groups];
669 //Move the objects to it
670 for (AIListContact *contact in objects) {
671 if (![contact.remoteGroups intersectsSet:oldGroups] && oldGroups.count) {
675 NSString *alias = [contact.parentContact preferenceForKey:@"Alias"
676 group:PREF_GROUP_ALIASES];
678 //Tell the purple thread to perform the serverside operation
679 [purpleAdapter moveUID:contact.UID onAccount:self fromGroups:sourceMappedNames toGroups:destinationMappedNames withAlias:alias];
681 for (AIListGroup *group in oldGroups) {
682 [contact removeRemoteGroupName:group.UID];
685 for (AIListGroup *group in groups) {
686 [contact addRemoteGroupName:group.UID];
691 - (void)renameGroup:(AIListGroup *)inGroup to:(NSString *)newName
693 NSString *groupName = [self _mapOutgoingGroupName:inGroup.UID];
695 //Tell the purple thread to perform the serverside operation
696 [purpleAdapter renameGroup:groupName onAccount:self to:newName];
698 //We must also update the remote grouping of all our contacts in that group
699 for (AIListContact *contact in [adium.contactController allContactsInObject:inGroup onAccount:self]) {
700 [contact removeRemoteGroupName:groupName];
701 //Evan: should we use groupName or newName here?
702 [contact addRemoteGroupName:newName];
706 - (void)deleteGroup:(AIListGroup *)inGroup
708 NSString *groupName = [self _mapOutgoingGroupName:inGroup.UID];
710 [purpleAdapter deleteGroup:groupName onAccount:self];
713 // Return YES if the contact list is editable
714 - (BOOL)contactListEditable
719 - (id)authorizationRequestWithDict:(NSDictionary*)dict
721 // We retain this in case libpurple wants to close the request early. It is freed below.
722 return [[AdiumAuthorization showAuthorizationRequestWithDict:dict forAccount:self] retain];
725 - (void)authorizationWithDict:(NSDictionary *)infoDict response:(AIAuthorizationResponse)authorizationResponse
728 NSValue *callback = nil;
730 switch (authorizationResponse) {
731 case AIAuthorizationAllowed:
732 callback = [[[infoDict objectForKey:@"authorizeCB"] retain] autorelease];
734 case AIAuthorizationDenied:
735 callback = [[[infoDict objectForKey:@"denyCB"] retain] autorelease];
737 case AIAuthorizationNoResponse:
742 //libpurple will remove its reference to the handle for this request, which is inDict, in response to this callback invocation
744 [purpleAdapter doAuthRequestCbValue:callback withUserDataValue:[[[infoDict objectForKey:@"userData"] retain] autorelease]];
746 /* Retained in -[self authorizationRequestWithDict:]. We kept it around before now in case libpurle wanted us to close it early, such as because the
747 * account disconnected.
751 [purpleAdapter closeAuthRequestWithHandle:infoDict];
757 #pragma mark Group chat ignore
758 - (BOOL)accountManagesGroupChatIgnore
763 - (BOOL)contact:(AIListContact *)inContact isIgnoredInChat:(AIChat *)chat
765 if (self.online && chat.isGroupChat) {
766 return [purpleAdapter contact:inContact isIgnoredInChat:chat];
772 - (void)setContact:(AIListContact *)inContact ignored:(BOOL)inIgnored inChat:(AIChat *)chat
774 if (self.online && chat.isGroupChat) {
775 [purpleAdapter setContact:inContact ignored:inIgnored inChat:chat];
779 //Chats ------------------------------------------------------------
781 - (void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
786 AIListContact *contact = [self contactWithUID:contactName];
787 [chat removeObject:contact];
789 if (contact.isStranger &&
790 ![adium.chatController allGroupChatsContainingContact:contact.parentContact].count &&
791 [adium.chatController existingChatWithContact:contact.parentContact]) {
792 // The contact is a stranger, not in any more group chats, but we have a message with them open.
793 // Set their status to unknown.
795 [contact setStatusWithName:nil
796 statusType:AIUnknownStatus
799 [contact setValue:nil
800 forProperty:@"Online"
803 [contact notifyOfChangedPropertiesSilently:NO];
807 - (void)removeUsersArray:(NSArray *)usersArray fromChat:(AIChat *)chat
809 for (NSString *contactName in usersArray) {
810 [self removeUser:contactName fromChat:chat];
814 - (void)updateUserListForChat:(AIChat *)chat users:(NSArray *)users newlyAdded:(BOOL)newlyAdded
816 NSMutableArray *newListObjects = [NSMutableArray array];
818 for (NSDictionary *user in users) {
819 AIListContact *contact = [self contactWithUID:[user objectForKey:@"UID"]];
821 [contact setOnline:YES notify:NotifyNever silently:YES];
823 [newListObjects addObject:contact];
826 [chat addParticipatingListObjects:newListObjects notify:newlyAdded];
828 for (NSDictionary *user in users) {
829 AIListContact *contact = [self contactWithUID:[user objectForKey:@"UID"]];
831 [chat setFlags:(AIGroupChatFlags)[[user objectForKey:@"Flags"] integerValue] forContact:contact];
833 if ([user objectForKey:@"Alias"]) {
834 [chat setAlias:[user objectForKey:@"Alias"] forContact:contact];
836 if (contact.isStranger) {
837 [contact setServersideAlias:[user objectForKey:@"Alias"] silently:NO];
842 // Post an update notification now that we've modified the flags and names.
843 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
847 - (void)renameParticipant:(NSString *)oldUID newName:(NSString *)newUID newAlias:(NSString *)newAlias flags:(AIGroupChatFlags)flags inChat:(AIChat *)chat
849 [chat removeSavedValuesForContactUID:oldUID];
851 AIListContact *contact = [adium.contactController existingContactWithService:self.service account:self UID:oldUID];
854 [adium.contactController setUID:newUID forContact:contact];
856 contact = [self contactWithUID:newUID];
859 [chat setFlags:flags forContact:contact];
860 [chat setAlias:newAlias forContact:contact];
862 if (contact.isStranger) {
863 [contact setServersideAlias:newAlias silently:NO];
866 // Post an update notification since we modified the user entirely.
867 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
871 - (void)setAttribute:(NSString *)name value:(NSString *)value forContact:(AIListContact *)contact
873 NSString *property = nil;
875 if ([name isEqualToString:@"userhost"]) {
876 property = @"User Host";
877 } else if ([name isEqualToString:@"realname"]) {
878 property = @"Real Name";
880 AILog(@"Unknown attribute: %@ value %@", name, value);
884 // Callsite should notify.
885 [contact setValue:value forProperty:property notify:NotifyLater];
890 - (void)updateUser:(NSString *)user
891 forChat:(AIChat *)chat
892 flags:(AIGroupChatFlags)flags
893 alias:(NSString *)alias
894 attributes:(NSDictionary *)attributes
896 BOOL triggerUserlistUpdate = NO;
898 AIListContact *contact = [self contactWithUID:user];
900 AIGroupChatFlags oldFlags = [chat flagsForContact:contact];
901 NSString *oldAlias = [chat aliasForContact:contact];
903 // Trigger an update if the alias or flags (ignoring away state) changes.
904 if ((alias && !oldAlias)
905 || (!alias && oldAlias)
906 || ![[chat aliasForContact:contact] isEqualToString:alias]
907 || (flags & ~AIGroupChatAway) != (oldFlags & ~AIGroupChatAway)) {
908 triggerUserlistUpdate = YES;
911 [chat setAlias:alias forContact:contact];
912 [chat setFlags:flags forContact:contact];
914 // Away changes only come in after the initial one, so we're safe in only updating it here.
915 if (contact.isStranger) {
916 [contact setStatusWithName:nil
917 statusType:((flags & AIGroupChatAway) == AIGroupChatAway) ? AIAwayStatusType : AIAvailableStatusType
921 for (NSString *key in attributes.allKeys) {
922 [self setAttribute:key value:[attributes objectForKey:key] forContact:contact];
925 [contact notifyOfChangedPropertiesSilently:YES];
927 // Post an update notification if we modified the flags; don't resort for away changes.
928 if (triggerUserlistUpdate) {
929 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
935 * @brief Called by Purple code when a chat should be opened by the interface
937 * If the user sent an initial message, this will be triggered and have no effect.
939 * If a remote user sent an initial message, however, a chat will be created without being opened. This call is our
940 * cue to actually open chat.
942 * Another situation in which this is relevant is when we request joining a group chat; the chat should only be actually
943 * opened once the server notifies us that we are in the room.
945 * This will ultimately call -[CBPurpleAccount openChat:] below if the chat was not previously open.
947 - (void)addChat:(AIChat *)chat
949 AILogWithSignature(@"");
953 if ([chat boolValueForProperty:@"Rejoining Chat"]) {
954 [self displayYouHaveConnectedInChat:chat];
956 [chat setValue:nil forProperty:@"Rejoining Chat" notify:NotifyNever];
960 [adium.interfaceController openChat:chat];
962 [chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Account Joined" notify:NotifyNow];
965 //Open a chat for Adium
966 - (BOOL)openChat:(AIChat *)chat
968 /* The #if 0'd block below causes crashes in msn_tooltip_text() on MSN */
970 AIListContact *listContact;
972 //Obtain the contact's information if it's a stranger
973 if ((listContact = chat.listObject) && (listContact.isStranger)) {
974 [self delayedUpdateContactStatus:listContact];
978 AILog(@"purple openChat:%@ for %@",chat,chat.uniqueChatID);
980 //Inform purple that we have opened this chat
981 [purpleAdapter openChat:chat onAccount:self];
983 //Created the chat successfully
987 - (BOOL)closeChat:(AIChat*)chat
989 [purpleAdapter closeChat:chat];
991 if (!chat.isGroupChat) {
992 //Be sure any remaining typing flag is cleared as the chat closes
993 [self setTypingFlagOfChat:chat to:nil];
996 AILog(@"purple closeChat:%@",chat.uniqueChatID);
1001 - (void)chatWasDestroyed:(AIChat *)chat
1003 [adium.chatController accountDidCloseChat:chat];
1006 - (void)chatJoinDidFail:(AIChat *)chat
1008 [adium.chatController accountDidCloseChat:chat];
1012 * @brief Rejoin a chat
1014 - (BOOL)rejoinChat:(AIChat *)chat
1018 PurpleConversation *conv = [[chat identifier] pointerValue];
1019 if (conv && conv->ui_data) {
1020 [(AIChat *)(conv->ui_data) release];
1021 conv->ui_data = NULL;
1024 /* The identifier is how we associate a PurpleConversation with an AIChat.
1025 * Clear the identifier so a new PurpleConversation will be made. The ChatCreationInfo for the chat is still around, so it can join.
1027 [chat setIdentifier:nil];
1029 [chat setValue:[NSNumber numberWithBool:YES] forProperty:@"Rejoining Chat" notify:NotifyNever];
1031 [purpleAdapter openChat:chat onAccount:self];
1035 //We don't get any immediate feedback as to our success; just return YES.
1040 * @brief A chat will be joined
1042 * This gives the account a chance to update any information in the chat's creation dictionary if desired.
1044 * @result The final chat creation dictionary to use.
1046 - (NSDictionary *)willJoinChatUsingDictionary:(NSDictionary *)chatCreationDictionary
1048 return chatCreationDictionary;
1051 - (BOOL)chatCreationDictionary:(NSDictionary *)chatCreationDict isEqualToDictionary:(NSDictionary *)baseDict
1053 return [chatCreationDict isEqualToDictionary:baseDict];
1056 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
1058 AILog(@"%@ needs an implementation of extractChatCreationDictionaryFromConversation to handle rejoins, bookmarks, and invitations properly", NSStringFromClass([self class]));
1062 - (AIChat *)chatWithContact:(AIListContact *)contact identifier:(id)identifier
1064 AIChat *chat = [adium.chatController chatWithContact:contact];
1065 [chat setIdentifier:identifier];
1071 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier
1073 return [adium.chatController chatWithName:name identifier:identifier onAccount:self chatCreationInfo:nil];
1076 //Typing update in an IM
1077 - (void)typingUpdateForIMChat:(AIChat *)chat typing:(NSNumber *)typingState
1079 [self setTypingFlagOfChat:chat
1083 //Multiuser chat update
1084 - (void)convUpdateForChat:(AIChat *)chat type:(NSNumber *)type
1090 * @brief Called when we are informed that we left a multiuser chat
1092 - (void)leftChat:(AIChat *)chat
1094 [chat setValue:nil forProperty:@"Account Joined" notify:NotifyNow];
1097 - (void)updateTopic:(NSString *)inTopic forChat:(AIChat *)chat withSource:(NSString *)source
1099 // Update (not set) the chat's topic
1100 [chat updateTopic:inTopic withSource:[self contactWithUID:source]];
1104 * @brief Set a chat's topic
1106 * This only has an effect on group chats.
1108 - (void)setTopic:(NSString *)topic forChat:(AIChat *)chat
1110 if (!chat.isGroupChat) {
1114 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
1116 if (prpl_info && prpl_info->set_chat_topic) {
1117 (prpl_info->set_chat_topic)(purple_account_get_connection(account),
1118 purple_conv_chat_get_id(purple_conversation_get_chat_data(convLookupFromChat(chat, self))),
1119 [topic UTF8String]);
1124 - (void)updateTitle:(NSString *)inTitle forChat:(AIChat *)chat
1126 [[chat displayArrayForKey:@"Display Name"] setObject:inTitle
1130 - (void)updateForChat:(AIChat *)chat type:(NSNumber *)type
1132 AIChatUpdateType updateType = [type integerValue];
1133 NSString *key = nil;
1134 switch (updateType) {
1135 case AIChatTimedOut:
1136 case AIChatClosedWindow:
1141 [chat setValue:[NSNumber numberWithBool:YES] forProperty:key notify:NotifyNow];
1142 [chat setValue:nil forProperty:key notify:NotifyNever];
1147 - (void)errorForChat:(AIChat *)chat type:(NSNumber *)type
1149 [chat receivedError:type];
1152 - (void)receivedIMChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
1154 PurpleMessageFlags flags = [[messageDict objectForKey:@"PurpleMessageFlags"] integerValue];
1156 NSAttributedString *attributedMessage;
1157 AIListContact *listContact;
1159 listContact = chat.listObject;
1161 attributedMessage = [adium.contentController decodedIncomingMessage:[messageDict objectForKey:@"Message"]
1162 fromContact:listContact
1165 //Clear the typing flag of the chat since a message was just received
1166 [self setTypingFlagOfChat:chat to:nil];
1168 [self _receivedMessage:attributedMessage
1170 fromListContact:listContact
1172 date:[messageDict objectForKey:@"Date"]];
1175 - (void)receivedEventForChat:(AIChat *)chat
1176 message:(NSString *)message
1178 flags:(NSNumber *)flagsNumber
1180 PurpleMessageFlags flags = [flagsNumber integerValue];
1182 AIContentEvent *event = [AIContentEvent eventInChat:chat
1186 message:[AIHTMLDecoder decodeHTML:message]
1187 withType:@"purple"];
1189 event.filterContent = (flags & PURPLE_MESSAGE_NO_LINKIFY) != PURPLE_MESSAGE_NO_LINKIFY;
1191 [adium.contentController receiveContentObject:event];
1194 - (void)receivedMultiChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
1196 PurpleMessageFlags flags = [[messageDict objectForKey:@"PurpleMessageFlags"] integerValue];
1197 NSAttributedString *attributedMessage = [messageDict objectForKey:@"AttributedMessage"];;
1198 NSString *source = [messageDict objectForKey:@"Source"];
1200 [self _receivedMessage:attributedMessage
1202 fromListContact:[self contactWithUID:source]
1204 date:[messageDict objectForKey:@"Date"]];
1207 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date
1209 if ((flags & PURPLE_MESSAGE_DELAYED) == PURPLE_MESSAGE_DELAYED) {
1210 // Display delayed messages as context.
1212 AIContentContext *messageObject = [AIContentContext messageInChat:chat
1213 withSource:sourceContact
1216 message:attributedMessage
1217 autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
1219 messageObject.trackContent = NO;
1221 [adium.contentController receiveContentObject:messageObject];
1224 AIContentMessage *messageObject = [AIContentMessage messageInChat:chat
1225 withSource:sourceContact
1228 message:attributedMessage
1229 autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
1231 [adium.contentController receiveContentObject:messageObject];
1235 /*********************/
1236 /* AIAccount_Content */
1237 /*********************/
1238 #pragma mark Content
1239 - (void)sendTypingObject:(AIContentTyping *)inContentTyping
1241 AIChat *chat = inContentTyping.chat;
1243 if (!chat.isGroupChat) {
1244 [purpleAdapter sendTyping:inContentTyping.typingState inChat:chat];
1248 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
1250 PurpleMessageFlags flags = PURPLE_MESSAGE_RAW;
1252 if ([inContentMessage isAutoreply]) {
1253 flags |= PURPLE_MESSAGE_AUTO_RESP;
1256 [purpleAdapter sendEncodedMessage:[inContentMessage encodedMessage]
1258 inChat:inContentMessage.chat
1264 - (BOOL)supportsSendingNotifications
1266 return (account ? ((PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(account)))->send_attention) != NULL) : NO);
1269 - (BOOL)sendNotificationObject:(AIContentNotification *)inContentNotification
1271 [purpleAdapter sendNotificationOfType:[inContentNotification notificationType]
1273 inChat:inContentNotification.chat];
1279 * @brief Return the string encoded for sending to a remote contact
1281 * We return nil if the string turns out to have been a / command.
1283 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
1285 BOOL didCommand = [purpleAdapter attemptPurpleCommandOnMessage:[inContentMessage.message string]
1286 fromAccount:(AIAccount *)[inContentMessage source]
1287 inChat:inContentMessage.chat];
1289 return (didCommand ? nil : [super encodedAttributedStringForSendingContentMessage:inContentMessage]);
1293 * @brief Libpurple prints file transfer messages to the chat window. The Adium core therefore shouldn't.
1295 - (BOOL)accountDisplaysFileTransferMessages
1301 * @brief Available for sending content
1303 * Returns YES if the contact is available for receiving content of the specified type. If contact is nil, instead
1304 * check for the availiability to send any content of the given type.
1306 * We override the default implementation to check -[self allowFileTransferWithListObject:] for file transfers
1308 * @param inType A string content type
1309 * @param inContact The destination contact, or nil to check global availability
1311 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact
1313 if (self.online && [inType isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
1315 return ([self conformsToProtocol:@protocol(AIAccount_Files)] &&
1316 ((inContact.online || inContact.isStranger) && [self allowFileTransferWithListObject:inContact]));
1318 return [self conformsToProtocol:@protocol(AIAccount_Files)];
1322 return [super availableForSendingContentType:inType toContact:inContact];
1325 - (BOOL)allowFileTransferWithListObject:(AIListObject *)inListObject
1327 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
1329 if (prpl_info && prpl_info->send_file)
1330 return (!prpl_info->can_receive_file || prpl_info->can_receive_file(purple_account_get_connection(account), [inListObject.UID UTF8String]));
1335 - (BOOL)supportsAutoReplies
1337 if (account && purple_account_get_connection(account)) {
1338 return ((purple_account_get_connection(account)->flags & PURPLE_CONNECTION_AUTO_RESP) != 0);
1344 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
1346 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
1348 if (prpl_info && prpl_info->offline_message) {
1350 return (prpl_info->offline_message(purple_find_buddy(account, [inContact.UID UTF8String])));
1357 #pragma mark Custom emoticons
1358 - (void)chat:(AIChat *)inChat isWaitingOnCustomEmoticon:(NSString *)emoticonEquivalent
1360 AIEmoticon *emoticon;
1362 //Look for an existing emoticon with this equivalent
1363 for (emoticon in inChat.customEmoticons) {
1364 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
1368 emoticon = [AIEmoticon emoticonWithIconPath:nil
1369 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
1370 name:emoticonEquivalent
1372 [inChat addCustomEmoticon:emoticon];
1375 if (![emoticon path]) {
1376 [emoticon setPath:[[NSBundle bundleForClass:[CBPurpleAccount class]] pathForResource:@"missing_image"
1382 * @brief Return the path at which to save an emoticon
1384 - (NSString *)_emoticonCachePathForEmoticon:(NSString *)emoticonEquivalent type:(AIBitmapImageFileType)fileType inChat:(AIChat *)inChat
1386 static unsigned long long emoticonID = 0;
1387 NSString *filename = [NSString stringWithFormat:@"TEMP-CustomEmoticon_%@_%@_%qu.%@",
1388 [inChat uniqueChatID], emoticonEquivalent, emoticonID++, [NSImage extensionForBitmapImageFileType:fileType]];
1389 return [[adium cachesPath] stringByAppendingPathComponent:[filename safeFilenameString]];
1393 - (void)chat:(AIChat *)inChat setCustomEmoticon:(NSString *)emoticonEquivalent withImageData:(NSData *)inImageData
1395 /* XXX Note: If we can set outgoing emoticons, this method needs to be updated to mark emoticons as incoming
1396 * and AIEmoticonController needs to be able to handle that.
1398 AIEmoticon *emoticon;
1400 //Look for an existing emoticon with this equivalent
1401 for (emoticon in inChat.customEmoticons) {
1402 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
1405 //Write out our image
1406 NSString *path = [self _emoticonCachePathForEmoticon:emoticonEquivalent
1407 type:[NSImage fileTypeOfData:inImageData]
1409 [inImageData writeToFile:path
1413 //If we already have an emoticon, just update its path
1414 [emoticon setPath:path];
1417 emoticon = [AIEmoticon emoticonWithIconPath:path
1418 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
1419 name:emoticonEquivalent
1421 [inChat addCustomEmoticon:emoticon];
1425 - (void)chat:(AIChat *)inChat closedCustomEmoticon:(NSString *)emoticonEquivalent
1427 AIEmoticon *emoticon;
1429 //Look for an existing emoticon with this equivalent
1430 for (emoticon in inChat.customEmoticons) {
1431 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
1435 [[NSNotificationCenter defaultCenter] postNotificationName:@"AICustomEmoticonUpdated"
1437 userInfo:[NSDictionary dictionaryWithObject:emoticon
1438 forKey:@"AIEmoticon"]];
1440 //This shouldn't happen; chat:setCustomEmoticon:withImageData: should have already been called.
1441 emoticon = [AIEmoticon emoticonWithIconPath:nil
1442 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
1443 name:emoticonEquivalent
1445 NSLog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
1446 AILog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
1450 /*********************/
1451 /* AIAccount_Privacy */
1452 /*********************/
1453 #pragma mark Privacy
1454 - (BOOL)addListObject:(AIListObject *)inObject toPrivacyList:(AIPrivacyType)type
1456 if (type == AIPrivacyTypePermit)
1457 return (purple_privacy_permit_add(account,[inObject.UID UTF8String],FALSE));
1459 return (purple_privacy_deny_add(account,[inObject.UID UTF8String],FALSE));
1462 - (BOOL)removeListObject:(AIListObject *)inObject fromPrivacyList:(AIPrivacyType)type
1464 if (type == AIPrivacyTypePermit)
1465 return (purple_privacy_permit_remove(account,[inObject.UID UTF8String],FALSE));
1467 return (purple_privacy_deny_remove(account,[inObject.UID UTF8String],FALSE));
1470 - (NSArray *)listObjectsOnPrivacyList:(AIPrivacyType)type
1472 NSMutableArray *array = [NSMutableArray array];
1475 GSList *sourceList = ((type == AIPrivacyTypePermit) ? account->permit : account->deny);
1477 for (list = sourceList; (list != NULL); list=list->next) {
1478 [array addObject:[self contactWithUID:[NSString stringWithUTF8String:(char *)list->data]]];
1485 - (void)accountPrivacyList:(AIPrivacyType)type added:(NSString *)sourceUID
1487 //Can't really trust sourceUID to not be @"" or something silly like that
1488 if ([sourceUID length]) {
1490 AIListContact *contact = [self contactWithUID:sourceUID];
1492 //Update Adium's knowledge of it
1493 [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? YES : NO) updateList:NO];
1497 - (void)privacyPermitListAdded:(NSString *)sourceUID
1499 [self accountPrivacyList:AIPrivacyTypePermit added:sourceUID];
1502 - (void)privacyDenyListAdded:(NSString *)sourceUID
1504 [self accountPrivacyList:AIPrivacyTypeDeny added:sourceUID];
1507 - (void)accountPrivacyList:(AIPrivacyType)type removed:(NSString *)sourceUID
1509 //Can't really trust sourceUID to not be @"" or something silly like that
1510 if ([sourceUID length]) {
1511 if (!namesAreCaseSensitive) {
1512 sourceUID = [sourceUID compactedString];
1515 //Get our contact, which must already exist for us to care about its removal
1516 AIListContact *contact = [adium.contactController existingContactWithService:service
1521 //Update Adium's knowledge of it
1522 [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? NO : YES) updateList:NO];
1527 - (void)privacyPermitListRemoved:(NSString *)sourceUID
1529 [self accountPrivacyList:AIPrivacyTypePermit removed:sourceUID];
1532 - (void)privacyDenyListRemoved:(NSString *)sourceUID
1534 [self accountPrivacyList:AIPrivacyTypeDeny removed:sourceUID];
1537 - (void)setPrivacyOptions:(AIPrivacyOption)option
1539 if (account && purple_account_get_connection(account)) {
1540 PurplePrivacyType privacyType;
1543 case AIPrivacyOptionAllowAll:
1545 privacyType = PURPLE_PRIVACY_ALLOW_ALL;
1547 case AIPrivacyOptionDenyAll:
1548 privacyType = PURPLE_PRIVACY_DENY_ALL;
1550 case AIPrivacyOptionAllowUsers:
1551 privacyType = PURPLE_PRIVACY_ALLOW_USERS;
1553 case AIPrivacyOptionDenyUsers:
1554 privacyType = PURPLE_PRIVACY_DENY_USERS;
1556 case AIPrivacyOptionAllowContactList:
1557 privacyType = PURPLE_PRIVACY_ALLOW_BUDDYLIST;
1562 if (account->perm_deny != privacyType) {
1563 account->perm_deny = privacyType;
1564 serv_set_permit_deny(purple_account_get_connection(account));
1565 AILog(@"Set privacy options for %@ (%x %x) to %i",
1566 self,account,purple_account_get_connection(account),account->perm_deny);
1568 [self setPreference:[NSNumber numberWithInteger:option]
1569 forKey:KEY_PRIVACY_OPTION
1570 group:GROUP_ACCOUNT_STATUS];
1573 AILog(@"Couldn't set privacy options for %@ (%x %x)",self,account,purple_account_get_connection(account));
1577 - (AIPrivacyOption)privacyOptions
1579 AIPrivacyOption privacyOption = -1;
1582 PurplePrivacyType privacyType = account->perm_deny;
1584 switch (privacyType) {
1585 case PURPLE_PRIVACY_ALLOW_ALL:
1587 privacyOption = AIPrivacyOptionAllowAll;
1589 case PURPLE_PRIVACY_DENY_ALL:
1590 privacyOption = AIPrivacyOptionDenyAll;
1592 case PURPLE_PRIVACY_ALLOW_USERS:
1593 privacyOption = AIPrivacyOptionAllowUsers;
1595 case PURPLE_PRIVACY_DENY_USERS:
1596 privacyOption = AIPrivacyOptionDenyUsers;
1598 case PURPLE_PRIVACY_ALLOW_BUDDYLIST:
1599 privacyOption = AIPrivacyOptionAllowContactList;
1603 AILog(@"%@: privacyOptions are %i",self,privacyOption);
1604 return privacyOption;
1607 /*****************************************************/
1608 /* File transfer / AIAccount_Files inherited methods */
1609 /*****************************************************/
1610 #pragma mark File Transfer
1611 - (BOOL)canSendFolders
1616 //Create a protocol-specific xfer object, set it up as requested, and begin sending
1617 - (void)_beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
1619 PurpleXfer *xfer = [self newOutgoingXferForFileTransfer:fileTransfer];
1622 //Associate the fileTransfer and the xfer with each other
1623 [fileTransfer setAccountData:[NSValue valueWithPointer:xfer]];
1624 xfer->ui_data = [fileTransfer retain];
1627 purple_xfer_set_local_filename(xfer, [[fileTransfer localFilename] UTF8String]);
1628 purple_xfer_set_filename(xfer, [[[fileTransfer localFilename] lastPathComponent] UTF8String]);
1631 Request that the transfer begins.
1632 We will be asked to accept it via:
1633 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1636 [purpleAdapter xferRequest:xfer];
1637 [fileTransfer setStatus: Waiting_on_Remote_User_FileTransfer];
1640 //By default, protocols can not create PurpleXfer objects
1641 - (PurpleXfer *)newOutgoingXferForFileTransfer:(ESFileTransfer *)fileTransfer
1643 PurpleXfer *newPurpleXfer = NULL;
1645 if (account && purple_account_get_connection(account)) {
1646 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
1648 if (prpl_info && prpl_info->new_xfer) {
1649 char *destsn = (char *)[[[fileTransfer contact] UID] UTF8String];
1650 newPurpleXfer = (prpl_info->new_xfer)(purple_account_get_connection(account), destsn);
1654 return newPurpleXfer;
1658 * @brief The account requested that we received a file.
1660 * Set up the ESFileTransfer and query the fileTransferController for a save location.
1663 - (void)requestReceiveOfFileTransfer:(ESFileTransfer *)fileTransfer
1665 AILog(@"File transfer request received: %@",fileTransfer);
1666 [adium.fileTransferController receiveRequestForFileTransfer:fileTransfer];
1669 //Create an ESFileTransfer object from an xfer
1670 - (ESFileTransfer *)newFileTransferObjectWith:(NSString *)destinationUID
1671 size:(unsigned long long)inSize
1672 remoteFilename:(NSString *)remoteFilename
1674 AIListContact *contact = [self contactWithUID:destinationUID];
1675 ESFileTransfer *fileTransfer;
1677 fileTransfer = [adium.fileTransferController newFileTransferWithContact:contact
1679 type:Unknown_FileTransfer];
1680 [fileTransfer setSize:inSize];
1681 [fileTransfer setRemoteFilename:remoteFilename];
1683 return fileTransfer;
1686 //Update an ESFileTransfer object progress
1687 - (void)updateProgressForFileTransfer:(ESFileTransfer *)fileTransfer percent:(NSNumber *)percent bytesSent:(NSNumber *)bytesSent
1689 CGFloat percentDone = [percent doubleValue];
1690 [fileTransfer setPercentDone:percentDone bytesSent:[bytesSent unsignedLongValue]];
1693 //The local side cancelled the transfer. We probably already have this status set, but set it just in case.
1694 - (void)fileTransferCancelledLocally:(ESFileTransfer *)fileTransfer
1696 if (![fileTransfer isStopped]) {
1697 [fileTransfer setStatus:Cancelled_Local_FileTransfer];
1701 //The remote side cancelled the transfer, the fool. Update our status.
1702 - (void)fileTransferCancelledRemotely:(ESFileTransfer *)fileTransfer
1704 if (![fileTransfer isStopped]) {
1705 [fileTransfer setStatus:Cancelled_Remote_FileTransfer];
1709 - (void)destroyFileTransfer:(ESFileTransfer *)fileTransfer
1711 AILog(@"Destroy file transfer %@",fileTransfer);
1712 [fileTransfer release];
1715 //Accept a send or receive ESFileTransfer object, beginning the transfer.
1716 //Subsequently inform the fileTransferController that the fun has begun.
1717 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1719 AILog(@"Accepted file transfer %@",fileTransfer);
1722 PurpleXferType xferType;
1724 xfer = [[fileTransfer accountData] pointerValue];
1726 xferType = purple_xfer_get_type(xfer);
1727 if (xferType == PURPLE_XFER_SEND) {
1728 [fileTransfer setFileTransferType:Outgoing_FileTransfer];
1730 } else if (xferType == PURPLE_XFER_RECEIVE) {
1731 [fileTransfer setFileTransferType:Incoming_FileTransfer];
1732 [fileTransfer setSize:purple_xfer_get_size(xfer)];
1735 //accept the request
1736 [purpleAdapter xferRequestAccepted:xfer withFileName:[fileTransfer localFilename]];
1738 [fileTransfer setStatus:Accepted_FileTransfer];
1741 //User refused a receive request. Tell purple; we don't release the ESFileTransfer object
1742 //since that will happen when the xfer is destroyed. This will end up calling back on
1743 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1744 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
1746 PurpleXfer *xfer = [[fileTransfer accountData] pointerValue];
1748 [purpleAdapter xferRequestRejected:xfer];
1752 //Cancel a file transfer in progress. Tell purple; we don't release the ESFileTransfer object
1753 //since that will happen when the xfer is destroyed. This will end up calling back on
1754 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1755 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
1757 PurpleXfer *xfer = [[fileTransfer accountData] pointerValue];
1759 [purpleAdapter xferCancel:xfer];
1763 //Account Connectivity -------------------------------------------------------------------------------------------------
1764 #pragma mark Connect
1765 //Connect this account (Our password should be in the instance variable 'password' all ready for us)
1768 finishedConnectProcess = NO;
1772 //Ensure we have a purple account if one does not already exist
1773 [self purpleAccount];
1775 //Make sure our settings are correct
1776 if ([self connectivityBasedOnNetworkReachability] &&
1777 ![self.host length]) {
1778 //If we use the network for connectivity, and we don't have a host, we need to get ourselves one. Prompt for it!
1779 [self promptForHostBeforeConnecting];
1781 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1787 finishedConnectProcess = NO;
1789 [purpleAdapter unregisterAccount:self];
1792 static void prompt_host_cancel_cb(CBPurpleAccount *self) {
1797 static void prompt_host_ok_cb(CBPurpleAccount *self, const char *host) {
1799 [self setPreference:[NSString stringWithUTF8String:host]
1800 forKey:KEY_CONNECT_HOST
1801 group:GROUP_ACCOUNT_STATUS];
1803 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1805 prompt_host_cancel_cb(self);
1809 - (void)promptForHostBeforeConnecting
1811 purple_request_input(NULL, [[NSString stringWithFormat:AILocalizedString(@"%@ (%@) Setup", "first %@ is an account name; second is a service. This is a title for a window"),
1812 self.formattedUID, [self.service shortDescription]] UTF8String],
1813 [AILocalizedString(@"No Server Specified", nil) UTF8String],
1814 [[NSString stringWithFormat:AILocalizedString(@"No server has been configured for the %@ account %@. Please enter one below to connect", nil),
1815 [self.service longDescription], self.formattedUID] UTF8String],
1816 /* default value */ "", /* multiline */ FALSE, /* masked */ FALSE, /* hint */ NULL,
1817 [AILocalizedString(@"Connect", "Button title to connect; this is a verb") UTF8String], G_CALLBACK(prompt_host_ok_cb),
1818 [AILocalizedString(@"Cancel", nil) UTF8String], G_CALLBACK(prompt_host_cancel_cb),
1819 /* account */ NULL, /* who */ NULL, /* conv */ NULL,
1825 - (void)continueConnectWithConfiguredPurpleAccount
1827 //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1828 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueConnectWithConfiguredProxy)];
1831 - (void)continueConnectWithConfiguredProxy
1833 //Set password and connect
1834 purple_account_set_password(account, [password UTF8String]);
1836 //Set our current status state after filtering its statusMessage as appropriate. This will take us online in the process.
1837 AIStatus *statusState = [self valueForProperty:@"StatusState"];
1838 if (!statusState || (statusState.statusType == AIOfflineStatusType)) {
1839 statusState = [adium.statusController defaultInitialStatusState];
1842 AILog(@"Adium: Connect: %@ initiating connection using status state %@ (%@).",self.UID,statusState,
1843 [statusState statusMessageString]);
1845 [self autoRefreshingOutgoingContentForStatusKey:@"StatusState"
1846 selector:@selector(gotFilteredStatusMessage:forStatusState:)
1847 context:statusState];
1850 //Make sure our settings are correct; notify target/selector when we're finished
1851 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector
1853 NSInvocation *contextInvocation;
1855 //Perform the synchronous configuration activities (subclasses may want to take action in this function)
1856 [self configurePurpleAccount];
1858 contextInvocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1860 [contextInvocation setTarget:target];
1861 [contextInvocation setSelector:selector];
1862 [contextInvocation retainArguments];
1864 //Set the text profile BEFORE beginning the connect process, to avoid problems with setting it while the
1865 //connect occurs. Once that's done, contextInvocation will be invoked, continuing the configurePurpleAccount process.
1866 [self autoRefreshingOutgoingContentForStatusKey:@"TextProfile"
1867 selector:@selector(setAccountProfileTo:configurePurpleAccountContext:)
1868 context:contextInvocation];
1872 * @brief The server name to be passed to libpurple
1873 * By default, this is the host as seen by the rest of Adium. Subclasses may choose to override this if
1874 * some trickery is desired between what is told to libpurple and what the rest of Adium sees.
1876 - (NSString *)hostForPurple
1881 //Synchronous purple account configuration activites, always performed after an account is created.
1882 //This is a definite subclassing point so prpls can apply their own account settings.
1883 - (void)configurePurpleAccount
1886 NSInteger portNumber;
1889 hostName = [self hostForPurple];
1890 if (hostName && [hostName length]) {
1891 purple_account_set_string(account, "server", [hostName UTF8String]);
1895 portNumber = [self port];
1897 purple_account_set_int(account, "port", portNumber);
1901 purple_account_set_check_mail(account, [[self shouldCheckMail] boolValue]);
1904 BOOL customEmoticons = [[self preferenceForKey:KEY_DISPLAY_CUSTOM_EMOTICONS group:GROUP_ACCOUNT_STATUS] boolValue];
1905 purple_account_set_bool(account, "custom_smileys", customEmoticons);
1907 //Update a few properties before we begin connecting. Libpurple will send these automatically
1908 [self updateStatusForKey:KEY_USER_ICON];
1912 * @brief Configure libpurple's proxy settings using the current system values
1914 * target/selector are used rather than a hardcoded callback (or getProxyConfigurationNotifyingTarget: directly) because this allows code reuse
1915 * between the connect and register processes, which are similar in their need for proxy configuration
1917 - (void)configureAccountProxyNotifyingTarget:(id)target selector:(SEL)selector
1919 NSInvocation *invocation;
1921 //Configure the invocation we will use when we are done configuring
1922 invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1923 [invocation setSelector:selector];
1924 [invocation setTarget:target];
1926 [self getProxyConfigurationNotifyingTarget:self
1927 selector:@selector(retrievedProxyConfiguration:context:)
1928 context:invocation];
1932 * @brief Callback for -[self getProxyConfigurationNotifyingTarget:selector:context:]
1934 - (void)retrievedProxyConfiguration:(NSDictionary *)proxyConfig context:(NSInvocation *)invocation
1936 PurpleProxyInfo *proxy_info;
1938 AdiumProxyType proxyType = [[proxyConfig objectForKey:@"AdiumProxyType"] integerValue];
1940 proxy_info = purple_proxy_info_new();
1941 purple_account_set_proxy_info(account, proxy_info);
1943 PurpleProxyType purpleAccountProxyType;
1945 switch (proxyType) {
1946 case Adium_Proxy_HTTP:
1947 case Adium_Proxy_Default_HTTP:
1948 purpleAccountProxyType = PURPLE_PROXY_HTTP;
1950 case Adium_Proxy_SOCKS4:
1951 case Adium_Proxy_Default_SOCKS4:
1952 purpleAccountProxyType = PURPLE_PROXY_SOCKS4;
1954 case Adium_Proxy_SOCKS5:
1955 case Adium_Proxy_Default_SOCKS5:
1956 purpleAccountProxyType = PURPLE_PROXY_SOCKS5;
1958 case Adium_Proxy_None:
1960 purpleAccountProxyType = PURPLE_PROXY_NONE;
1964 purple_proxy_info_set_type(proxy_info, purpleAccountProxyType);
1966 if (proxyType != Adium_Proxy_None) {
1967 purple_proxy_info_set_host(proxy_info, (char *)[[proxyConfig objectForKey:@"Host"] UTF8String]);
1968 purple_proxy_info_set_port(proxy_info, [[proxyConfig objectForKey:@"Port"] integerValue]);
1970 purple_proxy_info_set_username(proxy_info, (char *)[[proxyConfig objectForKey:@"Username"] UTF8String]);
1971 purple_proxy_info_set_password(proxy_info, (char *)[[proxyConfig objectForKey:@"Password"] UTF8String]);
1973 AILog(@"Connecting with proxy type %i and proxy host %@",proxyType, [proxyConfig objectForKey:@"Host"]);
1976 [invocation invoke];
1979 //Sublcasses should override to provide a string for each progress step
1980 - (NSString *)connectionStringForStep:(NSInteger)step { return nil; };
1983 * @brief Should the account's status be updated as soon as it is connected?
1985 * If YES, the StatusState and IdleSince properties will be told to update as soon as the account connects.
1986 * This will allow the account to send its status information to the server upon connecting.
1988 * If this information is already known by the account at the time it connects and further prompting to send it is
1989 * not desired, return NO.
1991 * libpurple should already have been told of our status before connecting began.
1993 - (BOOL)updateStatusImmediatelyAfterConnecting
2000 finishedConnectProcess = YES;
2004 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iTunesDidUpdate:) name:Adium_iTunesTrackChangedNotification object:nil];
2007 [self silenceAllContactUpdatesForInterval:18.0];
2008 [[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
2010 //Clear any previous disconnection error
2011 [self setLastDisconnectionError:nil];
2013 if (unregisterAfterConnecting)
2017 //Our account has connected
2018 - (void)accountConnectionConnected
2020 AILog(@"************ %@ CONNECTED ***********",self.UID);
2024 - (void)accountConnectionProgressStep:(NSNumber *)step percentDone:(NSNumber *)connectionProgressPrecent
2026 NSString *connectionProgressString = [self connectionStringForStep:[step integerValue]];
2028 [self setValue:connectionProgressString forProperty:@"ConnectionProgressString" notify:NO];
2029 [self setValue:connectionProgressPrecent forProperty:@"ConnectionProgressPercent" notify:NO];
2032 [self notifyOfChangedPropertiesSilently:NO];
2034 AILog(@"************ %@ --step-- %i",self.UID,[step integerValue]);
2038 * @brief Name to use when creating a PurpleAccount for this CBPurpleAccount
2040 * By default, we just use the formattedUID. Subclasses can override this to provide other handling,
2041 * such as appending \@mac.com if necessary for dotMac accounts.
2043 - (const char *)purpleAccountName
2045 return [self.formattedUID UTF8String];
2048 - (void)setPurpleAccount:(PurpleAccount *)inAccount
2050 account = inAccount;
2053 - (void)createNewPurpleAccount
2055 //Ensure libpurple is loaded and initialized
2056 [self purpleAdapter];
2058 //If loading libpurple didn't set an account for us, tell it to create one
2060 [[self purpleAdapter] addAdiumAccount:self];
2062 //-[SLPurpleCocoaAdapter addAdiumAccount:] should have immediately called back on setPurpleAccount. It's bad if it didn't.
2064 AILog(@"Created PurpleAccount 0x%x with UID %@ and protocolPlugin %s", account, self.UID, [self protocolPlugin]);
2066 AILog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
2067 self.purpleAccountName, [self protocolPlugin]);
2068 NSLog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
2069 self.purpleAccountName, [self protocolPlugin]);
2074 * @brief Returns a PurpleSslConnection for a given account.
2076 - (PurpleSslConnection *)secureConnection
2081 #pragma mark Disconnect
2084 * @brief Disconnect this account
2088 if (self.online || [self boolValueForProperty:@"Connecting"]) {
2089 //As per AIAccount's documentation, call super's implementation
2092 [[AIContactObserverManager sharedManager] delayListObjectNotificationsUntilInactivity];
2094 //Tell libpurple to disconnect
2095 [purpleAdapter disconnectAccount:self];
2099 - (void)setLastDisconnectionReason:(PurpleConnectionError)reason
2101 lastDisconnectionReason = reason;
2104 - (PurpleConnectionError)lastDisconnectionReason
2106 return lastDisconnectionReason;
2110 * @brief Our account was unexpectedly disconnected with an error message
2112 - (void)accountConnectionReportDisconnect:(NSString *)text withReason:(PurpleConnectionError)reason
2114 [self setLastDisconnectionError:text];
2115 [self setLastDisconnectionReason:reason];
2117 if (reason == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED)
2118 [self serverReportedInvalidPassword];
2120 //We are disconnecting
2121 [self setValue:[NSNumber numberWithBool:YES] forProperty:@"Disconnecting" notify:NotifyNow];
2123 AILog(@"%@ accountConnectionReportDisconnect: %@",self,lastDisconnectionError);
2126 - (void)accountConnectionNotice:(NSString *)connectionNotice
2128 [adium.interfaceController handleErrorMessage:[NSString stringWithFormat:AILocalizedString(@"%@ (%@) : Connection Notice",nil),self.formattedUID,[service description]]
2129 withDescription:connectionNotice];
2132 - (void)didDisconnect
2134 //Clear properties which don't make sense for a disconnected account
2135 [self setValue:nil forProperty:@"TextProfile" notify:NO];
2138 [self notifyOfChangedPropertiesSilently:NO];
2140 [[NSNotificationCenter defaultCenter] removeObserver:self
2141 name:Adium_iTunesTrackChangedNotification
2146 if (deletePurpleAccountAfterDisconnecting) {
2147 deletePurpleAccountAfterDisconnecting = FALSE;
2149 [[self purpleAdapter] removeAdiumAccount:self];
2152 [super didDisconnect];
2155 * @brief Our account has disconnected
2157 * This is called after the account disconnects for any reason
2159 - (void)accountConnectionDisconnected
2161 //Report that we disconnected
2162 AILog(@"%@: Telling the core we disconnected", self);
2163 [self didDisconnect];
2166 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
2168 AIReconnectDelayType reconnectDelayType;
2170 if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED) {
2171 [self setLastDisconnectionError:AILocalizedString(@"Incorrect username or password","Error message displayed when the server reports username or password as being incorrect.")];
2172 reconnectDelayType = AIReconnectImmediately;
2174 } else if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_INVALID_USERNAME) {
2175 [self setLastDisconnectionError:AILocalizedString(@"The name you entered is not registered. Check to ensure you typed it correctly.", nil)];
2176 reconnectDelayType = AIReconnectNever;
2178 } else if (disconnectionError && ([*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("SSL Handshake Failed")]] ||
2179 [*disconnectionError isEqualToString:[NSString stringWithUTF8String:_("SSL Connection Failed")]])) {
2180 /* This particular message comes with PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, which is a 'fatal' error according to libpurple. Other problems
2181 * with that message may be fatal, but this one isn't.
2183 reconnectDelayType = AIReconnectNormally;
2185 } else if (purple_connection_error_is_fatal([self lastDisconnectionReason])) {
2186 reconnectDelayType = AIReconnectNever;
2189 reconnectDelayType = AIReconnectNormally;
2192 return reconnectDelayType;
2195 #pragma mark Registering
2196 - (void)performRegisterWithPassword:(NSString *)inPassword
2198 //Save the new password
2199 if (inPassword && ![password isEqualToString:inPassword]) {
2200 [password release]; password = [inPassword retain];
2203 //Ensure we have a purple account if one does not already exist
2204 [self purpleAccount];
2207 [self setValue:[NSNumber numberWithBool:YES] forProperty:@"Connecting" notify:NotifyNow];
2209 //Make sure our settings are correct
2210 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredPurpleAccount)];
2213 - (void)continueRegisterWithConfiguredProxy
2215 //Set password and connect
2216 purple_account_set_password(account, [password UTF8String]);
2218 AILog(@"Adium: Register: %@ initiating connection.",self.UID);
2220 [purpleAdapter registerAccount:self];
2223 - (void)continueRegisterWithConfiguredPurpleAccount
2225 //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
2226 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredProxy)];
2229 - (void)purpleAccountRegistered:(BOOL)success
2231 if (success && [self.service accountViewController]) {
2232 NSString *username = (purple_account_get_username(account) ? [NSString stringWithUTF8String:purple_account_get_username(account)] : [NSNull null]);
2233 NSString *pw = (purple_account_get_password(account) ? [NSString stringWithUTF8String:purple_account_get_password(account)] : [NSNull null]);
2235 [[NSNotificationCenter defaultCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
2237 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
2238 username, @"username",
2244 //Account Status ------------------------------------------------------------------------------------------------------
2245 #pragma mark Account Status
2246 //Properties this account supports
2247 - (NSSet *)supportedPropertyKeys
2249 static NSMutableSet *supportedPropertyKeys = nil;
2251 if (!supportedPropertyKeys) {
2252 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
2256 @"DefaultUserIconFilename",
2257 KEY_ACCOUNT_CHECK_MAIL,
2259 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
2263 return supportedPropertyKeys;
2267 - (void)updateStatusForKey:(NSString *)key
2269 [super updateStatusForKey:key];
2271 //Now look at keys which only make sense if we have an account
2273 AILog(@"%@: Updating status for key: %@",self, key);
2275 if ([key isEqualToString:@"IdleSince"]) {
2276 NSDate *idleSince = [self preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
2279 idleSince = [adium.preferenceController preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
2282 [self setAccountIdleSinceTo:idleSince];
2284 } else if ([key isEqualToString:@"TextProfile"]) {
2285 [self autoRefreshingOutgoingContentForStatusKey:key selector:@selector(setAccountProfileTo:) context:nil];
2287 } else if ([key isEqualToString:KEY_ACCOUNT_CHECK_MAIL]) {
2288 //Update the mail checking setting if the account is already made (if it isn't, we'll set it when it is made)
2290 [purpleAdapter setCheckMail:[self shouldCheckMail]
2298 * @brief Return the purple status type to be used for a status
2300 * Most subclasses should override this method; these generic values may be appropriate for others.
2302 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
2303 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
2304 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
2305 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
2306 * statusState.statusType for a general idea of the status's type.
2308 * @param statusState The status for which to find the purple status ID
2309 * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
2311 * @result The purple status ID
2313 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
2314 arguments:(NSMutableDictionary *)arguments
2316 char *statusID = NULL;
2318 switch (statusState.statusType) {
2319 case AIAvailableStatusType:
2320 statusID = "available";
2322 case AIAwayStatusType:
2326 case AIInvisibleStatusType:
2327 statusID = "invisible";
2330 case AIOfflineStatusType:
2331 statusID = "offline";
2338 - (BOOL)shouldAddMusicalNoteToNowPlayingStatus
2343 - (BOOL)shouldSetITMSLinkForNowPlayingStatus
2348 - (NSDictionary *)purpleSongInfoDictionary
2350 NSMutableDictionary *arguments = nil;
2352 if (tuneinfo && [[tuneinfo objectForKey:ITUNES_PLAYER_STATE] isEqualToString:@"Playing"]) {
2353 arguments = [NSMutableDictionary dictionary];
2355 NSString *artist = [tuneinfo objectForKey:ITUNES_ARTIST];
2356 NSString *name = [tuneinfo objectForKey:ITUNES_NAME];
2358 [arguments setObject:(artist ? artist : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
2359 [arguments setObject:(name ? name : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
2360 [arguments setObject:([tuneinfo objectForKey:ITUNES_ALBUM] ? [tuneinfo objectForKey:ITUNES_ALBUM] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
2361 [arguments setObject:([tuneinfo objectForKey:ITUNES_GENRE] ? [tuneinfo objectForKey:ITUNES_GENRE] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
2362 [arguments setObject:([tuneinfo objectForKey:ITUNES_TOTAL_TIME] ? [tuneinfo objectForKey:ITUNES_TOTAL_TIME]:[NSNumber numberWithInteger:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
2363 [arguments setObject:([tuneinfo objectForKey:ITUNES_YEAR] ? [tuneinfo objectForKey:ITUNES_YEAR]:[NSNumber numberWithInteger:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
2364 [arguments setObject:([tuneinfo objectForKey:ITUNES_STORE_URL] ? [tuneinfo objectForKey:ITUNES_STORE_URL] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
2366 [arguments setObject:[NSString stringWithFormat:@"%@%@%@", (name ? name : @""), (name && artist ? @" - " : @""), (artist ? artist : @"")]
2367 forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
2373 - (void)iTunesDidUpdate:(NSNotification*)notification {
2375 tuneinfo = [[notification object] retain];
2377 /* Only if we're including the information in all statuses do we need to do an update;
2378 * if we just have a 'now playing' status, the dynamic stats update will call
2379 * -[self setStatusState:usingStatusMessage:] in a moment.
2381 [purpleAdapter setSongInformation:(shouldIncludeNowPlayingInformationInAllStatuses ? [self purpleSongInfoDictionary] : nil) onAccount:self];
2385 * @brief Should a status message be set when using the default "Away" state?
2387 - (BOOL)shouldSetStatusMessageForDefaultAwayState
2393 * @brief Perform the setting of a status state
2395 * Sets the account to a passed status state. The account should set itself to best possible status given the return
2396 * values of statusState's accessors. The passed statusMessage has been filtered; it should be used rather than
2397 * statusState.statusMessage, which returns an unfiltered statusMessage.
2399 * @param statusState The state to enter
2400 * @param statusMessage The filtered status message to use.
2402 - (void)setStatusState:(AIStatus *)statusState usingStatusMessage:(NSAttributedString *)statusMessage
2404 NSString *encodedStatusMessage;
2405 NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init];
2407 //Get the purple status type from this class or subclasses, which may also potentially modify or nullify our statusMessage
2408 const char *statusID = [self purpleStatusIDForStatus:statusState
2409 arguments:arguments];
2411 if (![statusMessage length] &&
2412 (statusState.statusType == AIAwayStatusType) &&
2413 statusState.statusName &&
2414 (!statusID || ((strcmp(statusID, "away") == 0) && [self shouldSetStatusMessageForDefaultAwayState]))) {
2415 /* If we don't have a status message, and the status type is away for a non-default away such as "Do Not Disturb", and we're only setting
2416 * a default away state becuse we don't know a better one for this service, get a default
2417 * description of this away state. This allows, for example, an AIM user to set the "Do Not Disturb" type provided by her ICQ account
2418 * and have the away message be set appropriately.
2420 statusMessage = [NSAttributedString stringWithString:[adium.statusController descriptionForStateOfStatus:statusState]];
2423 BOOL isNowPlayingStatus = ([statusState specialStatusType] == AINowPlayingSpecialStatusType);
2424 if (isNowPlayingStatus && [statusMessage length]) {
2425 if ([self shouldAddMusicalNoteToNowPlayingStatus]) {
2426 #define MUSICAL_NOTE_AND_SPACE [NSString stringWithUTF8String:"\xe2\x99\xab "]
2427 NSMutableAttributedString *temporaryStatusMessage;
2428 temporaryStatusMessage = [[[NSMutableAttributedString alloc] initWithString:MUSICAL_NOTE_AND_SPACE] autorelease];
2429 [temporaryStatusMessage appendAttributedString:statusMessage];
2431 statusMessage = temporaryStatusMessage;
2434 if ([self shouldSetITMSLinkForNowPlayingStatus]) {
2435 //Grab the message's subtext, which is the song link if we're using the Current iTunes Track status
2436 NSString *itmsStoreLink = [statusMessage attribute:@"AIMessageSubtext" atIndex:0 effectiveRange:NULL];
2437 if (itmsStoreLink) {
2438 [arguments setObject:itmsStoreLink
2443 NSDictionary *purpleSongInfoDictionary = [self purpleSongInfoDictionary];
2444 if (purpleSongInfoDictionary)
2445 [arguments addEntriesFromDictionary:purpleSongInfoDictionary];
2448 //Encode the status message if we have one
2449 encodedStatusMessage = (statusMessage ?
2450 [self encodedAttributedString:statusMessage
2451 forStatusState:statusState] :
2453 if (encodedStatusMessage) {
2454 [arguments setObject:encodedStatusMessage
2458 [self setStatusState:statusState
2460 isActive:[NSNumber numberWithBool:YES] /* We're only using exclusive states for now... I hope. */
2461 arguments:arguments];
2463 [arguments release];
2467 * @brief Perform the actual setting of a state
2469 * This is called by setStatusState. It allows subclasses to perform any other behaviors, such as modifying a display
2470 * name, which are called for by the setting of the state; most of the processing has already been done, however, so
2471 * most subclasses will not need to implement this.
2473 * @param statusState The AIStatus which is being set
2474 * @param statusID The Purple-sepcific statusID we are setting
2475 * @param isActive An NSNumber with a bool YES if we are activating (going to) the passed state, NO if we are deactivating (going away from) the passed state.
2476 * @param arguments Purple-specific arguments specified by the account. It must contain only NSString objects and keys.
2478 - (void)setStatusState:(AIStatus *)statusState statusID:(const char *)statusID isActive:(NSNumber *)isActive arguments:(NSMutableDictionary *)arguments
2480 [purpleAdapter setStatusID:statusID
2486 //Set our idle (Pass nil for no idle)
2487 - (void)setAccountIdleSinceTo:(NSDate *)idleSince
2489 [purpleAdapter setIdleSinceTo:idleSince onAccount:self];
2491 //We now should update our idle property
2492 [self setValue:([idleSince timeIntervalSinceNow] ? idleSince : nil)
2493 forProperty:@"IdleSince"
2497 //Set the profile, then invoke the passed invocation to return control to the target/selector specified
2498 //by a configurePurpleAccountNotifyingTarget:selector: call.
2499 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation
2501 [self setAccountProfileTo:profile];
2503 [inInvocation invoke];
2506 //Set our profile immediately on the purpleAdapter
2507 - (void)setAccountProfileTo:(NSAttributedString *)profile
2509 if (!profile || ![[profile string] isEqualToString:[[self valueForProperty:@"TextProfile"] string]]) {
2510 NSString *profileHTML = nil;
2512 //Convert the profile to HTML, and pass it to libpurple
2514 profileHTML = [self encodedAttributedString:profile forListObject:nil];
2517 [purpleAdapter setInfo:profileHTML onAccount:self];
2519 //We now have a profile
2520 [self setValue:profile forProperty:@"TextProfile" notify:NotifyNow];
2525 * @brief Set our user image
2527 * Pass nil for no image. This resizes and converts the image as needed for our protocol.
2528 * After setting it with purple, it sets it within Adium; if this is not called, the image will
2529 * show up neither locally nor remotely.
2531 - (void)setAccountUserImage:(NSImage *)image withData:(NSData *)originalData;
2534 NSData *imageData = originalData;
2535 NSSize imageSize = (image ? [image size] : NSZeroSize);
2536 NSData *buddyIconData = nil;
2538 /* Now pass libpurple the new icon. Check to be sure our image doesn't have an NSZeroSize size,
2539 * which would indicate currupt data */
2540 if (image && !NSEqualSizes(NSZeroSize, imageSize)) {
2541 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
2543 AILog(@"Original image of size %f %f",imageSize.width,imageSize.height);
2545 if (prpl_info && (prpl_info->icon_spec.format)) {
2546 BOOL smallEnough, prplScales;
2549 /* We need to scale it down if:
2550 * 1) The prpl needs to scale before it sends to the server or other buddies AND
2551 * 2) The image is larger than the maximum size allowed by the protocol
2552 * We ignore the minimum required size, as scaling up just leads to pixellated images.
2554 smallEnough = (prpl_info->icon_spec.max_width >= imageSize.width &&
2555 prpl_info->icon_spec.max_height >= imageSize.height);
2557 prplScales = (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_SEND) || (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_DISPLAY);
2559 if (prplScales && !smallEnough) {
2560 gint width = (gint)imageSize.width;
2561 gint height = (gint)imageSize.height;
2563 purple_buddy_icon_get_scale_size(&prpl_info->icon_spec, &width, &height);
2564 //Determine the scaled size. If it's too big, scale to the largest permissable size
2565 image = [image imageByScalingToSize:NSMakeSize(width, height)];
2567 /* Our original data is no longer valid, since we had to scale to a different size */
2569 AILog(@"%@: Scaled image to size %@", self, NSStringFromSize([image size]));
2572 if (!buddyIconData) {
2573 char **prpl_formats = g_strsplit(prpl_info->icon_spec.format,",",0);
2575 //Look for gif first if the image is animated
2576 NSImageRep *imageRep = [image bestRepresentationForDevice:nil] ;
2577 if ([imageRep isKindOfClass:[NSBitmapImageRep class]] &&
2578 [[(NSBitmapImageRep *)imageRep valueForProperty:NSImageFrameCount] integerValue] > 1) {
2580 for (i = 0; prpl_formats[i]; i++) {
2581 if (strcmp(prpl_formats[i],"gif") == 0) {
2582 /* Try to use our original data. If we had to scale, imageData will have been set
2583 * to nil and we'll continue below to convert the image. */
2584 AILog(@"l33t script kiddie animated GIF!!111");
2586 buddyIconData = imageData;
2593 if (!buddyIconData) {
2594 for (i = 0; prpl_formats[i]; i++) {
2595 if (strcmp(prpl_formats[i],"png") == 0) {
2596 buddyIconData = [image PNGRepresentation];
2600 } else if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2601 buddyIconData = [image JPEGRepresentationWithCompressionFactor:1.0];
2605 } else if ((strcmp(prpl_formats[i],"tiff") == 0) || (strcmp(prpl_formats[i],"tif") == 0)) {
2606 buddyIconData = [image TIFFRepresentation];
2610 } else if (strcmp(prpl_formats[i],"gif") == 0) {
2611 buddyIconData = [image GIFRepresentation];
2615 } else if (strcmp(prpl_formats[i],"bmp") == 0) {
2616 buddyIconData = [image BMPRepresentation];
2623 size_t maxSize = prpl_info->icon_spec.max_filesize;
2624 if (maxSize > 0 && ([buddyIconData length] > maxSize)) {
2625 AILog(@"Image %i is larger than %i!",[buddyIconData length],maxSize);
2626 for (i = 0; prpl_formats[i]; i++) {
2627 if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2628 buddyIconData = [image JPEGRepresentationWithMaximumByteSize:maxSize];
2634 g_strfreev(prpl_formats);
2639 AILogWithSignature(@"%@ setting icon data of length %i", self, [buddyIconData length]);
2640 [purpleAdapter setBuddyIcon:buddyIconData onAccount:self];
2643 [super setAccountUserImage:image withData:originalData];
2646 #pragma mark Group Chat
2647 - (BOOL)inviteContact:(AIListContact *)inContact toChat:(AIChat *)inChat withMessage:(NSString *)inviteMessage
2649 [purpleAdapter inviteContact:inContact toChat:inChat withMessage:inviteMessage];
2654 #pragma mark Buddy Menu Items
2655 //Action of a dynamically-generated contact menu item
2656 - (void)performContactMenuAction:(NSMenuItem *)sender
2658 NSDictionary *dict = [sender representedObject];
2660 [purpleAdapter performContactMenuActionFromDict:dict forAccount:self];
2664 * @brief Utility method when generating buddy-specific menu items
2666 * Adds the menu item for act to a growing array of NSMenuItems. If act has children (a submenu), this method is used recursively
2667 * to generate the submenu containing each child menu item.
2669 - (void)addMenuItemForMenuAction:(PurpleMenuAction *)act forListContact:(AIListContact *)inContact purpleBuddy:(PurpleBuddy *)buddy toArray:(NSMutableArray *)menuItemArray withServiceIcon:(NSImage *)serviceIcon
2672 NSMenuItem *menuItem;
2675 //If titleForContactMenuLabel:forContact: returns nil, we don't add the menuItem
2678 (title = [self titleForContactMenuLabel:act->label
2679 forContact:inContact])) {
2680 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2682 action:@selector(performContactMenuAction:)
2684 [menuItem setImage:serviceIcon];
2687 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2688 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2689 /* act->data may be freed by purple_menu_action_free() before we use it, I'm afraid... */
2690 [NSValue valueWithPointer:act->data],@"PurpleMenuActionData",
2691 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2694 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2695 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2696 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2700 [menuItem setRepresentedObject:dict];
2702 //If there is a submenu, generate and set it
2703 if (act->children) {
2704 NSMutableArray *childrenArray = [NSMutableArray array];
2706 //Add a NSMenuItem for each child
2707 for (l = ll = act->children; l; l = l->next) {
2708 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2709 forListContact:inContact
2711 toArray:childrenArray
2712 withServiceIcon:serviceIcon];
2714 g_list_free(act->children);
2716 if ([childrenArray count]) {
2717 NSMenu *submenu = [[NSMenu alloc] init];
2719 for (NSMenuItem *childMenuItem in childrenArray) {
2720 [submenu addItem:childMenuItem];
2723 [menuItem setSubmenu:submenu];
2728 [menuItemArray addObject:menuItem];
2732 purple_menu_action_free(act);
2735 //Returns an array of menuItems specific for this contact based on its account and potentially status
2736 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
2738 NSMutableArray *menuItemArray = nil;
2740 if (account && purple_account_is_connected(account)) {
2741 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
2745 //Find the PurpleBuddy
2746 buddy = purple_find_buddy(account, [inContact.UID UTF8String]);
2748 if (prpl_info && prpl_info->blist_node_menu && buddy) {
2749 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:self.service
2750 type:AIServiceIconSmall
2751 direction:AIIconNormal];
2753 menuItemArray = [NSMutableArray array];
2755 //Add a NSMenuItem for each node action specified by the prpl
2756 for (l = ll = prpl_info->blist_node_menu((PurpleBlistNode *)buddy); l; l = l->next) {
2757 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2758 forListContact:inContact
2760 toArray:menuItemArray
2761 withServiceIcon:serviceIcon];
2765 //Don't return an empty array
2766 if (![menuItemArray count]) menuItemArray = nil;
2770 return menuItemArray;
2773 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2774 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
2776 return [NSString stringWithUTF8String:label];
2780 * @brief Menu items for the account's actions
2782 * Returns an array of menu items for account-specific actions. This is the best place to add protocol-specific
2783 * actions that aren't otherwise supported by Adium. It will only be queried if the account is online.
2784 * @return NSArray of NSMenuItem instances for this account
2786 - (NSArray *)accountActionMenuItems
2788 NSMutableArray *menuItemArray = nil;
2790 if (account && purple_account_is_connected(account)) {
2791 PurplePlugin *plugin = purple_account_get_connection(account)->prpl;
2793 if (PURPLE_PLUGIN_HAS_ACTIONS(plugin)) {
2796 actions = PURPLE_PLUGIN_ACTIONS(plugin, purple_account_get_connection(account));
2798 //Avoid adding separators between nonexistant items (i.e. items which Purple shows but we don't)
2799 BOOL addedAnAction = NO;
2800 for (l = actions; l; l = l->next) {
2803 PurplePluginAction *action;
2805 NSMenuItem *menuItem;
2808 action = (PurplePluginAction *) l->data;
2810 //If titleForAccountActionMenuLabel: returns nil, we don't add the menuItem
2813 (title = [self titleForAccountActionMenuLabel:action->label])) {
2815 action->plugin = plugin;
2816 action->context = purple_account_get_connection(account);
2818 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2820 action:@selector(performAccountMenuAction:)
2821 keyEquivalent:@""] autorelease];
2822 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2823 [NSValue valueWithPointer:action->callback], @"PurplePluginActionCallback",
2824 [NSValue valueWithPointer:action->user_data], @"PurplePluginActionCallbackUserData",
2827 [menuItem setRepresentedObject:dict];
2829 if (!menuItemArray) menuItemArray = [NSMutableArray array];
2831 [menuItemArray addObject:menuItem];
2832 addedAnAction = YES;
2835 purple_plugin_action_free(action);
2838 if (addedAnAction) {
2839 [menuItemArray addObject:[NSMenuItem separatorItem]];
2845 g_list_free(actions);
2850 if([self encrypted] && [self secureConnection]) {
2851 if (menuItemArray.count) {
2852 [menuItemArray addObject:[NSMenuItem separatorItem]];
2855 NSMenuItem *showCertificateMenuItem = [[[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Show Server Certificate",nil)
2857 action:@selector(showServerCertificate)
2858 keyEquivalent:@""] autorelease];
2860 [menuItemArray addObject:showCertificateMenuItem];
2864 return menuItemArray;
2869 * @brief Shows the SSL certificate for the connection.
2871 - (void)showServerCertificate
2873 CFArrayRef certificates = [[self purpleAdapter] copyServerCertificates:[self secureConnection]];
2875 [AIPurpleCertificateViewer displayCertificateChain:certificates forAccount:self];
2877 CFRelease(certificates);
2881 //Action of a dynamically-generated contact menu item
2882 - (void)performAccountMenuAction:(NSMenuItem *)sender
2884 NSDictionary *dict = [sender representedObject];
2886 [purpleAdapter performAccountMenuActionFromDict:dict forAccount:self];
2889 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2890 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
2892 if ((strcmp(label, _("Change Password...")) == 0) || (strcmp(label, _("Change Password")) == 0)) {
2893 return [[NSString stringWithFormat:AILocalizedString(@"Change Password", "Menu item title for changing the password of an account")] stringByAppendingEllipsis];
2895 return [NSString stringWithUTF8String:label];
2899 /********************************/
2900 /* AIAccount subclassed methods */
2901 /********************************/
2902 #pragma mark AIAccount Subclassed Methods
2905 NSDictionary *defaults = [NSDictionary dictionaryNamed:[NSString stringWithFormat:@"PurpleDefaults%@",self.service.serviceID]
2906 forClass:[self class]];
2909 [adium.preferenceController registerDefaults:defaults
2910 forGroup:GROUP_ACCOUNT_STATUS
2913 AILog(@"Failed to load defaults for %@",[NSString stringWithFormat:@"PurpleDefaults%@",self.service.serviceID]);
2917 [self setLastDisconnectionError:nil];
2919 permittedContactsArray = [[NSMutableArray alloc] init];
2920 deniedContactsArray = [[NSMutableArray alloc] init];
2922 //We will create a purpleAccount the first time we attempt to connect
2925 //Observe preferences changes
2926 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ALIASES];
2927 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];
2930 - (BOOL)allowAccountUnregistrationIfSupportedByLibpurple
2936 * @brief The account will be deleted, we should ask the user for confirmation. If the prpl supports it, we can also remove
2937 * the account from the server (if the user wants us to do that)
2939 - (NSAlert*)alertForAccountDeletion
2941 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
2943 //Ensure libpurple has been loaded, since we need to know whether we can unregister this account
2944 [self purpleAdapter];
2947 prpl_info->unregister_user &&
2948 [self allowAccountUnregistrationIfSupportedByLibpurple]) {
2949 return [NSAlert alertWithMessageText:AILocalizedString(@"Delete Account",nil)
2950 defaultButton:AILocalizedString(@"Delete",nil)
2951 alternateButton:AILocalizedString(@"Cancel",nil)
2952 otherButton:AILocalizedString(@"Delete & Unregister",nil)
2953 informativeTextWithFormat:AILocalizedString(@"Delete the account %@? You can also optionally unregister the account on the server if possible.",nil), ([self.formattedUID length] ? self.formattedUID : NEW_ACCOUNT_DISPLAY_TEXT)];
2956 return [super alertForAccountDeletion];
2960 - (void)alertForAccountDeletion:(id<AIAccountControllerRemoveConfirmationDialog>)dialog didReturn:(NSInteger)returnCode
2962 PurplePluginProtocolInfo *prpl_info = self.protocolInfo;
2965 prpl_info->unregister_user) {
2966 switch (returnCode) {
2967 case NSAlertOtherReturn:
2968 // delete & unregister
2972 unregisterAfterConnecting = YES;
2973 [self setShouldBeOnline:YES];
2976 // further progress happens in -unregisteredAccount:
2978 case NSAlertDefaultReturn:
2979 // delete without unregistering
2980 [self performDelete];
2988 switch(returnCode) {
2989 case NSAlertDefaultReturn:
2990 [self performDelete];
2998 //Release dialog as required by AIAccount's documentation since we didn't call super's implementation.
3002 - (void)unregisteredAccount:(BOOL)success {
3004 /* We're not going to be online, but we *must* not disconnect within this run loop,
3005 * as libpurple may still have Things To Do with the connection and it has no concept of reference
3006 * counting with which to survive the disconnection. Performing a deletion would set us offline,
3007 * so wait until the next run loop.
3009 [self performSelector:@selector(performDelete)
3016 * @brief The account's UID changed
3018 - (void)didChangeUID
3020 //Only need to take action if we have a created PurpleAccount already
3021 if (account != NULL) {
3022 //Remove our current account
3023 [[self purpleAdapter] removeAdiumAccount:self];
3025 //Clear the reference to the PurpleAccount... it'll be created when needed
3031 * @brief The account will be deleted; it has already been told to disconnect
3033 - (void)willBeDeleted
3036 //Wait until we are finished disconnecting before removing ourselves from libpurple.
3037 deletePurpleAccountAfterDisconnecting = TRUE;
3040 [[self purpleAdapter] removeAdiumAccount:self];
3043 [super willBeDeleted];
3048 [adium.preferenceController unregisterPreferenceObserver:self];
3050 [permittedContactsArray release];
3051 [deniedContactsArray release];
3056 - (NSString *)unknownGroupName {
3057 return (@"Unknown");
3060 - (NSDictionary *)defaultProperties { return [NSDictionary dictionary]; }
3062 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
3064 return [self encodedAttributedString:inAttributedString forListObject:nil];
3067 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
3068 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
3070 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
3072 if ([group isEqualToString:PREF_GROUP_ALIASES]) {
3073 //If the notification object is a listContact belonging to this account, update the serverside information
3074 if ((account != nil) &&
3075 ([self shouldSetAliasesServerside]) &&
3076 ([key isEqualToString:@"Alias"])) {
3078 NSString *alias = [object preferenceForKey:@"Alias"
3079 group:PREF_GROUP_ALIASES
3082 if ([object isKindOfClass:[AIMetaContact class]]) {
3083 for(AIListContact *containedListContact in (AIMetaContact *)object) {
3084 if (containedListContact.account == self) {
3085 [purpleAdapter setAlias:alias forUID:containedListContact.UID onAccount:self];
3089 } else if ([object isKindOfClass:[AIListContact class]]) {
3090 if ([(AIListContact *)object account] == self) {
3091 [purpleAdapter setAlias:alias forUID:object.UID onAccount:self];
3095 } else if ([group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE]) {
3096 openPsychicChats = [[prefDict objectForKey:KEY_PSYCHIC] boolValue];
3098 } else if ([group isEqualToString:GROUP_ACCOUNT_STATUS]) {
3099 BOOL oldNowPlaying = shouldIncludeNowPlayingInformationInAllStatuses;
3101 shouldIncludeNowPlayingInformationInAllStatuses = [[self preferenceForKey:KEY_BROADCAST_MUSIC_INFO group:GROUP_ACCOUNT_STATUS] boolValue];
3103 if (oldNowPlaying && !shouldIncludeNowPlayingInformationInAllStatuses) {
3104 /* Clear any existing song info immediately if we're no longer supposed to broadcast it */
3105 [purpleAdapter setSongInformation:nil onAccount:self];
3111 * @brief When the account is edited, update our libpurple preferences.
3113 - (void)accountEdited
3115 // We only need to re-configure if we're online or connecting. If we're offline, our next connect will do this.
3116 if (self.online || [self boolValueForProperty:@"Connecting"]) {
3117 AILog(@"Re-configuring purple account due to preference changes.");
3118 [self configurePurpleAccount];
3122 #pragma mark Actions for chats
3124 /***************************/
3125 /* Account private methods */
3126 /***************************/
3127 #pragma mark Private
3128 - (void)setTypingFlagOfChat:(AIChat *)chat to:(NSNumber *)typingStateNumber
3130 NSAssert(!chat.isGroupChat, @"Chat cannot be a group chat for typing.");
3132 AITypingState currentTypingState = [chat integerValueForProperty:KEY_TYPING];
3133 AITypingState newTypingState = [typingStateNumber integerValue];
3135 if (currentTypingState != newTypingState) {
3136 if (newTypingState == AITyping && openPsychicChats && ![chat isOpen]) {
3137 [adium.interfaceController openChat:chat];
3140 * Use the Libpurple "psychic" tagline. If this is found to be confusing, we should switch to your own version.
3141 * The upside of using theirs is that clever gimmicky translations already exist.
3143 NSMutableString *forceString = [[NSString stringWithUTF8String:_("You feel a disturbance in the force...")] mutableCopy];
3144 [forceString replaceOccurrencesOfString:@"..."
3145 withString:[NSString ellipsis]
3146 options:NSLiteralSearch];
3147 AIContentEvent *statusMessage = [AIContentEvent eventInChat:chat
3148 withSource:chat.listObject
3151 message:[NSAttributedString stringWithString:forceString]
3152 withType:@"psychic"];
3154 // Don't log the psychic message.
3155 statusMessage.postProcessContent = NO;
3157 [forceString release];
3159 [adium.contentController receiveContentObject:statusMessage];
3162 [chat setValue:(newTypingState ? typingStateNumber : nil)
3163 forProperty:KEY_TYPING
3168 - (NSNumber *)shouldCheckMail
3170 return [self preferenceForKey:KEY_ACCOUNT_CHECK_MAIL group:GROUP_ACCOUNT_STATUS];
3173 - (BOOL)shouldSetAliasesServerside