/* Facebook plugin for Miranda NG Copyright © 2019-23 Miranda NG team 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 "stdafx.h" static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) { if (p1->id == p2->id) return 0; return (p1->id < p2->id) ? -1 : 1; } static int CompareMessages(const COwnMessage *p1, const COwnMessage *p2) { if (p1->msgId == p2->msgId) return 0; return (p1->msgId < p2->msgId) ? -1 : 1; } FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : PROTO(proto_name, username), m_impl(*this), m_users(50, CompareUsers), arOwnMessages(1, CompareMessages), m_bLoadAll(this, "LoadAllContacts", false), m_bKeepUnread(this, "KeepUnread", false), m_bUseBigAvatars(this, "UseBigAvatars", true), m_bUseGroupchats(this, "UseGroupChats", true), m_bHideGroupchats(this, "HideGroupChats", true), m_bLoginInvisible(this, "LoginInvisible", false), m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") { // to upgrade previous settings if (getByte("Compatibility") < 1) { setByte("Compatibility", 1); delSetting(DBKEY_DEVICE_ID); } m_szDeviceID = getMStringA(DBKEY_DEVICE_ID); if (m_szDeviceID.IsEmpty()) { UUID deviceId; UuidCreate(&deviceId); RPC_CSTR szId; UuidToStringA(&deviceId, &szId); m_szDeviceID = szId; setString(DBKEY_DEVICE_ID, m_szDeviceID); RpcStringFreeA(&szId); } m_szClientID = getMStringA(DBKEY_CLIENT_ID); if (m_szClientID.IsEmpty()) { for (int i = 0; i < 20; i++) { uint32_t dwRandon; Utils_GetRandom(&dwRandon, sizeof(dwRandon)); int c = dwRandon % 62; if (c >= 0 && c < 26) c += 'a'; else if (c >= 26 && c < 52) c += 'A' - 26; else if (c >= 52 && c < 62) c += '0' - 52; m_szClientID.AppendChar(c); } setString(DBKEY_CLIENT_ID, m_szClientID); } m_uid = _atoi64(getMStringA(DBKEY_ID)); m_sid = _atoi64(getMStringA(DBKEY_SID)); m_szSyncToken = getMStringA(DBKEY_SYNC_TOKEN); // Avatars CreateDirectoryTreeW(GetAvatarPath()); // Create standard network connection NETLIBUSER nlu = {}; nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; nlu.szSettingsModule = m_szModuleName; nlu.szDescriptiveName.w = m_tszUserName; m_hNetlibUser = Netlib_RegisterUser(&nlu); db_set_resident(m_szModuleName, "UpdateNeeded"); // Services CreateProtoService(PS_GETAVATARINFO, &FacebookProto::GetAvatarInfo); CreateProtoService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps); // Events HookProtoEvent(ME_GC_EVENT, &FacebookProto::GroupchatEventHook); HookProtoEvent(ME_GC_BUILDMENU, &FacebookProto::GroupchatMenuHook); HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit); // Group chats GCREGISTER gcr = {}; gcr.dwFlags = GC_TYPNOTIF | GC_DATABASE; gcr.ptszDispName = m_tszUserName; gcr.pszModule = m_szModuleName; Chat_Register(&gcr); } FacebookProto::~FacebookProto() { } ///////////////////////////////////////////////////////////////////////////////////////// // protocol events void FacebookProto::OnContactAdded(MCONTACT hContact) { __int64 userId = _atoi64(getMStringA(hContact, DBKEY_ID)); if (userId && !FindUser(userId)) { mir_cslock lck(m_csUsers); m_users.insert(new FacebookUser(userId, hContact)); } } void FacebookProto::OnModulesLoaded() { VARSW wszCache(L"%miranda_avatarcache%"); CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", wszCache.get(), m_szModuleName); SMADD_CONT cont = { 2, m_szModuleName, wszPath }; CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); wszPath.Format(L"%s\\%S\\Stickers\\*.webp", wszCache.get(), m_szModuleName); cont.path = wszPath; CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); // contacts cache for (auto &cc : AccContacts()) { CMStringA szId(getMStringA(cc, DBKEY_ID)); if (!szId.IsEmpty()) m_users.insert(new FacebookUser(_atoi64(szId), cc, isChatRoom(cc))); } // Default group Clist_GroupCreate(0, m_wszDefaultGroup); } void FacebookProto::OnShutdown() { if (m_mqttConn != nullptr) Netlib_Shutdown(m_mqttConn); } ///////////////////////////////////////////////////////////////////////////////////////// MCONTACT FacebookProto::AddToList(int, PROTOSEARCHRESULT *psr) { if (!mir_wstrlen(psr->id.w)) return 0; if (auto *pUser = FindUser(_wtoi64(psr->id.w))) return pUser->hContact; MCONTACT hContact = db_add_contact(); setWString(hContact, DBKEY_ID, psr->id.w); Proto_AddToContact(hContact, m_szModuleName); return hContact; } ///////////////////////////////////////////////////////////////////////////////////////// INT_PTR FacebookProto::GetCaps(int type, MCONTACT) { switch (type) { case PFLAGNUM_1: { DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ; if (getByte(DBKEY_SET_MIRANDA_STATUS)) return flags |= PF1_MODEMSG; else return flags |= PF1_MODEMSGRECV; } case PFLAGNUM_2: return PF2_ONLINE | PF2_SHORTAWAY | PF2_INVISIBLE | PF2_IDLE; case PFLAGNUM_3: if (getByte(DBKEY_SET_MIRANDA_STATUS)) return PF2_ONLINE; // | PF2_SHORTAWAY; else return 0; case PFLAGNUM_4: return PF4_NOCUSTOMAUTH | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_READNOTIFY; case PFLAG_MAXLENOFMESSAGE: return FACEBOOK_MESSAGE_LIMIT; case PFLAG_UNIQUEIDTEXT: return (INT_PTR) L"Facebook ID"; } return 0; } ///////////////////////////////////////////////////////////////////////////////////////// int FacebookProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) { if (!m_bOnline) { ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); return 1; } CMStringA userId(getMStringA(hContact, DBKEY_ID)); __int64 msgId; Utils_GetRandom(&msgId, sizeof(msgId)); msgId = abs(msgId); JSONNode root; root << CHAR_PARAM("body", pszSrc) << INT64_PARAM("msgid", msgId) << INT64_PARAM("sender_fbid", m_uid) << CHAR_PARAM("to", userId); MqttPublish("/send_message2", root); mir_cslock lck(m_csOwnMessages); arOwnMessages.insert(new COwnMessage(msgId, m_mid, hContact)); return m_mid; } ///////////////////////////////////////////////////////////////////////////////////////// int FacebookProto::SetStatus(int iNewStatus) { if (iNewStatus != ID_STATUS_OFFLINE && IsStatusConnecting(m_iStatus)) { debugLogA("=== Status is already connecting, no change"); return 0; } // Routing statuses not supported by Facebook switch (iNewStatus) { case ID_STATUS_ONLINE: case ID_STATUS_OFFLINE: break; default: iNewStatus = ID_STATUS_AWAY; break; } if (m_iStatus == iNewStatus) { debugLogA("=== Statuses are same, no change"); return 0; } m_iDesiredStatus = iNewStatus; int iOldStatus = m_iStatus; // log off & free all resources if (iNewStatus == ID_STATUS_OFFLINE) { OnShutdown(); m_iStatus = ID_STATUS_OFFLINE; } else if (m_iStatus == ID_STATUS_OFFLINE) { // we gonna connect debugLogA("*** Beginning SignOn process"); m_iStatus = ID_STATUS_CONNECTING; ForkThread(&FacebookProto::ServerThread); } else m_iStatus = iNewStatus; ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); return 0; } ////////////////////////////////////////////////////////////////////////////// int FacebookProto::UserIsTyping(MCONTACT hContact, int type) { JSONNode root; root << INT_PARAM("state", type == PROTOTYPE_SELFTYPING_ON) << CHAR_PARAM("to", getMStringA(hContact, DBKEY_ID)); MqttPublish("/typing", root); return 0; } ////////////////////////////////////////////////////////////////////////////// // Services MWindow FacebookProto::OnCreateAccMgrUI(MWindow hwndParent) { return CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), hwndParent, FBAccountProc, (LPARAM)this); }