diff options
Diffstat (limited to 'kopete/plugins/history')
20 files changed, 3685 insertions, 0 deletions
diff --git a/kopete/plugins/history/Makefile.am b/kopete/plugins/history/Makefile.am new file mode 100644 index 00000000..765e5197 --- /dev/null +++ b/kopete/plugins/history/Makefile.am @@ -0,0 +1,26 @@ +METASOURCES = AUTO + +INCLUDES = $(KOPETE_INCLUDES) $(all_includes) + +kde_module_LTLIBRARIES = kopete_history.la kcm_kopete_history.la + +kopete_history_la_SOURCES = historyplugin.cpp historydialog.cpp historyviewer.ui\ + historylogger.cpp converter.cpp historyguiclient.cpp historyconfig.kcfgc + +kopete_history_la_LDFLAGS = -module -no-undefined $(KDE_PLUGIN) $(all_libraries) +kopete_history_la_LIBADD = ../../libkopete/libkopete.la + +kcm_kopete_history_la_SOURCES = historyprefsui.ui historypreferences.cpp historyconfig.kcfgc +kcm_kopete_history_la_LDFLAGS = -module -no-undefined $(KDE_PLUGIN) $(all_libraries) +kcm_kopete_history_la_LIBADD = ../../libkopete/libkopete.la $(LIB_KUTILS) + +service_DATA = kopete_history.desktop +servicedir = $(kde_servicesdir) + +mydatadir = $(kde_datadir)/kopete_history +mydata_DATA = historyui.rc historychatui.rc + +kcm_DATA = kopete_history_config.desktop +kcmdir = $(kde_servicesdir)/kconfiguredialog + +kde_kcfg_DATA = historyconfig.kcfg diff --git a/kopete/plugins/history/converter.cpp b/kopete/plugins/history/converter.cpp new file mode 100644 index 00000000..22f662bc --- /dev/null +++ b/kopete/plugins/history/converter.cpp @@ -0,0 +1,341 @@ +//Olivier Goffart <ogoffart @ kde.org> +// 2003 06 26 + +#include "historyplugin.h" //just needed because we are a member of this class + // we don't use any history function here + +/**----------------------------------------------------------- + * CONVERTER from the old kopete history. + * it port history from kopete 0.6, 0.5 and above the actual + * this should be placed in a perl script handled by KConf_update + * but i need to acess to some info i don't have with perl, like + * the accountId, to know each protocol id, and more + *-----------------------------------------------------------*/ + +#include "kopetepluginmanager.h" +#include "kopeteaccount.h" +#include "kopeteaccountmanager.h" +#include "kopetecontact.h" +#include "kopetemessage.h" +#include "kopeteprotocol.h" +#include "kopeteuiglobal.h" + +#include <kconfig.h> +#include <kdebug.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kmessagebox.h> +#include <kprogress.h> +#include <kapplication.h> +#include <ksavefile.h> +#include <qdir.h> +#include <qdom.h> +#include <qregexp.h> + +#define CBUFLENGTH 512 // buffer length for fgets() + +void HistoryPlugin::convertOldHistory() +{ + bool deleteFiles= KMessageBox::questionYesNo( Kopete::UI::Global::mainWidget(), + i18n( "Would you like to remove old history files?" ) , i18n( "History Converter" ), KStdGuiItem::del(), i18n("Keep") ) == KMessageBox::Yes; + + KProgressDialog *progressDlg=new KProgressDialog(Kopete::UI::Global::mainWidget() , "history_progress_dlg" , i18n( "History converter" ) , + QString::null , true); //modal to make sure the user will not doing stupid things (we have a kapp->processEvents()) + progressDlg->setAllowCancel(false); //because i am too lazy to allow to cancel + + + QString kopetedir=locateLocal( "data", QString::fromLatin1( "kopete")); + QDir d( kopetedir ); //d should point to ~/.kde/share/apps/kopete/ + + d.setFilter( QDir::Dirs ); + + const QFileInfoList *list = d.entryInfoList(); + QFileInfoListIterator it( *list ); + QFileInfo *fi; + while ( (fi = it.current()) != 0 ) + { + QString protocolId; + QString accountId; + + if( Kopete::Protocol *p = dynamic_cast<Kopete::Protocol *>( Kopete::PluginManager::self()->plugin( fi->fileName() ) ) ) + { + protocolId=p->pluginId(); + QDictIterator<Kopete::Account> it(Kopete::AccountManager::self()->accounts(p)); + Kopete::Account *a = it.current(); + if(a) + accountId=a->accountId(); + } + + if(accountId.isNull() || protocolId.isNull()) + { + if(fi->fileName() == "MSNProtocol" || fi->fileName() == "msn_logs" ) + { + protocolId="MSNProtocol"; + KGlobal::config()->setGroup("MSN"); + accountId=KGlobal::config()->readEntry( "UserID" ); + } + else if(fi->fileName() == "ICQProtocol" || fi->fileName() == "icq_logs" ) + { + protocolId="ICQProtocol"; + KGlobal::config()->setGroup("ICQ"); + accountId=KGlobal::config()->readEntry( "UIN" ); + } + else if(fi->fileName() == "AIMProtocol" || fi->fileName() == "aim_logs" ) + { + protocolId="AIMProtocol"; + KGlobal::config()->setGroup("AIM"); + accountId=KGlobal::config()->readEntry( "UserID" ); + } + else if(fi->fileName() == "OscarProtocol" ) + { + protocolId="AIMProtocol"; + KGlobal::config()->setGroup("OSCAR"); + accountId=KGlobal::config()->readEntry( "UserID" ); + } + else if(fi->fileName() == "JabberProtocol" || fi->fileName() == "jabber_logs") + { + protocolId="JabberProtocol"; + KGlobal::config()->setGroup("Jabber"); + accountId=KGlobal::config()->readEntry( "UserID" ); + } + //TODO: gadu, wp + } + + if(!protocolId.isEmpty() || !accountId.isEmpty()) + { + QDir d2( fi->absFilePath() ); + d2.setFilter( QDir::Files ); + d2.setNameFilter("*.log"); + const QFileInfoList *list = d2.entryInfoList(); + QFileInfoListIterator it2( *list ); + QFileInfo *fi2; + + progressDlg->progressBar()->reset(); + progressDlg->progressBar()->setTotalSteps(d2.count()); + progressDlg->setLabel(i18n("Parsing old history in %1").arg(fi->fileName())); + progressDlg->show(); //if it was not already showed... + + while ( (fi2 = it2.current()) != 0 ) + { + //we assume that all "-" are dots. (like in hotmail.com) + QString contactId=fi2->fileName().replace(".log" , QString::null).replace("-" , "."); + + if(!contactId.isEmpty() ) + { + progressDlg->setLabel(i18n("Parsing old history in %1:\n%2").arg(fi->fileName()).arg(contactId)); + kapp->processEvents(0); //make sure the text is updated in the progressDlg + + int month=0; + int year=0; + QDomDocument doc; + QDomElement docElem; + + QDomElement msgelement; + QDomNode node; + QDomDocument xmllist; + Kopete::Message::MessageDirection dir; + QString body, date, nick; + QString buffer, msgBlock; + char cbuf[CBUFLENGTH]; // buffer for the log file + + QString logFileName = fi2->absFilePath(); + + // open the file + FILE *f = fopen(QFile::encodeName(logFileName), "r"); + + // create a new <message> block + while ( ! feof( f ) ) + { + fgets(cbuf, CBUFLENGTH, f); + buffer = QString::fromUtf8(cbuf); + + while ( strchr(cbuf, '\n') == NULL && !feof(f) ) + { + fgets( cbuf, CBUFLENGTH, f ); + buffer += QString::fromUtf8(cbuf); + } + + if( buffer.startsWith( QString::fromLatin1( "<message " ) ) ) + { + msgBlock = buffer; + + // find the end of the message block + while( !feof( f ) && buffer != QString::fromLatin1( "</message>\n" ) /*strcmp("</message>\n", cbuf )*/ ) + { + fgets(cbuf, CBUFLENGTH, f); + buffer = QString::fromUtf8(cbuf); + + while ( strchr(cbuf, '\n') == NULL && !feof(f) ) + { + fgets( cbuf, CBUFLENGTH, f ); + buffer += QString::fromUtf8(cbuf); + } + msgBlock.append(buffer); + } + + // now let's work on this new block + xmllist.setContent(msgBlock, false); + msgelement = xmllist.documentElement(); + node = msgelement.firstChild(); + + if( msgelement.attribute( QString::fromLatin1( "direction" ) ) == QString::fromLatin1( "inbound" ) ) + dir = Kopete::Message::Inbound; + else + dir = Kopete::Message::Outbound; + + // Read all the elements. + QString tagname; + QDomElement element; + + while ( ! node.isNull() ) + { + if ( node.isElement() ) + { + element = node.toElement(); + tagname = element.tagName(); + + if( tagname == QString::fromLatin1( "srcnick" ) ) + nick = element.text(); + + else if( tagname == QString::fromLatin1( "date" ) ) + date = element.text(); + else if( tagname == QString::fromLatin1( "body" ) ) + body = element.text().stripWhiteSpace(); + } + + node = node.nextSibling(); + } + //FIXME!! The date in logs writed with kopete running with QT 3.0 is Localised. + // so QT can't parse it correctly. + QDateTime dt=QDateTime::fromString(date); + if(dt.date().month() != month || dt.date().year() != year) + { + if(!docElem.isNull()) + { + QDate date(year,month,1); + QString name = protocolId.replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + QString::fromLatin1( "/" ) + + contactId.replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + date.toString(".yyyyMM"); + KSaveFile file( locateLocal( "data", QString::fromLatin1( "kopete/logs/" ) + name+ QString::fromLatin1( ".xml" ) ) ); + if( file.status() == 0 ) + { + QTextStream *stream = file.textStream(); + //stream->setEncoding( QTextStream::UnicodeUTF8 ); //???? oui ou non? + doc.save( *stream , 1 ); + file.close(); + } + } + + + month=dt.date().month(); + year=dt.date().year(); + docElem=QDomElement(); + } + + if(docElem.isNull()) + { + doc=QDomDocument("Kopete-History"); + docElem= doc.createElement( "kopete-history" ); + docElem.setAttribute ( "version" , "0.7" ); + doc.appendChild( docElem ); + QDomElement headElem = doc.createElement( "head" ); + docElem.appendChild( headElem ); + QDomElement dateElem = doc.createElement( "date" ); + dateElem.setAttribute( "year", QString::number(year) ); + dateElem.setAttribute( "month", QString::number(month) ); + headElem.appendChild(dateElem); + QDomElement myselfElem = doc.createElement( "contact" ); + myselfElem.setAttribute( "type", "myself" ); + myselfElem.setAttribute( "contactId", accountId ); + headElem.appendChild(myselfElem); + QDomElement contactElem = doc.createElement( "contact" ); + contactElem.setAttribute( "contactId", contactId ); + headElem.appendChild(contactElem); + QDomElement importElem = doc.createElement( "imported" ); + importElem.setAttribute( "from", fi->fileName() ); + importElem.setAttribute( "date", QDateTime::currentDateTime().toString() ); + headElem.appendChild(importElem); + } + QDomElement msgElem = doc.createElement( "msg" ); + msgElem.setAttribute( "in", dir==Kopete::Message::Outbound ? "0" : "1" ); + msgElem.setAttribute( "from", dir==Kopete::Message::Outbound ? accountId : contactId ); + msgElem.setAttribute( "nick", nick ); //do we have to set this? + msgElem.setAttribute( "time", QString::number(dt.date().day()) + " " + QString::number(dt.time().hour()) + ":" + QString::number(dt.time().minute()) ); + QDomText msgNode = doc.createTextNode( body.stripWhiteSpace() ); + docElem.appendChild( msgElem ); + msgElem.appendChild( msgNode ); + } + } + + fclose( f ); + if(deleteFiles) + d2.remove(fi2->fileName() , false); + + if(!docElem.isNull()) + { + QDate date(year,month,1); + QString name = protocolId.replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + QString::fromLatin1( "/" ) + + contactId.replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + date.toString(".yyyyMM"); + KSaveFile file( locateLocal( "data", QString::fromLatin1( "kopete/logs/" ) + name+ QString::fromLatin1( ".xml" ) ) ); + if( file.status() == 0 ) + { + QTextStream *stream = file.textStream(); + //stream->setEncoding( QTextStream::UnicodeUTF8 ); //???? oui ou non? + doc.save( *stream ,1 ); + file.close(); + } + } + + } + progressDlg->progressBar()->setProgress(progressDlg->progressBar()->progress()+1); + ++it2; + } + } + ++it; + } + delete progressDlg; + +} + + +bool HistoryPlugin::detectOldHistory() +{ + KGlobal::config()->setGroup("History Plugin"); + QString version=KGlobal::config()->readEntry( "Version" ,"0.6" ); + + if(version != "0.6") + return false; + + + QDir d( locateLocal( "data", QString::fromLatin1( "kopete/logs")) ); + d.setFilter( QDir::Dirs ); + if(d.count() >= 3) // '.' and '..' are included + return false; //the new history already exists + + QDir d2( locateLocal( "data", QString::fromLatin1( "kopete")) ); + d2.setFilter( QDir::Dirs ); + const QFileInfoList *list = d2.entryInfoList(); + QFileInfoListIterator it( *list ); + QFileInfo *fi; + while ( (fi = it.current()) != 0 ) + { + if( dynamic_cast<Kopete::Protocol *>( Kopete::PluginManager::self()->plugin( fi->fileName() ) ) ) + return true; + + if(fi->fileName() == "MSNProtocol" || fi->fileName() == "msn_logs" ) + return true; + else if(fi->fileName() == "ICQProtocol" || fi->fileName() == "icq_logs" ) + return true; + else if(fi->fileName() == "AIMProtocol" || fi->fileName() == "aim_logs" ) + return true; + else if(fi->fileName() == "OscarProtocol" ) + return true; + else if(fi->fileName() == "JabberProtocol" || fi->fileName() == "jabber_logs") + return true; + ++it; + } + return false; +} diff --git a/kopete/plugins/history/historychatui.rc b/kopete/plugins/history/historychatui.rc new file mode 100644 index 00000000..2f49392f --- /dev/null +++ b/kopete/plugins/history/historychatui.rc @@ -0,0 +1,17 @@ +<!DOCTYPE kpartgui> +<kpartgui version="19" name="kopetechatwindow"> + <MenuBar> + <Menu name="tools" > + <text>&Tools</text> + <Action name="historyPrevious" /> + <Action name="historyNext" /> + <Action name="historyLast" /> + </Menu> + </MenuBar> + + <ToolBar name="mainToolBar" fullWidth="true"> + <Action name="historyPrevious" /> + <Action name="historyNext" /> + </ToolBar> + +</kpartgui> diff --git a/kopete/plugins/history/historyconfig.kcfg b/kopete/plugins/history/historyconfig.kcfg new file mode 100644 index 00000000..58e6c9d2 --- /dev/null +++ b/kopete/plugins/history/historyconfig.kcfg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Author: Stefan Gehn --> +<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 + http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > + <kcfgfile name="kopeterc"/> + + <group name="History Plugin"> + <entry name="Auto_chatwindow" type="Bool"> + <label>Show previous messages in new chats.</label> + <default>false</default> + </entry> + + <entry name="Number_Auto_chatwindow" type="UInt"> + <label>Number of messages to show.</label> + <default>7</default> + </entry> + + <entry name="Number_ChatWindow" type="UInt"> + <label>Number of messages per page</label> + <default>20</default> + </entry> + + <entry name="History_color" type="Color"> + <label>Color of messages</label> + <default>170, 170, 127</default> + </entry> + + <entry name="BrowserStyle" type="Path"> + <label>Style to use in history-browser.</label> + </entry> + + </group> +</kcfg> diff --git a/kopete/plugins/history/historyconfig.kcfgc b/kopete/plugins/history/historyconfig.kcfgc new file mode 100644 index 00000000..1e985622 --- /dev/null +++ b/kopete/plugins/history/historyconfig.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=historyconfig.kcfg +ClassName=HistoryConfig +Singleton=true +Mutators=true +MemberVariables=private +GlobalEnums=true diff --git a/kopete/plugins/history/historydialog.cpp b/kopete/plugins/history/historydialog.cpp new file mode 100644 index 00000000..4dd98fee --- /dev/null +++ b/kopete/plugins/history/historydialog.cpp @@ -0,0 +1,613 @@ +/* + kopetehistorydialog.cpp - Kopete History Dialog + + Copyright (c) 2002 by Richard Stellingwerff <remenic@linuxfromscratch.org> + Copyright (c) 2004 by Stefan Gehn <metz AT gehn.net> + + Kopete (c) 2002-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#include "historydialog.h" +#include "historylogger.h" +#include "historyviewer.h" +#include "kopetemetacontact.h" +#include "kopeteprotocol.h" +#include "kopeteaccount.h" +#include "kopetecontactlist.h" +#include "kopeteprefs.h" + +#include <dom/dom_doc.h> +#include <dom/dom_element.h> +#include <dom/html_document.h> +#include <dom/html_element.h> +#include <khtml_part.h> +#include <khtmlview.h> + +#include <qpushbutton.h> +#include <qlineedit.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qdir.h> +#include <qdatetime.h> +#include <qheader.h> +#include <qlabel.h> +#include <qclipboard.h> + +#include <kapplication.h> +#include <kdebug.h> +#include <kiconloader.h> +#include <klocale.h> +#include <krun.h> +#include <kstandarddirs.h> +#include <klistview.h> +#include <klistviewsearchline.h> +#include <kprogress.h> +#include <kiconloader.h> +#include <kcombobox.h> +#include <kpopupmenu.h> +#include <kstdaction.h> +#include <kaction.h> + +class KListViewDateItem : public KListViewItem +{ +public: + KListViewDateItem(KListView* parent, QDate date, Kopete::MetaContact *mc); + QDate date() { return mDate; } + Kopete::MetaContact *metaContact() { return mMetaContact; } + +public: + int compare(QListViewItem *i, int col, bool ascending) const; +private: + QDate mDate; + Kopete::MetaContact *mMetaContact; +}; + + + +KListViewDateItem::KListViewDateItem(KListView* parent, QDate date, Kopete::MetaContact *mc) + : KListViewItem(parent, date.toString(Qt::ISODate), mc->displayName()) +{ + mDate = date; + mMetaContact = mc; +} + +int KListViewDateItem::compare(QListViewItem *i, int col, bool ascending) const +{ + if (col) + return QListViewItem::compare(i, col, ascending); + + //compare dates - do NOT use ascending var here + KListViewDateItem* item = static_cast<KListViewDateItem*>(i); + if ( mDate < item->date() ) + return -1; + return ( mDate > item->date() ); +} + + +HistoryDialog::HistoryDialog(Kopete::MetaContact *mc, QWidget* parent, + const char* name) : KDialogBase(parent, name, false, + i18n("History for %1").arg(mc->displayName()), 0), mSearching(false) +{ + QString fontSize; + QString htmlCode; + QString fontStyle; + + kdDebug(14310) << k_funcinfo << "called." << endl; + setWFlags(Qt::WDestructiveClose); // send SIGNAL(closing()) on quit + + // FIXME: Allow to show this dialog for only one contact + mMetaContact = mc; + + + + // Widgets initializations + mMainWidget = new HistoryViewer(this, "HistoryDialog::mMainWidget"); + mMainWidget->searchLine->setFocus(); + mMainWidget->searchLine->setTrapReturnKey (true); + mMainWidget->searchLine->setTrapReturnKey(true); + mMainWidget->searchErase->setPixmap(BarIcon("locationbar_erase")); + + mMainWidget->contactComboBox->insertItem(i18n("All")); + mMetaContactList = Kopete::ContactList::self()->metaContacts(); + QPtrListIterator<Kopete::MetaContact> it(mMetaContactList); + for(; it.current(); ++it) + { + mMainWidget->contactComboBox->insertItem((*it)->displayName()); + } + + if (mMetaContact) + mMainWidget->contactComboBox->setCurrentItem(mMetaContactList.find(mMetaContact)+1); + + mMainWidget->dateSearchLine->setListView(mMainWidget->dateListView); + mMainWidget->dateListView->setSorting(0, 0); //newest-first + + setMainWidget(mMainWidget); + + // Initializing HTML Part + mMainWidget->htmlFrame->setFrameStyle(QFrame::WinPanel | QFrame::Sunken); + QVBoxLayout *l = new QVBoxLayout(mMainWidget->htmlFrame); + mHtmlPart = new KHTMLPart(mMainWidget->htmlFrame, "htmlHistoryView"); + + //Security settings, we don't need this stuff + mHtmlPart->setJScriptEnabled(false); + mHtmlPart->setJavaEnabled(false); + mHtmlPart->setPluginsEnabled(false); + mHtmlPart->setMetaRefreshEnabled(false); + mHtmlPart->setOnlyLocalReferences(true); + + mHtmlView = mHtmlPart->view(); + mHtmlView->setMarginWidth(4); + mHtmlView->setMarginHeight(4); + mHtmlView->setFocusPolicy(NoFocus); + mHtmlView->setSizePolicy( + QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); + l->addWidget(mHtmlView); + + QTextOStream( &fontSize ) << KopetePrefs::prefs()->fontFace().pointSize(); + fontStyle = "<style>.hf { font-size:" + fontSize + ".0pt; font-family:" + KopetePrefs::prefs()->fontFace().family() + "; color: " + KopetePrefs::prefs()->textColor().name() + "; }</style>"; + + mHtmlPart->begin(); + htmlCode = "<html><head>" + fontStyle + "</head><body class=\"hf\"></body></html>"; + mHtmlPart->write( QString::fromLatin1( htmlCode.latin1() ) ); + mHtmlPart->end(); + + + connect(mHtmlPart->browserExtension(), SIGNAL(openURLRequestDelayed(const KURL &, const KParts::URLArgs &)), + this, SLOT(slotOpenURLRequest(const KURL &, const KParts::URLArgs &))); + connect(mMainWidget->dateListView, SIGNAL(clicked(QListViewItem*)), this, SLOT(dateSelected(QListViewItem*))); + connect(mMainWidget->searchButton, SIGNAL(clicked()), this, SLOT(slotSearch())); + connect(mMainWidget->searchLine, SIGNAL(returnPressed()), this, SLOT(slotSearch())); + connect(mMainWidget->searchLine, SIGNAL(textChanged(const QString&)), this, SLOT(slotSearchTextChanged(const QString&))); + connect(mMainWidget->searchErase, SIGNAL(clicked()), this, SLOT(slotSearchErase())); + connect(mMainWidget->contactComboBox, SIGNAL(activated(int)), this, SLOT(slotContactChanged(int))); + connect(mMainWidget->messageFilterBox, SIGNAL(activated(int)), this, SLOT(slotFilterChanged(int ))); + connect(mHtmlPart, SIGNAL(popupMenu(const QString &, const QPoint &)), this, SLOT(slotRightClick(const QString &, const QPoint &))); + + //initActions + KActionCollection* ac = new KActionCollection(this); + mCopyAct = KStdAction::copy( this, SLOT(slotCopy()), ac ); + mCopyURLAct = new KAction( i18n( "Copy Link Address" ), QString::fromLatin1( "editcopy" ), 0, this, SLOT( slotCopyURL() ), ac ); + + resize(650, 700); + centerOnScreen(this); + + // show the dialog before people get impatient + show(); + + // Load history dates in the listview + init(); +} + +HistoryDialog::~HistoryDialog() +{ + mSearching = false; +} + +void HistoryDialog::init() +{ + if(mMetaContact) + { + HistoryLogger logger(mMetaContact, this); + init(mMetaContact); + } + else + { + QPtrListIterator<Kopete::MetaContact> it(mMetaContactList); + for(; it.current(); ++it) + { + HistoryLogger logger(*it, this); + init(*it); + } + + } + + initProgressBar(i18n("Loading..."),mInit.dateMCList.count()); + QTimer::singleShot(0,this,SLOT(slotLoadDays())); +} + +void HistoryDialog::slotLoadDays() +{ + if(mInit.dateMCList.isEmpty()) + { + if (!mMainWidget->searchLine->text().isEmpty()) + QTimer::singleShot(0, this, SLOT(slotSearch())); + doneProgressBar(); + return; + } + + DMPair pair(mInit.dateMCList.first()); + mInit.dateMCList.pop_front(); + HistoryLogger logger(pair.metaContact(), this); + QValueList<int> dayList = logger.getDaysForMonth(pair.date()); + for (unsigned int i=0; i<dayList.count(); i++) + { + QDate c2Date(pair.date().year(),pair.date().month(),dayList[i]); + if (mInit.dateMCList.find(pair) == mInit.dateMCList.end()) + new KListViewDateItem(mMainWidget->dateListView, c2Date, pair.metaContact()); + } + mMainWidget->searchProgress->advance(1); + QTimer::singleShot(0,this,SLOT(slotLoadDays())); + + +} + +void HistoryDialog::init(Kopete::MetaContact *mc) +{ + QPtrList<Kopete::Contact> contacts=mc->contacts(); + QPtrListIterator<Kopete::Contact> it( contacts ); + + for( ; it.current(); ++it ) + { + init(*it); + } +} + +void HistoryDialog::init(Kopete::Contact *c) +{ + // Get year and month list + QRegExp rx( "\\.(\\d\\d\\d\\d)(\\d\\d)" ); + const QString contact_in_filename=c->contactId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ); + QFileInfo *fi; + + + // BEGIN check if there are Kopete 0.7.x + QDir d1(locateLocal("data",QString("kopete/logs/")+ + c->protocol()->pluginId().replace( QRegExp(QString::fromLatin1("[./~?*]")),QString::fromLatin1("-")) + )); + d1.setFilter( QDir::Files | QDir::NoSymLinks ); + d1.setSorting( QDir::Name ); + + const QFileInfoList *list1 = d1.entryInfoList(); + if ( list1 != 0 ) + { + QFileInfoListIterator it1( *list1 ); + while ( (fi = it1.current()) != 0 ) + { + if(fi->fileName().contains(contact_in_filename)) + { + rx.search(fi->fileName()); + + QDate cDate = QDate(rx.cap(1).toInt(), rx.cap(2).toInt(), 1); + + DMPair pair(cDate, c->metaContact()); + mInit.dateMCList.append(pair); + + } + ++it1; + } + } + // END of kopete 0.7.x check + + QString logDir = locateLocal("data",QString("kopete/logs/")+ + c->protocol()->pluginId().replace( QRegExp(QString::fromLatin1("[./~?*]")),QString::fromLatin1("-")) + + QString::fromLatin1( "/" ) + + c->account()->accountId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + ); + QDir d(logDir); + d.setFilter( QDir::Files | QDir::NoSymLinks ); + d.setSorting( QDir::Name ); + const QFileInfoList *list = d.entryInfoList(); + if ( list != 0 ) + { + QFileInfoListIterator it( *list ); + while ( (fi = it.current()) != 0 ) + { + if(fi->fileName().contains(contact_in_filename)) + { + + rx.search(fi->fileName()); + + // We search for an item in the list view with the same year. If then we add the month + QDate cDate = QDate(rx.cap(1).toInt(), rx.cap(2).toInt(), 1); + + DMPair pair(cDate, c->metaContact()); + mInit.dateMCList.append(pair); + } + ++it; + } + } +} + +void HistoryDialog::dateSelected(QListViewItem* it) +{ + KListViewDateItem *item = static_cast<KListViewDateItem*>(it); + + if (!item) return; + + QDate chosenDate = item->date(); + + HistoryLogger logger(item->metaContact(), this); + QValueList<Kopete::Message> msgs=logger.readMessages(chosenDate); + + setMessages(msgs); +} + +void HistoryDialog::setMessages(QValueList<Kopete::Message> msgs) +{ + // Clear View + DOM::HTMLElement htmlBody = mHtmlPart->htmlDocument().body(); + while(htmlBody.hasChildNodes()) + htmlBody.removeChild(htmlBody.childNodes().item(htmlBody.childNodes().length() - 1)); + // ---- + + QString dir = (QApplication::reverseLayout() ? QString::fromLatin1("rtl") : + QString::fromLatin1("ltr")); + + QValueList<Kopete::Message>::iterator it = msgs.begin(); + + + QString accountLabel; + QString resultHTML = "<b><font color=\"red\">" + (*it).timestamp().date().toString() + "</font></b><br/>"; + DOM::HTMLElement newNode = mHtmlPart->document().createElement(QString::fromLatin1("span")); + newNode.setAttribute(QString::fromLatin1("dir"), dir); + newNode.setInnerHTML(resultHTML); + mHtmlPart->htmlDocument().body().appendChild(newNode); + + // Populating HTML Part with messages + for ( it = msgs.begin(); it != msgs.end(); ++it ) + { + if ( mMainWidget->messageFilterBox->currentItem() == 0 + || ( mMainWidget->messageFilterBox->currentItem() == 1 && (*it).direction() == Kopete::Message::Inbound ) + || ( mMainWidget->messageFilterBox->currentItem() == 2 && (*it).direction() == Kopete::Message::Outbound ) ) + { + resultHTML = ""; + + if (accountLabel.isEmpty() || accountLabel != (*it).from()->account()->accountLabel()) + // If the message's account is new, just specify it to the user + { + if (!accountLabel.isEmpty()) + resultHTML += "<br/><br/><br/>"; + resultHTML += "<b><font color=\"blue\">" + (*it).from()->account()->accountLabel() + "</font></b><br/>"; + } + accountLabel = (*it).from()->account()->accountLabel(); + + QString body = (*it).parsedBody(); + + if (!mMainWidget->searchLine->text().isEmpty()) + // If there is a search, then we hightlight the keywords + { + body = body.replace(mMainWidget->searchLine->text(), "<span style=\"background-color:yellow\">" + mMainWidget->searchLine->text() + "</span>", false); + } + + resultHTML += "(<b>" + (*it).timestamp().time().toString() + "</b>) " + + ((*it).direction() == Kopete::Message::Outbound ? + "<font color=\"" + KopetePrefs::prefs()->textColor().dark().name() + "\"><b>></b></font> " + : "<font color=\"" + KopetePrefs::prefs()->textColor().light(200).name() + "\"><b><</b></font> ") + + body + "<br/>"; + + newNode = mHtmlPart->document().createElement(QString::fromLatin1("span")); + newNode.setAttribute(QString::fromLatin1("dir"), dir); + newNode.setInnerHTML(resultHTML); + + mHtmlPart->htmlDocument().body().appendChild(newNode); + } + } +} + +void HistoryDialog::slotFilterChanged(int /* index */) +{ + dateSelected(mMainWidget->dateListView->currentItem()); +} + +void HistoryDialog::slotOpenURLRequest(const KURL &url, const KParts::URLArgs &/*args*/) +{ + kdDebug(14310) << k_funcinfo << "url=" << url.url() << endl; + new KRun(url, 0, false); // false = non-local files +} + +// Disable search button if there is no search text +void HistoryDialog::slotSearchTextChanged(const QString& searchText) +{ + if (searchText.isEmpty()) + { + mMainWidget->searchButton->setEnabled(false); + slotSearchErase(); + } + else + { + mMainWidget->searchButton->setEnabled(true); + } +} + +void HistoryDialog::listViewShowElements(bool s) +{ + KListViewDateItem* item = static_cast<KListViewDateItem*>(mMainWidget->dateListView->firstChild()); + while (item != 0) + { + item->setVisible(s); + item = static_cast<KListViewDateItem*>(item->nextSibling()); + } +} + +// Erase the search line, show all date/metacontacts items in the list (accordint to the +// metacontact selected in the combobox) +void HistoryDialog::slotSearchErase() +{ + mMainWidget->searchLine->clear(); + listViewShowElements(true); +} + +/* +* How does the search work +* ------------------------ +* We do the search respecting the current metacontact filter item. To do this, we iterate over the +* elements in the KListView (KListViewDateItems) and, for each one, we iterate over its subcontacts, +* manually searching the log files of each one. To avoid searching files twice, the months that have +* been searched already are stored in searchedMonths. The matches are placed in the matches QMap. +* Finally, the current date item is checked in the matches QMap, and if it is present, it is shown. +* +* Keyword highlighting is done in setMessages() : if the search field isn't empty, we highlight the +* search keyword. +* +* The search is _not_ case sensitive +*/ +void HistoryDialog::slotSearch() +{ + if (mMainWidget->dateListView->childCount() == 0) return; + + QRegExp rx("^ <msg.*time=\"(\\d+) \\d+:\\d+:\\d+\" >([^<]*)<"); + QMap<QDate, QValueList<Kopete::MetaContact*> > monthsSearched; + QMap<QDate, QValueList<Kopete::MetaContact*> > matches; + + // cancel button pressed + if (mSearching) + { + listViewShowElements(true); + goto searchFinished; + } + + listViewShowElements(false); + + initProgressBar(i18n("Searching..."), mMainWidget->dateListView->childCount()); + mMainWidget->searchButton->setText(i18n("&Cancel")); + mSearching = true; + + // iterate over items in the date list widget + for(KListViewDateItem *curItem = static_cast<KListViewDateItem*>(mMainWidget->dateListView->firstChild()); + curItem != 0; + curItem = static_cast<KListViewDateItem *>(curItem->nextSibling()) + ) + { + qApp->processEvents(); + if (!mSearching) return; + + QDate month(curItem->date().year(),curItem->date().month(),1); + // if we haven't searched the relevant history logs, search them now + if (!monthsSearched[month].contains(curItem->metaContact())) + { + monthsSearched[month].push_back(curItem->metaContact()); + QPtrList<Kopete::Contact> contacts = curItem->metaContact()->contacts(); + for(QPtrListIterator<Kopete::Contact> it( contacts ); it.current(); ++it) + { + // get filename and open file + QString filename(HistoryLogger::getFileName(*it, curItem->date())); + if (!QFile::exists(filename)) continue; + QFile file(filename); + file.open(IO_ReadOnly); + if (!file.isOpen()) + { + kdWarning(14310) << k_funcinfo << "Error opening " << + file.name() << ": " << file.errorString() << endl; + continue; + } + + QTextStream stream(&file); + QString textLine; + while(!stream.atEnd()) + { + textLine = stream.readLine(); + if (textLine.contains(mMainWidget->searchLine->text(), false)) + { + if(rx.search(textLine) != -1) + { + // only match message body + if (rx.cap(2).contains(mMainWidget->searchLine->text())) + matches[QDate(curItem->date().year(),curItem->date().month(),rx.cap(1).toInt())].push_back(curItem->metaContact()); + } + // this will happen when multiline messages are searched, properly + // parsing the files would fix this + else { } + } + qApp->processEvents(); + if (!mSearching) return; + } + file.close(); + } + } + + // relevant logfiles have been searched now, check if current date matches + if (matches[curItem->date()].contains(curItem->metaContact())) + curItem->setVisible(true); + + // Next date item + mMainWidget->searchProgress->advance(1); + } + +searchFinished: + mMainWidget->searchButton->setText(i18n("Se&arch")); + mSearching = false; + doneProgressBar(); +} + + + +// When a contact is selected in the combobox. Item 0 is All contacts. +void HistoryDialog::slotContactChanged(int index) +{ + mMainWidget->dateListView->clear(); + if (index == 0) + { + setCaption(i18n("History for All Contacts")); + mMetaContact = 0; + init(); + } + else + { + mMetaContact = mMetaContactList.at(index-1); + setCaption(i18n("History for %1").arg(mMetaContact->displayName())); + init(); + } +} + +void HistoryDialog::initProgressBar(const QString& text, int nbSteps) +{ + mMainWidget->searchProgress->setTotalSteps(nbSteps); + mMainWidget->searchProgress->setProgress(0); + mMainWidget->searchProgress->show(); + mMainWidget->statusLabel->setText(text); +} + +void HistoryDialog::doneProgressBar() +{ + mMainWidget->searchProgress->hide(); + mMainWidget->statusLabel->setText(i18n("Ready")); +} + +void HistoryDialog::slotRightClick(const QString &url, const QPoint &point) +{ + KPopupMenu *chatWindowPopup = 0L; + chatWindowPopup = new KPopupMenu(); + + if ( !url.isEmpty() ) + { + mURL = url; + mCopyURLAct->plug( chatWindowPopup ); + chatWindowPopup->insertSeparator(); + } + mCopyAct->setEnabled( mHtmlPart->hasSelection() ); + mCopyAct->plug( chatWindowPopup ); + + connect( chatWindowPopup, SIGNAL( aboutToHide() ), chatWindowPopup, SLOT( deleteLater() ) ); + chatWindowPopup->popup(point); +} + +void HistoryDialog::slotCopy() +{ + QString qsSelection; + qsSelection = mHtmlPart->selectedText(); + if ( qsSelection.isEmpty() ) return; + + disconnect( kapp->clipboard(), SIGNAL( selectionChanged()), mHtmlPart, SLOT(slotClearSelection())); + QApplication::clipboard()->setText(qsSelection, QClipboard::Clipboard); + QApplication::clipboard()->setText(qsSelection, QClipboard::Selection); + connect( kapp->clipboard(), SIGNAL( selectionChanged()), mHtmlPart, SLOT(slotClearSelection())); +} + +void HistoryDialog::slotCopyURL() +{ + disconnect( kapp->clipboard(), SIGNAL( selectionChanged()), mHtmlPart, SLOT(slotClearSelection())); + QApplication::clipboard()->setText( mURL, QClipboard::Clipboard); + QApplication::clipboard()->setText( mURL, QClipboard::Selection); + connect( kapp->clipboard(), SIGNAL( selectionChanged()), mHtmlPart, SLOT(slotClearSelection())); +} + +#include "historydialog.moc" diff --git a/kopete/plugins/history/historydialog.h b/kopete/plugins/history/historydialog.h new file mode 100644 index 00000000..cf26037d --- /dev/null +++ b/kopete/plugins/history/historydialog.h @@ -0,0 +1,146 @@ +/* + kopetehistorydialog.h - Kopete History Dialog + + Copyright (c) 2002 by Richard Stellingwerff <remenic@linuxfromscratch.org> + Copyright (c) 2004 by Stefan Gehn <metz AT gehn.net> + + Kopete (c) 2002-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#ifndef _HISTORYDIALOG_H +#define _HISTORYDIALOG_H + +#include <qfile.h> +#include <qstringlist.h> + +#include <kdialogbase.h> +#include <klistview.h> + +#include "kopetemessage.h" + +class HistoryViewer; + +//class HistoryWidget; +namespace Kopete { class MetaContact; } +namespace Kopete { class XSLT; } +class HistoryLogger; +class KHTMLView; +class KHTMLPart; + +class KURL; +namespace KParts { struct URLArgs; class Part; } + + +class KListViewDateItem; + +class DMPair +{ + public: + DMPair() {md = QDate(0, 0, 0); mc = 0; } + DMPair(QDate d, Kopete::MetaContact *c) { md = d; mc =c; } + QDate date() const { return md; } + Kopete::MetaContact* metaContact() const { return mc; } + bool operator==(const DMPair p1) const { return p1.date() == this->date() && p1.metaContact() == this->metaContact(); } + private: + QDate md; + Kopete::MetaContact *mc; +}; + +/** + * @author Richard Stellingwerff <remenic@linuxfromscratch.org> + * @author Stefan Gehn <metz AT gehn.net> + */ +class HistoryDialog : public KDialogBase +{ + Q_OBJECT + + public: + HistoryDialog(Kopete::MetaContact *mc, QWidget* parent=0, + const char* name="HistoryDialog"); + ~HistoryDialog(); + + /** + * Calls init(Kopete::Contact *c) for each subcontact of the metacontact + */ + + + signals: + void closing(); + + private slots: + void slotOpenURLRequest(const KURL &url, const KParts::URLArgs &/*args*/); + + // Called when a date is selected in the treeview + void dateSelected(QListViewItem *); + + void slotSearch(); + + // Reinitialise search + void slotSearchErase(); + void slotSearchTextChanged(const QString& txt); // To enable/disable search button + void slotContactChanged(int index); + void slotFilterChanged(int index); + + void init(); + void slotLoadDays(); + + void slotRightClick(const QString &url, const QPoint &point); + void slotCopy(); + void slotCopyURL(); + + private: + enum Disabled { Prev=1, Next=2 }; + void refreshEnabled( /*Disabled*/ uint disabled ); + + void initProgressBar(const QString& text, int nbSteps); + void doneProgressBar(); + void init(Kopete::MetaContact *mc); + void init(Kopete::Contact *c); + + /** + * Show the messages in the HTML View + */ + void setMessages(QValueList<Kopete::Message> m); + + void listViewShowElements(bool s); + + /** + * Search if @param item already has @param text child + */ + bool hasChild(KListViewItem* item, int month); + + /** + * We show history dialog to look at the log for a metacontact. Here is this metacontact. + */ + Kopete::MetaContact *mMetaContact; + + QPtrList<Kopete::MetaContact> mMetaContactList; + + // History View + KHTMLView *mHtmlView; + KHTMLPart *mHtmlPart; + HistoryViewer *mMainWidget; + Kopete::XSLT *mXsltParser; + + struct Init + { + QValueList<DMPair> dateMCList; // mc for MetaContact + } mInit; + + bool mSearching; + + KAction *mCopyAct; + KAction *mCopyURLAct; + QString mURL; +}; + +#endif diff --git a/kopete/plugins/history/historyguiclient.cpp b/kopete/plugins/history/historyguiclient.cpp new file mode 100644 index 00000000..133e50a3 --- /dev/null +++ b/kopete/plugins/history/historyguiclient.cpp @@ -0,0 +1,115 @@ +/* + historyguiclient.cpp + + Copyright (c) 2003-2004 by Olivier Goffart <ogoffart @ kde.org> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ +#include "historyguiclient.h" +#include "historylogger.h" +#include "historyconfig.h" + +#include "kopetechatsession.h" +#include "kopetecontact.h" +#include "kopeteview.h" + +#include <kaction.h> +#include <klocale.h> +#include <kgenericfactory.h> + +class HistoryPlugin; + +HistoryGUIClient::HistoryGUIClient(Kopete::ChatSession *parent, const char *name) + : QObject(parent, name), KXMLGUIClient(parent) +{ + setInstance(KGenericFactory<HistoryPlugin>::instance()); + + m_manager = parent; + + // Refuse to build this client, it is based on wrong parameters + if(!m_manager || m_manager->members().isEmpty()) + deleteLater(); + + QPtrList<Kopete::Contact> mb=m_manager->members(); + m_logger=new HistoryLogger( mb.first() , this ); + + actionLast=new KAction( i18n("History Last" ), QString::fromLatin1( "finish" ), 0, this, SLOT(slotLast()), actionCollection() , "historyLast" ); + actionPrev = KStdAction::back( this, SLOT(slotPrevious()), actionCollection() , "historyPrevious" ); + actionNext = KStdAction::forward( this, SLOT(slotNext()), actionCollection() , "historyNext" ); + + // we are generally at last when begining + actionPrev->setEnabled(true); + actionNext->setEnabled(false); + actionLast->setEnabled(false); + + setXMLFile("historychatui.rc"); +} + + +HistoryGUIClient::~HistoryGUIClient() +{ +} + + +void HistoryGUIClient::slotPrevious() +{ + KopeteView *m_currentView = m_manager->view(true); + m_currentView->clear(); + + QPtrList<Kopete::Contact> mb = m_manager->members(); + QValueList<Kopete::Message> msgs = m_logger->readMessages( + HistoryConfig::number_ChatWindow(), /*mb.first()*/ 0L, + HistoryLogger::AntiChronological, true); + + actionPrev->setEnabled(msgs.count() == HistoryConfig::number_ChatWindow()); + actionNext->setEnabled(true); + actionLast->setEnabled(true); + + m_currentView->appendMessages(msgs); +} + +void HistoryGUIClient::slotLast() +{ + KopeteView *m_currentView = m_manager->view(true); + m_currentView->clear(); + + QPtrList<Kopete::Contact> mb = m_manager->members(); + m_logger->setPositionToLast(); + QValueList<Kopete::Message> msgs = m_logger->readMessages( + HistoryConfig::number_ChatWindow(), /*mb.first()*/ 0L, + HistoryLogger::AntiChronological, true); + + actionPrev->setEnabled(true); + actionNext->setEnabled(false); + actionLast->setEnabled(false); + + m_currentView->appendMessages(msgs); +} + + +void HistoryGUIClient::slotNext() +{ + KopeteView *m_currentView = m_manager->view(true); + m_currentView->clear(); + + QPtrList<Kopete::Contact> mb = m_manager->members(); + QValueList<Kopete::Message> msgs = m_logger->readMessages( + HistoryConfig::number_ChatWindow(), /*mb.first()*/ 0L, + HistoryLogger::Chronological, false); + + actionPrev->setEnabled(true); + actionNext->setEnabled(msgs.count() == HistoryConfig::number_ChatWindow()); + actionLast->setEnabled(msgs.count() == HistoryConfig::number_ChatWindow()); + + m_currentView->appendMessages(msgs); +} + +#include "historyguiclient.moc" diff --git a/kopete/plugins/history/historyguiclient.h b/kopete/plugins/history/historyguiclient.h new file mode 100644 index 00000000..420795e0 --- /dev/null +++ b/kopete/plugins/history/historyguiclient.h @@ -0,0 +1,55 @@ +/* + historyguiclient.h + + Copyright (c) 2003 by Olivier Goffart <ogoffart @ kde.org> + Kopete (c) 2003 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ +#ifndef HISTORYGUICLIENT_H +#define HISTORYGUICLIENT_H + +#include <qobject.h> +#include <kxmlguiclient.h> + +namespace Kopete { class ChatSession; } +class HistoryLogger; +class KAction; + +/** + *@author Olivier Goffart + */ +class HistoryGUIClient : public QObject , public KXMLGUIClient +{ +Q_OBJECT +public: + HistoryGUIClient(Kopete::ChatSession *parent = 0, const char *name = 0); + ~HistoryGUIClient(); + + HistoryLogger *logger() const { return m_logger; } + +private slots: + void slotPrevious(); + void slotLast(); + void slotNext(); + +private: + HistoryLogger *m_logger; + Kopete::ChatSession *m_manager; + //bool m_autoChatWindow; + //int m_nbAutoChatWindow; + //unsigned int m_nbChatWindow; + + KAction *actionPrev; + KAction *actionNext; + KAction *actionLast; +}; + +#endif diff --git a/kopete/plugins/history/historylogger.cpp b/kopete/plugins/history/historylogger.cpp new file mode 100644 index 00000000..7848136f --- /dev/null +++ b/kopete/plugins/history/historylogger.cpp @@ -0,0 +1,851 @@ +/* + historylogger.cpp + + Copyright (c) 2003-2004 by Olivier Goffart <ogoffart @ kde.org> + + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#include "historylogger.h" +#include "historyconfig.h" + +#include <qregexp.h> +#include <qfile.h> +#include <qdir.h> +#include <qdatetime.h> +#include <qdom.h> +#include <qtimer.h> + +#include <kdebug.h> +#include <kstandarddirs.h> +#include <ksavefile.h> + +#include "kopeteglobal.h" +#include "kopetecontact.h" +#include "kopeteprotocol.h" +#include "kopeteaccount.h" +#include "kopetemetacontact.h" +#include "kopetechatsession.h" + +// ----------------------------------------------------------------------------- +HistoryLogger::HistoryLogger( Kopete::MetaContact *m, QObject *parent, const char *name ) + : QObject(parent, name) +{ + m_saveTimer=0L; + m_saveTimerTime=0; + m_metaContact=m; + m_hideOutgoing=false; + m_cachedMonth=-1; + m_realMonth=QDate::currentDate().month(); + m_oldSens=Default; + + //the contact may be destroyed, for example, if the contact changes its metacontact + connect(m_metaContact , SIGNAL(destroyed(QObject *)) , this , SLOT(slotMCDeleted())); + + setPositionToLast(); +} + + +HistoryLogger::HistoryLogger( Kopete::Contact *c, QObject *parent, const char *name ) + : QObject(parent, name) +{ + m_saveTimer=0L; + m_saveTimerTime=0; + m_cachedMonth=-1; + m_metaContact=c->metaContact(); + m_hideOutgoing=false; + m_realMonth=QDate::currentDate().month(); + m_oldSens=Default; + + //the contact may be destroyed, for example, if the contact changes its metacontact + connect(m_metaContact , SIGNAL(destroyed(QObject *)) , this , SLOT(slotMCDeleted())); + + setPositionToLast(); +} + + +HistoryLogger::~HistoryLogger() +{ + if(m_saveTimer && m_saveTimer->isActive()) + saveToDisk(); +} + + +void HistoryLogger::setPositionToLast() +{ + setCurrentMonth(0); + m_oldSens = AntiChronological; + m_oldMonth=0; + m_oldElements.clear(); +} + + +void HistoryLogger::setPositionToFirst() +{ + setCurrentMonth( getFirstMonth() ); + m_oldSens = Chronological; + m_oldMonth=m_currentMonth; + m_oldElements.clear(); +} + + +void HistoryLogger::setCurrentMonth(int month) +{ + m_currentMonth = month; + m_currentElements.clear(); +} + + +QDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, unsigned int month , bool canLoad , bool* contain) +{ + if(m_realMonth!=QDate::currentDate().month()) + { //We changed month, our indice are not correct anymore, clean memory. + // or we will see what i called "the 31 midnight bug"(TM) :-) -Olivier + m_documents.clear(); + m_cachedMonth=-1; + m_currentMonth++; //Not usre it's ok, but should work; + m_oldMonth++; // idem + m_realMonth=QDate::currentDate().month(); + } + + if(!m_metaContact) + { //this may happen if the contact has been moved, and the MC deleted + if(c && c->metaContact()) + m_metaContact=c->metaContact(); + else + return QDomDocument(); + } + + if(!m_metaContact->contacts().contains(c)) + { + if(contain) + *contain=false; + return QDomDocument(); + } + + QMap<unsigned int , QDomDocument> documents = m_documents[c]; + if (documents.contains(month)) + return documents[month]; + + + QDomDocument doc = getDocument(c, QDate::currentDate().addMonths(0-month), canLoad, contain); + + documents.insert(month, doc); + m_documents[c]=documents; + + return doc; + +} + +QDomDocument HistoryLogger::getDocument(const Kopete::Contact *c, const QDate date , bool canLoad , bool* contain) +{ + if(!m_metaContact) + { //this may happen if the contact has been moved, and the MC deleted + if(c && c->metaContact()) + m_metaContact=c->metaContact(); + else + return QDomDocument(); + } + + if(!m_metaContact->contacts().contains(c)) + { + if(contain) + *contain=false; + return QDomDocument(); + } + + if(!canLoad) + { + if(contain) + *contain=false; + return QDomDocument(); + } + + QString FileName = getFileName(c, date); + + QDomDocument doc( "Kopete-History" ); + + QFile file( FileName ); + if ( !file.open( IO_ReadOnly ) ) + { + if(contain) + *contain=false; + return doc; + } + if ( !doc.setContent( &file ) ) + { + file.close(); + if(contain) + *contain=false; + return doc; + } + file.close(); + + if(contain) + *contain=true; + + return doc; +} + + +void HistoryLogger::appendMessage( const Kopete::Message &msg , const Kopete::Contact *ct ) +{ + if(!msg.from()) + return; + + // If no contact are given: If the manager is availiable, use the manager's + // first contact (the channel on irc, or the other contact for others protocols + const Kopete::Contact *c = ct; + if(!c && msg.manager() ) + { + QPtrList<Kopete::Contact> mb=msg.manager()->members() ; + c = mb.first(); + } + if(!c) //If the contact is still not initialized, use the message author. + c = msg.direction()==Kopete::Message::Outbound ? msg.to().first() : msg.from() ; + + + if(!m_metaContact) + { //this may happen if the contact has been moved, and the MC deleted + if(c && c->metaContact()) + m_metaContact=c->metaContact(); + else + return; + } + + + if(!c || !m_metaContact->contacts().contains(c) ) + { + /*QPtrList<Kopete::Contact> contacts= m_metaContact->contacts(); + QPtrListIterator<Kopete::Contact> it( contacts ); + for( ; it.current(); ++it ) + { + if( (*it)->protocol()->pluginId() == msg.from()->protocol()->pluginId() ) + { + c=*it; + break; + } + }*/ + //if(!c) + + kdWarning(14310) << k_funcinfo << "No contact found in this metacontact to" << + " append in the history" << endl; + return; + } + + QDomDocument doc=getDocument(c,0); + QDomElement docElem = doc.documentElement(); + + if(docElem.isNull()) + { + docElem= doc.createElement( "kopete-history" ); + docElem.setAttribute ( "version" , "0.9" ); + doc.appendChild( docElem ); + QDomElement headElem = doc.createElement( "head" ); + docElem.appendChild( headElem ); + QDomElement dateElem = doc.createElement( "date" ); + dateElem.setAttribute( "year", QString::number(QDate::currentDate().year()) ); + dateElem.setAttribute( "month", QString::number(QDate::currentDate().month()) ); + headElem.appendChild(dateElem); + QDomElement myselfElem = doc.createElement( "contact" ); + myselfElem.setAttribute( "type", "myself" ); + myselfElem.setAttribute( "contactId", c->account()->myself()->contactId() ); + headElem.appendChild(myselfElem); + QDomElement contactElem = doc.createElement( "contact" ); + contactElem.setAttribute( "contactId", c->contactId() ); + headElem.appendChild(contactElem); + } + + QDomElement msgElem = doc.createElement( "msg" ); + msgElem.setAttribute( "in", msg.direction()==Kopete::Message::Outbound ? "0" : "1" ); + msgElem.setAttribute( "from", msg.from()->contactId() ); + msgElem.setAttribute( "nick", msg.from()->property( Kopete::Global::Properties::self()->nickName() ).value().toString() ); //do we have to set this? + msgElem.setAttribute( "time", msg.timestamp().toString("d h:m:s") ); + + QDomText msgNode = doc.createTextNode( msg.plainBody() ); + docElem.appendChild( msgElem ); + msgElem.appendChild( msgNode ); + + + // I'm temporizing the save. + // On hight-traffic channel, saving can take lots of CPU. (because the file is big) + // So i wait a time proportional to the time needed to save.. + + const QString filename=getFileName(c,QDate::currentDate()); + if(!m_toSaveFileName.isEmpty() && m_toSaveFileName != filename) + { //that mean the contact or the month has changed, save it now. + saveToDisk(); + } + + m_toSaveFileName=filename; + m_toSaveDocument=doc; + + if(!m_saveTimer) + { + m_saveTimer=new QTimer(this); + connect( m_saveTimer, SIGNAL( timeout() ) , this, SLOT(saveToDisk()) ); + } + if(!m_saveTimer->isActive()) + m_saveTimer->start( m_saveTimerTime, true /*singleshot*/ ); +} + +void HistoryLogger::saveToDisk() +{ + if(m_saveTimer) + m_saveTimer->stop(); + if(m_toSaveFileName.isEmpty() || m_toSaveDocument.isNull()) + return; + + QTime t; + t.start(); //mesure the time needed to save. + + KSaveFile file( m_toSaveFileName ); + if( file.status() == 0 ) + { + QTextStream *stream = file.textStream(); + //stream->setEncoding( QTextStream::UnicodeUTF8 ); //???? oui ou non? + m_toSaveDocument.save( *stream, 1 ); + file.close(); + + m_saveTimerTime=QMIN(t.elapsed()*1000, 300000); + //a time 1000 times supperior to the time needed to save. but with a upper limit of 5 minutes + //on a my machine, (2.4Ghz, but old HD) it should take about 10 ms to save the file. + // So that would mean save every 10 seconds, which seems to be ok. + // But it may take 500 ms if the file to save becomes too big (1Mb). + kdDebug(14310) << k_funcinfo << m_toSaveFileName << " saved in " << t.elapsed() << " ms " <<endl ; + + m_toSaveFileName=QString::null; + m_toSaveDocument=QDomDocument(); + } + else + kdError(14310) << k_funcinfo << "impossible to save the history file " << m_toSaveFileName << endl; + +} + +QValueList<Kopete::Message> HistoryLogger::readMessages(QDate date) +{ + QRegExp rxTime("(\\d+) (\\d+):(\\d+)($|:)(\\d*)"); //(with a 0.7.x compatibility) + QValueList<Kopete::Message> messages; + + + QPtrList<Kopete::Contact> ct=m_metaContact->contacts(); + QPtrListIterator<Kopete::Contact> it( ct ); + + for( ; it.current(); ++it ) + { + QDomDocument doc=getDocument(*it,date, true, 0L); + QDomElement docElem = doc.documentElement(); + QDomNode n = docElem.firstChild(); + + while(!n.isNull()) + { + QDomElement msgElem2 = n.toElement(); + if( !msgElem2.isNull() && msgElem2.tagName()=="msg") + { + rxTime.search(msgElem2.attribute("time")); + QDateTime dt( QDate(date.year() , date.month() , rxTime.cap(1).toUInt()), QTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt(), rxTime.cap(5).toUInt() ) ); + + if (dt.date() != date) + { + n = n.nextSibling(); + continue; + } + + Kopete::Message::MessageDirection dir = (msgElem2.attribute("in") == "1") ? + Kopete::Message::Inbound : Kopete::Message::Outbound; + + if(!m_hideOutgoing || dir != Kopete::Message::Outbound) + { //parse only if we don't hide it + + QString f=msgElem2.attribute("from" ); + const Kopete::Contact *from=f.isNull()? 0L : (*it)->account()->contacts()[f]; + + if(!from) + from= dir==Kopete::Message::Inbound ? (*it) : (*it)->account()->myself(); + + Kopete::ContactPtrList to; + to.append( dir==Kopete::Message::Inbound ? (*it)->account()->myself() : *it ); + + Kopete::Message msg(dt, from, to, msgElem2.text(), dir); + msg.setBody( QString::fromLatin1("<span title=\"%1\">%2</span>") + .arg( dt.toString(Qt::LocalDate), msg.escapedBody() ), + Kopete::Message::RichText); + + + // We insert it at the good place, given its date + QValueListIterator<Kopete::Message> msgIt; + + for (msgIt = messages.begin(); msgIt != messages.end(); ++msgIt) + { + if ((*msgIt).timestamp() > msg.timestamp()) + break; + } + messages.insert(msgIt, msg); + } + } + + n = n.nextSibling(); + } // end while on messages + + } + return messages; +} + +QValueList<Kopete::Message> HistoryLogger::readMessages(unsigned int lines, + const Kopete::Contact *c, Sens sens, bool reverseOrder, bool colorize) +{ + //QDate dd = QDate::currentDate().addMonths(0-m_currentMonth); + + QValueList<Kopete::Message> messages; + + // A regexp useful for this function + QRegExp rxTime("(\\d+) (\\d+):(\\d+)($|:)(\\d*)"); //(with a 0.7.x compatibility) + + if(!m_metaContact) + { //this may happen if the contact has been moved, and the MC deleted + if(c && c->metaContact()) + m_metaContact=c->metaContact(); + else + return messages; + } + + if(c && !m_metaContact->contacts().contains(c) ) + return messages; + + if(sens ==0 ) //if no sens are selected, just continue in the previous sens + sens = m_oldSens ; + if( m_oldSens != 0 && sens != m_oldSens ) + { //we changed our sens! so retrieve the old position to fly in the other way + m_currentElements= m_oldElements; + m_currentMonth=m_oldMonth; + } + else + { + m_oldElements=m_currentElements; + m_oldMonth=m_currentMonth; + } + m_oldSens=sens; + + //getting the color for messages: + QColor fgColor = HistoryConfig::history_color(); + + //Hello guest! + + //there are two algoritms: + // - if a contact is given, or the metacontact contain only one contact, just read the history. + // - else, merge the history + + //the merging algoritm is the following: + // we see what contact we have to read first, and we look at the firt date before another contact + // has a message with a bigger date. + + QDateTime timeLimit; + const Kopete::Contact *currentContact=c; + if(!c && m_metaContact->contacts().count()==1) + currentContact=m_metaContact->contacts().first(); + else if(!c && m_metaContact->contacts().count()== 0) + { + return messages; + } + + while(messages.count() < lines) + { + timeLimit=QDateTime(); + QDomElement msgElem; //here is the message element + QDateTime timestamp; //and the timestamp of this message + + if(!c && m_metaContact->contacts().count()>1) + { //we have to merge the differents subcontact history + QPtrList<Kopete::Contact> ct=m_metaContact->contacts(); + QPtrListIterator<Kopete::Contact> it( ct ); + for( ; it.current(); ++it ) + { //we loop over each contact. we are searching the contact with the next message with the smallest date, + // it will becomes our current contact, and the contact with the mext message with the second smallest + // date, this date will bocomes the limit. + + QDomNode n; + if(m_currentElements.contains(*it)) + n=m_currentElements[*it]; + else //there is not yet "next message" register, so we will take the first (for the current month) + { + QDomDocument doc=getDocument(*it,m_currentMonth); + QDomElement docElem = doc.documentElement(); + n= (sens==Chronological)?docElem.firstChild() : docElem.lastChild(); + + //i can't drop the root element + workaround.append(docElem); + } + while(!n.isNull()) + { + QDomElement msgElem2 = n.toElement(); + if( !msgElem2.isNull() && msgElem2.tagName()=="msg") + { + rxTime.search(msgElem2.attribute("time")); + QDate d=QDate::currentDate().addMonths(0-m_currentMonth); + QDateTime dt( QDate(d.year() , d.month() , rxTime.cap(1).toUInt()), QTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt(), rxTime.cap(5).toUInt() ) ); + if(!timestamp.isValid() || ((sens==Chronological )? dt < timestamp : dt > timestamp) ) + { + timeLimit=timestamp; + timestamp=dt; + msgElem=msgElem2; + currentContact=*it; + + } + else if(!timeLimit.isValid() || ((sens==Chronological) ? timeLimit > dt : timeLimit < dt) ) + { + timeLimit=dt; + } + break; + } + n=(sens==Chronological)? n.nextSibling() : n.previousSibling(); + } + } + } + else //we don't have to merge the history. just take the next item in the contact + { + if(m_currentElements.contains(currentContact)) + msgElem=m_currentElements[currentContact]; + else + { + QDomDocument doc=getDocument(currentContact,m_currentMonth); + QDomElement docElem = doc.documentElement(); + QDomNode n= (sens==Chronological)?docElem.firstChild() : docElem.lastChild(); + msgElem=QDomElement(); + while(!n.isNull()) //continue until we get a msg + { + msgElem=n.toElement(); + if( !msgElem.isNull() && msgElem.tagName()=="msg") + { + m_currentElements[currentContact]=msgElem; + break; + } + n=(sens==Chronological)? n.nextSibling() : n.previousSibling(); + } + + //i can't drop the root element + workaround.append(docElem); + } + } + + + if(msgElem.isNull()) //we don't find ANY messages in any contact for this month. so we change the month + { + if(sens==Chronological) + { + if(m_currentMonth <= 0) + break; //there are no other messages to show. break even if we don't have nb messages + setCurrentMonth(m_currentMonth-1); + } + else + { + if(m_currentMonth >= getFirstMonth(c)) + break; //we don't have any other messages to show + setCurrentMonth(m_currentMonth+1); + } + continue; //begin the loop from the bottom, and find currentContact and timeLimit again + } + + while( + (messages.count() < lines) && + !msgElem.isNull() && + (!timestamp.isValid() || !timeLimit.isValid() || + ((sens==Chronological) ? timestamp <= timeLimit : timestamp >= timeLimit) + )) + { + // break this loop, if we have reached the correct number of messages, + // if there are no more messages for this contact, or if we reached + // the timeLimit msgElem is the next message, still not parsed, so + // we parse it now + + Kopete::Message::MessageDirection dir = (msgElem.attribute("in") == "1") ? + Kopete::Message::Inbound : Kopete::Message::Outbound; + + if(!m_hideOutgoing || dir != Kopete::Message::Outbound) + { //parse only if we don't hide it + + if( m_filter.isNull() || ( m_filterRegExp? msgElem.text().contains(QRegExp(m_filter,m_filterCaseSensitive)) : msgElem.text().contains(m_filter,m_filterCaseSensitive) )) + { + QString f=msgElem.attribute("from" ); + const Kopete::Contact *from=(f.isNull() || !currentContact) ? 0L : currentContact->account()->contacts()[f]; + + if(!from) + from= dir==Kopete::Message::Inbound ? currentContact : currentContact->account()->myself(); + + Kopete::ContactPtrList to; + to.append( dir==Kopete::Message::Inbound ? currentContact->account()->myself() : currentContact ); + + if(!timestamp.isValid()) + { + //parse timestamp only if it was not already parsed + rxTime.search(msgElem.attribute("time")); + QDate d=QDate::currentDate().addMonths(0-m_currentMonth); + timestamp=QDateTime( QDate(d.year() , d.month() , rxTime.cap(1).toUInt()), QTime( rxTime.cap(2).toUInt() , rxTime.cap(3).toUInt() , rxTime.cap(5).toUInt() ) ); + } + + Kopete::Message msg(timestamp, from, to, msgElem.text(), dir); + if (colorize) + { + msg.setBody( QString::fromLatin1("<span style=\"color:%1\" title=\"%2\">%3</span>") + .arg( fgColor.name(), timestamp.toString(Qt::LocalDate), msg.escapedBody() ), + Kopete::Message::RichText + ); + msg.setFg( fgColor ); + } + else + { + msg.setBody( QString::fromLatin1("<span title=\"%1\">%2</span>") + .arg( timestamp.toString(Qt::LocalDate), msg.escapedBody() ), + Kopete::Message::RichText + ); + } + + if(reverseOrder) + messages.prepend(msg); + else + messages.append(msg); + } + } + + //here is the point of workaround. If i drop the root element, this crashes + //get the next message + QDomNode node = ( (sens==Chronological) ? msgElem.nextSibling() : + msgElem.previousSibling() ); + + msgElem = QDomElement(); //n.toElement(); + while (!node.isNull() && msgElem.isNull()) + { + msgElem = node.toElement(); + if (!msgElem.isNull()) + { + if (msgElem.tagName() == "msg") + { + if (!c && (m_metaContact->contacts().count() > 1)) + { + // In case of hideoutgoing messages, it is faster to do + // this, so we don't parse the date if it is not needed + QRegExp rx("(\\d+) (\\d+):(\\d+):(\\d+)"); + rx.search(msgElem.attribute("time")); + + QDate d = QDate::currentDate().addMonths(0-m_currentMonth); + timestamp = QDateTime( + QDate(d.year(), d.month(), rx.cap(1).toUInt()), + QTime( rx.cap(2).toUInt(), rx.cap(3).toUInt() ) ); + } + else + timestamp = QDateTime(); //invalid + } + else + msgElem = QDomElement(); + } + + node = (sens == Chronological) ? node.nextSibling() : + node.previousSibling(); + } + m_currentElements[currentContact]=msgElem; //this is the next message + } + } + + if(messages.count() < lines) + m_currentElements.clear(); //current elements are null this can't be allowed + + return messages; +} + +QString HistoryLogger::getFileName(const Kopete::Contact* c, QDate date) +{ + + QString name = c->protocol()->pluginId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + QString::fromLatin1( "/" ) + + c->account()->accountId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + QString::fromLatin1( "/" ) + + c->contactId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + date.toString(".yyyyMM"); + + QString filename=locateLocal( "data", QString::fromLatin1( "kopete/logs/" ) + name+ QString::fromLatin1( ".xml" ) ) ; + + //Check if there is a kopete 0.7.x file + QFileInfo fi(filename); + if(!fi.exists()) + { + name = c->protocol()->pluginId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + QString::fromLatin1( "/" ) + + c->contactId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + + date.toString(".yyyyMM"); + + QString filename2=locateLocal( "data", QString::fromLatin1( "kopete/logs/" ) + name+ QString::fromLatin1( ".xml" ) ) ; + + QFileInfo fi2(filename2); + if(fi2.exists()) + return filename2; + } + + return filename; + +} + +unsigned int HistoryLogger::getFirstMonth(const Kopete::Contact *c) +{ + if(!c) + return getFirstMonth(); + + QRegExp rx( "\\.(\\d\\d\\d\\d)(\\d\\d)" ); + QFileInfo *fi; + + // BEGIN check if there are Kopete 0.7.x + QDir d1(locateLocal("data",QString("kopete/logs/")+ + c->protocol()->pluginId().replace( QRegExp(QString::fromLatin1("[./~?*]")),QString::fromLatin1("-")) + )); + d1.setFilter( QDir::Files | QDir::NoSymLinks ); + d1.setSorting( QDir::Name ); + + const QFileInfoList *list1 = d1.entryInfoList(); + QFileInfoListIterator it1( *list1 ); + + while ( (fi = it1.current()) != 0 ) + { + if(fi->fileName().contains(c->contactId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ))) + { + rx.search(fi->fileName()); + int result = 12*(QDate::currentDate().year() - rx.cap(1).toUInt()) +QDate::currentDate().month() - rx.cap(2).toUInt(); + + if(result < 0) + { + kdWarning(14310) << k_funcinfo << "Kopete only found log file from Kopete 0.7.x made in the future. Check your date!" << endl; + break; + } + return result; + } + ++it1; + } + // END of kopete 0.7.x check + + + QDir d(locateLocal("data",QString("kopete/logs/")+ + c->protocol()->pluginId().replace( QRegExp(QString::fromLatin1("[./~?*]")),QString::fromLatin1("-")) + + QString::fromLatin1( "/" ) + + c->account()->accountId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ) + )); + + d.setFilter( QDir::Files | QDir::NoSymLinks ); + d.setSorting( QDir::Name ); + + const QFileInfoList *list = d.entryInfoList(); + QFileInfoListIterator it( *list ); + while ( (fi = it.current()) != 0 ) + { + if(fi->fileName().contains(c->contactId().replace( QRegExp( QString::fromLatin1( "[./~?*]" ) ), QString::fromLatin1( "-" ) ))) + { + rx.search(fi->fileName()); + int result = 12*(QDate::currentDate().year() - rx.cap(1).toUInt()) +QDate::currentDate().month() - rx.cap(2).toUInt(); + if(result < 0) + { + kdWarning(14310) << k_funcinfo << "Kopete only found log file made in the future. Check your date!" << endl; + break; + } + return result; + } + ++it; + } + return 0; +} + +unsigned int HistoryLogger::getFirstMonth() +{ + if(m_cachedMonth!=-1) + return m_cachedMonth; + + if(!m_metaContact) + return 0; + + int m=0; + QPtrList<Kopete::Contact> contacts=m_metaContact->contacts(); + QPtrListIterator<Kopete::Contact> it( contacts ); + for( ; it.current(); ++it ) + { + int m2=getFirstMonth(*it); + if(m2>m) m=m2; + } + m_cachedMonth=m; + return m; +} + +void HistoryLogger::setHideOutgoing(bool b) +{ + m_hideOutgoing = b; +} + +void HistoryLogger::slotMCDeleted() +{ + m_metaContact = 0; +} + +void HistoryLogger::setFilter(const QString& filter, bool caseSensitive , bool isRegExp) +{ + m_filter=filter; + m_filterCaseSensitive=caseSensitive; + m_filterRegExp=isRegExp; +} + +QString HistoryLogger::filter() const +{ + return m_filter; +} + +bool HistoryLogger::filterCaseSensitive() const +{ + return m_filterCaseSensitive; +} + +bool HistoryLogger::filterRegExp() const +{ + return m_filterRegExp; +} + +QValueList<int> HistoryLogger::getDaysForMonth(QDate date) +{ + QRegExp rxTime("time=\"(\\d+) \\d+:\\d+(:\\d+)?\""); //(with a 0.7.x compatibility) + + QValueList<int> dayList; + + QPtrList<Kopete::Contact> contacts = m_metaContact->contacts(); + QPtrListIterator<Kopete::Contact> it(contacts); + + int lastDay=0; + for(; it.current(); ++it) + { +// kdDebug() << getFileName(*it, date) << endl; + QFile file(getFileName(*it, date)); + if(!file.open(IO_ReadOnly)) + { + continue; + } + QTextStream stream(&file); + QString fullText = stream.read(); + file.close(); + + int pos = 0; + while( (pos = rxTime.search(fullText, pos)) != -1) + { + pos += rxTime.matchedLength(); + int day=rxTime.capturedTexts()[1].toInt(); + + if ( day !=lastDay && dayList.find(day) == dayList.end()) // avoid duplicates + { + dayList.append(rxTime.capturedTexts()[1].toInt()); + lastDay=day; + } + } + } + return dayList; +} + +#include "historylogger.moc" diff --git a/kopete/plugins/history/historylogger.h b/kopete/plugins/history/historylogger.h new file mode 100644 index 00000000..85cdbdd7 --- /dev/null +++ b/kopete/plugins/history/historylogger.h @@ -0,0 +1,217 @@ +/* + historylogger.cpp + + Copyright (c) 2003-2004 by Olivier Goffart <ogoffart @ kde.org> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#ifndef HISTORYLOGGER_H +#define HISTORYLOGGER_H + +#include <qobject.h> +#include "kopetemessage.h" //TODO: REMOVE + +namespace Kopete { class Contact; } +namespace Kopete { class MetaContact; } +class QFile; +class QDomDocument; +class QTimer; + +/** + * One hinstance of this class is opened for every Kopete::ChatSession, + * or for the history dialog + * + * @author Olivier Goffart + */ +class HistoryLogger : public QObject +{ +Q_OBJECT +public: + + /** + * - Chronological: messages are read from the first to the last, in the time order + * - AntiChronological: messages are read from the last to the first, in the time reversed order + */ + enum Sens { Default , Chronological , AntiChronological }; + + /** + * Constructor, takes the contact, and the color of messages + */ + HistoryLogger( Kopete::MetaContact *m , QObject *parent = 0, const char *name = 0); + HistoryLogger( Kopete::Contact *c , QObject *parent = 0, const char *name = 0); + + + ~HistoryLogger(); + + /** + * return or setif yes or no outgoing message are hidden (and not parsed) + */ + bool hideOutgoing() const { return m_hideOutgoing; } + void setHideOutgoing(bool); + + /** + * set a searching filter + * @param filter is the string to search + * @param caseSensitive say if the case is important + * @param isRegExp say if the filter is a QRegExp, or a simle string + */ + void setFilter(const QString& filter, bool caseSensitive=false , bool isRegExp=false); + QString filter() const; + bool filterCaseSensitive() const ; + bool filterRegExp() const; + + + + //---------------------------------- + + /** + * log a message + * @param c add a presision to the contact to use, if null, autodetect. + */ + void appendMessage( const Kopete::Message &msg , const Kopete::Contact *c=0L ); + + /** + * read @param lines message from the current position + * from Kopete::Contact @param c in the given @param sens + */ + QValueList<Kopete::Message> readMessages(unsigned int lines, + const Kopete::Contact *c=0, Sens sens=Default, + bool reverseOrder=false, bool colorize=true); + + /** + * Same as the following, but for one date. I did'nt reuse the above function + * because its structure is really different. + * Read all the messages for the given @param date + */ + QValueList<Kopete::Message> readMessages(QDate date); + + + /** + * The pausition is set to the last message + */ + void setPositionToLast(); + + /** + * The position is set to the first message + */ + void setPositionToFirst(); + + /** + * Set the current month (in number of month since the actual month) + */ + void setCurrentMonth(int month); + + /** + * @return The list of the days for which there is a log for m_metaContact for month of * @param date (don't care of the day) + */ + QValueList<int> getDaysForMonth(QDate date); + + /** + * Get the filename of the xml file which contains the history from the + * contact in the specified @param date. Specify @param date in order to get the filename for + * the given date.year() date.month(). + */ + static QString getFileName(const Kopete::Contact* , QDate date); + +private: + bool m_hideOutgoing; + bool m_filterCaseSensitive; + bool m_filterRegExp; + QString m_filter; + + + /* + *contais all QDomDocument, for a KC, for a specified Month + */ + QMap<const Kopete::Contact*,QMap<unsigned int , QDomDocument> > m_documents; + + /** + * Contains the current message. + * in fact, this is the next, still not showed + */ + QMap<const Kopete::Contact*, QDomElement> m_currentElements; + + /** + * Get the document, open it is @param canload is true, contain is set to false if the document + * is not already contained + */ + QDomDocument getDocument(const Kopete::Contact *c, unsigned int month , bool canLoad=true , bool* contain=0L); + + QDomDocument getDocument(const Kopete::Contact *c, const QDate date, bool canLoad=true, bool* contain=0L); + + /** + * look over files to get the last month for this contact + */ + unsigned int getFirstMonth(const Kopete::Contact *c); + unsigned int getFirstMonth(); + + + /* + * the current month + */ + unsigned int m_currentMonth; + + /* + * the cached getFirstMonth + */ + int m_cachedMonth; + + + + /* + * the metacontact we are using + */ + Kopete::MetaContact *m_metaContact; + + /* + * keep the old position in memory, so if we change the sens, we can begin here + */ + QMap<const Kopete::Contact*, QDomElement> m_oldElements; + unsigned int m_oldMonth; + Sens m_oldSens; + + /** + * the timer used to save the file + */ + QTimer *m_saveTimer; + QDomDocument m_toSaveDocument; + QString m_toSaveFileName; + unsigned int m_saveTimerTime; //time in ms between each save + + /** + * workaround for the 31 midnight bug. + * it contains the number of the current month. + */ + int m_realMonth; + + /* + * FIXME: + * WORKAROUND + * due to a bug in QT, i have to keep the document element in the memory to + * prevent crashes + */ + QValueList<QDomElement> workaround; + +private slots: + /** + * the metacontact has been deleted + */ + void slotMCDeleted(); + + /** + * save the current month's document on the disk. + * connected to the m_saveTimer signal + */ + void saveToDisk(); +}; + +#endif diff --git a/kopete/plugins/history/historyplugin.cpp b/kopete/plugins/history/historyplugin.cpp new file mode 100644 index 00000000..bf8d70b4 --- /dev/null +++ b/kopete/plugins/history/historyplugin.cpp @@ -0,0 +1,194 @@ +/* + historyplugin.cpp + + Copyright (c) 2003-2004 by Olivier Goffart <ogoffart @ kde.org> + (c) 2003 by Stefan Gehn <metz AT gehn.net> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#include <kgenericfactory.h> +#include <kaboutdata.h> +#include <kaction.h> +#include <kmessagebox.h> +//#include <kconfig.h> +#include <kplugininfo.h> +#include <kdeversion.h> + +#include "kopetechatsessionmanager.h" +#include "kopetemetacontact.h" +#include "kopeteview.h" +#include "kopetecontactlist.h" +#include "kopeteuiglobal.h" +#include "kopetemessageevent.h" +#include "kopeteviewplugin.h" + +#include "historydialog.h" +#include "historyplugin.h" +#include "historylogger.h" +#include "historyguiclient.h" +#include "historyconfig.h" + +typedef KGenericFactory<HistoryPlugin> HistoryPluginFactory; +static const KAboutData aboutdata("kopete_history", I18N_NOOP("History") , "1.0" ); +K_EXPORT_COMPONENT_FACTORY( kopete_history, HistoryPluginFactory( &aboutdata ) ) + +HistoryPlugin::HistoryPlugin( QObject *parent, const char *name, const QStringList & /* args */ ) +: Kopete::Plugin( HistoryPluginFactory::instance(), parent, name ), m_loggerFactory( this ) +{ + KAction *viewMetaContactHistory = new KAction( i18n("View &History" ), + QString::fromLatin1( "history" ), 0, this, SLOT(slotViewHistory()), + actionCollection(), "viewMetaContactHistory" ); + viewMetaContactHistory->setEnabled( + Kopete::ContactList::self()->selectedMetaContacts().count() == 1 ); + + connect(Kopete::ContactList::self(), SIGNAL(metaContactSelected(bool)), + viewMetaContactHistory, SLOT(setEnabled(bool))); + + connect(Kopete::ChatSessionManager::self(), SIGNAL(viewCreated(KopeteView*)), + this, SLOT(slotViewCreated(KopeteView*))); + + connect(this, SIGNAL(settingsChanged()), this, SLOT(slotSettingsChanged())); + + setXMLFile("historyui.rc"); + if(detectOldHistory()) + { + if( + KMessageBox::questionYesNo(Kopete::UI::Global::mainWidget(), + i18n( "Old history files from Kopete 0.6.x or older has been detected.\n" + "Do you want to import and convert it to the new history format?" ), + i18n( "History Plugin" ), i18n("Import && Convert"), i18n("Do Not Import") ) == KMessageBox::Yes ) + { + convertOldHistory(); + } + } + + // Add GUI action to all existing kmm objects + // (Needed if the plugin is enabled while kopete is already running) + QValueList<Kopete::ChatSession*> sessions = Kopete::ChatSessionManager::self()->sessions(); + for (QValueListIterator<Kopete::ChatSession*> it= sessions.begin(); it!=sessions.end() ; ++it) + { + if(!m_loggers.contains(*it)) + { + m_loggers.insert(*it, new HistoryGUIClient( *it ) ); + connect( *it, SIGNAL(closing(Kopete::ChatSession*)), + this, SLOT(slotKMMClosed(Kopete::ChatSession*))); + } + } +} + + +HistoryPlugin::~HistoryPlugin() +{ +} + + +void HistoryMessageLogger::handleMessage( Kopete::MessageEvent *event ) +{ + history->messageDisplayed( event->message() ); + MessageHandler::handleMessage( event ); +} + +void HistoryPlugin::messageDisplayed(const Kopete::Message &m) +{ + if(m.direction()==Kopete::Message::Internal || !m.manager()) + return; + + if(!m_loggers.contains(m.manager())) + { + m_loggers.insert(m.manager() , new HistoryGUIClient( m.manager() ) ); + connect(m.manager(), SIGNAL(closing(Kopete::ChatSession*)), + this, SLOT(slotKMMClosed(Kopete::ChatSession*))); + } + + HistoryLogger *l=m_loggers[m.manager()]->logger(); + if(l) + { + QPtrList<Kopete::Contact> mb=m.manager()->members(); + l->appendMessage(m,mb.first()); + } + + m_lastmessage=m; +} + + +void HistoryPlugin::slotViewHistory() +{ + Kopete::MetaContact *m=Kopete::ContactList::self()->selectedMetaContacts().first(); + if(m) + { + int lines = HistoryConfig::number_ChatWindow(); + + // TODO: Keep track of open dialogs and raise instead of + // opening a new (duplicated) one + new HistoryDialog(m); + } +} + + +void HistoryPlugin::slotViewCreated( KopeteView* v ) +{ + if(v->plugin()->pluginInfo()->pluginName() != QString::fromLatin1("kopete_chatwindow") ) + return; //Email chat windows are not supported. + + bool autoChatWindow = HistoryConfig::auto_chatwindow(); + int nbAutoChatWindow = HistoryConfig::number_Auto_chatwindow(); + + KopeteView *m_currentView = v; + Kopete::ChatSession *m_currentChatSession = v->msgManager(); + QPtrList<Kopete::Contact> mb = m_currentChatSession->members(); + + if(!m_currentChatSession) + return; //i am sorry + + if(!m_loggers.contains(m_currentChatSession)) + { + m_loggers.insert(m_currentChatSession , new HistoryGUIClient( m_currentChatSession ) ); + connect( m_currentChatSession, SIGNAL(closing(Kopete::ChatSession*)), + this , SLOT(slotKMMClosed(Kopete::ChatSession*))); + } + + if(!autoChatWindow || nbAutoChatWindow == 0) + return; + + HistoryLogger *logger = m_loggers[m_currentChatSession]->logger(); + + logger->setPositionToLast(); + + QValueList<Kopete::Message> msgs = logger->readMessages(nbAutoChatWindow, + /*mb.first()*/ 0L, HistoryLogger::AntiChronological, true, true); + + // make sure the last message is not the one which will be appened right + // after the view is created (and which has just been logged in) + if( + (msgs.last().plainBody() == m_lastmessage.plainBody()) && + (m_lastmessage.manager() == m_currentChatSession)) + { + msgs.remove(msgs.fromLast()); + } + + m_currentView->appendMessages( msgs ); +} + + +void HistoryPlugin::slotKMMClosed( Kopete::ChatSession* kmm) +{ + m_loggers[kmm]->deleteLater(); + m_loggers.remove(kmm); +} + +void HistoryPlugin::slotSettingsChanged() +{ + kdDebug(14310) << k_funcinfo << "RELOADING CONFIG" << endl; + HistoryConfig::self()->readConfig(); +} + +#include "historyplugin.moc" diff --git a/kopete/plugins/history/historyplugin.h b/kopete/plugins/history/historyplugin.h new file mode 100644 index 00000000..63e2c87b --- /dev/null +++ b/kopete/plugins/history/historyplugin.h @@ -0,0 +1,106 @@ +/* + historyplugin.h + + Copyright (c) 2003-2005 by Olivier Goffart <ogoffart at kde.org> + (c) 2003 by Stefan Gehn <metz AT gehn.net> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#ifndef HISTORYPLUGIN_H +#define HISTORYPLUGIN_H + +#include <qobject.h> +#include <qmap.h> +#include <qstring.h> + +#include "kopeteplugin.h" + +#include "kopetemessage.h" +#include "kopetemessagehandler.h" + +class KopeteView; +class KActionCollection; + +namespace Kopete +{ +class MetaContact; +class ChatSession; +} + +class HistoryPreferences; +class HistoryGUIClient; +class HistoryPlugin; + +/** + * @author Richard Smith + */ +class HistoryMessageLogger : public Kopete::MessageHandler +{ + HistoryPlugin *history; +public: + HistoryMessageLogger( HistoryPlugin *history ) : history(history) {} + void handleMessage( Kopete::MessageEvent *event ); +}; + +class HistoryMessageLoggerFactory : public Kopete::MessageHandlerFactory +{ + HistoryPlugin *history; +public: + HistoryMessageLoggerFactory( HistoryPlugin *history ) : history(history) {} + Kopete::MessageHandler *create( Kopete::ChatSession * /*manager*/, Kopete::Message::MessageDirection direction ) + { + if( direction != Kopete::Message::Inbound ) + return 0; + return new HistoryMessageLogger(history); + } + int filterPosition( Kopete::ChatSession *, Kopete::Message::MessageDirection ) + { + return Kopete::MessageHandlerFactory::InStageToSent+5; + } +}; + +/** + * @author Olivier Goffart + */ +class HistoryPlugin : public Kopete::Plugin +{ + Q_OBJECT + public: + HistoryPlugin( QObject *parent, const char *name, const QStringList &args ); + ~HistoryPlugin(); + + /** + * convert the Kopete 0.6 / 0.5 history to the new format + */ + static void convertOldHistory(); + /** + * return true if an old history has been detected, and no new ones + */ + static bool detectOldHistory(); + + void messageDisplayed(const Kopete::Message &msg); + + private slots: + void slotViewCreated( KopeteView* ); + void slotViewHistory(); + void slotKMMClosed( Kopete::ChatSession* ); + void slotSettingsChanged(); + + private: + HistoryMessageLoggerFactory m_loggerFactory; + QMap<Kopete::ChatSession*,HistoryGUIClient*> m_loggers; + Kopete::Message m_lastmessage; +}; + +#endif + + diff --git a/kopete/plugins/history/historypreferences.cpp b/kopete/plugins/history/historypreferences.cpp new file mode 100644 index 00000000..61fce469 --- /dev/null +++ b/kopete/plugins/history/historypreferences.cpp @@ -0,0 +1,88 @@ +/* + historypreferences.cpp + + Copyright (c) 2003 by Olivier Goffart <ogoffart @ kde.org> + (c) 2003 by Stefan Gehn <metz AT gehn.net> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#include "historypreferences.h" +#include "historyconfig.h" +#include "historyprefsui.h" + +#include <kgenericfactory.h> +#include <qlayout.h> +#include <qgroupbox.h> +#include <kcolorbutton.h> +#include <knuminput.h> +#include <qcheckbox.h> + +typedef KGenericFactory<HistoryPreferences> HistoryConfigFactory; +K_EXPORT_COMPONENT_FACTORY( kcm_kopete_history, HistoryConfigFactory( "kcm_kopete_history" ) ) + +HistoryPreferences::HistoryPreferences(QWidget *parent, const char*/*name*/, const QStringList &args) + : KCModule(HistoryConfigFactory::instance(), parent, args) +{ + kdDebug(14310) << k_funcinfo << "called." << endl; + (new QVBoxLayout(this))->setAutoAdd(true); + p = new HistoryPrefsUI(this); + + connect(p->chkShowPrevious, SIGNAL(toggled(bool)), this, SLOT(slotShowPreviousChanged(bool))); + connect(p->Number_Auto_chatwindow, SIGNAL(valueChanged(int)), + this, SLOT(slotModified())); + connect(p->Number_ChatWindow, SIGNAL(valueChanged(int)), + this, SLOT(slotModified())); + connect(p->History_color, SIGNAL(changed(const QColor&)), + this, SLOT(slotModified())); + load(); +} + +HistoryPreferences::~HistoryPreferences() +{ + kdDebug(14310) << k_funcinfo << "called." << endl; +} + +void HistoryPreferences::load() +{ + kdDebug(14310) << k_funcinfo << "called." << endl; + HistoryConfig::self()->readConfig(); + p->chkShowPrevious->setChecked(HistoryConfig::auto_chatwindow()); + slotShowPreviousChanged(p->chkShowPrevious->isChecked()); + p->Number_Auto_chatwindow->setValue(HistoryConfig::number_Auto_chatwindow()); + p->Number_ChatWindow->setValue(HistoryConfig::number_ChatWindow()); + p->History_color->setColor(HistoryConfig::history_color()); + //p-> HistoryConfig::browserStyle(); + emit KCModule::changed(false); +} + +void HistoryPreferences::save() +{ + kdDebug(14310) << k_funcinfo << "called." << endl; + HistoryConfig::setAuto_chatwindow(p->chkShowPrevious->isChecked()); + HistoryConfig::setNumber_Auto_chatwindow(p->Number_Auto_chatwindow->value()); + HistoryConfig::setNumber_ChatWindow(p->Number_ChatWindow->value()); + HistoryConfig::setHistory_color(p->History_color->color()); + HistoryConfig::self()->writeConfig(); + emit KCModule::changed(false); +} + +void HistoryPreferences::slotModified() +{ + emit KCModule::changed(true); +} + +void HistoryPreferences::slotShowPreviousChanged(bool on) +{ + emit KCModule::changed(true); +} + +#include "historypreferences.moc" diff --git a/kopete/plugins/history/historypreferences.h b/kopete/plugins/history/historypreferences.h new file mode 100644 index 00000000..247e2bc8 --- /dev/null +++ b/kopete/plugins/history/historypreferences.h @@ -0,0 +1,48 @@ +/* + historypreferences.h + + Copyright (c) 2003 by Olivier Goffart <ogoffart @ kde.org> + (c) 2003 by Stefan Gehn <metz AT gehn.net> + Kopete (c) 2003-2004 by the Kopete developers <kopete-devel@kde.org> + + ************************************************************************* + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ************************************************************************* +*/ + +#ifndef HISTORYPREFERENCES_H +#define HISTORYPREFERENCES_H + +#include <kcmodule.h> +#include <qstring.h> + +class HistoryPrefsUI; + +/** + * @author Stefan Gehn + */ +class HistoryPreferences : public KCModule +{ + Q_OBJECT + public: + HistoryPreferences(QWidget *parent=0, const char* name=0, + const QStringList &args = QStringList()); + ~HistoryPreferences(); + + virtual void save(); + virtual void load(); + + private slots: + void slotModified(); + void slotShowPreviousChanged(bool); + + private: + HistoryPrefsUI *p; +}; + +#endif diff --git a/kopete/plugins/history/historyprefsui.ui b/kopete/plugins/history/historyprefsui.ui new file mode 100644 index 00000000..5942a07a --- /dev/null +++ b/kopete/plugins/history/historyprefsui.ui @@ -0,0 +1,187 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>HistoryPrefsUI</class> +<author>Olivier Goffart</author> +<widget class="QWidget"> + <property name="name"> + <cstring>HistoryPrefsWidget</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>363</width> + <height>212</height> + </rect> + </property> + <property name="caption"> + <string>HistoryPrefsWidget</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox"> + <property name="name"> + <cstring>grpChatHistory</cstring> + </property> + <property name="title"> + <string>Chat History</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel" row="3" column="0"> + <property name="name"> + <cstring>lblNoLinesPerPage</cstring> + </property> + <property name="text"> + <string>Number of messages per page:</string> + </property> + <property name="whatsThis" stdset="0"> + <string>The number of messages that are shown when browsing history in the chat window</string> + </property> + </widget> + <widget class="KIntSpinBox" row="3" column="1"> + <property name="name"> + <cstring>Number_ChatWindow</cstring> + </property> + <property name="maxValue"> + <number>32768</number> + </property> + <property name="minValue"> + <number>1</number> + </property> + <property name="value"> + <number>10</number> + </property> + <property name="whatsThis" stdset="0"> + <string>The number of message that are shown when borwsing history in the chat window</string> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>colorLabel</cstring> + </property> + <property name="text"> + <string>Color of messages:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>History_color</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>Color of history messages in the chat window</string> + </property> + </widget> + <widget class="KColorButton" row="2" column="1"> + <property name="name"> + <cstring>History_color</cstring> + </property> + <property name="text"> + <string></string> + </property> + <property name="color"> + <color> + <red>170</red> + <green>170</green> + <blue>127</blue> + </color> + </property> + <property name="whatsThis" stdset="0"> + <string>Color of history messages in the chat window</string> + </property> + </widget> + <widget class="KIntSpinBox" row="1" column="1"> + <property name="name"> + <cstring>Number_Auto_chatwindow</cstring> + </property> + <property name="maxValue"> + <number>32768</number> + </property> + <property name="minValue"> + <number>1</number> + </property> + <property name="value"> + <number>7</number> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the number of messages that will be added automatically in the chat window when opening a new chat.</string> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>numberLabel</cstring> + </property> + <property name="text"> + <string>Number of messages to show:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>Number_Auto_chatwindow</cstring> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the number of messages that will be added automatically in the chat window when opening a new chat.</string> + </property> + </widget> + <widget class="QCheckBox" row="0" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>chkShowPrevious</cstring> + </property> + <property name="text"> + <string>Show chat history in new chats</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <property name="whatsThis" stdset="0"> + <string>When a new chat is opened, automatically add the last few messages between you and that contact.</string> + </property> + </widget> + </grid> + </widget> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>31</width> + <height>90</height> + </size> + </property> + </spacer> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>chkShowPrevious</sender> + <signal>toggled(bool)</signal> + <receiver>numberLabel</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>chkShowPrevious</sender> + <signal>toggled(bool)</signal> + <receiver>Number_Auto_chatwindow</receiver> + <slot>setEnabled(bool)</slot> + </connection> +</connections> +<tabstops> + <tabstop>chkShowPrevious</tabstop> + <tabstop>Number_Auto_chatwindow</tabstop> + <tabstop>History_color</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>knuminput.h</includehint> + <includehint>kcolorbutton.h</includehint> + <includehint>knuminput.h</includehint> +</includehints> +</UI> diff --git a/kopete/plugins/history/historyui.rc b/kopete/plugins/history/historyui.rc new file mode 100644 index 00000000..5f72b22c --- /dev/null +++ b/kopete/plugins/history/historyui.rc @@ -0,0 +1,12 @@ +<!DOCTYPE kpartgui> +<kpartgui name="kopete_history" version="1"> + <MenuBar> + <Menu name="edit"> + <text>&Edit</text> + <Action name="viewMetaContactHistory" /> + </Menu> + </MenuBar> + <Menu name="contact_popup"> + <Action name="viewMetaContactHistory" /> + </Menu> +</kpartgui> diff --git a/kopete/plugins/history/historyviewer.ui b/kopete/plugins/history/historyviewer.ui new file mode 100644 index 00000000..4cef647e --- /dev/null +++ b/kopete/plugins/history/historyviewer.ui @@ -0,0 +1,347 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>HistoryViewer</class> +<widget class="QWidget"> + <property name="name"> + <cstring>HistoryViewer</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>682</width> + <height>634</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>200</height> + </size> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLayoutWidget" row="3" column="0"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>statusLabel</cstring> + </property> + <property name="maximumSize"> + <size> + <width>32767</width> + <height>20</height> + </size> + </property> + <property name="text"> + <string>Ready</string> + </property> + </widget> + <widget class="KProgress"> + <property name="name"> + <cstring>searchProgress</cstring> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget" row="2" column="0"> + <property name="name"> + <cstring>layout8</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QPushButton"> + <property name="name"> + <cstring>searchErase</cstring> + </property> + <property name="text"> + <string></string> + </property> + <property name="accel"> + <string></string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="text"> + <string>Search:</string> + </property> + </widget> + <widget class="KLineEdit"> + <property name="name"> + <cstring>searchLine</cstring> + </property> + </widget> + <widget class="QPushButton"> + <property name="name"> + <cstring>searchButton</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>70</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>150</width> + <height>32767</height> + </size> + </property> + <property name="text"> + <string>Se&arch</string> + </property> + </widget> + </hbox> + </widget> + <widget class="QSplitter" row="1" column="0"> + <property name="name"> + <cstring>splitter2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout5</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="KListViewSearchLine"> + <property name="name"> + <cstring>dateSearchLine</cstring> + </property> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>140</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>32767</width> + <height>32767</height> + </size> + </property> + </widget> + <widget class="KListView"> + <column> + <property name="text"> + <string>Date</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <column> + <property name="text"> + <string>Contact</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <property name="name"> + <cstring>dateListView</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>32767</width> + <height>32767</height> + </size> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + </widget> + </vbox> + </widget> + <widget class="QFrame"> + <property name="name"> + <cstring>htmlFrame</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>10</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="frameShape"> + <enum>WinPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + </widget> + </widget> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>layout11</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Contact:</string> + </property> + </widget> + <widget class="KComboBox"> + <property name="name"> + <cstring>contactComboBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1_2</cstring> + </property> + <property name="text"> + <string>Message Filter:</string> + </property> + </widget> + <widget class="QComboBox"> + <item> + <property name="text"> + <string>All messages</string> + </property> + </item> + <item> + <property name="text"> + <string>Only incoming</string> + </property> + </item> + <item> + <property name="text"> + <string>Only outgoing</string> + </property> + </item> + <property name="name"> + <cstring>messageFilterBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>200</width> + <height>0</height> + </size> + </property> + </widget> + </hbox> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kprogress.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>klistviewsearchline.h</includehint> + <includehint>klistview.h</includehint> + <includehint>kcombobox.h</includehint> +</includehints> +</UI> diff --git a/kopete/plugins/history/kopete_history.desktop b/kopete/plugins/history/kopete_history.desktop new file mode 100644 index 00000000..5f14aee0 --- /dev/null +++ b/kopete/plugins/history/kopete_history.desktop @@ -0,0 +1,139 @@ +[Desktop Entry] +Type=Service +X-Kopete-Version=1000900 +Icon=history +ServiceTypes=Kopete/Plugin +X-KDE-Library=kopete_history +X-KDE-PluginInfo-Author=Olivier Goffart +X-KDE-PluginInfo-Email=ogoffart@tiscalinet.be +X-KDE-PluginInfo-Name=kopete_history +X-KDE-PluginInfo-Version=0.8.0 +X-KDE-PluginInfo-Website=http://kopete.kde.org +X-KDE-PluginInfo-Category=Plugins +X-KDE-PluginInfo-Depends= +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +Name=History +Name[ar]=محفوظات +Name[az]=Keçmiş +Name[be]=Гісторыя +Name[bg]=История +Name[bn]=ইতিহাস +Name[br]=Istor +Name[bs]=Historija +Name[ca]=Historial +Name[cs]=Historie +Name[cy]=Hanes +Name[da]=Historik +Name[de]=Verlauf +Name[el]=Ιστορικό +Name[eo]=Historio +Name[es]=Historia +Name[et]=Ajalugu +Name[eu]=Historia +Name[fa]=تاریخچه +Name[fi]=Historia +Name[fr]=Historique +Name[ga]=Stair +Name[gl]=Historial +Name[he]=היסטוריה +Name[hi]=इतिहास +Name[hr]=Povijest +Name[hu]=Üzenetnapló +Name[id]=Sejarah +Name[is]=Ferill +Name[it]=Cronologia +Name[ja]=履歴 +Name[ka]=ისტორია +Name[kk]=Журнал +Name[km]=ប្រវត្តិ +Name[lt]=Istorija +Name[lv]=Vēsture +Name[mk]=Историја +Name[mt]=Kronoloġija +Name[nb]=Historie +Name[nds]=Vörgeschicht +Name[ne]=इतिहास +Name[nl]=Geschiedenis +Name[nn]=Historie +Name[pa]=ਅਤੀਤ +Name[pl]=Historia +Name[pt]=Histórico +Name[pt_BR]=História +Name[ro]=Istoric +Name[ru]=Журнал разговоров +Name[rw]=Amateka +Name[se]=Historihkka +Name[sk]=História +Name[sl]=Zgodovina +Name[sr]=Историја +Name[sr@Latn]=Istorija +Name[sv]=Historik +Name[ta]=வரலாறு +Name[tg]=Номнависи сӯҳбатҳо +Name[th]=ประวัติการใช้งาน +Name[tr]=Geçmiş +Name[uk]=Історія +Name[uz]=Tarix +Name[uz@cyrillic]=Тарих +Name[ven]=Divhazwakale +Name[wa]=Istwere +Name[xh]=Imbali +Name[zh_CN]=历史 +Name[zh_HK]=歷程紀錄 +Name[zh_TW]=歷史 +Name[zu]=Umlando +Comment=Log all messages to keep track of your conversations +Comment[ar]=سجل جميع الرسائل للمحافظة على محادثاتك +Comment[be]=Запісваць усе паведамленні для стварэння дзённікаў гутарак +Comment[bg]=Запис на всички съобщения с цел преглед и търсене в тях в бъдеще +Comment[bn]=আপনার কথোপকথনের খতিয়ান রাখতে সব বার্তা কার্যবিবরণীতে লিখে রাখে +Comment[bs]=Zapiši sve poruke u historiju +Comment[ca]=Registra tots els missatges per seguir les vostres converses +Comment[cs]=Záznam konverzace +Comment[cy]=Cofnodi pob neges er mwyn cadw trefn ar eich sgwrsiau +Comment[da]=Log alle beskeder for at holde styr på dine konversationer +Comment[de]=Protokolliert alle Nachrichten der eigenen Gespräche +Comment[el]=Καταγράψτε όλα τα μηνύματά σας για να διατηρήσετε αρχείο με τις συζητήσεις σας +Comment[es]=Registra todos los mensajes para guardar sus conversaciones +Comment[et]=Kõigi sõnumite logimine, et neil ka hiljem silm peal hoida +Comment[eu]=Gorde mezu guztiak zure elkarrizketak jarrai ahal ditzazun +Comment[fa]=برای ردگیری مکالمات خود همۀ پیامها را ثبت کنید +Comment[fi]=Laita kaikki viestisi lokiin +Comment[fr]=Enregistrer tous les messages pour conserver une trace de vos discussions +Comment[gl]=Rexitra tódolas mensajex para gardar conversacións +Comment[he]=שומר תיעוד מסודר שלך כל שיחותיך +Comment[hi]=आपके वार्तालाप की जानकारी बनाए रखने के लिए सभी संदेशों को लॉग करें +Comment[hr]=Upisuje u dnevnik sve poruke kako biste vodili evidenciju o svojim razgovorima +Comment[hu]=Az üzenetek archiválása +Comment[is]=Halda til haga samskiptaannál +Comment[it]=Effettua il log di tutti i messaggi in modo da avere traccia delle tue conversazioni +Comment[ja]=会話を残すためにメッセージのログを取る +Comment[ka]=ყველა შეტყობინების ჟურნალირება თქვენი საუბრების ჩასაწერად +Comment[kk]=Хабарласу барысын журналға жазып отыру +Comment[km]=ចុះកំណត់ហេតុសារទាំងអស់ ដើម្បីតាមដានការសន្ទនារបស់អ្នក +Comment[lt]=Įrašinėti visas žinutes ir vesti pokalbių žurnalą +Comment[mk]=Ги зачувува сите пораки за да ги следите вашите разговори +Comment[nb]=Logg alle meldinger for å ta vare på samtalene dine +Comment[nds]=All Narichten för't Nakieken na't Logbook schrieven +Comment[ne]=तपाईँको वार्तालापको ट्रयाक राख्न सबै सन्देश लग गर्नुहोस् +Comment[nl]=Bewaar alle berichten in een logboek om uw conversaties later opnieuw te kunnen bekijken +Comment[nn]=Logg alle meldingar for å ta vare på samtalane dine +Comment[pl]=Zapisuje wszystkie wiadomości, aby trzymać historię Twoich rozmów +Comment[pt]=Regista todas as mensagens para manter um registo da sua conversa +Comment[pt_BR]=Registra todas as mensagens para manter o histórico de suas conversações +Comment[ru]=Делать записи ваших разговоров в журнале +Comment[se]=Vurke buot dieđáhusaid vai oaidnit du ságastallamiid +Comment[sk]=Záznam všetkých správ, aby ste mohli sledovať vaše rozhovory +Comment[sl]=Beleži vsa sporočila za hranjenje vaših pogovorov +Comment[sr]=Уписује у дневник све поруке да би сте водили евиденцију о својим разговорима +Comment[sr@Latn]=Upisuje u dnevnik sve poruke da bi ste vodili evidenciju o svojim razgovorima +Comment[sv]=Logga alla meddelanden för att hålla ordning på samtalen +Comment[ta]=உங்கள் உரையாடலை கவனிக்க அனைத்து செய்திகளையும் புகுபதி +Comment[tg]=Сабти ҳамаи пайёмҳо барои пайгардии ҳамаи сӯҳбатҳои шумо +Comment[tr]=Konuşmalarınızın kaydedildiği bütün günlük mesajları +Comment[uk]=Робити записи в журналі для слідкування за вашими розмовами +Comment[wa]=Wårder on djournå di tos vos messaedjes, po vos poleur rivey li conversåcion +Comment[zh_CN]=记录您对话的全部消息 +Comment[zh_HK]=記錄所有訊息,讓您能追查您的對話紀錄 +Comment[zh_TW]=紀錄所有對話訊息 diff --git a/kopete/plugins/history/kopete_history_config.desktop b/kopete/plugins/history/kopete_history_config.desktop new file mode 100644 index 00000000..5ee2d6b2 --- /dev/null +++ b/kopete/plugins/history/kopete_history_config.desktop @@ -0,0 +1,141 @@ +[Desktop Entry] +Icon=history +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=kopete_history +X-KDE-FactoryName=HistoryConfigFactory +X-KDE-ParentApp=kopete_history +X-KDE-ParentComponents=kopete_history + +Name=History +Name[ar]=محفوظات +Name[az]=Keçmiş +Name[be]=Гісторыя +Name[bg]=История +Name[bn]=ইতিহাস +Name[br]=Istor +Name[bs]=Historija +Name[ca]=Historial +Name[cs]=Historie +Name[cy]=Hanes +Name[da]=Historik +Name[de]=Verlauf +Name[el]=Ιστορικό +Name[eo]=Historio +Name[es]=Historia +Name[et]=Ajalugu +Name[eu]=Historia +Name[fa]=تاریخچه +Name[fi]=Historia +Name[fr]=Historique +Name[ga]=Stair +Name[gl]=Historial +Name[he]=היסטוריה +Name[hi]=इतिहास +Name[hr]=Povijest +Name[hu]=Üzenetnapló +Name[id]=Sejarah +Name[is]=Ferill +Name[it]=Cronologia +Name[ja]=履歴 +Name[ka]=ისტორია +Name[kk]=Журнал +Name[km]=ប្រវត្តិ +Name[lt]=Istorija +Name[lv]=Vēsture +Name[mk]=Историја +Name[mt]=Kronoloġija +Name[nb]=Historie +Name[nds]=Vörgeschicht +Name[ne]=इतिहास +Name[nl]=Geschiedenis +Name[nn]=Historie +Name[pa]=ਅਤੀਤ +Name[pl]=Historia +Name[pt]=Histórico +Name[pt_BR]=História +Name[ro]=Istoric +Name[ru]=Журнал разговоров +Name[rw]=Amateka +Name[se]=Historihkka +Name[sk]=História +Name[sl]=Zgodovina +Name[sr]=Историја +Name[sr@Latn]=Istorija +Name[sv]=Historik +Name[ta]=வரலாறு +Name[tg]=Номнависи сӯҳбатҳо +Name[th]=ประวัติการใช้งาน +Name[tr]=Geçmiş +Name[uk]=Історія +Name[uz]=Tarix +Name[uz@cyrillic]=Тарих +Name[ven]=Divhazwakale +Name[wa]=Istwere +Name[xh]=Imbali +Name[zh_CN]=历史 +Name[zh_HK]=歷程紀錄 +Name[zh_TW]=歷史 +Name[zu]=Umlando +Comment=History Plugin +Comment[ar]=توصيلة المحفوظات +Comment[be]=Модуль гісторыі +Comment[bg]=Приставка за историята +Comment[bn]=ইতিহাস প্লাগিন +Comment[br]=Lugent an istorig +Comment[bs]=Dodatak za historiju +Comment[ca]=Connector de l'historial +Comment[cs]=Modul historie +Comment[cy]=Ategyn Hanes +Comment[da]=Historik-plugin +Comment[de]=Verlaufsmodul +Comment[el]=Πρόσθετο ιστορικού +Comment[eo]=Historio-kromaĵo +Comment[es]=Complemento de Historial +Comment[et]=Ajalooplugin +Comment[eu]=Historia plugin-a +Comment[fa]=وصلۀ تاریخچه +Comment[fi]=Historia-liitännäinen +Comment[fr]=Module d'historique +Comment[ga]=Breiseán Staire +Comment[gl]=Plugin de historial +Comment[he]=תוסף ההיסטוריה +Comment[hi]=इतिहास प्लगइन +Comment[hr]=Umetak za povijest +Comment[hu]=Előzmények bővítőmodul +Comment[is]=Ferilsíforrit +Comment[it]=Plugin cronologia +Comment[ja]=履歴プラグイン +Comment[ka]=ისტორიის მოდული +Comment[kk]=Журнал плагин модулі +Comment[km]=កម្មវិធីជំនួយប្រវត្តិ +Comment[lt]=Istorijos įskiepis +Comment[mk]=Приклучок за историја +Comment[nb]=Programtillegg for historie +Comment[nds]=Vörgeschichtmoduul +Comment[ne]=इतिहास प्लगइन +Comment[nl]=Geschiedenis-plugin +Comment[nn]=Programtillegg for historie +Comment[pl]=Wtyczka historii +Comment[pt]='Plugin' de Historial +Comment[pt_BR]=Plugin de Histórico +Comment[ro]=Modul istoric +Comment[ru]=Модуль журналирования +Comment[se]=Historihkkalassemoduvla +Comment[sk]=Modul histórie +Comment[sl]=Vstavek Zgodovina +Comment[sr]=Прикључак за историјат +Comment[sr@Latn]=Priključak za istorijat +Comment[sv]=Historikinsticksprogram +Comment[ta]=வரலாற்று செருகல் +Comment[tg]=Модули Номнависи сӯҳбатҳо +Comment[tr]=Geçmiş Eklentisi +Comment[uk]=Втулок історії +Comment[uz]=Tarix plagini +Comment[uz@cyrillic]=Тарих плагини +Comment[wa]=Tchôke-divins del istwere +Comment[zh_CN]=历史插件 +Comment[zh_HK]=歷程紀錄插件 +Comment[zh_TW]=歷史外掛程式 |