Source/DCMessageContextDisplayPlugin.m
author Zachary West <zacw@adium.im>
Wed Nov 25 23:53:28 2009 -0500 (2009-11-25)
changeset 2824 0eb39f6eb39c
parent 2823 24c471ce6c9e
child 2829 51687f71fc63
permissions -rw-r--r--
Fix the context parser ignoring pages. Fixes #13362.

Every iteration was doing:
1. Move to location for reading.
2. Read
3. Move to location for next read.

So we were skipping reads like a mofo here. I believe this fixes any remaining context issues that un-instance-variableing didn't hit.
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
David@0
   167
	//Iterate over the elements of the log path array.
David@0
   168
	NSEnumerator *pathsEnumerator = [logPaths objectEnumerator];
David@0
   169
	NSString *logPath = nil;
David@0
   170
	while (linesLeftToFind > 0 && (logPath = [pathsEnumerator nextObject])) {
David@0
   171
		//If it's not a .chatlog, ignore it.
David@0
   172
		if (![logPath hasSuffix:@".chatlog"])
David@0
   173
			continue;
David@0
   174
				
David@0
   175
		//Stick the base path on to the beginning
David@0
   176
		logPath = [baseLogPath stringByAppendingPathComponent:logPath];
David@0
   177
David@0
   178
		//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
   179
		NSString *xmlFilePath = logPath;
David@0
   180
David@0
   181
		BOOL isDir;
David@0
   182
		if ([[NSFileManager defaultManager] fileExistsAtPath:logPath isDirectory:&isDir]) {
David@0
   183
			/* If we have a chatLog bundle, we want to get the text content for the xml file inside */
David@0
   184
			NSString *baseURL;
David@0
   185
			if (isDir) {
David@0
   186
				baseURL = logPath;
David@0
   187
				xmlFilePath = [logPath stringByAppendingPathComponent:
David@0
   188
							   [[[logPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
David@0
   189
			} else {
David@0
   190
				baseURL = nil;
David@0
   191
			}
David@0
   192
			[decoder setBaseURL:baseURL];
David@0
   193
		}
David@0
   194
David@0
   195
		//Initialize the found messages array and element stack for us-as-delegate
zacw@2822
   196
		NSMutableArray *foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
zacw@2822
   197
		NSMutableArray *elementStack = [NSMutableArray array];
David@0
   198
David@0
   199
		//Create the parser and set ourselves as the delegate
David@0
   200
		LMXParser *parser = [LMXParser parser];
David@0
   201
		[parser setDelegate:self];
David@0
   202
David@0
   203
		//Set up info needed by elementStarted to create content objects.
David@0
   204
		NSMutableDictionary *contextInfo = nil;
David@0
   205
		{
David@0
   206
			//Get the service name from the path name
David@0
   207
			NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
David@0
   208
David@426
   209
			AIListObject *account = chat.account;
David@715
   210
			NSString	 *accountID = [NSString stringWithFormat:@"%@.%@", account.service.serviceID, account.UID];
David@0
   211
David@0
   212
			contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
David@0
   213
						   serviceName, @"Service name",
David@0
   214
						   account, @"Account",
David@0
   215
						   accountID, @"Account ID",
David@0
   216
						   chat, @"Chat",
David@0
   217
						   decoder, @"AIHTMLDecoder",
zacw@2820
   218
						   [NSValue valueWithPointer:&linesLeftToFind], @"LinesLeftToFindValue",
zacw@2822
   219
						   foundMessages, @"FoundMessages",
zacw@2822
   220
						   elementStack, @"ElementStack",
David@0
   221
						   nil];
David@0
   222
			[parser setContextInfo:(void *)contextInfo];
David@0
   223
		}
David@0
   224
David@0
   225
		//Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
David@0
   226
		NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:xmlFilePath];
David@0
   227
		[file seekToEndOfFile];
David@0
   228
		
David@0
   229
		//Set up some more doohickeys and then start the parse loop
David@3
   230
		NSInteger readSize = 4 * getpagesize(); //Read 4 pages at a time.
David@0
   231
		NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
David@3
   232
		NSInteger fd = [file fileDescriptor];
David@0
   233
		char *buf = [chunk mutableBytes];
David@0
   234
		off_t offset = [file offsetInFile];
David@0
   235
		enum LMXParseResult result = LMXParsedIncomplete;
David@0
   236
zacw@2823
   237
		// These set of file's autorelease pool.
zacw@2823
   238
		NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
zacw@2823
   239
		
David@0
   240
		do {
zacw@2824
   241
			//Calculate the new offset. This also leaves offset where it needs to be
zacw@2824
   242
			//for the next iteration, since we modify it for the round every time.
David@0
   243
			offset = (offset <= readSize) ? 0 : offset - readSize;
David@0
   244
			
David@0
   245
			//Seek to it and read greedily until we hit readSize or run out of file.
David@3
   246
			NSInteger idx = 0;
David@0
   247
			for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) { 
David@0
   248
				amountRead = pread(fd, buf + idx, readSize, offset + idx); 
David@0
   249
			   if (amountRead <= 0) break;
David@0
   250
			}
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);
zacw@2824
   257
		
zacw@2822
   258
		//Drain our autorelease pool.
zacw@2823
   259
		[parsingAutoreleasePool release];
David@0
   260
David@0
   261
		//Be a good citizen and close the file
David@0
   262
		[file closeFile];
zacw@2824
   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@2823
   268
		
zacw@2820
   269
	if (linesLeftToFind > 0) {
zacw@2820
   270
		AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
zacw@2820
   271
	}
zacw@2820
   272
	
David@0
   273
	return outerFoundContentContexts;
David@0
   274
}
David@0
   275
David@0
   276
#pragma mark LMX delegate
David@0
   277
David@0
   278
- (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
David@0
   279
{
zacw@2822
   280
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   281
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   282
	
David@0
   283
	if ([elementName isEqualToString:@"message"]) {
zacw@1226
   284
		[elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
David@0
   285
	}
David@0
   286
	else if ([elementStack count]) {
zacw@1226
   287
		AIXMLElement *element = [AIXMLElement elementWithName:elementName];
zacw@2798
   288
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
David@0
   289
		[elementStack insertObject:element atIndex:0U];
David@0
   290
	}
David@0
   291
}
David@0
   292
David@0
   293
- (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
David@0
   294
{
zacw@2822
   295
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   296
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   297
	
David@0
   298
	if ([elementStack count])
zacw@2798
   299
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
David@0
   300
}
David@0
   301
David@0
   302
- (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
David@0
   303
{
zacw@2822
   304
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   305
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   306
	
David@0
   307
	if ([elementStack count]) {
zacw@1226
   308
		AIXMLElement *element = [elementStack objectAtIndex:0U];
David@0
   309
		if (attributes) {
zacw@1226
   310
			[element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
David@0
   311
		}
David@0
   312
		
zacw@2823
   313
		NSMutableArray	*foundMessages = [contextInfo objectForKey:@"FoundMessages"];
zacw@2822
   314
		NSInteger	 *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
zacw@2822
   315
		
David@0
   316
		if ([elementName isEqualToString:@"message"]) {
David@0
   317
			//A message element has started!
zacw@1226
   318
			//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
   319
			//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
   320
David@0
   321
			NSString     *serviceName = [contextInfo objectForKey:@"Service name"];
David@0
   322
			AIListObject *account     = [contextInfo objectForKey:@"Account"];
David@0
   323
			NSString     *accountID   = [contextInfo objectForKey:@"Account ID"];
David@0
   324
			AIChat       *chat        = [contextInfo objectForKey:@"Chat"];
zacw@2820
   325
			
David@0
   326
			//Set up some doohickers.
zacw@1226
   327
			NSDictionary	*attributes = [element attributes];
zacw@1226
   328
			NSString		*timeString = [attributes objectForKey:@"time"];
David@0
   329
			//Create the context object
David@0
   330
			if (timeString) {
David@0
   331
				NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
David@0
   332
zacw@1226
   333
				NSString		*autoreplyAttribute = [attributes objectForKey:@"auto"];
zacw@1226
   334
				NSString		*sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
zacw@1226
   335
				BOOL			sentByMe = ([sender isEqualToString:accountID]);
David@0
   336
				
David@0
   337
				/*don't fade the messages if they're within the last 5 minutes
David@0
   338
				 *since that will be resuming a conversation, not starting a new one.
David@0
   339
				 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
David@0
   340
				 */
David@0
   341
				Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
zacw@890
   342
				
zacw@890
   343
				AIListContact *listContact = nil;
zacw@890
   344
				
zacw@890
   345
				if (chat.isGroupChat) {
zacw@1226
   346
					listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
zacw@890
   347
				} else {
zacw@890
   348
					listContact = chat.listObject;
zacw@890
   349
				}
zacw@890
   350
				
David@0
   351
				AIContentMessage *message = [messageClass messageInChat:chat 
zacw@890
   352
															 withSource:(sentByMe ? account : listContact)
zacw@890
   353
															destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
David@0
   354
																   date:time
zacw@1226
   355
																message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
David@0
   356
															  autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
David@0
   357
				
David@0
   358
				//Don't log this object
David@0
   359
				[message setPostProcessContent:NO];
David@0
   360
				[message setTrackContent:NO];
David@0
   361
David@0
   362
				//Add it to the array (in front, since we're working backwards, and we want the array in forward order)
David@0
   363
				[foundMessages insertObject:message atIndex:0];
David@0
   364
			} else {
David@0
   365
				NSLog(@"Null message context display time for %@",element);
David@0
   366
			}
David@0
   367
		}
zacw@2820
   368
		
David@0
   369
		[elementStack removeObjectAtIndex:0U];
zacw@2820
   370
		if ([foundMessages count] == *linesLeftToFind) {
David@0
   371
			if ([elementStack count]) [elementStack removeAllObjects];
David@0
   372
			[parser abortParsing];
David@0
   373
		}
David@0
   374
	}
David@0
   375
}
David@0
   376
David@0
   377
@end