/// /// 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.Diagnostics; using System.Drawing; using System.IO; using System.Reflection; using System.Threading; using System.Windows.Forms; using System.Xml; using JetBrains.Omea.AsyncProcessing; using JetBrains.Omea.Base; using JetBrains.Omea.Contacts; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.Conversations; using JetBrains.Omea.ResourceTools; using JetBrains.Omea.GUIControls; namespace JetBrains.Omea.InstantMessaging.Miranda { [PluginDescriptionAttribute("Miranda IM", "JetBrains Inc.", "Miranda IM conversation viewer.\n Extracts Miranda database and converts it into searchable conversations.", PluginDescriptionFormat.PlainText, "Icons/MirandaPluginIcon.png")] public class MirandaPlugin: IPlugin, IResourceDisplayer { private const string _MirandaOptionsDescription = "The Miranda options enable you to specify which Miranda accounts should be indexed, and how [product name] should build conversations from Miranda messages."; private IResourceStore _store; private string _profileToIndex; private string _dbPath; private FileSystemWatcher _mirandaWatcher; private MirandaImportJob _importJob; private AddressBook _mirandaAB; private static IMConversationsManager _convManager; private static CorrespondentCtrl _correspondentPane; private static MirandaPlugin _theInstance; void IPlugin.Register() { _theInstance = this; _store = Core.ResourceStore; Props.Register(); ResourceTypes.Register( this ); RegisterTypes(); IDisplayColumnManager colMgr = Core.DisplayColumnManager; colMgr.RegisterDisplayColumn( ResourceTypes.MirandaConversation, 0, new ColumnDescriptor( "From", 100 ) ); colMgr.RegisterDisplayColumn( ResourceTypes.MirandaConversation, 1, new ColumnDescriptor( "To", 100 ) ); colMgr.RegisterDisplayColumn( ResourceTypes.MirandaConversation, 2, new ColumnDescriptor( new[] { Core.ResourceStore.PropTypes [Core.Props.Subject].Name, "DisplayName" }, 300, ColumnDescriptorFlags.AutoSize ) ); colMgr.RegisterDisplayColumn( ResourceTypes.MirandaConversation, 3, new ColumnDescriptor( "Date", 120 ) ); if( !_store.PropTypes.Exist( "ConversationList" ) ) { ClearConversations(); } _convManager = new IMConversationsManager( ResourceTypes.MirandaConversation, "Miranda Conversation", "Subject", IniSettings.ConversationPeriodTimeSpan, Props.MirandaAcct, Props.FromAccount, Props.ToAccount, this ); _convManager.ReverseMode = IniSettings.LatestOnTop; Core.PluginLoader.RegisterResourceTextProvider( ResourceTypes.MirandaConversation, _convManager ); Core.PluginLoader.RegisterResourceTextProvider( "Contact", new MirandaContactTextProvider() ); Core.PluginLoader.RegisterResourceDisplayer( ResourceTypes.MirandaConversation, this ); Core.ActionManager.RegisterLinkClickAction( new ConversationLinkClickAction(), ResourceTypes.MirandaConversation, null ); Core.PluginLoader.RegisterResourceSerializer( ResourceTypes.MirandaAIMAccount, new MirandaAccountSerializer( Props.ScreenName ) ); Core.PluginLoader.RegisterResourceSerializer( ResourceTypes.MirandaICQAccount, new MirandaAccountSerializer( Props.UIN ) ); string[] dbNames = ProfileManager.GetProfileList(); if ( dbNames.Length > 0 ) { 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." ); uiMgr.RegisterWizardPane( "Miranda", CreateMirandaOptions, 11 ); uiMgr.RegisterOptionsPane( "Instant Messaging", "Miranda", CreateMirandaOptions, _MirandaOptionsDescription ); uiMgr.AddOptionsChangesListener( "Instant Messaging", "Miranda", OnMirandaOptionsChanged ); Core.TabManager.RegisterResourceTypeTab( "IM", "IM", "MirandaConversation", 2 ); _correspondentPane = new CorrespondentCtrl(); _correspondentPane.IniSection = "Miranda"; _correspondentPane.SetCorresponentFilterList( Core.ResourceStore.FindResourcesWithProp( null, Props.MirandaAcct ) ); Image img = Utils.TryGetEmbeddedResourceImageFromAssembly( Assembly.GetExecutingAssembly(), "OmniaMea.InstantMessaging.Miranda.Icons.correspondents.ico" ); Core.LeftSidebar.RegisterResourceStructurePane( "MirandaCorrespondents", "IM", "Miranda Correspondents", img, _correspondentPane ); Core.LeftSidebar.RegisterViewPaneShortcut( "MirandaCorrespondents", Keys.Control | Keys.Alt | Keys.M ); SaveConversationAction saveConvAction = new SaveConversationAction( _convManager, Props.NickName ); Core.ActionManager.RegisterContextMenuAction( saveConvAction, ActionGroups.ITEM_OPEN_ACTIONS, ListAnchor.Last, "Save to File...", null, "MirandaConversation", null ); Core.ActionManager.RegisterActionComponent( saveConvAction, "SaveAs", "MirandaConversation", null ); EmailConversationAction mailConvAction = new EmailConversationAction( _convManager, Props.NickName ); Core.ActionManager.RegisterContextMenuAction( mailConvAction, ActionGroups.ITEM_OPEN_ACTIONS, ListAnchor.Last, "Send by Email", null, "MirandaConversation", null ); Core.ActionManager.RegisterActionComponent( mailConvAction, "SendByMail", "MirandaConversation", null ); Core.ResourceBrowser.RegisterLinksGroup( "Accounts", new[] { Props.MirandaAcct }, ListAnchor.First ); Core.ResourceBrowser.RegisterLinksPaneFilter( ResourceTypes.MirandaConversation, new ItemRecipientsFilter() ); _mirandaAB = new AddressBook( "Miranda Contacts", ResourceTypes.MirandaConversation ); _mirandaAB.IsExportable = false; // Upgrade information about Miranda address book - set its // ContentType property so that it could be filtered out when // this plugin is switched off. _mirandaAB.Resource.SetProp( Core.Props.ContentType, ResourceTypes.MirandaConversation ); Core.ResourceBrowser.SetDefaultViewSettings( "IM", AutoPreviewMode.Off, true ); } Core.PluginLoader.RegisterResourceDeleter( ResourceTypes.MirandaConversation, new MirandaConversationDeleter() ); } private void RegisterTypes() { _store.RegisterUniqueRestriction( ResourceTypes.MirandaYahooAccount, Props.YahooId ); IResource typeMirandaMessage = _store.FindUniqueResource( "ResourceType", "Name", "MirandaMessage" ); if ( typeMirandaMessage != null ) { _store.GetAllResources( "MirandaMessage" ).DeleteAll(); ClearConversations(); } } public void Shutdown() { DisposeMirandaWatcher(); } private static AbstractOptionsPane CreateMirandaOptions() { return new MirandaOptionsPane(); } internal static CorrespondentCtrl CorresponentPane { get { return _correspondentPane; } } internal static IMConversationsManager ConversationManager { get { return _convManager; } } internal static MirandaImportJob ImportJob { get { return _theInstance._importJob; } } private void ClearConversations() { _store.GetAllResources( ResourceTypes.MirandaConversation ).DeleteAll(); ClearTimeBounds( _store.GetAllResources( ResourceTypes.MirandaAIMAccount ) ); ClearTimeBounds( _store.GetAllResources( ResourceTypes.MirandaICQAccount ) ); ClearTimeBounds( _store.GetAllResources( ResourceTypes.MirandaJabberAccount ) ); ClearTimeBounds( _store.GetAllResources( ResourceTypes.MirandaYahooAccount ) ); } void IPlugin.Startup() { _profileToIndex = Core.SettingStore.ReadString( "Miranda", "ProfileToIndex" ); if ( string.IsNullOrEmpty( _profileToIndex ) ) return; _dbPath = ProfileManager.GetDatabasePath( _profileToIndex ); if ( _dbPath == null ) return; IContactService contactService = (IContactService) Core.PluginLoader.GetPluginService( typeof(IContactService) ); if ( contactService != null ) { ContactBlockCreator creator = MirandaContactBlock.CreateBlock; contactService.RegisterContactEditBlock( 0, ListAnchor.Last, "Miranda", creator ); contactService.RegisterContactEditBlock( ContactTabNames.GeneralTab, ListAnchor.Last, "Miranda", creator ); } ImportDatabase(); if ( IniSettings.IdleIndexing && !IniSettings.FullIndexingCompleted ) { Core.ResourceAP.QueueIdleJob( new DelegateJob( new MethodInvoker( DoIdleImport ), new object[] {} ) ); } if ( IniSettings.SyncImmediate ) { CreateMirandaWatcher(); } } /// /// Creates a FileSystemWatcher for monitoring the changes to the Miranda DB. /// private void CreateMirandaWatcher() { if ( _mirandaWatcher != null ) return; if ( _dbPath != null && File.Exists( _dbPath ) ) { _mirandaWatcher = new FileSystemWatcher( Path.GetDirectoryName( _dbPath ), Path.GetFileName( _dbPath ) ); _mirandaWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; _mirandaWatcher.Changed += AsyncImportDatabase; _mirandaWatcher.EnableRaisingEvents = true; } } /** * Disposes the Miranda FileSystemWatcher. */ private void DisposeMirandaWatcher() { if ( _mirandaWatcher != null ) { _mirandaWatcher.Dispose(); _mirandaWatcher = null; } } private void OnMirandaOptionsChanged( object sender, EventArgs e ) { if ( IniSettings.SyncImmediate ) { CreateMirandaWatcher(); } else { DisposeMirandaWatcher(); } _convManager.ReverseMode = IniSettings.LatestOnTop; if ( !_convManager.ConversationPeriod.Equals( IniSettings.ConversationPeriodTimeSpan ) ) { _convManager.ConversationPeriod = IniSettings.ConversationPeriodTimeSpan; Core.ResourceAP.QueueJob( JobPriority.BelowNormal, new MethodInvoker( RebuildConversations ) ); } else { IResourceBrowser rBrowser = Core.ResourceBrowser; if( rBrowser.SelectedResources.AllResourcesOfType( "MirandaConversation" ) ) { rBrowser.RedisplaySelectedResource(); } } } private void RebuildConversations() { ClearConversations(); _importJob.ClearEventOffsets(); ImportDatabase(); } /** * event handler on changes in Miranda databases * starts async update of Miranda history */ private void AsyncImportDatabase( object sender, FileSystemEventArgs e ) { Core.ResourceAP.QueueJobAt( DateTime.Now.AddSeconds( 10 ), new MethodInvoker( ImportDatabase ) ); } private static void ClearTimeBounds( IResourceList accounts ) { foreach( IResource account in accounts ) { account.DeleteProp( Props.FirstMirandaImport ); account.DeleteProp( Props.LastMirandaImport ); } } /** * Imports the database which was configured for importing by the user. */ private void ImportDatabase() { if ( _importJob == null ) { _importJob = new MirandaImportJob( _dbPath, _convManager, _mirandaAB ); } Core.ResourceAP.QueueJob( JobPriority.BelowNormal, _importJob ); } private void DoIdleImport() { Trace.WriteLine( "Miranda Idle Import" ); _importJob.ResetIndexStartDate(); _importJob.ExecuteInIdle = true; Core.ResourceAP.QueueJob( JobPriority.Immediate, _importJob ); } // -- IResourceDisplayer implementation ------------------------------ IDisplayPane IResourceDisplayer.CreateDisplayPane( string resourceType ) { if ( resourceType == ResourceTypes.MirandaConversation ) { return new MirandaConversationDisplayPane( _convManager, Props.NickName ); } return null; } internal delegate void StringCallback( string arg ); internal static void HandleDatabaseOpenError( string message ) { DialogResult dr = MessageBox.Show( Core.MainWindow, message + ". Would you like to turn off synchronization for that database?", Core.ProductFullName, MessageBoxButtons.YesNo ); if ( dr == DialogResult.Yes ) { Core.SettingStore.WriteString( "Miranda", "ProfileToIndex", "" ); } } public static void DoRebuildConversations() { if ( Core.ProgressWindow != null ) { Core.ProgressWindow.UpdateProgress( 0, "Clearing old conversations...", null ); } Core.ResourceAP.RunJob( new MethodInvoker( _theInstance.ClearConversations ) ); ImportJob.ResetIndexStartDate(); ImportJob.ExecuteInIdle = false; Core.ResourceAP.RunJob( ImportJob ); while( !ImportJob.Completed ) { Thread.Sleep( 50 ); } } } internal class ConversationLinkClickAction: SimpleAction { public override void Execute( IActionContext context ) { IResource conversation = context.SelectedResources [0]; IResource fromContact = conversation.GetLinkProp( "From" ); if ( fromContact.HasProp( "MySelf" ) ) { fromContact = conversation.GetLinkProp( "To" ); } Core.UIManager.BeginUpdateSidebar(); Core.TabManager.CurrentTabId = "IM"; Core.LeftSidebar.ActivateViewPane( "MirandaCorrespondents" ); Core.UIManager.EndUpdateSidebar(); MirandaPlugin.CorresponentPane.SelectResource( fromContact, false ); Core.ResourceBrowser.SelectResource( context.SelectedResources [0] ); } } internal class ResourceTypes { internal const string MirandaICQAccount = "MirandaICQAccount"; internal const string MirandaAIMAccount = "MirandaAIMAccount"; internal const string MirandaJabberAccount = "MirandaJabberAccount"; internal const string MirandaYahooAccount = "MirandaYahooAccount"; internal const string MirandaConversation = "MirandaConversation"; internal static void Register( IPlugin ownerPlugin ) { IResourceStore store = Core.ResourceStore; store.ResourceTypes.Register( MirandaICQAccount, "Miranda ICQ Account", "NickName UIN", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex, ownerPlugin ); store.ResourceTypes.Register( MirandaAIMAccount, "Miranda AIM Account", "ScreenName", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex, ownerPlugin ); store.ResourceTypes.Register( MirandaJabberAccount, "Miranda Jabber Account", "JabberID", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex, ownerPlugin ); store.ResourceTypes.Register( MirandaYahooAccount, "Miranda Yahoo Account", "YahooID", ResourceTypeFlags.Internal | ResourceTypeFlags.NoIndex, ownerPlugin ); store.ResourceTypes.Register( MirandaConversation, "Miranda Conversation", Core.ResourceStore.PropTypes[ Core.Props.Subject ].Name ); } } internal class Props { private static int _propMirandaAcct; private static int _propNickName; private static int _propScreenName; private static int _propJabberID; private static int _propYahooID; private static int _propUIN; private static int _propFirstMirandaImport; private static int _propLastMirandaImport; private static int _propFromAccount; private static int _propToAccount; internal static void Register() { IResourceStore store = Core.ResourceStore; IPropTypeCollection propTypes = store.PropTypes; _propUIN = propTypes.Register( "UIN", PropDataType.Int ); _propNickName = propTypes.Register( "NickName", PropDataType.String ); _propScreenName = propTypes.Register( "ScreenName", PropDataType.String ); _propJabberID = propTypes.Register( "JabberID", PropDataType.String ); _propYahooID = propTypes.Register( "YahooID", PropDataType.String ); _propFirstMirandaImport = propTypes.Register( "FirstMirandaImport", PropDataType.Date ); _propLastMirandaImport = propTypes.Register( "LastMirandaImport", PropDataType.Date ); _propFromAccount = propTypes.Register( "FromAccount", PropDataType.Link, PropTypeFlags.Internal ); _propToAccount = propTypes.Register( "ToAccount", PropDataType.Link, PropTypeFlags.Internal ); IResource propMirandaAcct = store.FindUniqueResource( "PropType", "Name", "MirandaAcct" ); if ( propMirandaAcct != null ) { propMirandaAcct.SetProp( "Flags", (int) PropTypeFlags.ContactAccount ); _propMirandaAcct = propMirandaAcct.GetIntProp( "ID" ); } else { _propMirandaAcct = store.PropTypes.Register( "MirandaAcct", PropDataType.Link, PropTypeFlags.ContactAccount ); } store.PropTypes.RegisterDisplayName( _propMirandaAcct, "Miranda Account" ); } public static int MirandaAcct { get { return _propMirandaAcct; } } public static int UIN { get { return _propUIN; } } public static int NickName { get { return _propNickName; } } public static int ScreenName { get { return _propScreenName; } } public static int JabberId { get { return _propJabberID; } } public static int YahooId { get { return _propYahooID; } } public static int FirstMirandaImport { get { return _propFirstMirandaImport; } } public static int LastMirandaImport { get { return _propLastMirandaImport; } } public static int FromAccount { get { return _propFromAccount; } } public static int ToAccount { get { return _propToAccount; } } } internal class MirandaContactTextProvider: IResourceTextProvider { public bool ProcessResourceText( IResource res, IResourceTextConsumer consumer ) { foreach( IResource account in res.GetLinksOfType( null, Props.MirandaAcct ) ) { foreach( int propId in new[] { Props.NickName, Props.ScreenName, Props.JabberId, Props.YahooId } ) { consumer.AddDocumentFragment( res.Id, account.GetStringProp( propId) ); } } return true; } } internal class MirandaAccountSerializer: IResourceSerializer { private readonly int _keyProp; public MirandaAccountSerializer( int keyProp ) { _keyProp = keyProp; } public void AfterSerialize( IResource parentResource, IResource res, XmlNode node ) { } public IResource AfterDeserialize( IResource parentResource, IResource res, XmlNode node ) { IResource account = Core.ResourceStore.FindUniqueResource( res.Type, _keyProp, res.GetProp( _keyProp ) ); return account ?? res; } public SerializationMode GetSerializationMode( IResource res, string propertyType ) { return SerializationMode.Default; } } internal class MirandaConversationDeleter: DefaultResourceDeleter { public override bool CanDeleteResource( IResource res, bool permanent ) { return !permanent; } public override void DeleteResourcePermanent( IResource res ) { // no-op - Miranda conversations may not be deleted permanently } } internal class IniSettings { internal static bool CreateCategories { get { return Core.SettingStore.ReadBool( "Miranda", "CreateCategories", true ); } set { Core.SettingStore.WriteBool( "Miranda", "CreateCategories", value ); } } internal static bool LatestOnTop { get { return Core.SettingStore.ReadBool( "Miranda", "LatestOnTop", false ); } set { Core.SettingStore.WriteBool( "Miranda", "LatestOnTop", value ); } } internal static bool SyncImmediate { get { return Core.SettingStore.ReadBool( "Miranda", "SyncImmediate", true ); } set { Core.SettingStore.WriteBool( "Miranda", "SyncImmediate", value ); } } internal static int ConversationPeriod { get { return Core.SettingStore.ReadInt( "Miranda", "ConversationPeriod", 3600 ); } set { Core.SettingStore.WriteInt( "Miranda", "ConversationPeriod", value ); } } internal static TimeSpan ConversationPeriodTimeSpan { get { int convPeriod = ConversationPeriod; return new TimeSpan( convPeriod / 3600, (convPeriod / 60) % 60, convPeriod % 60 ); } } internal static bool TraceImport { get { return Core.SettingStore.ReadBool( "Miranda", "TraceImport", false ); } } internal static bool FullIndexingCompleted { get { return Core.SettingStore.ReadBool( "Miranda", "FullIndexingCompleted", false ); } set { Core.SettingStore.WriteBool( "Miranda", "FullIndexingCompleted", value ); } } internal static DateTime IndexStartDate { get { return Core.SettingStore.ReadDate( "Startup", "IndexStartDate", DateTime.MinValue ); } } internal static bool IdleIndexing { get { return Core.SettingStore.ReadBool( "Startup", "IdleIndexing", false ); } } } public class RebuildMirandaConversationsAction: IAction { public void Execute(IActionContext context) { Core.UIManager.RunWithProgressWindow("Rebuilding Miranda conversations…", MirandaPlugin.DoRebuildConversations); } public void Update( IActionContext context, ref ActionPresentation presentation ) { presentation.Visible = ( Core.TabManager.CurrentTabId == "IM" || Core.TabManager.CurrentTabId == "All" ) && MirandaPlugin.ImportJob != null; } } public class MirandaAccountClickAction: ActionOnSingleResource { public override void Execute( IActionContext context ) { Core.UIManager.BeginUpdateSidebar(); Core.TabManager.CurrentTabId = "IM"; Core.UIManager.EndUpdateSidebar(); IResource account = context.SelectedResources [0]; IResourceList resList = account.GetLinksOfType( null, Props.FromAccount ); resList = resList.Union( account.GetLinksOfType( null, Props.ToAccount ) ); ResourceListDisplayOptions options = new ResourceListDisplayOptions(); options.Caption = "Messages for " + account.DisplayName; options.SetTransientContainer( Core.ResourceTreeManager.ResourceTreeRoot, StandardViewPanes.ViewsCategories ); Core.ResourceBrowser.DisplayResourceList( null, resList, options ); } } }