Fixed localization of the 'Sticky' checkbox in the Growl action
[adiumx.git] / Source / AIContentController.m
blob94e6a68c659fa4e362e87290aac19d04178355e4
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 // $Id$
19 #import "AIContentController.h"
21 #import "AdiumTyping.h"
22 #import "AdiumFormatting.h"
23 #import "AdiumMessageEvents.h"
24 #import "AdiumContentFiltering.h"
26 #import <Adium/AIAccountControllerProtocol.h>
27 #import <Adium/AIChatControllerProtocol.h>
28 #import <Adium/AIContactControllerProtocol.h>
29 #import <Adium/AIInterfaceControllerProtocol.h>
30 #import <Adium/AIPreferenceControllerProtocol.h>
31 #import <Adium/AIContactAlertsControllerProtocol.h>
32 #import <Adium/AIFileTransferControllerProtocol.h>
33 #import <Adium/AIAccount.h>
34 #import <Adium/AIChat.h>
35 #import <Adium/AIContentMessage.h>
36 #import <Adium/AIContentObject.h>
37 #import <Adium/AIContentEvent.h>
38 #import <Adium/AIHTMLDecoder.h>
39 #import <Adium/AIListContact.h>
40 #import <Adium/AIListGroup.h>
41 #import <Adium/AIListObject.h>
42 #import <Adium/AIMetaContact.h>
43 #import <Adium/ESFileTransfer.h>
44 #import <Adium/AITextAttachmentExtension.h>
45 #import <AIUtilities/AIArrayAdditions.h>
46 #import <AIUtilities/AIAttributedStringAdditions.h>
47 #import <AIUtilities/AIColorAdditions.h>
48 #import <AIUtilities/AIDictionaryAdditions.h>
49 #import <AIUtilities/AIFontAdditions.h>
50 #import <AIUtilities/AIMenuAdditions.h>
51 #import <AIUtilities/AIStringAdditions.h>
52 #import <AIUtilities/AITextAttachmentAdditions.h>
53 #import <AIUtilities/AITextAttributes.h>
54 #import <AIUtilities/AIImageAdditions.h>
56 @interface AIContentController (PRIVATE)
57 - (void)finishReceiveContentObject:(AIContentObject *)inObject;
58 - (void)finishSendContentObject:(AIContentObject *)inObject;
59 - (void)finishDisplayContentObject:(AIContentObject *)inObject;
61 - (BOOL)processAndSendContentObject:(AIContentObject *)inContentObject;
62 @end
64 /*!
65  * @class AIContentController
66  * @brief Controller to manage incoming and outgoing content and chats.
67  *
68  * This controller handles default formatting and text entry filters, which can respond as text is entered in a message
69  * window.  It the center for content filtering, including registering/unregistering of content filters.
70  * It handles sending and receiving of content objects.  It manages chat observers, which are objects notified as
71  * status objects are set and removed on AIChat objects.  It manages chats themselves, tracking open ones, closing
72  * them when needed, etc.  Finally, it provides Events related to sending and receiving content, such as Message Received.
73  */
74 @implementation AIContentController
76 /*!
77  * @brief Initialize the controller
78  */
79 - (id)init
81         if ((self = [super init])) {
82                 adiumTyping = [[AdiumTyping alloc] init];
83                 adiumFormatting = [[AdiumFormatting alloc] init];
84                 adiumContentFiltering = [[AdiumContentFiltering alloc] init];
85                 adiumMessageEvents = [[AdiumMessageEvents alloc] init];
87                 objectsBeingReceived = [[NSMutableSet alloc] init];
88         }
89         
90         return self;
93 - (void)controllerDidLoad
95         [adiumFormatting controllerDidLoad];
96         [adiumMessageEvents controllerDidLoad];
99 /*!
100  * @brief Close the controller
101  */
102 - (void)controllerWillClose
108  * @brief Deallocate
109  */
110 - (void)dealloc
112         [objectsBeingReceived release]; objectsBeingReceived = nil;
113         [adiumTyping release]; adiumTyping = nil;
114         [adiumFormatting release]; adiumFormatting = nil;
115         [adiumContentFiltering release]; adiumContentFiltering = nil;
116         [adiumEncryptor release];
118     [super dealloc];
122  * @brief Set the encryptor
124  * NB: We must _always_ have an encryptor.
125  */
126 - (void)setEncryptor:(id<AdiumMessageEncryptor>)inEncryptor
128         NSParameterAssert([inEncryptor conformsToProtocol:@protocol(AdiumMessageEncryptor)]);
130         [adiumEncryptor release];
131         adiumEncryptor = [inEncryptor retain];
135 #pragma mark Typing
137  * @brief User is currently changing the content in a chat
139  * This should  be called by a text entry control like an NSTextView.
141  * @param chat The chat
142  * @param hasEnteredText YES if there are one or more characters typed into the text entry area
143  */
144 - (void)userIsTypingContentForChat:(AIChat *)chat hasEnteredText:(BOOL)hasEnteredText {
145         [adiumTyping userIsTypingContentForChat:chat hasEnteredText:hasEnteredText];
148 #pragma mark Formatting
149 - (NSDictionary *)defaultFormattingAttributes {
150         return [adiumFormatting defaultFormattingAttributes];
153 #pragma mark Content Filtering
154 - (void)registerContentFilter:(id <AIContentFilter>)inFilter
155                                            ofType:(AIFilterType)type
156                                         direction:(AIFilterDirection)direction {
157         [adiumContentFiltering registerContentFilter:inFilter ofType:type direction:direction];
159 - (void)registerDelayedContentFilter:(id <AIDelayedContentFilter>)inFilter
160                                                           ofType:(AIFilterType)type
161                                                    direction:(AIFilterDirection)direction {
162         [adiumContentFiltering registerDelayedContentFilter:inFilter ofType:type direction:direction];
164 - (void)unregisterContentFilter:(id <AIContentFilter>)inFilter {
165         [adiumContentFiltering unregisterContentFilter:inFilter];
167 - (void)registerFilterStringWhichRequiresPolling:(NSString *)inPollString {
168         [adiumContentFiltering registerFilterStringWhichRequiresPolling:inPollString];
170 - (BOOL)shouldPollToUpdateString:(NSString *)inString {
171         return [adiumContentFiltering shouldPollToUpdateString:inString];
173 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)attributedString
174                                                            usingFilterType:(AIFilterType)type
175                                                                          direction:(AIFilterDirection)direction
176                                                                            context:(id)context {
177         return [adiumContentFiltering filterAttributedString:attributedString
178                                                                                  usingFilterType:type
179                                                                                            direction:direction
180                                                                                                  context:context];
182 - (void)filterAttributedString:(NSAttributedString *)attributedString
183                            usingFilterType:(AIFilterType)type
184                                          direction:(AIFilterDirection)direction
185                                  filterContext:(id)filterContext
186                            notifyingTarget:(id)target
187                                           selector:(SEL)selector
188                                            context:(id)context {
189         [adiumContentFiltering filterAttributedString:attributedString
190                                                                   usingFilterType:type
191                                                                                 direction:direction
192                                                                         filterContext:filterContext
193                                                                   notifyingTarget:target
194                                                                                  selector:selector
195                                                                                   context:context];
197 - (void)delayedFilterDidFinish:(NSAttributedString *)attributedString uniqueID:(unsigned long long)uniqueID
199         [adiumContentFiltering delayedFilterDidFinish:attributedString
200                                                                                  uniqueID:uniqueID];
203 //Messaging ------------------------------------------------------------------------------------------------------------
204 #pragma mark Messaging
205 //Receiving step 1: Add an incoming content object - entry point
206 - (void)receiveContentObject:(AIContentObject *)inObject
208         if (inObject) {
209                 AIChat                  *chat = [inObject chat];
211                 //Only proceed if the contact is not ignored
212                 if (![chat isListContactIgnored:[inObject source]]) {
213                         //Notify: Will Receive Content
214                         if ([inObject trackContent]) {
215                                 [[adium notificationCenter] postNotificationName:Content_WillReceiveContent
216                                                                                                                   object:chat
217                                                                                                                 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
218                         }
220                         //Run the object through our incoming content filters
221                         if ([inObject filterContent]) {
222                                 //Track that we are in the process of receiving this object
223                                 [objectsBeingReceived addObject:inObject];
225                                 [self filterAttributedString:[inObject message]
226                                                          usingFilterType:AIFilterContent
227                                                                    direction:AIFilterIncoming
228                                                            filterContext:inObject
229                                                          notifyingTarget:self
230                                                                         selector:@selector(didFilterAttributedString:receivingContext:)
231                                                                          context:inObject];
232                                 
233                         } else {
234                                 [self finishReceiveContentObject:inObject];
235                         }
236                 }
237     }
240 //Receiving step 2: filtering callback
241 - (void)didFilterAttributedString:(NSAttributedString *)filteredMessage receivingContext:(AIContentObject *)inObject
243         [inObject setMessage:filteredMessage];
244         
245         [self finishReceiveContentObject:inObject];
248 //Receiving step 3: Display the content
249 - (void)finishReceiveContentObject:(AIContentObject *)inObject
251         //Display the content
252         [self displayContentObject:inObject];
255 //Sending step 1: Entry point for any method in Adium which sends content
257  * @brief Send a content object
259  * Sending step 1: Public method to send a content object.
261  * This method checks to be sure that messages are sent by accounts in the order they are sent by the user;
262  * this can only be problematic when a delayedFilter is involved, leading to the user sending more messages before
263  * the first finished sending.
264  */
265 - (BOOL)sendContentObject:(AIContentObject *)inObject
267         //Only proceed if the chat allows it; if it doesn't, it will handle calling this method again when it is ready
268         if ([[inObject chat] willBeginSendingContentObject:inObject]) {
270                 //Run the object through our outgoing content filters
271                 if ([inObject filterContent]) {
272                         //Track that we are in the process of send this object
273                         [objectsBeingReceived addObject:inObject];
275                         [self filterAttributedString:[inObject message]
276                                                  usingFilterType:AIFilterContent
277                                                            direction:AIFilterOutgoing
278                                                    filterContext:inObject
279                                                  notifyingTarget:self
280                                                                 selector:@selector(didFilterAttributedString:contentSendingContext:)
281                                                                  context:inObject];
282                         
283                 } else {
284                         [self finishSendContentObject:inObject];
285                 }
286         }
288         // XXX
289         return YES;
292 //Sending step 2: Sending filter callback
293 -(void)didFilterAttributedString:(NSAttributedString *)filteredString contentSendingContext:(AIContentObject *)inObject
295         [inObject setMessage:filteredString];
297         //Special outgoing content filter for AIM away message bouncing.  Used to filter %n,%t,...
298         if ([inObject isKindOfClass:[AIContentMessage class]] && [(AIContentMessage *)inObject isAutoreply]) {
299                 [self filterAttributedString:[inObject message]
300                                          usingFilterType:AIFilterAutoReplyContent
301                                                    direction:AIFilterOutgoing
302                                            filterContext:inObject
303                                          notifyingTarget:self
304                                                         selector:@selector(didFilterAttributedString:autoreplySendingContext:)
305                                                          context:inObject];
306         } else {                
307                 [self finishSendContentObject:inObject];
308         }
311 //Sending step 3, applicable only when sending an autreply: Filter callback
312 -(void)didFilterAttributedString:(NSAttributedString *)filteredString autoreplySendingContext:(AIContentObject *)inObject
314         [inObject setMessage:filteredString];
316         [self finishSendContentObject:inObject];
319 //Sending step 4: Post notifications and ask the account to actually send the content.
320 - (void)finishSendContentObject:(AIContentObject *)inObject
322     AIChat              *chat = [inObject chat];
323         
324         //Notify: Will Send Content
325     if ([inObject trackContent]) {
326         [[adium notificationCenter] postNotificationName:Content_WillSendContent
327                                                                                                   object:chat 
328                                                                                                 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
329     }
330         
331     //Send the object
332         if ([inObject sendContent]) {
333                 if ([self processAndSendContentObject:inObject]) {
334                         if ([inObject displayContent]) {
335                                 //Add the object
336                                 [self displayContentObject:inObject];
338                         } else {
339                                 //We are no longer in the process of receiving this object
340                                 [objectsBeingReceived removeObject:inObject];
341                         }
342                         
343                         if ([inObject trackContent]) {
344                                 //Did send content
345                                 [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_SENT
346                                                                                                  forListObject:[chat listObject]
347                                                                                                           userInfo:[NSDictionary dictionaryWithObjectsAndKeys:chat,@"AIChat",inObject,@"AIContentObject",nil]
348                                                                   previouslyPerformedActionIDs:nil];
349                                 
350                                 [chat setHasSentOrReceivedContent:YES];
351                         }
353                 } else {
354                         //We are no longer in the process of receiving this object
355                         [objectsBeingReceived removeObject:inObject];
356                         
357                         NSString *message = [NSString stringWithFormat:AILocalizedString(@"Could not send from %@ to %@",nil),
358                                 [[inObject source] formattedUID],[[inObject destination] formattedUID]];
360                         [self displayEvent:message
361                                                 ofType:@"chat-error"
362                                                 inChat:chat];                   
363                 }
364         }
365         
366         //Let the chat know we finished sending
367         [chat finishedSendingContentObject:inObject];
370 //Display a content object
371 //Add content to the message view.  Doesn't do any sending or receiving, just adds the content.
372 - (void)displayContentObject:(AIContentObject *)inObject usingContentFilters:(BOOL)useContentFilters
374         [self displayContentObject:inObject usingContentFilters:useContentFilters immediately:NO];
377 //Immediately YES means the main thread will halt until the content object is displayed;
378 //Immediately NO shuffles it off into the filtering thread, which will handle content sequentially but allows the main
379 //thread to continue operation.  
380 //This facility primarily exists for message history, which needs to put its display in before the first message;
381 //without this, the use of threaded filtering means that message history shows up after the first message.
382 - (void)displayContentObject:(AIContentObject *)inObject usingContentFilters:(BOOL)useContentFilters immediately:(BOOL)immediately
384         if (useContentFilters) {
385                 
386                 if (immediately) {
387                         //Filter in the main thread, set the message, and continue
388                         [inObject setMessage:[self filterAttributedString:[inObject message]
389                                                                                           usingFilterType:AIFilterContent
390                                                                                                         direction:([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming)
391                                                                                                           context:inObject]];
392                         [self displayContentObject:inObject immediately:YES];
393                         
394                         
395                 } else {
396                         //Filter in the filter thread
397                         [self filterAttributedString:[inObject message]
398                                                  usingFilterType:AIFilterContent
399                                                            direction:([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming)
400                                                    filterContext:inObject
401                                                  notifyingTarget:self
402                                                                 selector:@selector(didFilterAttributedString:contentFilterDisplayContext:)
403                                                                  context:inObject];
404                 }
405         } else {
406                 //Just continue
407                 [self displayContentObject:inObject immediately:immediately];
408         }
411 - (void)didFilterAttributedString:(NSAttributedString *)filteredString contentFilterDisplayContext:(AIContentObject *)inObject
413         [inObject setMessage:filteredString];
414         
415         //Continue
416         [self displayContentObject:inObject immediately:NO];
419 //Display a content object
420 //Add content to the message view.  Doesn't do any sending or receiving, just adds the content.
421 - (void)displayContentObject:(AIContentObject *)inObject
423         [self displayContentObject:inObject immediately:NO];
426 - (void)displayContentObject:(AIContentObject *)inObject immediately:(BOOL)immediately
428     //Filter the content object
429     if ([inObject filterContent]) {
430                 BOOL                            message = ([inObject isKindOfClass:[AIContentMessage class]] && ![(AIContentMessage *)inObject isAutoreply]);
431                 AIFilterType            filterType = (message ? AIFilterMessageDisplay : AIFilterDisplay);
432                 AIFilterDirection       direction = ([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming);
433                 
434                 if (immediately) {
435                         
436                         //Set it after filtering in the main thread, then display it
437                         [inObject setMessage:[self filterAttributedString:[inObject message]
438                                                                                           usingFilterType:filterType
439                                                                                                         direction:direction
440                                                                                                           context:inObject]];
441                         [self finishDisplayContentObject:inObject];             
442                         
443                 } else {
444                         //Filter in the filtering thread
445                         [self filterAttributedString:[inObject message]
446                                                  usingFilterType:filterType
447                                                            direction:direction
448                                                    filterContext:inObject
449                                                  notifyingTarget:self
450                                                                 selector:@selector(didFilterAttributedString:displayContext:)
451                                                                  context:inObject];
452                 }
453                 
454     } else {
455                 [self finishDisplayContentObject:inObject];
456         }
460 - (void)didFilterAttributedString:(NSAttributedString *)filteredString displayContext:(AIContentObject *)inObject
462         [inObject setMessage:filteredString];
463         
464         [self finishDisplayContentObject:inObject];
467 - (void)finishDisplayContentObject:(AIContentObject *)inObject
469     //Check if the object should display
470     if ([inObject displayContent] && ([[inObject message] length] > 0)) {
471                 AIChat                  *chat = [inObject chat];
472                 NSDictionary    *userInfo;
473                 BOOL                    contentReceived, shouldPostContentReceivedEvents;
475                 //If the chat of the content object has been cleared, we can't do anything with it, so simply return
476                 if (!chat) return;
477                 
478                 contentReceived = (([inObject isMemberOfClass:[AIContentMessage class]]) &&
479                                                    (![inObject isOutgoing]));
480                 shouldPostContentReceivedEvents = contentReceived && [inObject trackContent];
481                 
482                 if (![chat isOpen]) {
483                         /* Tell the interface to open the chat
484                          * For incoming messages, we don't open the chat until we're sure that new content is being received.
485                          */
486                         [[adium interfaceController] openChat:chat];
487                 }
489                 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:chat, @"AIChat", inObject, @"AIContentObject", nil];
491                 //Notify: Content Object Added
492                 [[adium notificationCenter] postNotificationName:Content_ContentObjectAdded
493                                                                                                   object:chat
494                                                                                                 userInfo:userInfo];             
495                 
496                 if (shouldPostContentReceivedEvents) {
497                         NSSet                   *previouslyPerformedActionIDs = nil;
498                         AIListObject    *listObject = [chat listObject];
500                         if (![chat hasSentOrReceivedContent]) {
501                                 //If the chat wasn't open before, generate CONTENT_MESSAGE_RECEIVED_FIRST
502                                 if (![chat isGroupChat]) {
503                                         previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED_FIRST
504                                                                                                                                                                 forListObject:listObject
505                                                                                                                                                                          userInfo:userInfo
506                                                                                                                                  previouslyPerformedActionIDs:nil];
507                                 }
508                                 [chat setHasSentOrReceivedContent:YES];
509                         }
510                         
511                         if (chat != [[adium interfaceController] activeChat]) {
512                                 //If the chat is not currently active, generate CONTENT_MESSAGE_RECEIVED_BACKGROUND
513                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:([chat isGroupChat] ? CONTENT_MESSAGE_RECEIVED_BACKGROUND_GROUP : CONTENT_MESSAGE_RECEIVED_BACKGROUND)
514                                                                                                                                                                 forListObject:listObject
515                                                                                                                                                                          userInfo:userInfo
516                                                                                                                                  previouslyPerformedActionIDs:previouslyPerformedActionIDs];                                    
517                         }
518                         
519                         [[adium contactAlertsController] generateEvent:([chat isGroupChat] ? CONTENT_MESSAGE_RECEIVED_GROUP : CONTENT_MESSAGE_RECEIVED)
520                                                                                          forListObject:listObject
521                                                                                                   userInfo:userInfo
522                                                           previouslyPerformedActionIDs:previouslyPerformedActionIDs];                           
523                 }               
524     }
526         //We are no longer in the process of receiving this object
527         [objectsBeingReceived removeObject:inObject];
530 #pragma mark -
533  * @brief Send any NSTextAttachments embedded in inContentMessage's message
535  * This method will remove such attachments after requesting their files being sent.
537  * If the account supports sending images on this message's chat and a file is an image it will be left in the
538  * attributed string for processing later by AIHTMLDecoder.
539  */
540 - (void)handleFileSendsForContentMessage:(AIContentMessage *)inContentMessage
542         if (![inContentMessage destination] ||
543                 ![[inContentMessage destination] isKindOfClass:[AIListContact class]] ||
544                 ![[[inContentMessage chat] account] availableForSendingContentType:CONTENT_FILE_TRANSFER_TYPE
545                                                                                                                                  toContact:(AIListContact *)[inContentMessage destination]]) {
546                 //Simply return if we can't do anything about file sends for this message.
547                 return;
548         }
549         
550         NSMutableAttributedString       *newAttributedString = nil;
551         NSAttributedString                      *attributedMessage = [inContentMessage message];
552         unsigned                                        length = [attributedMessage length];
554         if (length) {
555                 NSRange                                         searchRange = NSMakeRange(0,0);
556                 NSAttributedString                      *currentAttributedString = attributedMessage;
558                 while (searchRange.location < length) {
559                         NSTextAttachment *textAttachment = [currentAttributedString attribute:NSAttachmentAttributeName
560                                                                                                                                                   atIndex:searchRange.location
561                                                                                                                                    effectiveRange:&searchRange];
562                         if (textAttachment) {
563                                 BOOL shouldSendAttachmentAsFile;
564                                 //Invariant within the loop, but most calls to handleFileSendsForContentMessage: don't get here at all
565                                 BOOL canSendImages = [(AIAccount *)[inContentMessage source] canSendImagesForChat:[inContentMessage chat]];
567                                 if ([textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
568                                         AITextAttachmentExtension *textAttachmentExtension = (AITextAttachmentExtension *)textAttachment;
569                                         
570                                         /* Send if:
571                                          *              This attachment isn't just for display (i.e. isn't an emoticon) AND
572                                          *              This chat can't send images, or it can but this attachment isn't an image
573                                          */
574                                         shouldSendAttachmentAsFile = (![textAttachmentExtension shouldAlwaysSendAsText] &&
575                                                                                                   (!canSendImages || ![textAttachmentExtension attachesAnImage]));
576                                         
577                                 } else {
578                                         shouldSendAttachmentAsFile = (!canSendImages || ![textAttachment wrapsImage]);
579                                 }
581                                 if (shouldSendAttachmentAsFile) {
582                                         if (!newAttributedString) {
583                                                 newAttributedString = [[attributedMessage mutableCopy] autorelease];
584                                                 currentAttributedString = newAttributedString;
585                                         }
586                                         
587                                         NSString        *path;
588                                         if ([textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
589                                                 path = [(AITextAttachmentExtension *)textAttachment path];
590                                                 AILog(@"Sending text attachment %@ which has path %@",textAttachment,path);
591                                         } else {
592                                                 //Write out the file so we can send it if we have a standard NSTextAttachment to send
593                                                 NSFileWrapper *fileWrapper = [textAttachment fileWrapper];
594                                         
595                                                 //Desired folder: /private/tmp/$UID/`uuidgen`
596                                                 NSString *tmpDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
597                                                 NSString *filename = [fileWrapper preferredFilename];
598                                                 if (!filename) filename = [NSString randomStringOfLength:5];
599                                                 
600                                                 path = [tmpDir stringByAppendingPathComponent:filename];
602                                                 if ([fileWrapper writeToFile:tmpDir atomically:YES updateFilenames:YES]) {
603                                                         AILog(@"Wrote out the file to %@ for sending",path);
604                                                 } else {
605                                                         NSLog(@"Failed to write out the file to %@ for sending", path);
606                                                         AILog(@"Failed to write out the file to %@ for sending", path);
608                                                         //The transfer is not going to happen so clear path
609                                                         path = nil;
610                                                 }
611                                         }
612                                         if (path) {
613                                                 [[adium fileTransferController] sendFile:path
614                                                                                                    toListContact:(AIListContact *)[inContentMessage destination]];
615                                         } else {
616                                                 NSLog(@"-[AIContentController handleFileSendsForContentMessage:]: Warning: Failed to have a path for sending an inline file!");
617                                                 AILog(@"-[AIContentController handleFileSendsForContentMessage:]: Warning: Failed to have a path for sending an inline file for content message %@!",
618                                                           inContentMessage);
619                                         }
621                                         //Now remove the attachment
622                                         [newAttributedString removeAttribute:NSAttachmentAttributeName range:NSMakeRange(searchRange.location,
623                                                                                                                                                                                                          searchRange.length)];
624                                         [newAttributedString replaceCharactersInRange:searchRange withString:@""];
625                                         //Decrease length by the number of characters we replaced
626                                         length -= searchRange.length;
627                                         
628                                         //And don't increase our location in the searchRange.location += searchRange.length below
629                                         searchRange.length = 0;
630                                 }
631                         }
632                         
633                         //Onward and upward
634                         searchRange.location += searchRange.length;
635                 }
636         }
637         
638         //If any  changes were made, update the AIContentMessage
639         if (newAttributedString) {
640                 [inContentMessage setMessage:newAttributedString];
641         }
645  * @brief Handle sending a content object
647  * This method must return YES for the content to be displayed
649  * For a typing content object, the account is informed.
650  * For a message content object, the account is told to send the message; any imbedded file transfers will also be sent.
651  * For a file transfer content object, YES is always returned, as this is actually just for display purposes.
652  */
653 - (BOOL)processAndSendContentObject:(AIContentObject *)inContentObject
655         AIAccount       *sendingAccount = (AIAccount *)[inContentObject source];
656         BOOL            success = YES;
658         if ([inContentObject isKindOfClass:[AIContentTyping class]]) {
659                 /* Typing */
660                 [sendingAccount sendTypingObject:(AIContentTyping *)inContentObject];
661         
662         } else if ([inContentObject isKindOfClass:[AIContentMessage class]]) {
663                 /* Sending a message */
664                 AIContentMessage *contentMessage = (AIContentMessage *)inContentObject;
665                 NSString                 *encodedOutgoingMessage;
667                 //Before we send the message on to the account, we need to look for embedded files which should be sent as file transfers
668                 [self handleFileSendsForContentMessage:contentMessage];
669                 
670                 /* Let the account encode it as appropriate for sending. Note that we succeeded in sending if we have no length
671                  * as that means that somewhere we meant to stop the send -- a file send, an encryption message, etc.
672                  */
673                 if ([[contentMessage message] length]) {
674                         encodedOutgoingMessage = [sendingAccount encodedAttributedStringForSendingContentMessage:contentMessage];
675                         
676                         if (encodedOutgoingMessage && [encodedOutgoingMessage length]) {                        
677                                 [contentMessage setEncodedMessage:encodedOutgoingMessage];
678                                 [adiumEncryptor willSendContentMessage:contentMessage];
679                                 
680                                 if ([contentMessage encodedMessage]) {
681                                         success = [sendingAccount sendMessageObject:contentMessage];
682                                 }
683                         } else {
684                                 //If the account returns nil when encoding the attributed string, we shouldn't display it on-screen.
685                                 [contentMessage setDisplayContent:NO];
686                         }
687                 }
689         } else if ([inContentObject isKindOfClass:[ESFileTransfer class]]) {
690                 success = YES;
692         } else {
693                 /* Eating a tasty sandwich */
694                 success = NO;
695         }
697         if (!success) AILog(@"Failed to send %@ (sendingAccount %@)",inContentObject,sendingAccount);
699         return success;
703  * @brief Send a message as-specified without going through any filters or notifications
704  */
705 - (void)sendRawMessage:(NSString *)inString toContact:(AIListContact *)inContact
707         AIAccount                *account = [inContact account];
708         AIChat                   *chat;
709         AIContentMessage *contentMessage;
711         if (!(chat = [[adium chatController] existingChatWithContact:inContact])) {
712                 chat = [[adium chatController] chatWithContact:inContact];
713         }
715         contentMessage = [AIContentMessage messageInChat:chat
716                                                                                   withSource:account
717                                                                                  destination:inContact
718                                                                                                 date:nil
719                                                                                          message:nil
720                                                                                    autoreply:NO];
721         [contentMessage setEncodedMessage:inString];
723         [account sendMessageObject:contentMessage];
727  * @brief Given an incoming message, decrypt it.  It is likely not yet ready for display when returned, as it may still include HTML.
728  */
729 - (NSString *)decryptedIncomingMessage:(NSString *)inString fromContact:(AIListContact *)inListContact onAccount:(AIAccount *)inAccount
731         return [adiumEncryptor decryptIncomingMessage:inString fromContact:inListContact onAccount:inAccount];
735  * @brief Given an incoming message, decrypt it if necessary then convert it to an NSAttributedString, processing HTML if possible
736  */
737 - (NSAttributedString *)decodedIncomingMessage:(NSString *)inString fromContact:(AIListContact *)inListContact onAccount:(AIAccount *)inAccount
739         return [AIHTMLDecoder decodeHTML:[self decryptedIncomingMessage:inString
740                                                                                                                 fromContact:inListContact
741                                                                                                                   onAccount:inAccount]];
744 #pragma mark OTR
745 - (void)requestSecureOTRMessaging:(BOOL)inSecureMessaging inChat:(AIChat *)inChat
747         [adiumEncryptor requestSecureOTRMessaging:inSecureMessaging inChat:inChat];
750 - (void)promptToVerifyEncryptionIdentityInChat:(AIChat *)inChat
752         [adiumEncryptor promptToVerifyEncryptionIdentityInChat:inChat];
755 #pragma mark -
757  * @brief Is the passed chat currently receiving content?
759  * Note: This may be irrelevent if threaded filtering is removed.
760  */
761 - (BOOL)chatIsReceivingContent:(AIChat *)inChat
763         BOOL isReceivingContent = NO;
765         NSEnumerator    *objectsBeingReceivedEnumerator = [objectsBeingReceived objectEnumerator];
766         AIContentObject *contentObject;
767         while ((contentObject = [objectsBeingReceivedEnumerator nextObject])) {
768                 if ([contentObject chat] == inChat) {
769                         isReceivingContent = YES;
770                         break;
771                 }
772         }
774         return isReceivingContent;
777 - (void)displayEvent:(NSString *)message ofType:(NSString *)type inChat:(AIChat *)inChat
779         AIContentStatus         *content;
780         NSAttributedString      *attributedMessage;
781         
782         //Create our content object
783         attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:[self defaultFormattingAttributes]];
785         content = [AIContentEvent statusInChat:inChat
786                                                                 withSource:[inChat listObject]
787                                                            destination:[inChat account]
788                                                                           date:[NSDate date]
789                                                                    message:attributedMessage
790                                                                   withType:type];
792         //Add the object
793         [self receiveContentObject:content];
796 //Returns YES if the account/chat is available for sending content
797 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact onAccount:(AIAccount *)inAccount 
799         return [inAccount availableForSendingContentType:inType toContact:inContact];
802 /*! 
803  * @brief Generate a menu of encryption preference choices
804  */
805 - (NSMenu *)encryptionMenuNotifyingTarget:(id)target withDefault:(BOOL)withDefault
807         NSMenu          *encryptionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
808         NSMenuItem      *menuItem;
810         [encryptionMenu setTitle:ENCRYPTION_MENU_TITLE];
812         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Disable chat encryption",nil)
813                                                                                   target:target
814                                                                                   action:@selector(selectedEncryptionPreference:)
815                                                                    keyEquivalent:@""];
816         
817         [menuItem setTag:EncryptedChat_Never];
818         [encryptionMenu addItem:menuItem];
819         [menuItem release];
820         
821         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats as requested",nil)
822                                                                                   target:target
823                                                                                   action:@selector(selectedEncryptionPreference:)
824                                                                    keyEquivalent:@""];
825         
826         [menuItem setTag:EncryptedChat_Manually];
827         [encryptionMenu addItem:menuItem];
828         [menuItem release];
829         
830         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats automatically",nil)
831                                                                                   target:target
832                                                                                   action:@selector(selectedEncryptionPreference:)
833                                                                    keyEquivalent:@""];
834         
835         [menuItem setTag:EncryptedChat_Automatically];
836         [encryptionMenu addItem:menuItem];
837         [menuItem release];
838         
839         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Force encryption and refuse plaintext",nil)
840                                                                                   target:target
841                                                                                   action:@selector(selectedEncryptionPreference:)
842                                                                    keyEquivalent:@""];
843         
844         [menuItem setTag:EncryptedChat_RejectUnencryptedMessages];
845         [encryptionMenu addItem:menuItem];
846         [menuItem release];
847         
848         if (withDefault) {
849                 [encryptionMenu addItem:[NSMenuItem separatorItem]];
850                 
851                 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Default",nil)
852                                                                                                                   target:target
853                                                                                                                   action:@selector(selectedEncryptionPreference:)
854                                                                                                    keyEquivalent:@""];
855                 
856                 [menuItem setTag:EncryptedChat_Default];
857                 [encryptionMenu addItem:menuItem];
858                 [menuItem release];
859         }
860         
861         return [encryptionMenu autorelease];
864 @end