/* Copyright © 2009 Jim Porter 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. 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, see . */ #include "common.h" #include "proto.h" void CALLBACK TwitterProto::APC_callback(ULONG_PTR p) { reinterpret_cast(p)->LOG("***** Executing APC"); } template inline static T db_pod_get(HANDLE hContact, const char *module, const char *setting, T errorValue) { DBVARIANT dbv; DBCONTACTGETSETTING cgs; cgs.szModule = module; cgs.szSetting = setting; cgs.pValue = &dbv; if (CallService(MS_DB_CONTACT_GETSETTING, (WPARAM)hContact, (LPARAM)&cgs)) return errorValue; // TODO: remove this, it's just a temporary workaround if (dbv.type == DBVT_DWORD) return dbv.dVal; if (dbv.cpbVal != sizeof(T)) return errorValue; return *reinterpret_cast(dbv.pbVal); } template inline static INT_PTR db_pod_set(HANDLE hContact, const char *module, const char *setting, T val) { DBCONTACTWRITESETTING cws; cws.szModule = module; cws.szSetting = setting; cws.value.type = DBVT_BLOB; cws.value.cpbVal = sizeof(T); cws.value.pbVal = reinterpret_cast(&val); return CallService(MS_DB_CONTACT_WRITESETTING, (WPARAM)hContact, (LPARAM)&cws); } void TwitterProto::SignOn(void*) { LOG("***** Beginning SignOn process"); WaitForSingleObject(&signon_lock_, INFINITE); // Kill the old thread if it's still around if (hMsgLoop_) { LOG("***** Requesting MessageLoop to exit"); QueueUserAPC(APC_callback, hMsgLoop_, (ULONG_PTR)this); LOG("***** Waiting for old MessageLoop to exit"); WaitForSingleObject(hMsgLoop_, INFINITE); CloseHandle(hMsgLoop_); } if (NegotiateConnection()) // Could this be? The legendary Go Time?? { if (!in_chat_ && db_byte_get(0, m_szModuleName, TWITTER_KEY_CHATFEED, 0)) OnJoinChat(0, true); SetAllContactStatuses(ID_STATUS_ONLINE); hMsgLoop_ = ForkThreadEx(&TwitterProto::MessageLoop, this); } ReleaseMutex(signon_lock_); LOG("***** SignOn complete"); } bool TwitterProto::NegotiateConnection() { LOG("***** Negotiating connection with Twitter"); int old_status = m_iStatus; std::string user, pass; DBVARIANT dbv; if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_UN, &dbv)) { user = dbv.pszVal; DBFreeVariant(&dbv); } else { ShowPopup(TranslateT("Please enter a username.")); return false; } if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_PASS, &dbv)) { CallService(MS_DB_CRYPT_DECODESTRING, strlen(dbv.pszVal)+1, reinterpret_cast(dbv.pszVal)); pass = dbv.pszVal; DBFreeVariant(&dbv); } else { ShowPopup(TranslateT("Please enter a password.")); return false; } if ( !DBGetContactSettingString(0, m_szModuleName, TWITTER_KEY_BASEURL, &dbv)) { ScopedLock s(twitter_lock_); twit_.set_base_url(dbv.pszVal); DBFreeVariant(&dbv); } bool success; { ScopedLock s(twitter_lock_); success = twit_.set_credentials(user, pass); } if (!success) { ShowPopup(TranslateT("Your username/password combination was incorrect.")); ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)old_status, m_iStatus); // Set to offline old_status = m_iStatus; m_iDesiredStatus = m_iStatus = ID_STATUS_OFFLINE; ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); return false; } m_iStatus = m_iDesiredStatus; ProtoBroadcastAck(m_szModuleName, 0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); return true; } void TwitterProto::MessageLoop(void*) { LOG("***** Entering Twitter::MessageLoop"); since_id_ = db_pod_get(0, m_szModuleName, TWITTER_KEY_SINCEID, 0); dm_since_id_ = db_pod_get(0, m_szModuleName, TWITTER_KEY_DMSINCEID, 0); bool new_account = db_byte_get(0, m_szModuleName, TWITTER_KEY_NEW, 1) != 0; bool popups = db_byte_get(0, m_szModuleName, TWITTER_KEY_POPUP_SIGNON, 1) != 0; int poll_rate = db_dword_get(0, m_szModuleName, TWITTER_KEY_POLLRATE, 80); for(unsigned int i=0;;i++) { if (m_iStatus != ID_STATUS_ONLINE) goto exit; if (i%4 == 0) UpdateFriends(); if (m_iStatus != ID_STATUS_ONLINE) goto exit; UpdateStatuses(new_account, popups); if (m_iStatus != ID_STATUS_ONLINE) goto exit; UpdateMessages(new_account); if (new_account) // Not anymore! { new_account = false; DBWriteContactSettingByte(0, m_szModuleName, TWITTER_KEY_NEW, 0); } if (m_iStatus != ID_STATUS_ONLINE) goto exit; LOG("***** TwitterProto::MessageLoop going to sleep..."); if (SleepEx(poll_rate*1000, true) == WAIT_IO_COMPLETION) goto exit; LOG("***** TwitterProto::MessageLoop waking up..."); popups = true; } exit: { ScopedLock s(twitter_lock_); twit_.set_credentials("", "", false); } LOG("***** Exiting TwitterProto::MessageLoop"); } struct update_avatar { update_avatar(HANDLE hContact, const std::string &url) : hContact(hContact), url(url) {} HANDLE hContact; std::string url; }; void TwitterProto::UpdateAvatarWorker(void *p) { if (p == 0) return; std::auto_ptr data( static_cast(p)); DBVARIANT dbv; if (DBGetContactSettingTString(data->hContact, m_szModuleName, TWITTER_KEY_UN, &dbv)) return; std::string ext = data->url.substr(data->url.rfind('.')); std::string filename = GetAvatarFolder() + '\\' + dbv.pszVal + ext; DBFreeVariant(&dbv); PROTO_AVATAR_INFORMATION ai = {sizeof(ai)}; ai.hContact = data->hContact; ai.format = ext_to_format(ext); strncpy(ai.filename, filename.c_str(), MAX_PATH); LOG("***** Updating avatar: %s", data->url.c_str()); WaitForSingleObjectEx(avatar_lock_, INFINITE, true); if (CallService(MS_SYSTEM_TERMINATED, 0, 0)) { LOG("***** Terminating avatar update early: %s", data->url.c_str()); return; } if (save_url(hAvatarNetlib_, data->url, filename)) { DBWriteContactSettingString(data->hContact, m_szModuleName, TWITTER_KEY_AV_URL, data->url.c_str()); ProtoBroadcastAck(m_szModuleName, data->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai, 0); } else ProtoBroadcastAck(m_szModuleName, data->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, &ai, 0); ReleaseMutex(avatar_lock_); LOG("***** Done avatar: %s", data->url.c_str()); } void TwitterProto::UpdateAvatar(HANDLE hContact, const std::string &url, bool force) { DBVARIANT dbv; if ( !force && ( !DBGetContactSettingString(hContact, m_szModuleName, TWITTER_KEY_AV_URL, &dbv) && url == dbv.pszVal)) { LOG("***** Avatar already up-to-date: %s", url.c_str()); } else { // TODO: more defaults (configurable?) if (url == "http://static.twitter.com/images/default_profile_normal.png") { PROTO_AVATAR_INFORMATION ai = {sizeof(ai), hContact}; db_string_set(hContact, m_szModuleName, TWITTER_KEY_AV_URL, url.c_str()); ProtoBroadcastAck(m_szModuleName, hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai, 0); } else { ForkThread(&TwitterProto::UpdateAvatarWorker, this, new update_avatar(hContact, url)); } } DBFreeVariant(&dbv); } void TwitterProto::UpdateFriends() { try { ScopedLock s(twitter_lock_); std::vector friends = twit_.get_friends(); s.Unlock(); for(std::vector::iterator i=friends.begin(); i!=friends.end(); ++i) { if (i->username == twit_.get_username()) continue; HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), i->status.text.c_str()); UpdateAvatar(hContact, i->profile_image_url); } LOG("***** Friends list updated"); } catch(const bad_response &) { LOG("***** Bad response from server, signing off"); SetStatus(ID_STATUS_OFFLINE); } catch(const std::exception &e) { ShowPopup( (std::string("While updating friends list, an error occurred: ") +e.what()).c_str()); LOG("***** Error updating friends list: %s", e.what()); } } void TwitterProto::ShowContactPopup(HANDLE hContact, const std::tstring &text) { if (!ServiceExists(MS_POPUP_ADDPOPUPT) || DBGetContactSettingByte(0, m_szModuleName, TWITTER_KEY_POPUP_SHOW, 0) == 0) return; POPUPDATAT popup = {}; popup.lchContact = hContact; popup.iSeconds = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_TIMEOUT, 0); popup.colorBack = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_COLBACK, 0); if (popup.colorBack == 0xFFFFFFFF) popup.colorBack = GetSysColor(COLOR_WINDOW); popup.colorText = db_dword_get(0, m_szModuleName, TWITTER_KEY_POPUP_COLTEXT, 0); if (popup.colorBack == 0xFFFFFFFF) popup.colorBack = GetSysColor(COLOR_WINDOWTEXT); DBVARIANT dbv; if ( !DBGetContactSettingString(hContact, "CList", "MyHandle", &dbv) || !DBGetContactSettingString(hContact, m_szModuleName, TWITTER_KEY_UN, &dbv)) { mbcs_to_tcs(CP_UTF8, dbv.pszVal, popup.lptzContactName, MAX_CONTACTNAME); DBFreeVariant(&dbv); } CallService(MS_POPUP_ADDPOPUPT, reinterpret_cast(text.c_str()), 0); } void TwitterProto::UpdateStatuses(bool pre_read, bool popups) { try { ScopedLock s(twitter_lock_); twitter::status_list updates = twit_.get_statuses(200, since_id_); s.Unlock(); if (!updates.empty()) since_id_ = std::max(since_id_, updates[0].status.id); for(twitter::status_list::reverse_iterator i=updates.rbegin(); i!=updates.rend(); ++i) { if (!pre_read && in_chat_) UpdateChat(*i); if (i->username == twit_.get_username()) continue; HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), _T("")); DBEVENTINFO dbei = {sizeof(dbei)}; dbei.pBlob = (BYTE*)(i->status.text.c_str()); dbei.cbBlob = i->status.text.size()+1; dbei.eventType = TWITTER_DB_EVENT_TYPE_TWEET; dbei.flags = DBEF_UTF; //dbei.flags = DBEF_READ; dbei.timestamp = static_cast(i->status.time); dbei.szModule = m_szModuleName; CallService(MS_DB_EVENT_ADD, (WPARAM)hContact, (LPARAM)&dbei); DBWriteContactSettingTString(hContact, "CList", "StatusMsg", i->status.text.c_str()); if ( !pre_read && popups ) ShowContactPopup(hContact, i->status.text); } db_pod_set(0, m_szModuleName, TWITTER_KEY_SINCEID, since_id_); LOG("***** Status messages updated"); } catch(const bad_response &) { LOG("***** Bad response from server, signing off"); SetStatus(ID_STATUS_OFFLINE); } catch(const std::exception &e) { ShowPopup( (std::string("While updating status messages, an error occurred: ") +e.what()).c_str()); LOG("***** Error updating status messages: %s", e.what()); } } void TwitterProto::UpdateMessages(bool pre_read) { try { ScopedLock s(twitter_lock_); twitter::status_list messages = twit_.get_direct(dm_since_id_); s.Unlock(); if (messages.size()) dm_since_id_ = std::max(dm_since_id_, messages[0].status.id); for(twitter::status_list::reverse_iterator i=messages.rbegin(); i!=messages.rend(); ++i) { HANDLE hContact = AddToClientList( _A2T(i->username.c_str()), _T("")); PROTORECVEVENT recv = {}; CCSDATA ccs = {}; recv.flags = PREF_TCHAR; if (pre_read) recv.flags |= PREF_CREATEREAD; recv.szMessage = ( char* )i->status.text.c_str(); recv.timestamp = static_cast(i->status.time); ccs.hContact = hContact; ccs.szProtoService = PSR_MESSAGE; ccs.wParam = ID_STATUS_ONLINE; ccs.lParam = reinterpret_cast(&recv); CallService(MS_PROTO_CHAINRECV, 0, reinterpret_cast(&ccs)); } db_pod_set(0, m_szModuleName, TWITTER_KEY_DMSINCEID, dm_since_id_); LOG("***** Direct messages updated"); } catch(const bad_response &) { LOG("***** Bad response from server, signing off"); SetStatus(ID_STATUS_OFFLINE); } catch(const std::exception &e) { ShowPopup( (std::string("While updating direct messages, an error occurred: ") +e.what()).c_str()); LOG("***** Error updating direct messages: %s", e.what()); } }