/* Facebook plugin for Miranda Instant Messenger _____________________________________________ Copyright © 2009-11 Michal Zelinka, 2011-13 Robert Pösel 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" FacebookProto::FacebookProto(const char* proto_name,const TCHAR* username) { ProtoConstructor(this, proto_name, username); m_szProtoName = mir_strdup(proto_name); facy.parent = this; signon_lock_ = CreateMutex( NULL, FALSE, NULL ); avatar_lock_ = CreateMutex( NULL, FALSE, NULL ); log_lock_ = CreateMutex( NULL, FALSE, NULL ); update_loop_lock_ = CreateEvent( NULL, FALSE, FALSE, NULL); facy.buddies_lock_ = CreateMutex( NULL, FALSE, NULL ); facy.send_message_lock_ = CreateMutex( NULL, FALSE, NULL ); facy.fcb_conn_lock_ = CreateMutex( NULL, FALSE, NULL ); CreateProtoService(m_szModuleName, PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI, this); CreateProtoService(m_szModuleName, PS_GETMYAWAYMSG, &FacebookProto::GetMyAwayMsg, this); CreateProtoService(m_szModuleName, PS_GETMYAVATART, &FacebookProto::GetMyAvatar, this); CreateProtoService(m_szModuleName, PS_GETAVATARINFOT, &FacebookProto::GetAvatarInfo, this); CreateProtoService(m_szModuleName, PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps, this); CreateProtoService(m_szModuleName, PS_JOINCHAT, &FacebookProto::OnJoinChat, this); CreateProtoService(m_szModuleName, PS_LEAVECHAT, &FacebookProto::OnLeaveChat, this); HookProtoEvent(ME_CLIST_PREBUILDSTATUSMENU, &FacebookProto::OnBuildStatusMenu, this); HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit, this); HookProtoEvent(ME_GC_EVENT, &FacebookProto::OnChatOutgoing, this); HookProtoEvent(ME_IDLE_CHANGED, &FacebookProto::OnIdleChanged, this); // Create standard network connection TCHAR descr[512]; NETLIBUSER nlu = {sizeof(nlu)}; nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_TCHAR; nlu.szSettingsModule = m_szModuleName; char module[512]; mir_snprintf(module, SIZEOF(module), "%s", m_szModuleName); nlu.szSettingsModule = module; mir_sntprintf(descr, SIZEOF(descr), TranslateT("%s server connection"), m_tszUserName); nlu.ptszDescriptiveName = descr; m_hNetlibUser = (HANDLE)CallService(MS_NETLIB_REGISTERUSER, 0, (LPARAM)&nlu); if (m_hNetlibUser == NULL) MessageBox(NULL, TranslateT("Unable to get Netlib connection for Facebook"), m_tszUserName, MB_OK); facy.set_handle(m_hNetlibUser); SkinAddNewSoundExT( "Notification", m_tszUserName, LPGENT( "Notification" )); SkinAddNewSoundExT( "NewsFeed", m_tszUserName, LPGENT( "News Feed" )); SkinAddNewSoundExT( "OtherEvent", m_tszUserName, LPGENT( "Other Event" )); TCHAR *profile = Utils_ReplaceVarsT( _T("%miranda_avatarcache%")); def_avatar_folder_ = std::tstring(profile) + _T("\\") + m_tszUserName; mir_free(profile); hAvatarFolder_ = FoldersRegisterCustomPathT(m_szModuleName, "Avatars", def_avatar_folder_.c_str()); // Set all contacts offline -- in case we crashed SetAllContactStatuses( ID_STATUS_OFFLINE, true ); } FacebookProto::~FacebookProto( ) { Netlib_CloseHandle( m_hNetlibUser ); WaitForSingleObject( signon_lock_, IGNORE ); WaitForSingleObject( avatar_lock_, IGNORE ); WaitForSingleObject( log_lock_, IGNORE ); WaitForSingleObject( facy.buddies_lock_, IGNORE ); WaitForSingleObject( facy.send_message_lock_, IGNORE ); CloseHandle( signon_lock_ ); CloseHandle( avatar_lock_ ); CloseHandle( log_lock_ ); CloseHandle( update_loop_lock_ ); CloseHandle( facy.buddies_lock_ ); CloseHandle( facy.send_message_lock_ ); CloseHandle( facy.fcb_conn_lock_ ); mir_free( m_szProtoName ); ProtoDestructor(this); } ////////////////////////////////////////////////////////////////////////////// DWORD_PTR FacebookProto::GetCaps( int type, HANDLE hContact ) { switch(type) { case PFLAGNUM_1: { DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ | /*PF1_ADDED |*/ PF1_BASICSEARCH | PF1_USERIDISEMAIL | PF1_SEARCHBYEMAIL | PF1_SEARCHBYNAME | PF1_ADDSEARCHRES; // | PF1_VISLIST | PF1_INVISLIST; if ( getByte( FACEBOOK_KEY_SET_MIRANDA_STATUS, 0 )) return flags |= PF1_MODEMSG; else return flags |= PF1_MODEMSGRECV; } case PFLAGNUM_2: return PF2_ONLINE | PF2_INVISIBLE | PF2_ONTHEPHONE | PF2_IDLE; // | PF2_SHORTAWAY; case PFLAGNUM_3: if ( getByte( FACEBOOK_KEY_SET_MIRANDA_STATUS, 0 )) return PF2_ONLINE; // | PF2_SHORTAWAY; else return 0; case PFLAGNUM_4: return PF4_NOCUSTOMAUTH | PF4_FORCEADDED | PF4_IMSENDUTF | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE; case PFLAGNUM_5: return PF2_ONTHEPHONE; case PFLAG_MAXLENOFMESSAGE: return FACEBOOK_MESSAGE_LIMIT; case PFLAG_UNIQUEIDTEXT: return (DWORD_PTR) "Facebook ID"; case PFLAG_UNIQUEIDSETTING: return (DWORD_PTR) FACEBOOK_KEY_ID; } return 0; } ////////////////////////////////////////////////////////////////////////////// int FacebookProto::SetStatus( int new_status ) { LOG("===== Beginning SetStatus process"); // Routing statuses not supported by Facebook switch ( new_status ) { case ID_STATUS_INVISIBLE: case ID_STATUS_OFFLINE: m_iDesiredStatus = new_status; break; // TODO RM: needed/useful? case ID_STATUS_CONNECTING: m_iDesiredStatus = ID_STATUS_OFFLINE; break; case ID_STATUS_IDLE: default: m_iDesiredStatus = ID_STATUS_INVISIBLE; if (DBGetContactSettingByte(NULL,m_szModuleName,FACEBOOK_KEY_MAP_STATUSES, DEFAULT_MAP_STATUSES)) break; case ID_STATUS_ONLINE: case ID_STATUS_FREECHAT: m_iDesiredStatus = ID_STATUS_ONLINE; break; } if ( m_iStatus == ID_STATUS_CONNECTING ) { LOG("===== Status is connecting, no change"); return 0; } if ( m_iStatus == m_iDesiredStatus) { LOG("===== Statuses are same, no change"); return 0; } facy.invisible_ = ( new_status == ID_STATUS_INVISIBLE ); ForkThread( &FacebookProto::ChangeStatus, this ); return 0; } int FacebookProto::SetAwayMsg( int status, const PROTOCHAR *msg ) { if (!msg) { last_status_msg_.clear(); return 0; } char *narrow = mir_utf8encodeT(msg); if (last_status_msg_ != narrow) last_status_msg_ = narrow; utils::mem::detract(narrow); if (isOnline() && getByte(FACEBOOK_KEY_SET_MIRANDA_STATUS, DEFAULT_SET_MIRANDA_STATUS)) { ForkThread(&FacebookProto::SetAwayMsgWorker, this, NULL); } return 0; } void FacebookProto::SetAwayMsgWorker(void *) { if ( !last_status_msg_.empty()) facy.set_status( last_status_msg_ ); } HANDLE FacebookProto::SearchBasic( const PROTOCHAR* id ) { if (isOffline()) return 0; TCHAR* email = mir_tstrdup(id); ForkThread(&FacebookProto::SearchAckThread, this, (void*)email); return email; } HANDLE FacebookProto::SearchByEmail( const PROTOCHAR* email ) { return SearchBasic(email); } HANDLE FacebookProto::SearchByName( const PROTOCHAR* nick, const PROTOCHAR* firstName, const PROTOCHAR* lastName ) { TCHAR arg[200]; _sntprintf (arg, SIZEOF(arg), _T("%s %s %s"), nick, firstName, lastName); return SearchBasic(arg); } HANDLE FacebookProto::AddToList(int flags, PROTOSEARCHRESULT* psr) { char *id = mir_t2a_cp(psr->id, CP_UTF8); char *name = mir_t2a_cp(psr->firstName, CP_UTF8); char *surname = mir_t2a_cp(psr->lastName, CP_UTF8); facebook_user fbu; fbu.user_id = id; fbu.real_name = name; fbu.real_name += " "; fbu.real_name += surname; HANDLE hContact = AddToContactList(&fbu, FACEBOOK_CONTACT_NONE, false, fbu.real_name.c_str()); if (hContact) { if (flags & PALF_TEMPORARY) { DBWriteContactSettingByte(hContact, "Clist", "Hidden", 1); DBWriteContactSettingByte(hContact, "Clist", "NotOnList", 1); } else if (DBGetContactSettingByte(hContact, "CList", "NotOnList", 0)) { DBDeleteContactSetting(hContact, "CList", "Hidden"); DBDeleteContactSetting(hContact, "CList", "NotOnList"); } } mir_free(id); mir_free(name); mir_free(surname); return hContact; } int FacebookProto::AuthRequest(HANDLE hContact,const PROTOCHAR *message) { return RequestFriendship((WPARAM)hContact, NULL); } int FacebookProto::Authorize(HANDLE hDbEvent) { if (!isOffline() && hDbEvent) { HANDLE hContact = HContactFromAuthEvent( hDbEvent ); if (hContact == INVALID_HANDLE_VALUE) return 1; return ApproveFriendship((WPARAM)hContact, NULL); } return 1; } int FacebookProto::AuthDeny(HANDLE hDbEvent, const PROTOCHAR *reason) { if (!isOffline() && hDbEvent) { HANDLE hContact = HContactFromAuthEvent(hDbEvent); if (hContact == INVALID_HANDLE_VALUE) return 1; // TODO: hide from facebook requests list if (DBGetContactSettingByte(hContact, "CList", "NotOnList", 0)) CallService(MS_DB_CONTACT_DELETE, (WPARAM)hContact, 0); return 0; } return 1; } ////////////////////////////////////////////////////////////////////////////// // SERVICES int FacebookProto::GetMyAwayMsg( WPARAM wParam, LPARAM lParam ) { DBVARIANT dbv = { DBVT_TCHAR }; if ( !getTString( "StatusMsg", &dbv ) && lstrlen( dbv.ptszVal ) != 0 ) { int res = (lParam & SGMA_UNICODE) ? (INT_PTR)mir_t2u(dbv.ptszVal) : (INT_PTR)mir_t2a(dbv.ptszVal); DBFreeVariant( &dbv ); return res; } else { return 0; } } int FacebookProto::OnIdleChanged( WPARAM wParam, LPARAM lParam ) { if (m_iStatus == ID_STATUS_INVISIBLE || m_iStatus <= ID_STATUS_OFFLINE) return 0; bool bIdle = (lParam & IDF_ISIDLE) != 0; bool bPrivacy = (lParam & IDF_PRIVACY) != 0; if (facy.is_idle_ && !bIdle) { facy.is_idle_ = false; SetStatus(m_iDesiredStatus); } else if (!facy.is_idle_ && bIdle && !bPrivacy && m_iDesiredStatus != ID_STATUS_INVISIBLE) { facy.is_idle_ = true; SetStatus(ID_STATUS_IDLE); } return 0; } ////////////////////////////////////////////////////////////////////////////// int FacebookProto::OnEvent(PROTOEVENTTYPE event,WPARAM wParam,LPARAM lParam) { switch(event) { case EV_PROTO_ONLOAD: return OnModulesLoaded(wParam,lParam); case EV_PROTO_ONEXIT: return OnPreShutdown(wParam,lParam); case EV_PROTO_ONOPTIONS: return OnOptionsInit(wParam,lParam); case EV_PROTO_ONCONTACTDELETED: return OnContactDeleted(wParam,lParam); } return 1; } ////////////////////////////////////////////////////////////////////////////// // EVENTS int FacebookProto::SvcCreateAccMgrUI(WPARAM wParam,LPARAM lParam) { return (int)CreateDialogParam(g_hInstance,MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), (HWND)lParam, FBAccountProc, (LPARAM)this ); } int FacebookProto::OnModulesLoaded(WPARAM wParam,LPARAM lParam) { // Register group chat GCREGISTER gcr = {sizeof(gcr)}; gcr.dwFlags = 0; //GC_ACKMSG; gcr.pszModule = m_szModuleName; gcr.pszModuleDispName = m_szModuleName; gcr.iMaxText = FACEBOOK_MESSAGE_LIMIT; gcr.nColors = 0; gcr.pColors = NULL; CallService(MS_GC_REGISTER,0,reinterpret_cast(&gcr)); return 0; } int FacebookProto::OnPreShutdown(WPARAM wParam,LPARAM lParam) { SetStatus( ID_STATUS_OFFLINE ); return 0; } int FacebookProto::OnOptionsInit(WPARAM wParam,LPARAM lParam) { OPTIONSDIALOGPAGE odp = {sizeof(odp)}; odp.hInstance = g_hInstance; odp.ptszTitle = m_tszUserName; odp.dwInitParam = LPARAM(this); odp.flags = ODPF_BOLDGROUPS | ODPF_TCHAR | ODPF_DONTTRANSLATE; odp.position = 271828; odp.ptszGroup = LPGENT("Network"); odp.ptszTab = LPGENT("Account"); odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS); odp.pfnDlgProc = FBOptionsProc; Options_AddPage(wParam, &odp); odp.position = 271829; odp.ptszTab = LPGENT("Advanced"); odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS_ADVANCED); odp.pfnDlgProc = FBOptionsAdvancedProc; Options_AddPage(wParam, &odp); odp.position = 271830; if (ServiceExists(MS_POPUP_ADDPOPUPT)) odp.ptszGroup = LPGENT("PopUps"); odp.ptszTab = LPGENT("Events"); odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS_EVENTS); odp.pfnDlgProc = FBEventsProc; Options_AddPage(wParam, &odp); return 0; } int FacebookProto::OnMind(WPARAM,LPARAM) { HWND hDlg = CreateDialogParam( g_hInstance, MAKEINTRESOURCE( IDD_MIND ), ( HWND )0, FBMindProc, reinterpret_cast( this )); ShowWindow( hDlg, SW_SHOW ); return FALSE; } int FacebookProto::CheckNewsfeeds(WPARAM, LPARAM) { if (!isOffline()) { facy.client_notify( TranslateT("Loading newsfeeds...")); facy.feeds(); } return 0; } int FacebookProto::CheckFriendRequests(WPARAM, LPARAM) { if (!isOffline()) { facy.client_notify( TranslateT("Checking friend requests...")); ProcessFriendRequests(NULL); } return 0; } int FacebookProto::RefreshBuddyList(WPARAM, LPARAM) { if (!isOffline()) { facy.client_notify( TranslateT("Refreshing buddy list...")); facy.buddy_list(); } return 0; } int FacebookProto::VisitProfile(WPARAM wParam,LPARAM lParam) { HANDLE hContact = reinterpret_cast(wParam); std::string url = FACEBOOK_URL_PROFILE; DBVARIANT dbv; // TODO: why isn't wParam == 0 when is status menu moved to main menu? if (wParam != 0 && IsMyContact(hContact)) { if (!DBGetContactSettingString(hContact, m_szModuleName, "Homepage", &dbv)) { // Homepage link already present, get it url = dbv.pszVal; DBFreeVariant(&dbv); } else if (!DBGetContactSettingString(hContact, m_szModuleName, FACEBOOK_KEY_ID, &dbv)) { // No homepage link, create and save it url += dbv.pszVal; DBWriteContactSettingString(hContact, m_szModuleName, "Homepage", url.c_str()); DBFreeVariant(&dbv); } } CallService(MS_UTILS_OPENURL, 1, reinterpret_cast(url.c_str())); return 0; } int FacebookProto::CancelFriendship(WPARAM wParam,LPARAM lParam) { if (wParam == NULL || isOffline()) return 0; bool deleting = (lParam == 1); HANDLE hContact = reinterpret_cast(wParam); // Ignore groupchats and, if deleting, also not-friends if (DBGetContactSettingByte(hContact, m_szModuleName, "ChatRoom", 0) || (deleting && DBGetContactSettingByte(hContact, m_szModuleName, FACEBOOK_KEY_CONTACT_TYPE, 0) != FACEBOOK_CONTACT_FRIEND)) return 0; DBVARIANT dbv; TCHAR tstr[256]; if ( !DBGetContactSettingTString(hContact, m_szModuleName, FACEBOOK_KEY_NAME, &dbv)) { mir_sntprintf(tstr,SIZEOF(tstr),TranslateT("Do you want to cancel your friendship with '%s'?"), dbv.ptszVal); DBFreeVariant(&dbv); } else if ( !DBGetContactSettingTString(hContact,m_szModuleName,FACEBOOK_KEY_ID,&dbv)) { mir_sntprintf(tstr,SIZEOF(tstr),TranslateT("Do you want to cancel your friendship with '%s'?"), dbv.ptszVal); DBFreeVariant(&dbv); } if (MessageBox( 0, tstr, m_tszUserName, MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2 ) == IDYES) { if ( !DBGetContactSettingString(hContact,m_szModuleName,FACEBOOK_KEY_ID,&dbv)) { std::string* id = new std::string(dbv.pszVal); if (deleting) { facebook_user* fbu = facy.buddies.find( (*id)); if (fbu != NULL) { fbu->handle = NULL; } } ForkThread( &FacebookProto::DeleteContactFromServer, this, ( void* )id ); DBFreeVariant(&dbv); } } return 0; } int FacebookProto::RequestFriendship(WPARAM wParam,LPARAM lParam) { if (wParam == NULL || isOffline()) return 0; HANDLE hContact = reinterpret_cast(wParam); DBVARIANT dbv; if ( !DBGetContactSettingString(hContact,m_szModuleName,FACEBOOK_KEY_ID,&dbv)) { std::string* id = new std::string(dbv.pszVal); ForkThread( &FacebookProto::AddContactToServer, this, ( void* )id ); DBFreeVariant(&dbv); } return 0; } int FacebookProto::ApproveFriendship(WPARAM wParam,LPARAM lParam) { if (wParam == NULL || isOffline()) return 0; HANDLE *hContact = new HANDLE(reinterpret_cast(wParam)); ForkThread( &FacebookProto::ApproveContactToServer, this, ( void* )hContact ); return 0; } int FacebookProto::OnCancelFriendshipRequest(WPARAM wParam,LPARAM lParam) { if (wParam == NULL || isOffline()) return 0; HANDLE *hContact = new HANDLE(reinterpret_cast(wParam)); ForkThread( &FacebookProto::CancelFriendsRequest, this, ( void* )hContact ); return 0; } HANDLE FacebookProto::HContactFromAuthEvent(HANDLE hEvent) { DWORD body[2]; DBEVENTINFO dbei = { sizeof(dbei) }; dbei.cbBlob = sizeof(DWORD)*2; dbei.pBlob = (PBYTE)&body; if (CallService(MS_DB_EVENT_GET, (WPARAM)hEvent, (LPARAM)&dbei)) return INVALID_HANDLE_VALUE; if (dbei.eventType != EVENTTYPE_AUTHREQUEST) return INVALID_HANDLE_VALUE; if (strcmp(dbei.szModule, m_szModuleName)) return INVALID_HANDLE_VALUE; return DbGetAuthEventContact(&dbei); }