merged [21403]: Fixed an exception thrown when clearing all complete file transfers...
[adiumx.git] / Source / AIContentController.m
blobdcb2ec2611743123c7664fd4ca366195d79aa043
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                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED_FIRST
503                                                                                                                                                                 forListObject:listObject
504                                                                                                                                                                          userInfo:userInfo
505                                                                                                                                  previouslyPerformedActionIDs:nil];
506                                 [chat setHasSentOrReceivedContent:YES];
507                         }
508                         
509                         if (chat != [[adium interfaceController] activeChat]) {
510                                 //If the chat is not currently active, generate CONTENT_MESSAGE_RECEIVED_BACKGROUND
511                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED_BACKGROUND
512                                                                                                                                                                 forListObject:listObject
513                                                                                                                                                                          userInfo:userInfo
514                                                                                                                                  previouslyPerformedActionIDs:previouslyPerformedActionIDs];
515                         }
516                         
517                         [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED
518                                                                                          forListObject:listObject
519                                                                                                   userInfo:userInfo
520                                                           previouslyPerformedActionIDs:previouslyPerformedActionIDs];
521                 }               
522     }
524         //We are no longer in the process of receiving this object
525         [objectsBeingReceived removeObject:inObject];
528 #pragma mark -
531  * @brief Send any NSTextAttachments embedded in inContentMessage's message
533  * This method will remove such attachments after requesting their files being sent.
535  * If the account supports sending images on this message's chat and a file is an image it will be left in the
536  * attributed string for processing later by AIHTMLDecoder.
537  */
538 - (void)handleFileSendsForContentMessage:(AIContentMessage *)inContentMessage
540         if (![inContentMessage destination] ||
541                 ![[inContentMessage destination] isKindOfClass:[AIListContact class]] ||
542                 ![[[inContentMessage chat] account] availableForSendingContentType:CONTENT_FILE_TRANSFER_TYPE
543                                                                                                                                  toContact:(AIListContact *)[inContentMessage destination]]) {
544                 //Simply return if we can't do anything about file sends for this message.
545                 return;
546         }
547         
548         NSMutableAttributedString       *newAttributedString = nil;
549         NSAttributedString                      *attributedMessage = [inContentMessage message];
550         unsigned                                        length = [attributedMessage length];
552         if (length) {
553                 NSRange                                         searchRange = NSMakeRange(0,0);
554                 NSAttributedString                      *currentAttributedString = attributedMessage;
556                 while (searchRange.location < length) {
557                         NSTextAttachment *textAttachment = [currentAttributedString attribute:NSAttachmentAttributeName
558                                                                                                                                                   atIndex:searchRange.location
559                                                                                                                                    effectiveRange:&searchRange];
560                         if (textAttachment) {
561                                 BOOL shouldSendAttachmentAsFile;
562                                 //Invariant within the loop, but most calls to handleFileSendsForContentMessage: don't get here at all
563                                 BOOL canSendImages = [(AIAccount *)[inContentMessage source] canSendImagesForChat:[inContentMessage chat]];
565                                 if ([textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
566                                         AITextAttachmentExtension *textAttachmentExtension = (AITextAttachmentExtension *)textAttachment;
567                                         
568                                         /* Send if:
569                                          *              This attachment isn't just for display (i.e. isn't an emoticon) AND
570                                          *              This chat can't send images, or it can but this attachment isn't an image
571                                          */
572                                         shouldSendAttachmentAsFile = (![textAttachmentExtension shouldAlwaysSendAsText] &&
573                                                                                                   (!canSendImages || ![textAttachmentExtension attachesAnImage]));
574                                         
575                                 } else {
576                                         shouldSendAttachmentAsFile = (!canSendImages || ![textAttachment wrapsImage]);
577                                 }
579                                 if (shouldSendAttachmentAsFile) {
580                                         if (!newAttributedString) {
581                                                 newAttributedString = [[attributedMessage mutableCopy] autorelease];
582                                                 currentAttributedString = newAttributedString;
583                                         }
584                                         
585                                         NSString        *path;
586                                         if ([textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
587                                                 path = [(AITextAttachmentExtension *)textAttachment path];
588                                                 AILog(@"Sending text attachment %@ which has path %@",textAttachment,path);
589                                         } else {
590                                                 //Write out the file so we can send it if we have a standard NSTextAttachment to send
591                                                 NSFileWrapper *fileWrapper = [textAttachment fileWrapper];
592                                         
593                                                 //Desired folder: /private/tmp/$UID/`uuidgen`
594                                                 NSString *tmpDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
595                                                 NSString *filename = [fileWrapper preferredFilename];
596                                                 if (!filename) filename = [NSString randomStringOfLength:5];
597                                                 
598                                                 path = [tmpDir stringByAppendingPathComponent:filename];
600                                                 if ([fileWrapper writeToFile:tmpDir atomically:YES updateFilenames:YES]) {
601                                                         AILog(@"Wrote out the file to %@ for sending",path);
602                                                 } else {
603                                                         NSLog(@"Failed to write out the file to %@ for sending", path);
604                                                         AILog(@"Failed to write out the file to %@ for sending", path);
606                                                         //The transfer is not going to happen so clear path
607                                                         path = nil;
608                                                 }
609                                         }
610                                         if (path) {
611                                                 [[adium fileTransferController] sendFile:path
612                                                                                                    toListContact:(AIListContact *)[inContentMessage destination]];
613                                         } else {
614                                                 NSLog(@"-[AIContentController handleFileSendsForContentMessage:]: Warning: Failed to have a path for sending an inline file!");
615                                                 AILog(@"-[AIContentController handleFileSendsForContentMessage:]: Warning: Failed to have a path for sending an inline file for content message %@!",
616                                                           inContentMessage);
617                                         }
619                                         //Now remove the attachment
620                                         [newAttributedString removeAttribute:NSAttachmentAttributeName range:NSMakeRange(searchRange.location,
621                                                                                                                                                                                                          searchRange.length)];
622                                         [newAttributedString replaceCharactersInRange:searchRange withString:@""];
623                                         //Decrease length by the number of characters we replaced
624                                         length -= searchRange.length;
625                                         
626                                         //And don't increase our location in the searchRange.location += searchRange.length below
627                                         searchRange.length = 0;
628                                 }
629                         }
630                         
631                         //Onward and upward
632                         searchRange.location += searchRange.length;
633                 }
634         }
635         
636         //If any  changes were made, update the AIContentMessage
637         if (newAttributedString) {
638                 [inContentMessage setMessage:newAttributedString];
639         }
643  * @brief Handle sending a content object
645  * This method must return YES for the content to be displayed
647  * For a typing content object, the account is informed.
648  * For a message content object, the account is told to send the message; any imbedded file transfers will also be sent.
649  * For a file transfer content object, YES is always returned, as this is actually just for display purposes.
650  */
651 - (BOOL)processAndSendContentObject:(AIContentObject *)inContentObject
653         AIAccount       *sendingAccount = (AIAccount *)[inContentObject source];
654         BOOL            success = YES;
656         if ([inContentObject isKindOfClass:[AIContentTyping class]]) {
657                 /* Typing */
658                 [sendingAccount sendTypingObject:(AIContentTyping *)inContentObject];
659         
660         } else if ([inContentObject isKindOfClass:[AIContentMessage class]]) {
661                 /* Sending a message */
662                 AIContentMessage *contentMessage = (AIContentMessage *)inContentObject;
663                 NSString                 *encodedOutgoingMessage;
665                 //Before we send the message on to the account, we need to look for embedded files which should be sent as file transfers
666                 [self handleFileSendsForContentMessage:contentMessage];
667                 
668                 /* Let the account encode it as appropriate for sending. Note that we succeeded in sending if we have no length
669                  * as that means that somewhere we meant to stop the send -- a file send, an encryption message, etc.
670                  */
671                 if ([[contentMessage message] length]) {
672                         encodedOutgoingMessage = [sendingAccount encodedAttributedStringForSendingContentMessage:contentMessage];
673                         
674                         if (encodedOutgoingMessage && [encodedOutgoingMessage length]) {                        
675                                 [contentMessage setEncodedMessage:encodedOutgoingMessage];
676                                 [adiumEncryptor willSendContentMessage:contentMessage];
677                                 
678                                 if ([contentMessage encodedMessage]) {
679                                         success = [sendingAccount sendMessageObject:contentMessage];
680                                 }
681                         } else {
682                                 //If the account returns nil when encoding the attributed string, we shouldn't display it on-screen.
683                                 [contentMessage setDisplayContent:NO];
684                         }
685                 }
687         } else if ([inContentObject isKindOfClass:[ESFileTransfer class]]) {
688                 success = YES;
690         } else {
691                 /* Eating a tasty sandwich */
692                 success = NO;
693         }
695         if (!success) AILog(@"Failed to send %@ (sendingAccount %@)",inContentObject,sendingAccount);
697         return success;
701  * @brief Send a message as-specified without going through any filters or notifications
702  */
703 - (void)sendRawMessage:(NSString *)inString toContact:(AIListContact *)inContact
705         AIAccount                *account = [inContact account];
706         AIChat                   *chat;
707         AIContentMessage *contentMessage;
709         if (!(chat = [[adium chatController] existingChatWithContact:inContact])) {
710                 chat = [[adium chatController] chatWithContact:inContact];
711         }
713         contentMessage = [AIContentMessage messageInChat:chat
714                                                                                   withSource:account
715                                                                                  destination:inContact
716                                                                                                 date:nil
717                                                                                          message:nil
718                                                                                    autoreply:NO];
719         [contentMessage setEncodedMessage:inString];
721         [account sendMessageObject:contentMessage];
725  * @brief Given an incoming message, decrypt it.  It is likely not yet ready for display when returned, as it may still include HTML.
726  */
727 - (NSString *)decryptedIncomingMessage:(NSString *)inString fromContact:(AIListContact *)inListContact onAccount:(AIAccount *)inAccount
729         return [adiumEncryptor decryptIncomingMessage:inString fromContact:inListContact onAccount:inAccount];
733  * @brief Given an incoming message, decrypt it if necessary then convert it to an NSAttributedString, processing HTML if possible
734  */
735 - (NSAttributedString *)decodedIncomingMessage:(NSString *)inString fromContact:(AIListContact *)inListContact onAccount:(AIAccount *)inAccount
737         return [AIHTMLDecoder decodeHTML:[self decryptedIncomingMessage:inString
738                                                                                                                 fromContact:inListContact
739                                                                                                                   onAccount:inAccount]];
742 #pragma mark OTR
743 - (void)requestSecureOTRMessaging:(BOOL)inSecureMessaging inChat:(AIChat *)inChat
745         [adiumEncryptor requestSecureOTRMessaging:inSecureMessaging inChat:inChat];
748 - (void)promptToVerifyEncryptionIdentityInChat:(AIChat *)inChat
750         [adiumEncryptor promptToVerifyEncryptionIdentityInChat:inChat];
753 #pragma mark -
755  * @brief Is the passed chat currently receiving content?
757  * Note: This may be irrelevent if threaded filtering is removed.
758  */
759 - (BOOL)chatIsReceivingContent:(AIChat *)inChat
761         BOOL isReceivingContent = NO;
763         NSEnumerator    *objectsBeingReceivedEnumerator = [objectsBeingReceived objectEnumerator];
764         AIContentObject *contentObject;
765         while ((contentObject = [objectsBeingReceivedEnumerator nextObject])) {
766                 if ([contentObject chat] == inChat) {
767                         isReceivingContent = YES;
768                         break;
769                 }
770         }
772         return isReceivingContent;
775 - (void)displayEvent:(NSString *)message ofType:(NSString *)type inChat:(AIChat *)inChat
777         AIContentStatus         *content;
778         NSAttributedString      *attributedMessage;
779         
780         //Create our content object
781         attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:[self defaultFormattingAttributes]];
783         content = [AIContentEvent statusInChat:inChat
784                                                                 withSource:[inChat listObject]
785                                                            destination:[inChat account]
786                                                                           date:[NSDate date]
787                                                                    message:attributedMessage
788                                                                   withType:type];
790         //Add the object
791         [self receiveContentObject:content];
794 //Returns YES if the account/chat is available for sending content
795 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact onAccount:(AIAccount *)inAccount 
797         return [inAccount availableForSendingContentType:inType toContact:inContact];
800 /*! 
801  * @brief Generate a menu of encryption preference choices
802  */
803 - (NSMenu *)encryptionMenuNotifyingTarget:(id)target withDefault:(BOOL)withDefault
805         NSMenu          *encryptionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
806         NSMenuItem      *menuItem;
808         [encryptionMenu setTitle:ENCRYPTION_MENU_TITLE];
810         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Disable chat encryption",nil)
811                                                                                   target:target
812                                                                                   action:@selector(selectedEncryptionPreference:)
813                                                                    keyEquivalent:@""];
814         
815         [menuItem setTag:EncryptedChat_Never];
816         [encryptionMenu addItem:menuItem];
817         [menuItem release];
818         
819         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats as requested",nil)
820                                                                                   target:target
821                                                                                   action:@selector(selectedEncryptionPreference:)
822                                                                    keyEquivalent:@""];
823         
824         [menuItem setTag:EncryptedChat_Manually];
825         [encryptionMenu addItem:menuItem];
826         [menuItem release];
827         
828         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats automatically",nil)
829                                                                                   target:target
830                                                                                   action:@selector(selectedEncryptionPreference:)
831                                                                    keyEquivalent:@""];
832         
833         [menuItem setTag:EncryptedChat_Automatically];
834         [encryptionMenu addItem:menuItem];
835         [menuItem release];
836         
837         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Force encryption and refuse plaintext",nil)
838                                                                                   target:target
839                                                                                   action:@selector(selectedEncryptionPreference:)
840                                                                    keyEquivalent:@""];
841         
842         [menuItem setTag:EncryptedChat_RejectUnencryptedMessages];
843         [encryptionMenu addItem:menuItem];
844         [menuItem release];
845         
846         if (withDefault) {
847                 [encryptionMenu addItem:[NSMenuItem separatorItem]];
848                 
849                 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Default",nil)
850                                                                                                                   target:target
851                                                                                                                   action:@selector(selectedEncryptionPreference:)
852                                                                                                    keyEquivalent:@""];
853                 
854                 [menuItem setTag:EncryptedChat_Default];
855                 [encryptionMenu addItem:menuItem];
856                 [menuItem release];
857         }
858         
859         return [encryptionMenu autorelease];
862 @end