Source/DCMessageContextDisplayPlugin.m
author Zachary West <zacw@adium.im>
Wed Nov 25 22:41:51 2009 -0500 (2009-11-25)
changeset 2822 2a385da3eace
parent 2821 fa988de96a94
child 2823 24c471ce6c9e
permissions -rw-r--r--
Don't use any instance variables to control parsing, use the parser's context info. Refs #13362.
David@0
     1
/* 
David@0
     2
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
David@0
     3
 * with this source distribution.
David@0
     4
 * 
David@0
     5
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
David@0
     6
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
David@0
     7
 * or (at your option) any later version.
David@0
     8
 * 
David@0
     9
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
David@0
    10
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
David@0
    11
 * Public License for more details.
David@0
    12
 * 
David@0
    13
 * You should have received a copy of the GNU General Public License along with this program; if not,
David@0
    14
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
David@0
    15
 */
David@0
    16
David@0
    17
#import <Adium/AIContentControllerProtocol.h>
David@0
    18
#import "DCMessageContextDisplayPlugin.h"
David@0
    19
#import "DCMessageContextDisplayPreferences.h"
David@0
    20
#import <AIUtilities/AIDictionaryAdditions.h>
David@0
    21
#import <Adium/AIChat.h>
David@0
    22
#import <Adium/AIContentContext.h>
David@715
    23
#import <Adium/AIService.h>
David@715
    24
David@0
    25
//Old school
David@0
    26
#import <Adium/AIListContact.h>
David@0
    27
#import <AIUtilities/AIAttributedStringAdditions.h>
David@0
    28
#import <Adium/AIAccountControllerProtocol.h>
David@0
    29
David@0
    30
//omg crawsslinkz
David@0
    31
#import "AILoggerPlugin.h"
David@0
    32
David@0
    33
//LMX
David@0
    34
#import <LMX/LMXParser.h>
zacw@1226
    35
#import <Adium/AIXMLElement.h>
David@0
    36
#import <AIUtilities/AIStringAdditions.h>
David@0
    37
#import "unistd.h"
David@0
    38
#import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
David@0
    39
#import <Adium/AIContactControllerProtocol.h>
David@0
    40
#import <Adium/AIHTMLDecoder.h>
David@0
    41
David@0
    42
#define RESTORED_CHAT_CONTEXT_LINE_NUMBER 50
David@0
    43
David@0
    44
/**
David@0
    45
 * @class DCMessageContextDisplayPlugin
David@0
    46
 * @brief Component to display in-window message history
David@0
    47
 *
David@0
    48
 * The amount of history, and criteria of when to display history, are determined in the Advanced->Message History preferences.
David@0
    49
 */
David@84
    50
@interface DCMessageContextDisplayPlugin ()
David@0
    51
- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
David@0
    52
							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime;
David@0
    53
- (NSArray *)contextForChat:(AIChat *)chat;
David@0
    54
@end
David@0
    55
David@0
    56
@implementation DCMessageContextDisplayPlugin
David@0
    57
David@0
    58
/**
David@0
    59
 * @brief Install
David@0
    60
 */
David@0
    61
- (void)installPlugin
David@0
    62
{
David@0
    63
	isObserving = NO;
David@0
    64
	
David@0
    65
	//Setup our preferences
David@95
    66
    [adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:CONTEXT_DISPLAY_DEFAULTS
David@0
    67
																		forClass:[self class]] 
David@0
    68
										  forGroup:PREF_GROUP_CONTEXT_DISPLAY];
David@0
    69
	
David@0
    70
	//Observe preference changes for whether or not to display message history
David@95
    71
	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTEXT_DISPLAY];
David@0
    72
}
David@0
    73
David@0
    74
/**
David@0
    75
 * @brief Uninstall
David@0
    76
 */
David@0
    77
