* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "CBPurpleOscarAccount.h" #import <AdiumLibpurple/SLPurpleCocoaAdapter.h> #import <Adium/AIAccountControllerProtocol.h> #import <Adium/AIContactControllerProtocol.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIStatusControllerProtocol.h> #import <Adium/ESFileTransfer.h> #import <Adium/AIHTMLDecoder.h> #import <Adium/AIListContact.h> #import <Adium/AIService.h> #import <Adium/AIStatus.h> #import <AIUtilities/AIAttributedStringAdditions.h> #import <AIUtilities/AIDateFormatterAdditions.h> #import <AIUtilities/AIImageAdditions.h> #import <AIUtilities/AIObjectAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <libkern/OSAtomic.h> #define DELAYED_UPDATE_INTERVAL 2.0 @interface CBPurpleOscarAccount () - ( NSString * ) stringByProcessingImgTagsForDirectIM : ( NSString * ) inString forContactWithUID : ( const char * ) who ; // migration from 1.4.1 to 1.4.2 @implementation CBPurpleOscarAccount - ( const char * ) protocolPlugin NSLog ( @"WARNING: Subclass must override" ); // Migrate from SSL/no SSL to the tri-state radio button if ( ! [ self preferenceForKey : PREFERENCE_ENCRYPTION_TYPE group : GROUP_ACCOUNT_STATUS ]) { AILogWithSignature ( @"Migrating %@ to new encryption settings" , self ); [ self setPreference : nil forKey : PREFERENCE_SSL_CONNECTION group : GROUP_ACCOUNT_STATUS ]; #pragma mark AIListContact and AIService special cases for OSCAR //Override contactWithUID to mark mobile and ICQ users as such via the displayServiceID - ( AIListContact * ) contactWithUID: ( NSString * ) sourceUID if ( ! namesAreCaseSensitive ) { sourceUID = [ sourceUID compactedString ]; contact = [ adium . contactController existingContactWithService : service contact = [ adium . contactController contactWithService : [ self _serviceForUID : sourceUID ] - ( void ) configurePurpleAccount [ super configurePurpleAccount ]; purple_account_set_bool ( account , "always_use_rv_proxy" , [[ self preferenceForKey : PREFERENCE_FT_PROXY_SERVER group : GROUP_ACCOUNT_STATUS ] boolValue ]); purple_account_set_bool ( account , "allow_multiple_logins" , [[ self preferenceForKey : PREFERENCE_ALLOW_MULTIPLE_LOGINS group : GROUP_ACCOUNT_STATUS ] boolValue ]); //Always yes, so SSL on ICQ works again. Note that we'll disable it if we're using a proxy server. purple_account_set_bool ( account , "use_clientlogin" , TRUE ); if ([[ self preferenceForKey : PREFERENCE_ENCRYPTION_TYPE group : GROUP_ACCOUNT_STATUS ] isEqualToString : PREFERENCE_ENCRYPTION_TYPE_OPPORTUNISTIC ]) { purple_account_set_string ( account , "encryption" , "opportunistic_encryption" ); } else if ([[ self preferenceForKey : PREFERENCE_ENCRYPTION_TYPE group : GROUP_ACCOUNT_STATUS ] isEqualToString : PREFERENCE_ENCRYPTION_TYPE_REQUIRED ]) { purple_account_set_string ( account , "encryption" , "require_encryption" ); purple_account_set_string ( account , "encryption" , "no_encryption" ); - ( void ) continueConnectWithConfiguredProxy PurpleProxyInfo * proxy_info = purple_account_get_proxy_info ( account ); if (( purple_proxy_info_get_type ( proxy_info ) != PURPLE_PROXY_NONE ) && purple_proxy_info_get_host ( proxy_info ) && strlen ( purple_proxy_info_get_host ( proxy_info ))) { /* Proxy servers and client login don't currently get along. This should be fixed in libpurple, but until then, * just don't use it, unless the hidden preference is set. purple_account_set_bool ( account , "use_clientlogin" , [[ NSUserDefaults standardUserDefaults ] boolForKey : @"AIUseClientLoginWithProxies" ]); [ super continueConnectWithConfiguredProxy ]; // ICQ and AIM should override this return self . online && [ self secureConnection ]; - ( PurpleSslConnection * ) secureConnection { PurpleConnection * gc = purple_account_get_connection ( self . purpleAccount ); OscarData * oscarData = ( OscarData * ) gc -> proto_data ; for ( GSList * oscarConnections = oscarData -> oscar_connections ; oscarConnections ; oscarConnections = g_slist_next ( oscarConnections )) { FlapConnection * flapConnection = ( FlapConnection * ) oscarConnections -> data ; if ( flapConnection -> gsc ) { return flapConnection -> gsc ; - ( AIService * ) _serviceForUID: ( NSString * ) contactUID AIService * contactService ; NSString * contactServiceID = nil ; const char firstCharacter = ([ contactUID length ] ? [ contactUID characterAtIndex : 0 ] : '\0' ); //Determine service based on UID if ([ contactUID hasSuffix : @"@mac.com" ]) { contactServiceID = @"libpurple-oscar-Mac" ; } else if ([ contactUID hasSuffix : @"@me.com" ]) { contactServiceID = @"libpurple-oscar-MobileMe" ; } else if ( firstCharacter && ( firstCharacter >= '0' && firstCharacter <= '9' )) { contactServiceID = @"libpurple-oscar-ICQ" ; contactServiceID = @"libpurple-oscar-AIM" ; contactService = [ adium . accountController serviceWithUniqueID : contactServiceID ]; #pragma mark Account Connection - ( AIReconnectDelayType ) shouldAttemptReconnectAfterDisconnectionError: ( NSString ** ) disconnectionError AIReconnectDelayType shouldAttemptReconnect = [ super shouldAttemptReconnectAfterDisconnectionError : disconnectionError ]; if ([ self lastDisconnectionReason ] == PURPLE_CONNECTION_ERROR_INVALID_USERNAME ) { shouldAttemptReconnect = AIReconnectNever ; * disconnectionError = AILocalizedString ( @"The screen name you entered is not registered. Check to ensure you typed it correctly. If it is a new name, you must register it at www.aim.com before you can use it." , "Invalid name on AIM" ); return shouldAttemptReconnect ; - ( NSString * ) connectionStringForStep: ( NSInteger ) step return AILocalizedString ( @"Connecting" , nil ); return AILocalizedString ( @"Screen name sent" , nil ); return AILocalizedString ( @"Password sent" , nil ); return AILocalizedString ( @"Received authorization" , nil ); return AILocalizedString ( @"Connection established" , nil ); return AILocalizedString ( @"Finalizing connection" , nil ); #pragma mark Account status - ( const char * ) purpleStatusIDForStatus: ( AIStatus * ) statusState arguments :( NSMutableDictionary * ) arguments switch ( statusState . statusType ) { case AIAvailableStatusType : statusID = OSCAR_STATUS_ID_AVAILABLE ; statusID = OSCAR_STATUS_ID_AWAY ; case AIInvisibleStatusType : statusID = OSCAR_STATUS_ID_INVISIBLE ; case AIOfflineStatusType : statusID = OSCAR_STATUS_ID_OFFLINE ; - ( BOOL ) shouldSetITMSLinkForNowPlayingStatus #pragma mark Contact notes -( NSString * ) serversideCommentForContact: ( AIListContact * ) theContact NSString * serversideComment = nil ; if ( purple_account_is_connected ( account )) { const char * uidUTF8String = [ theContact . UID UTF8String ]; if (( buddy = purple_find_buddy ( account , uidUTF8String ))) { if (( g = purple_buddy_get_group ( buddy )) && ( od = purple_account_get_connection ( account ) -> proto_data ) && ( comment = aim_ssi_getcomment ( od -> ssi . local , purple_group_get_name ( g ), purple_buddy_get_name ( buddy )))) { comment_utf8 = purple_utf8_try_convert ( comment ); serversideComment = [ NSString stringWithUTF8String : comment_utf8 ]; return serversideComment ; - ( void ) preferencesChangedForGroup: ( NSString * ) group key: ( NSString * ) key object :( AIListObject * ) object preferenceDict: ( NSDictionary * ) prefDict firstTime: ( BOOL ) firstTime [ super preferencesChangedForGroup : group key : key object : object preferenceDict : prefDict firstTime : firstTime ]; if ([ group isEqualToString : PREF_GROUP_NOTES ]) { //If the notification object is a listContact belonging to this account, update the serverside information [ object isKindOfClass : [ AIListContact class ]] && [( AIListContact * ) object account ] == self ) { if ([ key isEqualToString : @"Notes" ]) { NSString * comment = [ object preferenceForKey : @"Notes" [[ super purpleAdapter ] OSCAREditComment : comment forUID : object . UID onAccount : self ]; #pragma mark Delayed updates - ( void ) _performDelayedUpdates: ( NSTimer * ) timer if ([ arrayOfContactsForDelayedUpdates count ]) { AIListContact * theContact = [ arrayOfContactsForDelayedUpdates objectAtIndex : 0 ]; [ theContact setValue : [ self serversideCommentForContact : theContact ] //Request ICQ contacts' info to get the nickname const char * contactUIDUTF8String = [ theContact . UID UTF8String ]; if ( oscar_util_valid_name_icq ( contactUIDUTF8String )) { if (( purple_account_is_connected ( account )) && ( od = purple_account_get_connection ( account ) -> proto_data )) { aim_icq_getalias ( od , contactUIDUTF8String , FALSE , NULL ); [ arrayOfContactsForDelayedUpdates removeObjectAtIndex : 0 ]; [ arrayOfContactsForDelayedUpdates release ]; arrayOfContactsForDelayedUpdates = nil ; [ delayedSignonUpdateTimer invalidate ]; [ delayedSignonUpdateTimer release ]; delayedSignonUpdateTimer = nil ; - ( void ) gotGroupForContact: ( AIListContact * ) theContact if ( ! arrayOfContactsForDelayedUpdates ) arrayOfContactsForDelayedUpdates = [[ NSMutableArray alloc ] init ]; [ arrayOfContactsForDelayedUpdates addObject : theContact ]; if ( ! delayedSignonUpdateTimer ) { delayedSignonUpdateTimer = [[ NSTimer scheduledTimerWithTimeInterval : DELAYED_UPDATE_INTERVAL selector : @selector ( _performDelayedUpdates : ) - ( void ) removeContacts: ( NSArray * ) objects fromGroups: ( NSArray * ) groups for ( AIListObject * object in objects ) { BOOL completelyRemoving = YES ; for ( AIListGroup * group in object . groups ) { if ( ! [ groups containsObject : group ]) { if ( completelyRemoving ) { //Stop any pending delayed updates for these objects [ arrayOfContactsForDelayedUpdates removeObjectsInArray : objects ]; [ super removeContacts : objects fromGroups : groups ]; #pragma mark File transfer - ( void ) acceptFileTransferRequest: ( ESFileTransfer * ) fileTransfer [ super acceptFileTransferRequest : fileTransfer ]; - ( void ) beginSendOfFileTransfer: ( ESFileTransfer * ) fileTransfer [ super _beginSendOfFileTransfer : fileTransfer ]; - ( void ) rejectFileReceiveRequest: ( ESFileTransfer * ) fileTransfer [ super rejectFileReceiveRequest : fileTransfer ]; - ( void ) cancelFileTransfer: ( ESFileTransfer * ) fileTransfer [ super cancelFileTransfer : fileTransfer ]; return [ super canSendFolders ]; * @brief Can we send images for this chat? - ( BOOL ) canSendImagesForChat: ( AIChat * ) inChat if ( inChat . isGroupChat ) return NO ; OscarData * od = (( account && purple_account_get_connection ( account )) ? purple_account_get_connection ( account ) -> proto_data : NULL ); AIListObject * listObject = [ inChat listObject ]; const char * contactUID = [ listObject . UID UTF8String ]; aim_userinfo_t * userinfo = aim_locate_finduserinfo ( od , contactUID ); oscar_util_name_compare ( purple_account_get_username ( account ), contactUID ) && return (( userinfo -> capabilities & OSCAR_CAPABILITY_DIRECTIM ) != 0 ); - ( NSString * ) encodedAttributedString: ( NSAttributedString * ) inAttributedString forListObject: ( AIListObject * ) inListObject static AIHTMLDecoder * encoderCloseFontTagsAttachmentsAsText = nil ; if ( ! encoderCloseFontTagsAttachmentsAsText ) { AIHTMLDecoder * newEncoder = [[ AIHTMLDecoder alloc ] initWithHeaders : YES onlyIncludeOutgoingImages : YES allowJavascriptURLs : YES ]; if ( ! OSAtomicCompareAndSwapPtrBarrier ( nil , newEncoder , ( void * ) & encoderCloseFontTagsAttachmentsAsText )) [ encoderCloseFontTagsAttachmentsAsText setAllowAIMsubprofileLinks : YES ]; return ([ inAttributedString length ] ? [ encoderCloseFontTagsAttachmentsAsText encodeHTML : inAttributedString imagesPath : nil ] : nil ); - ( NSString * ) encodedAttributedStringForSendingContentMessage: ( AIContentMessage * ) inContentMessage AIListObject * inListObject = [ inContentMessage destination ]; NSAttributedString * inAttributedString = inContentMessage . message ; static AIHTMLDecoder * encoderCloseFontTags = nil ; static AIHTMLDecoder * encoderGroupChat = nil ; if ( ! ( encoderCloseFontTags && encoderGroupChat )) { AIHTMLDecoder * newEncoder = [[ AIHTMLDecoder alloc ] initWithHeaders : YES onlyIncludeOutgoingImages : YES allowJavascriptURLs : YES ]; if ( ! OSAtomicCompareAndSwapPtrBarrier ( nil , newEncoder , ( void * ) & encoderCloseFontTags )) newEncoder = [[ AIHTMLDecoder alloc ] initWithHeaders : NO onlyIncludeOutgoingImages : YES allowJavascriptURLs : YES ]; if ( ! OSAtomicCompareAndSwapPtrBarrier ( nil , newEncoder , ( void * ) & encoderGroupChat )) [ encoderCloseFontTags setAllowAIMsubprofileLinks : YES ]; [ encoderGroupChat setAllowAIMsubprofileLinks : YES ]; if ( inContentMessage . chat . isSecure && oscar_util_valid_name_icq ([ inContentMessage . source . UID UTF8String ]) && oscar_util_valid_name_icq ([ inListObject . UID UTF8String ])) { /* If we're an ICQ account and they're an ICQ account, we need to strip HTML now since the * encrypted message won't be able to be processed by libpurple */ encodedString = [[ inAttributedString attributedStringByConvertingLinksToStrings ] string ]; } else if ([ self canSendImagesForChat : inContentMessage . chat ]) { //Encode to HTML and look for outgoing images if the chat supports it encodedString = [ encoderCloseFontTags encodeHTML : inAttributedString if ([ encodedString rangeOfString : @"<IMG " options : NSCaseInsensitiveSearch ]. location != NSNotFound ) { /* There's an image... we need to see about a Direct Connect, aborting the send attempt if none is established * and sending after it is if one is established. * Note that an encrypted session won't ever be able to succeed with a DirectIM at present, because * libpurple only sees the encrypted message :( //Check for a PeerConnection for a direct IM currently open OscarData * od = ( OscarData * ) purple_account_get_connection ( account ) -> proto_data ; const char * who = [ inListObject . UID UTF8String ]; conn = peer_connection_find_by_type ( od , who , OSCAR_CAPABILITY_DIRECTIM ); encodedString = [ self stringByProcessingImgTagsForDirectIM : encodedString forContactWithUID : who ]; if (( conn != NULL ) && ( conn -> ready )) { //We have a connected dim already; simply continue, and we'll be told to send it in a moment //Either no dim, or the dim we have is no longer conected (oscar_direct_im_initiate_immediately will reconnect it) peer_connection_propose ( od , OSCAR_CAPABILITY_DIRECTIM , who ); //Add this content message to the sending queue for this contact to be sent once a connection is established if ( ! directIMQueue ) directIMQueue = [[ NSMutableDictionary alloc ] init ]; NSMutableArray * thisContactQueue = [ directIMQueue objectForKey : inListObject . internalObjectID ]; thisContactQueue = [ NSMutableArray array ]; [ directIMQueue setObject : thisContactQueue forKey : inListObject . internalObjectID ]; [ thisContactQueue addObject : inContentMessage ]; encodedString = [ self encodedAttributedString : inAttributedString forListObject : inListObject ]; } else { //Send HTML when signed in as an AIM account and we don't know what sort of user we are sending to (most likely multiuser chat) AILog ( @"Encoding %@ for no contact" , inAttributedString ); encodedString = [ encoderGroupChat encodeHTML : inAttributedString - ( BOOL ) sendMessageObject: ( AIContentMessage * ) inContentMessage NSMutableArray * thisContactQueue = [ directIMQueue objectForKey : [[ inContentMessage destination ] internalObjectID ]]; if ([ thisContactQueue containsObject : inContentMessage ]) { //This message is in our queue of messages to send... OscarData * od = ( OscarData * ) purple_account_get_connection ( account ) -> proto_data ; const char * who = [ inContentMessage . destination . UID UTF8String ]; conn = peer_connection_find_by_type ( od , who , OSCAR_CAPABILITY_DIRECTIM ); if (( conn != NULL ) && ( conn -> ready )) { //We have a connected dim ready; send it! We already displayed it, though, so don't do that. [ inContentMessage setDisplayContent : NO ]; return [ super sendMessageObject : inContentMessage ]; //Don't send now, as we'll do the actual send when the dim is connected, in directIMConnected: above, and return here. BOOL success = [ super sendMessageObject : inContentMessage ]; if ( purpleImagesToUnref ) { NSNumber * imgstoreNumber ; for ( imgstoreNumber in purpleImagesToUnref ) { purple_imgstore_unref_by_id ([ imgstoreNumber intValue ]); [ purpleImagesToUnref release ]; purpleImagesToUnref = nil ; - ( BOOL ) shouldSendAutoreplyToMessage: ( AIContentMessage * ) message return ! [[ message . message string ] hasPrefix : @"[Offline IM sent" ]; #pragma mark DirectIM (IM Image) //We are now connected via DirectIM to theContact - ( void ) directIMConnected: ( AIListContact * ) theContact AILog ( @"Direct IM Connected: %@" , theContact . UID ); [ adium . contentController displayEvent : AILocalizedString ( @"Direct IM connected" , "Direct IM is an AIM-specific phrase for transferring images in the message window" ) ofType : @"directIMConnected" inChat :[ adium . chatController chatWithContact : theContact ]]; //Send any pending directIM messages for this contact NSMutableArray * thisContactQueue = [ directIMQueue objectForKey : theContact . internalObjectID ]; AIContentObject * contentObject ; for ( contentObject in thisContactQueue ) { [ adium . contentController sendContentObject : contentObject ]; [ directIMQueue removeObjectForKey : theContact . internalObjectID ]; if ( ! [ directIMQueue count ]) { [ directIMQueue release ]; directIMQueue = nil ; - ( void ) directIMDisconnected: ( AIListContact * ) theContact AILog ( @"Direct IM Disconnected: %@" , theContact . UID ); [ adium . contentController displayEvent : AILocalizedString ( @"Direct IM disconnected" , "Direct IM is an AIM-specific phrase for transferring images in the message window" ) ofType : @"directIMDisconnected" inChat :[ adium . chatController chatWithContact : theContact ]]; - ( NSString * ) stringByProcessingImgTagsForDirectIM: ( NSString * ) inString forContactWithUID: ( const char * ) who static NSCharacterSet * elementEndCharacters = nil ; if ( ! elementEndCharacters ) elementEndCharacters = [[ NSCharacterSet characterSetWithCharactersInString : @" >" ] retain ]; static NSString * tagStart = @"<" , * tagEnd = @">" ; NSMutableString * processedString ; scanner = [ NSScanner scannerWithString : inString ]; [ scanner setCaseSensitive : NO ]; [ scanner setCharactersToBeSkipped : [ NSCharacterSet characterSetWithCharactersInString : @"" ]]; processedString = [[ NSMutableString alloc ] init ]; while ( ! [ scanner isAtEnd ]) { if ([ scanner scanUpToString : @"<img" intoString :& chunkString ]) { //Append the text leading up the the IMG tag; a directIM may have image tags inline with message text [ processedString appendString : chunkString ]; //Look for the start of a tag if ([ scanner scanString : tagStart intoString : nil ]) { if ([ scanner scanUpToCharactersFromSet : elementEndCharacters intoString :& chunkString ]) { if ([ chunkString caseInsensitiveCompare : @"IMG" ] == NSOrderedSame ) { if ([ scanner scanUpToString : tagEnd intoString :& chunkString ]) { NSDictionary * imgArguments = [ AIHTMLDecoder parseArguments : chunkString ]; NSString * source = [ imgArguments objectForKey : @"src" ]; NSString * alt = [ imgArguments objectForKey : @"alt" ]; NSData * imageData = [ NSData dataWithContentsOfURL : [ NSURL URLWithString : source ]]; BOOL requiresConversionToJPEG = NO ; //Store the src image's data purpleside filename = ( alt ? alt : [ source lastPathComponent ]); extension = [ filename pathExtension ]; extension = [ NSImage extensionForBitmapImageFileType : [ NSImage fileTypeOfData : imageData ]]; if (([ extension caseInsensitiveCompare : @"jpg" ] != NSOrderedSame ) && ([ extension caseInsensitiveCompare : @"jpeg" ] != NSOrderedSame ) && ([ extension caseInsensitiveCompare : @"gif" ] != NSOrderedSame )) { //Old versions of AIM for Windows only supports JPEG and GIF images, so we need to pick a format it does support. aim_userinfo_t * userinfo ; if ( purple_account_is_connected ( account ) && ( od = purple_account_get_connection ( account ) -> proto_data ) && ( userinfo = aim_locate_finduserinfo ( od , who ))) { /* There's no explicit AIM for Windows capability, but only AIM for Windows advertises AOL's AIM games. * Not all AIM for Windows clients advertise those, though. OSCAR_CAPABILITY_ADDINS is only advertised * by official AIM clients, so it's a good sensitive-but-not-specific test. if (( userinfo -> capabilities & OSCAR_CAPABILITY_GAMES ) || ( userinfo -> capabilities & OSCAR_CAPABILITY_GAMES2 ) || ( userinfo -> capabilities & OSCAR_CAPABILITY_ADDINS )) requiresConversionToJPEG = YES ; if ( requiresConversionToJPEG ) { NSImage * image = [[ NSImage alloc ] initWithData : imageData ]; imageData = [[[ image JPEGRepresentationWithCompressionFactor : 1.0f ] retain ] autorelease ]; } else if ( ! [ extension length ]) { //We don't know what we're working with. Try to produce a PNG so we know the format. NSImage * image = [[ NSImage alloc ] initWithData : imageData ]; imageData = [[[ image PNGRepresentation ] retain ] autorelease ]; //Delete any existing wrong extension if ([[ filename pathExtension ] caseInsensitiveCompare : extension ] != NSOrderedSame ) filename = [ filename stringByDeletingPathExtension ]; //Add the right extension if needed if ( ! [[ filename pathExtension ] length ]) filename = [ filename stringByAppendingPathExtension : extension ]; NSUInteger imgBytesLength = [ imageData length ]; gpointer imgBytes = malloc ( imgBytesLength ); [ imageData getBytes : imgBytes ]; /* purple_imgstore_add_with_id() will take ownership of imgBytes and free it when done*/ NSInteger imgstore = purple_imgstore_add_with_id ( imgBytes , imgBytesLength , [ filename UTF8String ]); AILog ( @"Adding image id %li with name %s" , imgstore , ( filename ? [ filename UTF8String ] : "(null)" )); NSString * newTag = [ NSString stringWithFormat : @"<IMG ID= \" %li \" CLASS= \" scaledToFitImage \" >" , imgstore ]; [ processedString appendString : newTag ]; if ( ! purpleImagesToUnref ) purpleImagesToUnref = [[ NSMutableSet alloc ] init ]; [ purpleImagesToUnref addObject : [ NSNumber numberWithInteger : imgstore ]]; if ( ! [ scanner isAtEnd ]) { [ scanner setScanLocation : [ scanner scanLocation ] + 1 ]; return ([ processedString autorelease ]); * @brief Should set aliases serverside? * AIM and ICQ support serverside aliases. - ( BOOL ) shouldSetAliasesServerside * @brief Update the status message and away state of the contact - ( void ) updateStatusForContact: ( AIListContact * ) theContact toStatusType :( NSNumber * ) statusTypeNumber statusName :( NSString * ) statusName statusMessage :( NSAttributedString * ) inStatusMessage * Libpurple, as of 2.0.0 but not before so far as we've seen, sometimes feeds us truncated AIM away messages. * The full message will then follow... followed by the truncated one... and so on. This makes the message * change in the buddy list and in the message window repeatedly. * I'm not sure how long the truncated versions are - I've seen 40 to 70 characters. We'll therefore ignore * an incoming message which is the same as the first 40 characters of the existing one. I wonder how long * before someone will notice this "odd" behavior and file a bug report... -evands if (( theContact . statusType == [ statusTypeNumber integerValue ]) && (( statusName && ! theContact . statusName ) || [ theContact . statusName isEqualToString : statusName ])) { NSString * currentStatusMessage = [ theContact statusMessageString ]; if ( currentStatusMessage && ([ currentStatusMessage length ] > [ inStatusMessage length ]) && ([ inStatusMessage length ] >= 40 ) && ([ currentStatusMessage rangeOfString : [ inStatusMessage string ] options : NSAnchoredSearch ]. location == 0 )) { /* New message is shorter but at least 40 characters, and it matches the start of the current one. [ super updateStatusForContact : theContact toStatusType : statusTypeNumber statusName : statusName statusMessage : inStatusMessage isMobile : isMobile ]; * @brief Is a contact on the contact list intentionally listed? * This is used by AIListContact to determine if the prescence of itself on the list is indicative of a degree * of trust, for preferences such as "automatically accept files from contacts on my contact list". - ( BOOL ) isContactIntentionallyListed: ( AIListContact * ) contact //if the only group it's in is recent buddies, it's not intentionally listed return ! ( contact . countOfRemoteGroupNames == 1 && [ contact . remoteGroupNames containsObject : @"Recent Buddies" ]); - ( NSString * ) localizedDateAndTimeFromString: ( NSString * ) inDateAndTime includeTimeWithDay: ( BOOL ) includeTimeWithDay NSString * replacementString = nil ; if ( inDateAndTime && ( strptime ([ inDateAndTime UTF8String ], "%c" , & tm ) != NULL )) { __block NSString * valueDay , * valueTime ; /* Not set by strptime(); tells mktime() * to determine whether daylight saving time date = [ NSDate dateWithTimeIntervalSince1970 : mktime ( & tm )]; [ NSDateFormatter withLocalizedDateFormatterPerform :^ ( NSDateFormatter * dayFormatter ){ valueDay = [[ dayFormatter stringForObjectValue : date ] retain ]; [ NSDateFormatter withLocalizedDateFormatterShowingSeconds : NO showingAMorPM : YES perform :^ ( NSDateFormatter * timeFormatter ) { valueTime = [[ timeFormatter stringForObjectValue : date ] retain ]; if ( valueDay && valueTime ) { [ NSDateFormatter withLocalizedDateFormatterPerform :^ ( NSDateFormatter * dayFormatter ){ cond = [[ dayFormatter stringForObjectValue : [ NSDate date ]] isEqualToString : valueDay ]; replacementString = valueTime ; replacementString = ( includeTimeWithDay ? [ NSString stringWithFormat : @"%@, %@" , valueDay , valueTime ] : valueDay ); return replacementString ; - ( NSMutableArray * ) arrayOfDictionariesFromPurpleNotifyUserInfo: ( PurpleNotifyUserInfo * ) user_info forContact: ( AIListContact * ) contact NSMutableArray * array = [ super arrayOfDictionariesFromPurpleNotifyUserInfo : user_info forContact : contact ]; NSString * onlineSinceKey = [ NSString stringWithUTF8String : _ ( "Online Since" )]; NSString * memberSinceKey = [ NSString stringWithUTF8String : _ ( "Member Since" )]; NSUInteger count = [ array count ]; for ( i = 0 ; i < count ; i ++ ) { NSDictionary * dict = [ array objectAtIndex : i ]; NSString * key = [ dict objectForKey : KEY_KEY ]; if ([ key isEqualToString : onlineSinceKey ]) { NSString * replacementString = [ self localizedDateAndTimeFromString : [ dict objectForKey : KEY_VALUE ] NSMutableDictionary * replacementDict = [ dict mutableCopy ]; [ replacementDict setObject : replacementString forKey : KEY_VALUE ]; [ array replaceObjectAtIndex : i withObject : replacementDict ]; [ replacementDict release ]; } else if ([ key isEqualToString : memberSinceKey ]) { [ array removeObjectAtIndex : i ]; //Decrement i so we look at the next item rather than skipping it #pragma mark Contact List Menu Items - ( NSString * ) titleForContactMenuLabel: ( const char * ) label forContact: ( AIListContact * ) inContact if ( strcmp ( label , _ ( "Edit Buddy Comment" )) == 0 ) { } else if ( strcmp ( label , _ ( "Re-request Authorization" )) == 0 ) { return [ NSString stringWithFormat : AILocalizedString ( @"Re-request Authorization from %@" , nil ), inContact . formattedUID ]; } else if ( strcmp ( label , _ ( "Get AIM Info" )) == 0 ) { } else if ( strcmp ( label , _ ( "Direct IM" )) == 0 ) { return [ NSString stringWithFormat : AILocalizedString ( @"Initiate Direct IM with %@" , nil ), inContact . formattedUID ]; return [ super titleForContactMenuLabel : label forContact : inContact ]; #pragma mark Account Action Menu Items - ( NSString * ) titleForAccountActionMenuLabel: ( const char * ) label if ( strcmp ( label , _ ( "Set User Info..." )) == 0 ) { } else if ( strcmp ( label , _ ( "Edit Buddy Comment" )) == 0 ) { } else if ( strcmp ( label , _ ( "Show Buddies Awaiting Authorization" )) == 0 ) { return AILocalizedString ( @"Show Contacts Awaiting Authorization" , "Account action menu item to show a list of contacts for whom this account is awaiting authorization to be able to show them in the contact list" ); } else if ( strcmp ( label , _ ( "Configure IM Forwarding (URL)" )) == 0 ) { return [ AILocalizedString ( @"Configure IM Forwarding" , nil ) stringByAppendingEllipsis ]; } else if ( strcmp ( label , _ ( "Change Password (URL)" )) == 0 ) { //There's no reason to have the URL version available - we have Change Password for in-app changing. } else if ( strcmp ( label , _ ( "Display Currently Registered E-Mail Address" )) == 0 ) { return AILocalizedString ( @"Display Currently Registered Email Address" , nil ); } else if ( strcmp ( label , _ ( "Change Currently Registered E-Mail Address..." )) == 0 ) { return [ AILocalizedString ( @"Change Currently Registered Email Address" , nil ) stringByAppendingEllipsis ]; } else if ( strcmp ( label , _ ( "Search for Buddy by E-Mail Address..." )) == 0 ) { return [ AILocalizedString ( @"Search for Contact By Email Address" , nil ) stringByAppendingEllipsis ]; } else if ( strcmp ( label , _ ( "Set User Info (URL)..." )) == 0 ) { return [ AILocalizedString ( @"Set User Info" , nil ) stringByAppendingEllipsis ]; } else if ( strcmp ( label , _ ( "Set Privacy Options..." )) == 0 ) { return [ AILocalizedString ( @"Set Privacy Options" , nil ) stringByAppendingEllipsis ]; return [ super titleForAccountActionMenuLabel : label ]; #pragma mark Buddy status - ( NSString * ) statusNameForPurpleBuddy: ( PurpleBuddy * ) buddy NSString * statusName = nil ; if ( oscar_util_valid_name_icq ( purple_buddy_get_name ( buddy ))) { PurplePresence * presence = purple_buddy_get_presence ( buddy ); PurpleStatus * status = purple_presence_get_active_status ( presence ); const char * purpleStatusID = purple_status_get_id ( status ); if ( ! strcmp ( purpleStatusID , OSCAR_STATUS_ID_INVISIBLE )) { statusName = STATUS_NAME_INVISIBLE ; } else if ( ! strcmp ( purpleStatusID , OSCAR_STATUS_ID_OCCUPIED )) { statusName = STATUS_NAME_OCCUPIED ; } else if ( ! strcmp ( purpleStatusID , OSCAR_STATUS_ID_NA )) { statusName = STATUS_NAME_NOT_AVAILABLE ; } else if ( ! strcmp ( purpleStatusID , OSCAR_STATUS_ID_DND )) { statusName = STATUS_NAME_DND ; } else if ( ! strcmp ( purpleStatusID , OSCAR_STATUS_ID_FREE4CHAT )) { statusName = STATUS_NAME_FREE_FOR_CHAT ;