Source/DCMessageContextDisplayPlugin.m
author Zachary West <zacw@adium.im>
Thu Nov 26 10:33:53 2009 -0500 (2009-11-26)
changeset 2829 51687f71fc63
parent 2824 0eb39f6eb39c
child 2924 90cc6e5031bb
permissions -rw-r--r--
Fix a few more issues with context reading; if we're reading less than readSize, we need to decrement readSize too. 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
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
		off_t offset = [file offsetInFile];
David@0
   234
		enum LMXParseResult result = LMXParsedIncomplete;
David@0
   235
zacw@2823
   236
		// These set of file's autorelease pool.
zacw@2823
   237
		NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
zacw@2823
   238
		
David@0
   239
		do {
zacw@2829
   240
			// The location we're going to read for *this* set of reads.
zacw@2829
   241
			off_t readOffset = offset - readSize;
zacw@2829
   242
zacw@2829
   243
			if (readOffset < 0) {
zacw@2829
   244
				// Decrease it by the amount we're over.
zacw@2829
   245
				readSize += readOffset;
zacw@2829
   246
				// Start from the beginning.
zacw@2829
   247
				readOffset = 0;
zacw@2829
   248
			}
David@0
   249
			
zacw@2829
   250
			if (chunk.length != readSize) {
zacw@2829
   251
				// In case we short-read last time, or we're reading a smaller amount this time.
zacw@2829
   252
				[chunk setLength:readSize];				
zacw@2829
   253
			}
zacw@2829
   254
			
zacw@2829
   255
			char *buf = [chunk mutableBytes];
zacw@2829
   256
David@0
   257
			//Seek to it and read greedily until we hit readSize or run out of file.
David@3
   258
			NSInteger idx = 0;
zacw@2829
   259
			ssize_t amountRead = 0;
zacw@2829
   260
			for (amountRead = 0; idx < readSize; idx += amountRead) { 
zacw@2829
   261
				amountRead = pread(fd, buf + idx, readSize, readOffset + idx); 
David@0
   262
			   if (amountRead <= 0) break;
David@0
   263
			}
David@0
   264
			
zacw@2829
   265
			if (idx != readSize) {
zacw@2829
   266
				// If we short read, we don't want to read unknown buffer contents.
zacw@2829
   267
				[chunk setLength:idx];
zacw@2829
   268
			}
zacw@2829
   269
			
zacw@2829
   270
			// Adjust the real offset
zacw@2829
   271
			offset -= idx;
zacw@2829
   272
David@0
   273
			//Parse
David@0
   274
			result = [parser parseChunk:chunk];
David@0
   275
			
David@0
   276
		//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
   277
		} while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
zacw@2824
   278
		
zacw@2822
   279
		//Drain our autorelease pool.
zacw@2823
   280
		[parsingAutoreleasePool release];
David@0
   281
David@0
   282
		//Be a good citizen and close the file
David@0
   283
		[file closeFile];
zacw@2824
   284
		
David@0
   285
		//Add our locals to the outer array; we're probably looping again.
David@0
   286
		[outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
David@0
   287
		linesLeftToFind -= [outerFoundContentContexts count];
David@0
   288
	}
zacw@2829
   289
	
zacw@2820
   290
	if (linesLeftToFind > 0) {
zacw@2820
   291
		AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
zacw@2820
   292
	}
zacw@2820
   293
	
David@0
   294
	return outerFoundContentContexts;
David@0
   295
}
David@0
   296
David@0
   297
#pragma mark LMX delegate
David@0
   298
David@0
   299