- (void)uninstallPlugin
David@0
    78
{
David@95
    79
	[adium.preferenceController unregisterPreferenceObserver:self];
David@1109
    80
	[[NSNotificationCenter defaultCenter] removeObserver:self];
David@0
    81
}
David@0
    82
David@0
    83
- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
David@0
    84
								object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
David@0
    85
{
David@0
    86
	if (!object) {		
David@0
    87
		shouldDisplay = [[prefDict objectForKey:KEY_DISPLAY_CONTEXT] boolValue];
David@3
    88
		linesToDisplay = [[prefDict objectForKey:KEY_DISPLAY_LINES] integerValue];
David@0
    89
David@0
    90
		if (shouldDisplay && linesToDisplay > 0 && !isObserving) {
David@0
    91
			//Observe new message windows only if we aren't already observing them
David@0
    92
			isObserving = YES;
David@1109
    93
			[[NSNotificationCenter defaultCenter] addObserver:self
David@0
    94
										   selector:@selector(addContextDisplayToWindow:)
David@0
    95
											   name:Chat_DidOpen 
David@0
    96
											 object:nil];
David@0
    97
			
David@0
    98
		} else if (isObserving && (!shouldDisplay || linesToDisplay <= 0)) {
David@0
    99
			//Remove observer
David@0
   100
			isObserving = NO;
David@1109
   101
			[[NSNotificationCenter defaultCenter] removeObserver:self name:Chat_DidOpen object:nil];
David@0
   102
			
David@0
   103
		}
David@0
   104
	}
David@0
   105
}
David@0
   106
David@0
   107
/**
David@0
   108
 * @brief Retrieve and display in-window message history
David@0
   109
 *
David@0
   110
 * Called in response to the Chat_DidOpen notification
David@0
   111
 */
David@0
   112
- (void)addContextDisplayToWindow:(NSNotification *)notification
David@0
   113
{
David@0
   114
	AIChat	*chat = (AIChat *)[notification object];
David@0
   115
	
David@0
   116
	NSArray	*context = [self contextForChat:chat];
David@0
   117
David@0
   118
	if (context && [context count] > 0 && shouldDisplay) {
zacw@2820
   119
		AIContentContext	*contextMessage;
zacw@2820
   120
zacw@2820
   121
		for(contextMessage in context) {
zacw@2820
   122
			/* Don't display immediately, so the message view can aggregate multiple message history items.
zacw@2820
   123
			 * As required, we post Content_ChatDidFinishAddingUntrackedContent when finished adding. */
zacw@2820
   124
			[contextMessage setDisplayContentImmediately:NO];
David@0
   125
		
zacw@2820
   126
			[adium.contentController displayContentObject:contextMessage
zacw@2820
   127
										usingContentFilters:YES
zacw@2820
   128
												immediately:YES];
zacw@2820
   129
		}
David@0
   130
zacw@2820
   131
		//We finished adding untracked content
zacw@2820
   132
		[[NSNotificationCenter defaultCenter] postNotificationName:Content_ChatDidFinishAddingUntrackedContent
zacw@2820
   133
												  object:chat];
David@0
   134
	}
David@0
   135
}
David@0
   136
/*!
David@0
   137
 * @brief Retrieve the message history for a particular chat
David@0
   138
 *
David@0
   139
 * Asks AILoggerPlugin for the path to the right file, and then uses LMX to parse that file backwards.
David@0
   140
 */
David@0
   141
- (NSArray *)contextForChat:(AIChat *)chat
David@0
   142
{
David@0
   143
	//If there's no log there, there's no message history. Bail out.
David@0
   144
	NSArray *logPaths = [AILoggerPlugin sortedArrayOfLogFilesForChat:chat];
David@0
   145
	if(!logPaths) return nil;
zacw@2820
   146
	
zacw@2820
   147
	NSInteger linesLeftToFind = 0;
David@0
   148
David@0
   149
	AIHTMLDecoder *decoder = [AIHTMLDecoder decoder];
David@0
   150
David@721
   151
	NSString *logObjectUID = chat.name;
David@426
   152
	if (!logObjectUID) logObjectUID = chat.listObject.UID;
David@0
   153
	logObjectUID = [logObjectUID safeFilenameString];
David@0
   154
David@0
   155
	NSString *baseLogPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:
David@426
   156
		[AILoggerPlugin relativePathForLogWithObject:logObjectUID onAccount:chat.account]];	
