/* * astyle --force-indent=tab=4 --brackets=linux --indent-switches * --pad=oper --one-line=keep-blocks --unpad=paren * * Miranda NG: the free IM client for Microsoft* Windows* * * Copyright (c) 2000-09 Miranda ICQ/IM project, * all portions of this codebase are copyrighted to the people * listed in contributors.txt. * * This programm 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * you should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * part of tabSRMM messaging plugin for Miranda. * * (C) 2005-2010 by silvercircle _at_ gmail _dot_ com and contributors * * contact cache implementation * * the contact cache provides various services to the message window(s) * it also abstracts meta contacts. * */ #include "commonheaders.h" static OBJLIST arContacts(50, HandleKeySortT); CContactCache::CContactCache(const HANDLE hContact) { m_hContact = hContact; m_wOldStatus = m_wStatus = m_wMetaStatus = ID_STATUS_OFFLINE; if (hContact) { m_szProto = ::GetContactProto(m_hContact); if (m_szProto) m_tszProto = mir_a2t(m_szProto); initPhaseTwo(); } else { m_szProto = C_INVALID_PROTO; m_tszProto = C_INVALID_PROTO_T; m_szAccount = C_INVALID_ACCOUNT; m_isMeta = false; m_Valid = false; } } /** * 2nd part of the object initialization that must be callable during the * object's lifetime (not only on construction). */ void CContactCache::initPhaseTwo() { m_szAccount = 0; if (m_szProto) { PROTOACCOUNT *acc = reinterpret_cast(::CallService(MS_PROTO_GETACCOUNT, 0, (LPARAM)m_szProto)); if (acc && acc->tszAccountName) m_szAccount = acc->tszAccountName; } m_Valid = (m_szProto != 0 && m_szAccount != 0) ? true : false; if (m_Valid) { m_isMeta = PluginConfig.bMetaEnabled && !strcmp(m_szProto, PluginConfig.szMetaName); m_isSubcontact = db_get_b(m_hContact, PluginConfig.szMetaName, "IsSubcontact", 0) != 0; if (m_isMeta) updateMeta(true); updateState(); updateFavorite(); } else { m_szProto = C_INVALID_PROTO; m_tszProto = C_INVALID_PROTO_T; m_szAccount = C_INVALID_ACCOUNT; m_isMeta = false; } } /** * reset meta contact information. Used when meta contacts are disabled * on user's request. */ void CContactCache::resetMeta() { m_isMeta = false; m_szMetaProto = 0; m_hSubContact = 0; m_tszMetaProto[0] = 0; initPhaseTwo(); } /** * if the contact has an open message window, close it. * window procedure will use setWindowData() to reset m_hwnd to 0. */ void CContactCache::closeWindow() { if (m_hwnd) ::SendMessage(m_hwnd, WM_CLOSE, 1, 2); } void CContactCache::updateState() { updateNick(); updateStatus(); } /** * update private copy of the nick name. Use contact list name cache * * @return bool: true if nick has changed. */ bool CContactCache::updateNick() { bool fChanged = false; if (m_Valid) { TCHAR *tszNick = pcli->pfnGetContactDisplayName(m_hContact, 0); if (tszNick) fChanged = (_tcscmp(m_szNick, tszNick) ? true : false); mir_sntprintf(m_szNick, 80, _T("%s"), tszNick ? tszNick : _T("")); } return(fChanged); } /** * update status mode * @return bool: true if status mode has changed, false if not. */ bool CContactCache::updateStatus() { if (!m_Valid) return false; m_wOldStatus = m_wStatus; m_wStatus = (WORD)db_get_w(m_hContact, m_szProto, "Status", ID_STATUS_OFFLINE); return m_wOldStatus != m_wStatus; } /** * update meta (subcontact and -protocol) status. This runs when the * MC protocol fires one of its events OR when a relevant database value changes * in the master contact. */ void CContactCache::updateMeta(bool fForce) { if (m_Valid) { HANDLE hSubContact = (HANDLE)CallService(MS_MC_GETMOSTONLINECONTACT, (WPARAM)m_hContact, 0); if (hSubContact && (hSubContact != m_hSubContact || fForce)) { m_hSubContact = hSubContact; m_szMetaProto = GetContactProto(m_hSubContact); if (m_szMetaProto) { PROTOACCOUNT *acc = reinterpret_cast(::CallService(MS_PROTO_GETACCOUNT, 0, (LPARAM)m_szMetaProto)); if (acc && acc->tszAccountName) m_szAccount = acc->tszAccountName; m_wMetaStatus = db_get_w(m_hSubContact, m_szMetaProto, "Status", ID_STATUS_OFFLINE); MultiByteToWideChar(CP_ACP, 0, m_szMetaProto, -1, m_tszMetaProto, 40); m_tszMetaProto[39] = 0; } else { m_wMetaStatus = ID_STATUS_OFFLINE; m_tszMetaProto[0] = 0; } } } } /** * obtain the UIN. This is only maintained for open message windows * it also run when the subcontact for a MC changes. */ bool CContactCache::updateUIN() { m_szUIN[0] = 0; if (m_Valid) { CONTACTINFO ci = { sizeof(ci) }; ci.hContact = getActiveContact(); ci.szProto = const_cast(getActiveProto()); ci.dwFlag = CNF_DISPLAYUID | CNF_TCHAR; if (!CallService(MS_CONTACT_GETCONTACTINFO, 0, (LPARAM)&ci)) { switch (ci.type) { case CNFT_ASCIIZ: mir_sntprintf(m_szUIN, SIZEOF(m_szUIN), _T("%s"), reinterpret_cast(ci.pszVal)); mir_free((void*)ci.pszVal); break; case CNFT_DWORD: mir_sntprintf(m_szUIN, SIZEOF(m_szUIN), _T("%u"), ci.dVal); break; } } } return false; } void CContactCache::updateStats(int iType, size_t value) { if (m_stats == 0) allocStats(); switch(iType) { case TSessionStats::UPDATE_WITH_LAST_RCV: if (!m_stats->lastReceivedChars) break; m_stats->iReceived++; m_stats->messageCount++; m_stats->iReceivedBytes += m_stats->lastReceivedChars; m_stats->lastReceivedChars = 0; break; case TSessionStats::INIT_TIMER: m_stats->started = time(0); break; case TSessionStats::SET_LAST_RCV: m_stats->lastReceivedChars = (unsigned int)value; break; case TSessionStats::BYTES_SENT: m_stats->iSent++; m_stats->messageCount++; m_stats->iSentBytes += (unsigned int)value; break; } } void CContactCache::allocStats() { if (m_stats == 0) { m_stats = new TSessionStats; ::ZeroMemory(m_stats, sizeof(TSessionStats)); } } /** * set the window data for this contact. The window procedure of the message * dialog will use this in WM_INITDIALOG and WM_DESTROY to tell the cache * that a message window is open for this contact. * * @param hwnd: window handle * @param dat: _MessageWindowData - window data structure */ void CContactCache::setWindowData(const HWND hwnd, TWindowData *dat) { m_hwnd = hwnd; m_dat = dat; if (hwnd && dat && m_history == 0) allocHistory(); if (hwnd) updateStatusMsg(); else { /* release memory - not needed when window isn't open */ if (m_szStatusMsg) { mir_free(m_szStatusMsg); m_szStatusMsg = 0; } if (m_ListeningInfo) { mir_free(m_ListeningInfo); m_ListeningInfo = 0; } if (m_xStatusMsg) { mir_free(m_xStatusMsg); m_xStatusMsg = 0; } } } /** * saves message to the input history. * it's using streamout in UTF8 format - no unicode "issues" and all RTF formatting is saved to the history. */ void CContactCache::saveHistory(WPARAM wParam, LPARAM lParam) { size_t iLength = 0, iStreamLength = 0; int oldTop = 0; char* szFromStream = NULL; if (m_hwnd == 0 || m_dat == 0) return; if (wParam) { oldTop = m_iHistoryTop; m_iHistoryTop = (int)wParam; } szFromStream = ::Message_GetFromStream(GetDlgItem(m_hwnd, IDC_MESSAGE), m_dat, (CP_UTF8 << 16) | (SF_RTFNOOBJS | SFF_PLAINRTF | SF_USECODEPAGE)); iLength = iStreamLength = (strlen(szFromStream) + 1); if (iLength > 0 && m_history != NULL) { if ((m_iHistoryTop == m_iHistorySize) && oldTop == 0) { // shift the stack down... TInputHistory ihTemp = m_history[0]; m_iHistoryTop--; ::MoveMemory((void*)&m_history[0], (void*)&m_history[1], (m_iHistorySize - 1) * sizeof(TInputHistory)); m_history[m_iHistoryTop] = ihTemp; } if (iLength > m_history[m_iHistoryTop].lLen) { if (m_history[m_iHistoryTop].szText == NULL) { if (iLength < HISTORY_INITIAL_ALLOCSIZE) iLength = HISTORY_INITIAL_ALLOCSIZE; m_history[m_iHistoryTop].szText = (TCHAR*)mir_alloc(iLength); m_history[m_iHistoryTop].lLen = iLength; } else { if (iLength > m_history[m_iHistoryTop].lLen) { m_history[m_iHistoryTop].szText = (TCHAR*)mir_realloc(m_history[m_iHistoryTop].szText, iLength); m_history[m_iHistoryTop].lLen = iLength; } } } ::CopyMemory(m_history[m_iHistoryTop].szText, szFromStream, iStreamLength); if (!oldTop) { if (m_iHistoryTop < m_iHistorySize) { m_iHistoryTop++; m_iHistoryCurrent = m_iHistoryTop; } } } if (szFromStream) mir_free(szFromStream); if (oldTop) m_iHistoryTop = oldTop; } /** * handle the input history scrolling for the message input area * @param wParam: VK_ keyboard code (VK_UP or VK_DOWN) */ void CContactCache::inputHistoryEvent(WPARAM wParam) { if (m_hwnd == 0 || m_dat == 0) return; if (m_history != NULL && m_history[0].szText != NULL) { // at least one entry needs to be alloced, otherwise we get a nice infinite loop ;) HWND hwndEdit = ::GetDlgItem(m_hwnd, IDC_MESSAGE); SETTEXTEX stx = {ST_DEFAULT, CP_UTF8}; if (m_dat->dwFlags & MWF_NEEDHISTORYSAVE) { m_iHistoryCurrent = m_iHistoryTop; if (::GetWindowTextLengthA(hwndEdit) > 0) saveHistory((WPARAM)m_iHistorySize, 0); else m_history[m_iHistorySize].szText[0] = (TCHAR)'\0'; } if (wParam == VK_UP) { if (m_iHistoryCurrent == 0) return; m_iHistoryCurrent--; } else { m_iHistoryCurrent++; if (m_iHistoryCurrent > m_iHistoryTop) m_iHistoryCurrent = m_iHistoryTop; } if (m_iHistoryCurrent == m_iHistoryTop) { if (m_history[m_iHistorySize].szText != NULL) { // replace the temp buffer ::SetWindowText(hwndEdit, _T("")); ::SendMessage(hwndEdit, EM_SETTEXTEX, (WPARAM)&stx, (LPARAM)m_history[m_iHistorySize].szText); ::SendMessage(hwndEdit, EM_SETSEL, (WPARAM)- 1, (LPARAM)- 1); } } else { if (m_history[m_iHistoryCurrent].szText != NULL) { ::SetWindowText(hwndEdit, _T("")); ::SendMessage(hwndEdit, EM_SETTEXTEX, (WPARAM)&stx, (LPARAM)m_history[m_iHistoryCurrent].szText); ::SendMessage(hwndEdit, EM_SETSEL, (WPARAM)- 1, (LPARAM)- 1); } else ::SetWindowText(hwndEdit, _T("")); } ::SendMessage(m_hwnd, WM_COMMAND, MAKEWPARAM(::GetDlgCtrlID(hwndEdit), EN_CHANGE), (LPARAM)hwndEdit); m_dat->dwFlags &= ~MWF_NEEDHISTORYSAVE; } } /** * allocate the input history (on-demand, when it is requested by * opening a message window for this contact). * * note: it allocs historysize + 1 elements, because the + 1 is used * for the temporary buffer which saves the current input line when * using input history scrolling. */ void CContactCache::allocHistory() { m_iHistorySize = M.GetByte("historysize", 15); if (m_iHistorySize < 10) m_iHistorySize = 10; m_history = (TInputHistory *)mir_alloc(sizeof(TInputHistory) * (m_iHistorySize + 1)); m_iHistoryCurrent = 0; m_iHistoryTop = 0; if (m_history) ZeroMemory(m_history, sizeof(TInputHistory) * m_iHistorySize); m_history[m_iHistorySize].szText = (TCHAR*)mir_alloc((HISTORY_INITIAL_ALLOCSIZE + 1) * sizeof(TCHAR)); m_history[m_iHistorySize].lLen = HISTORY_INITIAL_ALLOCSIZE; } /** * release additional memory resources */ void CContactCache::releaseAlloced() { int i; if (m_stats) { delete m_stats; m_stats = 0; } if (m_history) { for (i=0; i <= m_iHistorySize; i++) { if (m_history[i].szText != 0) { mir_free(m_history[i].szText); } } mir_free(m_history); m_history = 0; } if ( lstrcmp( m_tszProto, C_INVALID_PROTO_T )) mir_free(m_tszProto); m_tszProto = NULL; mir_free(m_szStatusMsg); m_szStatusMsg = NULL; } /** * when a contact is deleted, mark it as invalid in the cache and release * all memory it has allocated. */ void CContactCache::deletedHandler() { m_Valid = false; if (m_hwnd) ::SendMessage(m_hwnd, WM_CLOSE, 1, 2); releaseAlloced(); m_hContact = (HANDLE)-1; } /** * udpate favorite or recent state. runs when user manually adds * or removes a user from that list or when database setting is * changed from elsewhere */ void CContactCache::updateFavorite() { m_isFavorite = db_get_b(m_hContact, SRMSGMOD_T, "isFavorite", 0) != 0; m_isRecent = M.GetDword(m_hContact, "isRecent", 0) ? true : false; } /** * update all or only the given status message information from the database * * @param szKey: char* database key name or 0 to reload all messages */ void CContactCache::updateStatusMsg(const char *szKey) { DBVARIANT dbv = {0}; BYTE bStatusMsgValid = 0; INT_PTR res = 0; if (!m_Valid) return; if (szKey == 0 || (szKey && !strcmp("StatusMsg", szKey))) { if (m_szStatusMsg) mir_free(m_szStatusMsg); m_szStatusMsg = 0; res = db_get_ts(m_hContact, "CList", "StatusMsg", &dbv); if (res == 0) { m_szStatusMsg = (lstrlen(dbv.ptszVal) > 0 ? getNormalizedStatusMsg(dbv.ptszVal) : 0); db_free(&dbv); } } if (szKey == 0 || (szKey && !strcmp("ListeningTo", szKey))) { if (m_ListeningInfo) mir_free(m_ListeningInfo); m_ListeningInfo = 0; res = db_get_ts(m_hContact, m_szProto, "ListeningTo", &dbv); if (res == 0) { m_ListeningInfo = (lstrlen(dbv.ptszVal) > 0 ? mir_tstrdup(dbv.ptszVal) : 0); db_free(&dbv); } } if (szKey == 0 || (szKey && !strcmp("XStatusMsg", szKey))) { if (m_xStatusMsg) mir_free(m_xStatusMsg); m_xStatusMsg = 0; res = db_get_ts(m_hContact, m_szProto, "XStatusMsg", &dbv); if (res == 0) { m_xStatusMsg = (lstrlen(dbv.ptszVal) > 0 ? mir_tstrdup(dbv.ptszVal) : 0); db_free(&dbv); } } m_xStatus = db_get_b(m_hContact, m_szProto, "XStatusId", 0); } /** * retrieve contact cache entry for the given contact. It _never_ returns zero, for a hContact * 0, it retrieves a dummy object. * Non-existing cache entries are created on demand. * * @param hContact: contact handle * @return CContactCache* pointer to the cache entry for this contact */ CContactCache* CContactCache::getContactCache(HANDLE hContact) { CContactCache *cc = arContacts.find((CContactCache*)&hContact); if (cc == NULL) { cc = new CContactCache(hContact); arContacts.insert(cc); } return cc; } /** * when the state of the meta contacts protocol changes from enabled to disabled * (or vice versa), this updates the contact cache * * it is ONLY called from the DBSettingChanged() event handler when the relevant * database value is touched. */ void CContactCache::cacheUpdateMetaChanged() { bool fMetaActive = (PluginConfig.g_MetaContactsAvail && PluginConfig.bMetaEnabled) ? true : false; for (int i=0; i < arContacts.getCount(); i++) { CContactCache &c = arContacts[i]; if (c.isMeta() && PluginConfig.bMetaEnabled == false) { c.closeWindow(); c.resetMeta(); } // meta contacts are enabled, but current contact is a subcontact - > close window if (fMetaActive && c.isSubContact()) c.closeWindow(); // reset meta contact information, if metacontacts protocol became avail if (fMetaActive && !strcmp(c.getProto(), PluginConfig.szMetaName)) c.resetMeta(); } } /** * normalize the status message with proper cr/lf sequences. * @param src TCHAR*: original status message * @param fStripAll bool: strip all cr/lf sequences and replace them with spaces (use for title bar) * @return TCHAR*: converted status message. CALLER is responsible to mir_free it, MUST use mir_free() */ TCHAR* CContactCache::getNormalizedStatusMsg(const TCHAR *src, bool fStripAll) { size_t k = 0, i = 0; TCHAR* tszResult = 0; if (src == 0 || lstrlen(src) < 2) return 0; tstring dest; for (i=0; i < _tcslen(src); i++) { if (src[i] == 0x0d || src[i] == '\t') continue; if (i && src[i] == (TCHAR)0x0a) { if (fStripAll) { dest.append(_T(" ")); continue; } dest.append(_T("\n")); continue; } dest += src[i]; } if (i) { tszResult = (TCHAR*)mir_alloc((dest.length() + 1) * sizeof(TCHAR)); _tcscpy(tszResult, dest.c_str()); tszResult[dest.length()] = 0; } return(tszResult); } /** * retrieve the tab/title icon for the corresponding session. */ HICON CContactCache::getIcon(int& iSize) const { HICON hIcon; if (m_dat && m_hwnd) { if (m_dat->dwFlags & MWF_ERRORSTATE) hIcon = PluginConfig.g_iconErr; else if (m_dat->mayFlashTab) hIcon = m_dat->iFlashIcon; else { if (m_dat->si && m_dat->iFlashIcon) { int sizeX, sizeY; hIcon = m_dat->iFlashIcon; Utils::getIconSize(hIcon, sizeX, sizeY); iSize = sizeX; } else if (m_dat->hTabIcon == m_dat->hTabStatusIcon && m_dat->hXStatusIcon) hIcon = m_dat->hXStatusIcon; else hIcon = m_dat->hTabIcon; } } else hIcon = LoadSkinnedProtoIcon(m_szProto, m_wStatus); return(hIcon); } int CContactCache::getMaxMessageLength() { HANDLE hContact; const char* szProto; hContact = getActiveContact(); szProto = getActiveProto(); if (szProto) { m_nMax = CallProtoService(szProto, PS_GETCAPS, PFLAG_MAXLENOFMESSAGE, reinterpret_cast(hContact)); if (m_nMax) { if (M.GetByte("autosplit", 0)) { if (m_hwnd) ::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, 20000); } else { if (m_hwnd) ::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, (LPARAM)m_nMax); } } else { if (m_hwnd) ::SendDlgItemMessage(m_hwnd, IDC_MESSAGE, EM_EXLIMITTEXT, 0, 20000); m_nMax = 20000; } } return(m_nMax); }