- (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
David@0
   300
{
zacw@2822
   301
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   302
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   303
	
David@0
   304
	if ([elementName isEqualToString:@"message"]) {
zacw@1226
   305
		[elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
David@0
   306
	}
David@0
   307
	else if ([elementStack count]) {
zacw@1226
   308
		AIXMLElement *element = [AIXMLElement elementWithName:elementName];
zacw@2798
   309
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
David@0
   310
		[elementStack insertObject:element atIndex:0U];
David@0
   311
	}
David@0
   312
}
David@0
   313
David@0
   314
- (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
David@0
   315
{
zacw@2822
   316
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   317
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   318
	
David@0
   319
	if ([elementStack count])
zacw@2798
   320
		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
David@0
   321
}
David@0
   322
David@0
   323
- (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
David@0
   324
{
zacw@2822
   325
	NSMutableDictionary *contextInfo = [parser contextInfo];
zacw@2822
   326
	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
zacw@2822
   327
	
David@0
   328
	if ([elementStack count]) {
zacw@1226
   329
		AIXMLElement *element = [elementStack objectAtIndex:0U];
David@0
   330
		if (attributes) {
zacw@1226
   331
			[element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
David@0
   332
		}
David@0
   333
		
zacw@2823
   334
		NSMutableArray	*foundMessages = [contextInfo objectForKey:@"FoundMessages"];
zacw@2822
   335
		NSInteger	 *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
zacw@2822
   336
		
David@0
   337
		if ([elementName isEqualToString:@"message"]) {
David@0
   338
			//A message element has started!
zacw@1226
   339
			//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
   340
			//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
   341
David@0
   342
			NSString     *serviceName = [contextInfo objectForKey:@"Service name"];
David@0
   343
			AIListObject *account     = [contextInfo objectForKey:@"Account"];
David@0
   344
			NSString     *accountID   = [contextInfo objectForKey:@"Account ID"];
David@0
   345
			AIChat       *chat        = [contextInfo objectForKey:@"Chat"];
zacw@2820
   346
			
David@0
   347
			//Set up some doohickers.
zacw@1226
   348
			NSDictionary	*attributes = [element attributes];
zacw@1226
   349
			NSString		*timeString = [attributes objectForKey:@"time"];
David@0
   350
			//Create the context object
David@0
   351
			if (timeString) {
David@0
   352
				NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
David@0
   353
zacw@1226
   354
				NSString		*autoreplyAttribute = [attributes objectForKey:@"auto"];
zacw@1226
   355
				NSString		*sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
zacw@1226
   356
				BOOL			sentByMe = ([sender isEqualToString:accountID]);
David@0
   357
				
David@0
   358
				/*don't fade the messages if they're within the last 5 minutes
David@0
   359
				 *since that will be resuming a conversation, not starting a new one.
David@0
   360
				 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
David@0
   361
				 */
David@0
   362
				Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
zacw@890
   363
				
zacw@890
   364
				AIListContact *listContact = nil;
zacw@890
   365
				
zacw@890
   366
				if (chat.isGroupChat) {
zacw@1226
   367
					listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
zacw@890
   368
				} else {
zacw@890
   369
					listContact = chat.listObject;
zacw@890
   370
				}
zacw@890
   371
				
David@0
   372
				AIContentMessage *message = [messageClass messageInChat:chat 
zacw@890
   373
															 withSource:(sentByMe ? account : listContact)
zacw@890
   374
															destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
David@0
   375
																   date:time
zacw@1226
   376
																message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
David@0
   377
															  autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
David@0
   378
				
David@0
   379
				//Don't log this object
David@0
   380
				[message setPostProcessContent:NO];
David@0
   381
				[message setTrackContent:NO];
zacw@2829
   382
				
David@0
   383
				//Add it to the array (in front, since we're working backwards, and we want the array in forward order)
David@0
   384
				[foundMessages insertObject:message atIndex:0];
David@0
   385
			} else {
David@0
   386
				NSLog(@"Null message context display time for %@",element);
David@0
   387
			}
David@0
   388
		}
zacw@2820
   389
		
David@0
   390
		[elementStack removeObjectAtIndex:0U];
zacw@2820
   391
		if ([foundMessages count] == *linesLeftToFind) {
David@0
   392
			if ([elementStack count]) [elementStack removeAllObjects];
David@0
   393
			[parser abortParsing];
David@0
   394
		}
David@0
   395
	}
David@0
   396
}
David@0
   397
David@0
   398
@end