David@0
   157
David@393
   158
	if ([chat boolValueForProperty:@"Restored Chat"] && linesToDisplay < RESTORED_CHAT_CONTEXT_LINE_NUMBER) {
zacw@2820
   159
		linesLeftToFind = MAX(linesLeftToFind, RESTORED_CHAT_CONTEXT_LINE_NUMBER);
David@0
   160
	} else {
David@0
   161
		linesLeftToFind = linesToDisplay;		
David@0
   162
	}
David@0
   163
			
David@0
   164
	//Initialize a place to store found messages
David@0
   165
	NSMutableArray *outerFoundContentContexts = [NSMutableArray arrayWithCapacity:linesLeftToFind]; 
David@0
   166
zacw@2822
   167
	// These set of file's autorelease pool.
zacw@2822
   168
	NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
zacw@2822
   169
	
David@0
   170
	//Iterate over the elements of the log path array.
David@0
   171
	NSEnumerator *pathsEnumerator = [logPaths objectEnumerator];
David@0
   172
	NSString *logPath = nil;
David@0
   173
	while (linesLeftToFind > 0 && (logPath = [pathsEnumerator nextObject])) {
David@0
   174
		//If it's not a .chatlog, ignore it.
David@0
   175
		if (![logPath hasSuffix:@".chatlog"])
David@0
   176
			continue;
David@0
   177
				
David@0
   178
		//Stick the base path on to the beginning
David@0
   179
		logPath = [baseLogPath stringByAppendingPathComponent:logPath];
David@0
   180
David@0
   181
		//By default, the xmlFilePath is the chat log file/bundle... if we find that the chatlog is a bundle, we'll use the xml file inside.
David@0
   182
		NSString *xmlFilePath = logPath;
David@0
   183
David@0
   184
		BOOL isDir;
David@0
   185
		if ([[NSFileManager defaultManager] fileExistsAtPath:logPath isDirectory:&isDir]) {
David@0
   186
			/* If we have a chatLog bundle, we want to get the text content for the xml file inside */
David@0
   187
			NSString *baseURL;
David@0
   188
			if (isDir) {
David@0
   189
				baseURL = logPath;
David@0
   190
				xmlFilePath = [logPath stringByAppendingPathComponent:
David@0
   191
							   [[[logPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
David@0
   192
			} else {
David@0
   193
				baseURL = nil;
David@0
   194
			}
David@0
   195
			[decoder setBaseURL:baseURL];
David@0
   196
		}
David@0
   197
David@0
   198
		//Initialize the found messages array and element stack for us-as-delegate
zacw@2822
   199
		NSMutableArray *foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
zacw@2822
   200
		NSMutableArray *elementStack = [NSMutableArray array];
David@0
   201
David@0
   202
		//Create the parser and set ourselves as the delegate
David@0
   203
		LMXParser *parser = [LMXParser parser];
David@0
   204
		[parser setDelegate:self];
David@0
   205
David@0
   206
		//Set up info needed by elementStarted to create content objects.
David@0
   207
		NSMutableDictionary *contextInfo = nil;
David@0
   208
		{
David@0
   209
			//Get the service name from the path name
David@0
   210
			NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
David@0
   211
David@426
   212
			AIListObject *account = chat.account;
David@715
   213
			NSString	 *accountID = [NSString stringWithFormat:@"%@.%@", account.service.serviceID, account.UID];
David@0
   214
David@0
   215
			contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
David@0
   216
						   serviceName, @"Service name",
David@0
   217
						   account, @"Account",
David@0
   218
						   accountID, @"Account ID",
David@0
   219
						   chat, @"Chat",
David@0
   220
						   decoder, @"AIHTMLDecoder",
zacw@2820
   221
						   [NSValue valueWithPointer:&linesLeftToFind], @"LinesLeftToFindValue",
zacw@2822
   222
						   foundMessages, @"FoundMessages",
zacw@2822
   223
						   elementStack, @"ElementStack",
David@0
   224
						   nil];
David@0
   225
			[parser setContextInfo:(void *)contextInfo];
David@0
   226
		}
David@0
   227
David@0
   228
		//Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
David@0
   229
		NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:xmlFilePath];
David@0
   230
		[file seekToEndOfFile];
David@0
   231
		
David@0
   232
		//Set up some more doohickeys and then start the parse loop
David@3
   233
		NSInteger readSize = 4 * getpagesize(); //Read 4 pages at a time.
David@0
   234
		NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
David@3
   235
		NSInteger fd = [file fileDescriptor];
David@0
   236
		char *buf = [chunk mutableBytes];
David@0
   237
		off_t offset = [file offsetInFile];
David@0
   238
		enum LMXParseResult result = LMXParsedIncomplete;
David@0
   239
David@0
   240
		do {
David@0
   241
			//Calculate the new offset
David@0
   242
			offset = (offset <= readSize) ? 0 : offset - readSize;
David@0
   243
			
David@0
   244
			//Seek to it and read greedily until we hit readSize or run out of file.
David@3
   245
			NSInteger idx = 0;
David@0
   246
			for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) { 
David@0
   247
				amountRead = pread(fd, buf + idx, readSize, offset + idx); 
David@0
   248
			   if (amountRead <= 0) break;
David@0
   249
			}
David@0
   250
			offset -= idx;
David@0
   251
			
David@0
   252
			//Parse
David@0
   253
			result = [parser parseChunk:chunk];
David@0
   254
			
David@0
   255
		//Continue to parse as long as we need more elements, we have data to read, and LMX doesn't think we're done.
David@0
   256
		} while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
David@0
   257
zacw@2822
   258
		//Drain our autorelease pool.
zacw@2822
   259
		[parsingAutoreleasePool drain];
David@0
   260
David@0
   261
		//Be a good citizen and close the file
David@0
   262
		[file closeFile];
David@0
   263
David@0
   264
		//Add our locals to the outer array; we're probably looping again.
David@0
   265
		[outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
David@0
   266
		linesLeftToFind -= [outerFoundContentContexts count];
David@0
   267
	}
zacw@2820
   268
	
zacw@2822
   269
	[parsingAutoreleasePool release];
zacw@2822
   270
	
zacw@2820
   271
	if (linesLeftToFind > 0) {
zacw@2820
   272
		AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
zacw@2820
   273
	}
zacw@2820
   274
	
David@0
   275
	return outerFoundContentContexts;
David@0
   276
}
David@0
   277
David@0
   278
#pragma mark LMX delegate
David@0
   279
David@0
   280
- (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
David@0
   281
{
zacw@2822
   282
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   283
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   284
	
David@0
   285
	if ([elementName isEqualToString:@"message"]) {
zacw@1226
   286
		[elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
David@0
   287
	}
David@0
   288
	else if ([elementStack count]) {
zacw@1226
   289
		AIXMLElement *element = [AIXMLElement elementWithName:elementName];
zacw@2798
   290
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
David@0
   291
		[elementStack insertObject:element atIndex:0U];
David@0
   292
	}
David@0
   293
}
David@0
   294
David@0
   295
- (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
David@0
   296
{
zacw@2822
   297
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   298
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   299
	
David@0
   300
	if ([elementStack count])
zacw@2798
   301
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
David@0
   302
}
David@0
   303
David@0
   304
- (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
David@0
   305
{
zacw@2822
   306
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   307
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   308
	
David@0
   309
	if ([elementStack count]) {
zacw@1226
   310
		AIXMLElement *element = [elementStack objectAtIndex:0U];
David@0
   311
		if (attributes) {
zacw@1226
   312
			[element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
David@0
   313
		}
David@0
   314
		
zacw@2822
   315
		NSMutableArray	*foundMessages = [contextInfo objectForKey:@"FoundMessagesValue"];
zacw@2822
   316
		NSInteger	 *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
zacw@2822
   317
		
David@0
   318
		if ([elementName isEqualToString:@"message"]) {
David@0
   319
			//A message element has started!
zacw@1226
   320
			//This means that we have all of this message now, and therefore can create a single content object from the AIXMLElement tree and then throw away that tree.
zacw@1226
   321
			//This saves memory when a message element contains many elements (since each one is represented by an AIXMLElement sub-tree in the AIXMLElement tree, as opposed to a simple NSAttributeRun in the NSAttributedString of the content object).
David@0
   322
David@0
   323
			NSString     *serviceName = [contextInfo objectForKey:@"Service name"];
David@0
   324
			AIListObject *account     = [contextInfo objectForKey:@"Account"];
David@0
   325
			NSString     *accountID   = [contextInfo objectForKey:@"Account ID"];
David@0
   326
			AIChat       *chat        = [contextInfo objectForKey:@"Chat"];
zacw@2820
   327
			
David@0
   328
			//Set up some doohickers.
zacw@1226
   329
			NSDictionary	*attributes = [element attributes];
zacw@1226
   330
			NSString		*timeString = [attributes objectForKey:@"time"];
David@0
   331
			//Create the context object
David@0
   332
			if (timeString) {
David@0
   333
				NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
David@0
   334
zacw@1226
   335
				NSString		*autoreplyAttribute = [attributes objectForKey:@"auto"];
zacw@1226
   336
				NSString		*sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
zacw@1226
   337
				BOOL			sentByMe = ([sender isEqualToString:accountID]);
David@0
   338
				
David@0
   339
				/*don't fade the messages if they're within the last 5 minutes
David@0
   340
				 *since that will be resuming a conversation, not starting a new one.
David@0
   341
				 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
David@0
   342
				 */
David@0
   343
				Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
zacw@890
   344
				
zacw@890
   345
				AIListContact *listContact = nil;
zacw@890
   346
				
zacw@890
   347
				if (chat.isGroupChat) {
zacw@1226
   348
					listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
zacw@890
   349
				} else {
zacw@890
   350
					listContact = chat.listObject;
zacw@890
   351
				}
zacw@890
   352
				
David@0
   353
				AIContentMessage *message = [messageClass messageInChat:chat 
zacw@890
   354
															 withSource:(sentByMe ? account : listContact)
zacw@890
   355
															destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
David@0
   356
																   date:time
zacw@1226
   357
																message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
David@0
   358
															  autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
David@0
   359
				
David@0
   360
				//Don't log this object
David@0
   361
				[message setPostProcessContent:NO];
David@0
   362
				[message setTrackContent:NO];
David@0
   363
David@0
   364
				//Add it to the array (in front, since we're working backwards, and we want the array in forward order)
David@0
   365
				[foundMessages insertObject:message atIndex:0];
David@0
   366
			} else {
David@0
   367
				NSLog(@"Null message context display time for %@",element);
David@0
   368
			}
David@0
   369
		}
zacw@2820
   370
		
David@0
   371
		[elementStack removeObjectAtIndex:0U];
zacw@2820
   372
		if ([foundMessages count] == *linesLeftToFind) {
David@0
   373
			if ([elementStack count]) [elementStack removeAllObjects];
David@0
   374
			[parser abortParsing];
David@0
   375
		}
David@0
   376
	}
David@0
   377
}
David@0
   378
David@0
   379
@end