/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.Conversations; using JetBrains.Omea.ResourceTools; using JetBrains.Omea.GUIControls; namespace JetBrains.Omea.InstantMessaging.Trillian { /// /// The plugin for importing Trillian logs into OmniaMea. /// [PluginDescriptionAttribute("Trillian IM", "JetBrains Inc.", "Trillian IM conversation viewer.\n Extracts Trillian database and converts it into searchable conversations.", PluginDescriptionFormat.PlainText, "Icons/TrillainPluginIcon.png")] public class TrillianPlugin: IPlugin, IResourceDisplayer { private ICore _environment; private TrillianProfileManager _profileManager; // Resource types used by the plugin. private const string _typeTrillianAccount = "TrillianAccount"; private const string _typeTrillianConversation = "TrillianConversation"; // IDs of the properties used by the plugin. Initialized in RegisterTypes(). private int _propProtocol; private int _propIMAddress; private int _propNick; private int _propTrillianAcct; private int _propFromAccount; private int _propToAccount; private int _propLastImportOffset; // The contact describing the current user of OmniaMea. private IResource _myselfContact; // The class which manages creating IM conversations from individual messages, // converting conversations to HTML and so on. private IMConversationsManager _convManager; // The address book where we will collect the contacts imported from Trillian. private AddressBook _trillianAB; /** * Initializes the plugin, registers the resource and property types * and UI elements used by the plugin, returns the array of resource * types for which the plugin is responsible. */ public void Register() { _environment = ICore.Instance; _profileManager = new TrillianProfileManager(); if ( _profileManager.ProfileCount == 0 ) { // Do not show the plugin if Trillian is not installed or no profiles // for it are defined. return; } RegisterTypes(); // Initialize the conversation manager. It will by itself register some of the // resource and property types used by conversations, so we need to supply // it with information. // The TimeSpan parameter specifies the maximum interval between messages for // which they are still considered a single conversation. We could make this // configurable, like the ICQ plugin does, but for simplicity we don't. _convManager = new IMConversationsManager( _typeTrillianConversation, "Trillian Conversation", "Subject", new TimeSpan( 1, 0, 0 ), _propTrillianAcct, _propFromAccount, _propToAccount, this); // The conversation manager will also take responsibility for passing the text of // conversations for text indexing. _environment.PluginLoader.RegisterResourceTextProvider( _typeTrillianConversation, _convManager ); // Register our plugin as the displayer for TrillianConversation resources. _environment.PluginLoader.RegisterResourceDisplayer( _typeTrillianConversation, this ); IUIManager uiMgr = Core.UIManager; uiMgr.RegisterOptionsGroup( "Instant Messaging", "The Instant Messaging options enable you to control how [product name] works with supported instant messaging programs." ); // Registers the options pane for the plugin to be shown in the Options // dialog and the Startup Wizard. uiMgr.RegisterOptionsPane( "Instant Messaging", "Trillian", CreateTrillianOptionsPane, null ); uiMgr.RegisterWizardPane( "Trillian", CreateTrillianOptionsPane, 11 ); } /** * Registers the resource and property types used by the plugin. */ private void RegisterTypes() { // Registers the property that will be used to store the protocol of // a Trillian account (ICQ, AIM, MSN and so on). Note that all properties // used in the display name mask of a resource type need to be registered // before the resource type itself. _propProtocol = _environment.ResourceStore.PropTypes.Register( "Protocol", PropDataType.String ); // The property which will be used to store the protocol-specific ID of // the account (UIN, screen name and so on). _propIMAddress = _environment.ResourceStore.PropTypes.Register( "IMAddress", PropDataType.String ); // The property for storing the account-specific nickname of the contact. _propNick = _environment.ResourceStore.PropTypes.Register( "Nick", PropDataType.String ); // The properties for linking messages to accounts through which they were // sent or received. _propFromAccount = _environment.ResourceStore.PropTypes.Register( "FromAccount", PropDataType.Link, PropTypeFlags.Internal ); _propToAccount = _environment.ResourceStore.PropTypes.Register( "ToAccount", PropDataType.Link, PropTypeFlags.Internal ); // The property for storing the last offset in the Trillian log which // was imported. This property is set on TrillianAccount instances. // The property is registered as Internal because we don't want it to be // visible in the resource browser column selector. _propLastImportOffset = _environment.ResourceStore.PropTypes.Register( "LastImportOffset", PropDataType.Int, PropTypeFlags.Internal ); // The property for linking the TrillianAccount resource to a contact. // Since we won't be looking on a TrillianAccount resource directly, // there is no need to register the link as directed. // Note that the property ID may not contain spaces, but the user-friendly // display name has no such restriction. // Also note that, for contact merging to work correctly, we need to mark // the link type with the ContactAccount flag. _propTrillianAcct = _environment.ResourceStore.PropTypes.Register( "TrillianAcct", PropDataType.Link, PropTypeFlags.ContactAccount ); _environment.ResourceStore.PropTypes.RegisterDisplayName( _propTrillianAcct, "Trillian Account"); // Registers the resource type which will be used for storing information // about the IM accounts of a contact. Each instance of this resource type // will have properties Protocol, IMAddress and Nick, and these resources will // be linked to Contact resources. We set the Internal flag because the // account does not need to appear in any views, and the NoIndex flag because // the account has no data that could be added to the full-text index. _environment.ResourceStore.ResourceTypes.Register( _typeTrillianAccount, "Protocol IMAddress", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex ); } /** * Creates the options pane for the Trillian plugin. The options panes are * created lazily, so this method is called only when the "Trillian" page * is actually selected in the Options dialog. */ private AbstractOptionsPane CreateTrillianOptionsPane() { TrillianOptionsPane optionsPane = new TrillianOptionsPane(); optionsPane.ProfileManager = _profileManager; return optionsPane; } /** * Performs the startup activities of the plugin and starts any * background processes (if needed). Returns true if the startup * was successful. */ public void Startup() { // Read the "profiles to index" setting. Note that the Startup Wizard // is invoked after Register(), so it's too early to read settings in // Register(). string profilesToIndex = _environment.SettingStore.ReadString( "Trillian", "ProfilesToIndex" ); ArrayList profileList = new ArrayList( profilesToIndex.Split( ';' ) ); if ( profileList.Count == 0 ) { return; } // Create the address book for Trillian contacts. (If an address // book with the same name already exists, it will attach to that instance.) _trillianAB = new AddressBook( "Trillian Contacts" ); _trillianAB.IsExportable = false; foreach( TrillianProfile profile in _profileManager.Profiles ) { if ( profileList.IndexOf( profile.Name ) >= 0 ) { ImportProfile( profile ); } } } /** * Ends the work of the plugin, stops any background processes (if needed), * releases the resources. */ public void Shutdown() { } #region IResourceDisplayer Members /** * This method is called to create an instance of the control which we will * use to display our resources. We use a standard helper class that displays * resources in an instance of the embedded browser and pass to it the delegate * that will format our resources as HTML. */ public IDisplayPane CreateDisplayPane( string resourceType ) { if ( resourceType == _typeTrillianConversation ) { return new BrowserDisplayPane( DisplayConversation ); } return null; } /** * This method is called by IEBrowserDisplayPane to display the specified resource * in the browser. */ private void DisplayConversation( IResource resource, AbstractWebBrowser browser, WordPtr[] wordsToHighlight ) { // Ask ConversationManager to format the conversation as HTML and // feed it into IEBrowser. string htmlString = _convManager.ToHtmlString( resource, _propNick ); browser.ShowHtml( htmlString, WebSecurityContext.Restricted, DocumentSection.RestrictResults(wordsToHighlight, DocumentSection.BodySection) ); } #endregion /** * Imports a single Trillian profile. */ private void ImportProfile( TrillianProfile profile ) { _environment.ProgressWindow.UpdateProgress( 0, "Importing Trillian profile " + profile.Name + "...", null ); ImportBuddyGroup( profile.Buddies ); string name = profile.ReadICQSetting( "name" ); string nick = profile.ReadICQSetting( "nick name" ); string firstName = profile.ReadICQSetting( "first name" ); string lastName = profile.ReadICQSetting( "last name" ); string email = profile.ReadICQSetting( "email" ); // The ICQ profile contains the most information that we could use to // find or create Myself contact, so we use it. If the ICQ profile was // not configured, we don't have enough data to create a useful Myself // contact anyway if it does not already exist. So simply use the (empty) // ICQ data in this case too. IContact mySelfContact = Core.ContactManager.FindOrCreateMySelfContact( email, firstName + " " + lastName ); _myselfContact = mySelfContact.Resource; if ( name.Length > 0 ) { IResource icqAccount = FindTrillianAccount( "ICQ", name ); if ( icqAccount == null ) { icqAccount = CreateTrillianAccount( "ICQ", name, nick ); _myselfContact.AddLink( _propTrillianAcct, icqAccount ); } TrillianLog[] logs = profile.GetLogs( "ICQ" ); foreach( TrillianLog log in logs ) { ImportLog( "ICQ", log, icqAccount ); } } /* foreach( string protocol in new string[] { "ICQ", "AIM", "IRC", "MSN", "Yahoo" } ) { TrillianLog[] logs = profile.GetLogs( protocol ); foreach( TrillianLog log in logs ) { ImportLog( protocol, log ); } } */ } /** * Imports a single buddy group. */ private void ImportBuddyGroup( TrillianBuddyGroup group ) { foreach( TrillianBuddy buddy in group.Buddies ) { IResource account = FindOrCreateTrillianAccount( buddy.Protocol, buddy.Address, buddy.Nick ); IResource contact = account.GetLinkProp( _propTrillianAcct ); _trillianAB.AddContact( contact ); } foreach( TrillianBuddyGroup childGroup in group.Groups ) { ImportBuddyGroup( childGroup ); } } /** * Finds an existing Trillian account with the specified protocol and UIN, * or creates a new one. */ private IResource FindOrCreateTrillianAccount( string protocol, string uin, string nick ) { // Check if we already have an account for that buddy. IResource existingAccount = FindTrillianAccount( protocol, uin ); if ( existingAccount != null ) return existingAccount; IResource account = CreateTrillianAccount( protocol, uin, nick ); // Now link the account to a contact. Since Trillian, unlike ICQ or Miranda, // doesn't store any information about contacts in the contact list, we could // identify the contact only by nickname. This means that we'll create bogus // contacts most of the time, and the user will need to use the contact merging // feature to link the Trillian conversations to the correct contact. IContact contact = Core.ContactManager.FindOrCreateContact( null, nick ?? uin ); contact.Resource.AddLink( _propTrillianAcct, account ); return account; } /** * Finds an existing Trillian account with the specified protocol and UIN. */ private IResource FindTrillianAccount( string protocol, string uin ) { // The FindResources() method allows to specify only one search condition, // so we need to get the buddies with the same UIN and manually loop through // them to see if they have the right protocol. IResourceList resList = _environment.ResourceStore.FindResources( _typeTrillianAccount, _propIMAddress, uin ); foreach( IResource res in resList ) { if ( res.GetStringProp( _propProtocol ) == protocol ) { return res; } } return null; } /** * Creates a Trillian account with the specified parameters. */ private IResource CreateTrillianAccount( string protocol, string uin, string nick ) { // Create the Trillian account from the specified data. The import // is called from the Startup() method, which is running in the resource // thread, so we can work with the resource store directly and need not use // ResourceProxy. However, using BeginNewResource() is a good idea in any case. IResource account = _environment.ResourceStore.BeginNewResource( _typeTrillianAccount ); account.SetProp( _propProtocol, protocol ); account.SetProp( _propIMAddress, uin ); if ( nick != null ) { account.SetProp( _propNick, nick ); } account.EndUpdate(); return account; } /** * Imports a single Trillian log. */ private void ImportLog( string protocol, TrillianLog log, IResource selfAccount ) { // For ICQ, the first session does not contain the nickname of the correspondent. // Thus, to avoid creating number-only contacts, we buffer messages until we // get a real nick, then create the contact and account, process the messages from // the buffer and continue parsing normally. ArrayList messageBuffer = new ArrayList(); IResource correspondentAcct = FindTrillianAccount( protocol, log.GetName() ); if ( correspondentAcct != null ) { // GetIntProp() returns 0 if the property was not defined int lastImportOffset = correspondentAcct.GetIntProp( _propLastImportOffset ); // If the log size is less than the last import offset, it means that the // log was truncated. Redo import from start. if ( lastImportOffset > log.Size ) { lastImportOffset = 0; } if ( lastImportOffset == log.Size ) { // the log is already completely imported return; } log.Seek( lastImportOffset ); } while( true ) { TrillianLogMessage msg = log.ReadNextMessage(); if ( msg == null ) break; if ( correspondentAcct == null ) { int foo; if ( protocol == "ICQ" && Int32.TryParse( log.CurCorrespondentName, out foo ) ) { messageBuffer.Add( msg ); continue; } correspondentAcct = FindOrCreateTrillianAccount( protocol, log.GetName(), log.CurCorrespondentName ); ImportFromBuffer( messageBuffer, selfAccount, correspondentAcct ); messageBuffer.Clear(); } IResource fromAccount = msg.Incoming ? correspondentAcct : selfAccount; IResource toAccount = msg.Incoming ? selfAccount : correspondentAcct; IResource conversation = _convManager.Update( msg.Text, msg.Time, fromAccount, toAccount ); // Request text indexing of the conversation. Repeated indexing requests // will be merged, so there is no harm in requesting to index the same // conversation multiple times. if ( conversation != null ) { _environment.TextIndexManager.QueryIndexing( conversation.Id ); } } if ( correspondentAcct == null && messageBuffer.Count > 0 ) { // no nickname until end of log => process messages accumulated in the buffer correspondentAcct = FindOrCreateTrillianAccount( protocol, log.GetName(), null ); ImportFromBuffer( messageBuffer, selfAccount, correspondentAcct ); } correspondentAcct.SetProp( _propLastImportOffset, log.Size ); } /** * Imports log messages from the specified buffer. */ private void ImportFromBuffer( ArrayList buffer, IResource selfAccount, IResource correspondentAcct ) { foreach( TrillianLogMessage msg in buffer ) { IResource fromAccount = msg.Incoming ? correspondentAcct : selfAccount; IResource toAccount = msg.Incoming ? selfAccount : correspondentAcct; IResource conversation = _convManager.Update( msg.Text, msg.Time, fromAccount, toAccount ); if ( conversation != null ) { _environment.TextIndexManager.QueryIndexing( conversation.Id ); } } } } }