/* Facebook plugin for Miranda Instant Messenger _____________________________________________ Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-19 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" /** * Helper function for loading name from database (or use default one specified as parameter), used for title of few notifications. */ void FacebookProto::ProcessBuddylistUpdate(void*) { if (isOffline()) return; facy.handle_entry("buddylist_update"); // Get friends list http::response resp = facy.sendRequest(facy.buddylistUpdate()); if (resp.code != HTTP_CODE_OK) { facy.handle_error("buddylist_update"); return; } debugLogA("*** Starting processing buddylist update"); if (ParseBuddylistUpdate(&resp.data) != EXIT_SUCCESS) { debugLogA("*** Error processing buddylist update"); return; } debugLogA("*** Buddylist update processed"); } void FacebookProto::ProcessFriendList(void*) { if (isOffline()) return; facy.handle_entry("load_friends"); // Get friends list http::response resp = facy.sendRequest(facy.userInfoAllRequest()); if (resp.code != HTTP_CODE_OK) { facy.handle_error("load_friends"); return; } debugLogA("*** Starting processing friend list"); bool loadAllContacts = getBool(FACEBOOK_KEY_LOAD_ALL_CONTACTS, DEFAULT_LOAD_ALL_CONTACTS); bool pagesAlwaysOnline = getBool(FACEBOOK_KEY_PAGES_ALWAYS_ONLINE, DEFAULT_PAGES_ALWAYS_ONLINE); std::map friends; if (ParseFriends(&resp.data, &friends, loadAllContacts) != EXIT_SUCCESS) { debugLogA("*** Error processing friend list"); return; } // Check and update old contacts for (auto &hContact : AccContacts()) { if (isChatRoom(hContact)) continue; // TODO RM: change name of "Deleted" key to "DeletedTS", remove this code in some next version int deletedTS = getDword(hContact, "Deleted", 0); if (deletedTS != 0) { delSetting(hContact, "Deleted"); setDword(hContact, FACEBOOK_KEY_DELETED, deletedTS); } // If this contact is page, set it as invisible (if enabled in options) if (pagesAlwaysOnline && getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE) == CONTACT_PAGE) setWord(hContact, "Status", ID_STATUS_INVISIBLE); ptrA id(getStringA(hContact, FACEBOOK_KEY_ID)); if (id != nullptr) { std::map< std::string, facebook_user* >::iterator iter; if ((iter = friends.find(std::string(id))) != friends.end()) { // Found contact, update it and remove from map facebook_user *fbu = iter->second; // TODO RM: remove, because contacts cant change it, so its only for "first run" // - but what with contacts, that was added after logon? // Update gender setByte(hContact, "Gender", (int)fbu->gender); // TODO: remove this in some future version? // Remove old useless "RealName" field delSetting(hContact, "RealName"); // Update real name and nick if (!fbu->real_name.empty()) SaveName(hContact, fbu); // Update username if (!fbu->username.empty()) setString(hContact, FACEBOOK_KEY_USERNAME, fbu->username.c_str()); else delSetting(hContact, FACEBOOK_KEY_USERNAME); // Update contact type setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, fbu->type); // TODO: remove that popup and use "Contact added you" event? // Wasn't contact removed from "server-list" someday? And is it friend now? (as we can get also non-friends from this request now)? if (fbu->type == CONTACT_FRIEND && getDword(hContact, FACEBOOK_KEY_DELETED, 0)) { delSetting(hContact, FACEBOOK_KEY_DELETED); // Notify it, if user wants to be notified if (getByte(FACEBOOK_KEY_EVENT_FRIENDSHIP_ENABLE, DEFAULT_EVENT_FRIENDSHIP_ENABLE)) { std::string url = FACEBOOK_URL_PROFILE + fbu->user_id; NotifyEvent(Clist_GetContactDisplayName(hContact), TranslateT("Contact is back on server-list."), hContact, EVENT_FRIENDSHIP, &url); } } // Check avatar change CheckAvatarChange(hContact, fbu->image_url); // Mark this contact as deleted ("processed") and delete them later (as there may be some duplicit contacts to use) fbu->deleted = true; } else { // Contact is not on "server-list", notify it was removed (if it was real friend before) // Was this real friend before? if (getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE) == CONTACT_FRIEND) { setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE); // Wasn't we already been notified about this contact? if (!getDword(hContact, FACEBOOK_KEY_DELETED, 0)) { setDword(hContact, FACEBOOK_KEY_DELETED, ::time(0)); // Notify it, if user wants to be notified if (getByte(FACEBOOK_KEY_EVENT_FRIENDSHIP_ENABLE, DEFAULT_EVENT_FRIENDSHIP_ENABLE)) { std::string url = FACEBOOK_URL_PROFILE + std::string(id); NotifyEvent(Clist_GetContactDisplayName(hContact), TranslateT("Contact is no longer on server-list."), hContact, EVENT_FRIENDSHIP, &url); } } } } } } // Check remaining contacts in map and add them to contact list for (auto &it : friends) { if (!it.second->deleted) AddToContactList(it.second, true); // we know this contact doesn't exists, so we force add it delete it.second; } friends.clear(); debugLogA("*** Friend list processed"); } void FacebookProto::ProcessUnreadMessages(void*) { if (isOffline()) return; facy.handle_entry("ProcessUnreadMessages"); http::response resp = facy.sendRequest(facy.unreadThreadsRequest()); if (resp.code != HTTP_CODE_OK) { facy.handle_error("ProcessUnreadMessages"); return; } std::vector threads; if (ParseUnreadThreads(&resp.data, &threads) == EXIT_SUCCESS) { ForkThread(&FacebookProto::ProcessUnreadMessage, new std::vector(threads)); debugLogA("*** Unread threads list processed"); } else debugLogA("*** Error processing unread threads list"); facy.handle_success("ProcessUnreadMessages"); } void FacebookProto::ProcessUnreadMessage(void *pParam) { if (pParam == nullptr) return; std::vector *threads = (std::vector*)pParam; if (isOffline()) { delete threads; return; } facy.handle_entry("ProcessUnreadMessage"); int limit = 21; // FIXME: Rework this whole request as offset doesn't work anyway, and allow to load all the unread messages for each thread (IMHO could be done in 2 single requests = 1) get number of messages for all threads 2) load the counts of messages for all threads) // TODO: First load info about amount of unread messages, then load exactly this amount for each thread while (!threads->empty()) { LIST ids(1); for (std::vector::size_type i = 0; i < threads->size(); i++) ids.insert(mir_strdup(threads->at(i).c_str())); http::response resp = facy.sendRequest(facy.threadInfoRequest(ids, limit)); FreeList(ids); ids.destroy(); if (resp.code == HTTP_CODE_OK) { std::vector messages; if (ParseThreadMessages(&resp.data, &messages, false) == EXIT_SUCCESS) { ReceiveMessages(messages, true); debugLogA("*** Unread messages processed"); } else debugLogA("*** Error processing unread messages"); facy.handle_success("ProcessUnreadMessage"); } else facy.handle_error("ProcessUnreadMessage"); // limit = 20; // TODO: use better limits? threads->clear(); // TODO: if we have limit messages from one user, there may be more unread messages... continue with it... otherwise remove that threadd from threads list -- or do it in json parser? hm = allow more than "limit" unread messages to be parsed } delete threads; } void FacebookProto::LoadLastMessages(void *pParam) { if (pParam == nullptr) return; MCONTACT hContact = *(MCONTACT*)pParam; delete (MCONTACT*)pParam; if (isOffline()) return; facy.handle_entry("LoadLastMessages"); if (!isOnline()) return; bool isChat = isChatRoom(hContact); if (isChat && (!m_enableChat || IsSpecialChatRoom(hContact))) // disabled chats or special chatroom (e.g. nofitications) return; ptrA item_id(getStringA(hContact, isChat ? FACEBOOK_KEY_TID : FACEBOOK_KEY_ID)); if (item_id == nullptr) { debugLogA("!!! LoadLastMessages(): Contact has no TID/ID"); return; } int count = min(FACEBOOK_MESSAGES_ON_OPEN_LIMIT, getByte(FACEBOOK_KEY_MESSAGES_ON_OPEN_COUNT, DEFAULT_MESSAGES_ON_OPEN_COUNT)); http::response resp = facy.sendRequest(facy.threadInfoRequest(isChat, (const char*)item_id, nullptr, count, true)); if (resp.code != HTTP_CODE_OK || resp.data.empty()) { facy.handle_error("LoadLastMessages"); return; } // Temporarily disable marking messages as read for this contact facy.ignore_read.insert(hContact); std::vector messages; if (ParseThreadMessages(&resp.data, &messages, false) == EXIT_SUCCESS) { ReceiveMessages(messages, true); debugLogA("*** Thread messages processed"); } else debugLogA("*** Error processing thread messages"); facy.handle_success("LoadLastMessages"); // Enable marking messages as read for this contact facy.ignore_read.erase(hContact); // And force mark read OnDbEventRead(hContact, 0); } void FacebookProto::LoadHistory(void *pParam) { if (pParam == nullptr) return; MCONTACT hContact = *(MCONTACT*)pParam; delete (MCONTACT*)pParam; mir_cslock s(facy.loading_history_lock_); // Allow loading history only from one contact at a time if (!isOnline() || facy.loading_history) return; facy.handle_entry("LoadHistory"); bool isChat = isChatRoom(hContact); if (isChat) return; ptrA item_id(getStringA(hContact, isChat ? FACEBOOK_KEY_TID : FACEBOOK_KEY_ID)); if (item_id == nullptr) { debugLogA("!!! LoadHistory(): Contact has no TID/ID"); return; } // first get info about this thread and how many messages is there http::response resp = facy.sendRequest(facy.threadInfoRequest(isChat, (char*)item_id)); if (resp.code != HTTP_CODE_OK || resp.data.empty()) { facy.handle_error("LoadHistory"); return; } int messagesCount = -1; int unreadCount = -1; if (ParseMessagesCount(&resp.data, &messagesCount, &unreadCount) == EXIT_FAILURE) { facy.handle_error("LoadHistory"); return; } // Temporarily disable marking messages as read for this contact facy.ignore_read.insert(hContact); // Mark we're loading history, so we can behave differently (e.g., stickers won't be refreshed as it slows the whole process down drastically) facy.loading_history = true; POPUPDATAW ppd; ppd.iSeconds = 5; ppd.lchContact = hContact; ppd.lchIcon = IcoLib_GetIconByHandle(g_plugin.getIconHandle(IDI_CONVERSATION)); // TODO: Use better icon wcsncpy(ppd.lpwzContactName, m_tszUserName, MAX_CONTACTNAME); wcsncpy(ppd.lpwzText, TranslateT("Loading history started."), MAX_SECONDLINE); HWND popupHwnd = (HWND)PUAddPopupW(&ppd, (LPARAM)APF_RETURN_HWND); std::vector messages; std::string firstTimestamp; std::string firstMessageId; std::string lastMessageId; int loadedMessages = 0; int messagesPerBatch = messagesCount > 10000 ? 500 : 100; for (int batch = 0, batchLimit = messagesCount + messagesPerBatch; batch < batchLimit; batch += messagesPerBatch) { if (!isOnline()) break; // Load batch of messages resp = facy.sendRequest(facy.threadInfoRequest(isChat, item_id, firstTimestamp.c_str(), messagesPerBatch, true)); if (resp.code != HTTP_CODE_OK || resp.data.empty()) { facy.handle_error("LoadHistory"); break; } // Parse the result messages.clear(); if (ParseHistory(&resp.data, messages, &firstTimestamp) == EXIT_SUCCESS) { // Receive messages std::string previousFirstMessageId = firstMessageId; for (std::vector::size_type i = 0; i < messages.size(); i++) { facebook_message &msg = messages[i]; // First message might overlap (as we are using it's timestamp for the next loading), so we need to check for it if (i == 0) firstMessageId = msg.message_id; if (previousFirstMessageId == msg.message_id) continue; lastMessageId = msg.message_id; // We don't use ProtoChainRecvMsg here as this is just loading of old messages, which we just add to log DBEVENTINFO dbei = {}; if (msg.type == MESSAGE) dbei.eventType = EVENTTYPE_MESSAGE; else if (msg.type == VIDEO_CALL || msg.type == PHONE_CALL) dbei.eventType = FACEBOOK_EVENTTYPE_CALL; dbei.flags = DBEF_UTF; if (!msg.isIncoming) dbei.flags |= DBEF_SENT; if (!msg.isUnread) dbei.flags |= DBEF_READ; dbei.szModule = m_szModuleName; dbei.timestamp = msg.time; dbei.cbBlob = (DWORD)msg.message_text.length() + 1; dbei.pBlob = (PBYTE)msg.message_text.c_str(); db_event_add(hContact, &dbei); loadedMessages++; } // Save last message id of first batch which is latest message completely, because we're going backwards if (batch == 0 && !lastMessageId.empty()) { setString(hContact, FACEBOOK_KEY_MESSAGE_ID, lastMessageId.c_str()); } debugLogA("*** Load history messages processed"); } else { debugLogA("*** Error processing load history messages"); break; } // Update progress popup CMStringW text; text.AppendFormat(TranslateT("Loading messages: %d/%d"), loadedMessages, messagesCount); if (popupHwnd) PUChangeTextW(popupHwnd, text); else { wcsncpy(ppd.lpwzText, text, MAX_SECONDLINE); ppd.iSeconds = 1; popupHwnd = (HWND)PUAddPopupW(&ppd); } // There is no more messages if (messages.empty() || loadedMessages > messagesCount) { break; } } facy.handle_success("LoadHistory"); // Enable marking messages as read for this contact facy.ignore_read.erase(hContact); // Reset loading history flag facy.loading_history = false; if (popupHwnd) PUChangeTextW(popupHwnd, TranslateT("Loading history completed.")); else { ppd.iSeconds = 5; wcsncpy(ppd.lpwzText, TranslateT("Loading history completed."), MAX_SECONDLINE); popupHwnd = (HWND)PUAddPopupW(&ppd); } } void parseFeeds(const std::string &text, std::vector &news, DWORD &last_post_time, bool filterAds = true) { std::string::size_type pos = 0; UINT limit = 0; DWORD new_time = last_post_time; while ((pos = text.find("fbUserPost\"", pos)) != std::string::npos && limit <= 25) { std::string post = text.substr(pos, text.find("", pos) - pos); pos += 5; std::string post_header = utils::text::source_get_value(&post, 3, "", ""); std::string post_message = utils::text::source_get_value(&post, 3, " userContent\"", ">", "", "", "", ""); std::string time = utils::text::source_get_value(&post_time, 2, "data-utime=\"", "\""); std::string time_text = utils::text::source_get_value(&post_time, 2, ">", ""); if (time.empty()) { // alternative parsing (probably page like or advertisement) time = utils::text::source_get_value(&post, 2, "content_timestamp":"", """); } DWORD ttime; if (!utils::conversion::from_string(ttime, time, std::dec)) continue; if (ttime > new_time) new_time = ttime; // remember newest time from all these posts else if (ttime <= last_post_time) continue; // ignore posts older than newest post of previous check std::string timeandloc = utils::text::trim(utils::text::html_entities_decode(utils::text::remove_html(time_text))); std::string post_place = utils::text::source_get_value(&post, 4, "", "", ""); post_place = utils::text::trim(utils::text::remove_html(post_place)); if (!post_place.empty()) timeandloc += " · " + post_place; // in title keep only name, end of events like "X shared link" put into message std::string::size_type pos2 = post_header.find(""); std::string header_author = utils::text::trim( utils::text::html_entities_decode( utils::text::remove_html( post_header.substr(0, pos2)))); std::string header_rest = utils::text::trim( utils::text::html_entities_decode( utils::text::remove_html( post_header.substr(pos2, post_header.length() - pos2)))); // Strip "Translate" and other buttons do { pos2 = post_message.find("role=\"button\""); if (pos2 != std::string::npos) { pos2 = post_message.find(">", pos2); if (pos2 != std::string::npos) { std::string::size_type pos3 = post_message.find("", pos2); std::string tmp = post_message.substr(0, pos2); if (pos3 != std::string::npos) tmp += post_message.substr(pos3, post_message.length() - pos3); post_message = tmp; } } } while (pos2 != std::string::npos); // Strip "See more" link pos2 = post_message.find(""); if (pos2 != std::string::npos) post_message = post_message.substr(0, pos2); // Process attachment (if present) std::string post_attachment = ""; pos2 = post_message.find("class=\"mtm\">"); if (pos2 != std::string::npos) { pos2 += 12; post_attachment = post_message.substr(pos2, post_message.length() - pos2); post_message = post_message.substr(0, pos2); // Add new lines between some elements to improve formatting utils::text::replace_all(&post_attachment, "", "\n"); utils::text::replace_all(&post_attachment, "ellipsis\">", "ellipsis\">\n"); post_attachment = utils::text::trim( utils::text::html_entities_decode( utils::text::remove_html(post_attachment))); post_attachment = utils::text::truncate_utf8(post_attachment, MAX_LINK_DESCRIPTION_LEN); if (post_attachment.empty()) // This is some textless attachment, so mention it post_attachment = ptrA(mir_utf8encode(Translate(""))); } post_message = utils::text::trim(utils::text::html_entities_decode(utils::text::remove_html(post_message))); // Truncate text of newsfeed when it's too long post_message = utils::text::truncate_utf8(post_message, MAX_NEWSFEED_LEN); std::string content = ""; if (header_rest.length() > 2) content += TEXT_ELLIPSIS + header_rest + "\n"; if (!post_message.empty()) content += post_message + "\n"; if (!post_attachment.empty()) content += TEXT_EMOJI_LINK" " + post_attachment + "\n"; if (!timeandloc.empty()) content += TEXT_EMOJI_CLOCK" " + timeandloc; facebook_newsfeed* nf = new facebook_newsfeed; nf->title = header_author; nf->user_id = utils::text::source_get_value(&post_header, 2, "user.php?id=", "&"); nf->link = utils::text::html_entities_decode(post_link); // Check if we don't want to show ads posts bool filtered = filterAds && (nf->link.find("/about/ads") != std::string::npos || post.find("class=\"uiStreamSponsoredLink\"") != std::string::npos || post.find("href=\"/about/ads\"") != std::string::npos); nf->text = utils::text::trim(content); if (filtered || nf->title.empty() || nf->text.empty()) { delete nf; continue; } news.push_back(nf); pos++; limit++; } last_post_time = new_time; } void FacebookProto::ProcessMemories(void *p) { if (isOffline()) return; bool manuallyTriggered = (p == MANUALLY_TRIGGERED); if (manuallyTriggered) facy.info_notify(TranslateT("Loading memories...")); size_t numMemories = 0; facy.handle_entry(__FUNCTION__); http::response resp = facy.sendRequest(facy.memoriesRequest()); if (resp.code != HTTP_CODE_OK || resp.data.empty()) { facy.handle_error(__FUNCTION__); return; } std::string jsonData = resp.data.substr(9); JSONNode root = JSONNode::parse(jsonData.c_str()); if (root) { const JSONNode &html_ = root["domops"].at((json_index_t)0).at((json_index_t)3).at("__html"); if (html_) { std::string html = utils::text::html_entities_decode(utils::text::slashu_to_utf8(html_.as_string())); std::vector news; DWORD new_time = 0; parseFeeds(html, news, new_time, true); if (!news.empty()) Skin_PlaySound("Memories"); numMemories = news.size(); for (std::vector::size_type i = 0; i < news.size(); i++) { ptrW tszTitle(mir_utf8decodeW(news[i]->title.c_str())); ptrW tszText(mir_utf8decodeW(news[i]->text.c_str())); NotifyEvent(TranslateT("On this day"), tszText, 0, EVENT_ON_THIS_DAY, &news[i]->link); delete news[i]; } news.clear(); } } if (manuallyTriggered) facy.info_notify(CMStringW(FORMAT, TranslateT("Found %d memories."), numMemories)); facy.handle_success(__FUNCTION__); } void FacebookProto::ReceiveMessages(std::vector &messages, bool check_duplicates) { bool naseemsSpamMode = getBool(FACEBOOK_KEY_NASEEMS_SPAM_MODE, false); // TODO: make this checking more lightweight as now it is not effective at all... if (check_duplicates) { // 1. check if there are some message that we already have (compare FACEBOOK_KEY_MESSAGE_ID = last received message ID) for (size_t i = 0; i < messages.size(); i++) { facebook_message &msg = messages[i]; MCONTACT hContact = msg.isChat ? ChatIDToHContact(msg.thread_id) : ContactIDToHContact(msg.user_id); if (hContact == 0) continue; ptrA lastId(getStringA(hContact, FACEBOOK_KEY_MESSAGE_ID)); if (lastId == nullptr) continue; if (!msg.message_id.compare(lastId)) { // Equal, ignore all older messages (including this) from same contact for (std::vector::size_type j = 0; j < messages.size(); j++) { bool equalsId = msg.isChat ? (messages[j].thread_id == msg.thread_id) : (messages[j].user_id == msg.user_id); if (equalsId && messages[j].time <= msg.time) messages[j].flag_ = 1; } } } // 2. remove all marked messages from list for (auto it = messages.begin(); it != messages.end();) { if ((*it).flag_ == 1) it = messages.erase(it); else ++it; } } std::set *hChatContacts = new std::set(); for (auto &msg : messages) { if (msg.isChat) { if (!m_enableChat) continue; // Multi-user message debugLogA(" < Got chat message ID: %s", msg.message_id.c_str()); facebook_chatroom *fbc; std::string thread_id = msg.thread_id.c_str(); auto it = facy.chat_rooms.find(thread_id); if (it != facy.chat_rooms.end()) fbc = it->second; else { // In Naseem's spam mode we ignore outgoing messages sent from other instances if (naseemsSpamMode && !msg.isIncoming) continue; // We don't have this chat loaded in memory yet, lets load some info (name, list of users) fbc = new facebook_chatroom(thread_id); LoadChatInfo(fbc); facy.chat_rooms.insert(std::make_pair(thread_id, fbc)); } MCONTACT hChatContact = 0; // RM TODO: better use check if chatroom exists/is in db/is online... no? // like: if (ChatIDToHContact(thread_id) == nullptr) { ptrA users(GetChatUsers(fbc->thread_id.c_str())); if (users == nullptr) { AddChat(fbc->thread_id.c_str(), fbc->chat_name.c_str()); hChatContact = ChatIDToHContact(fbc->thread_id); // Set thread id (TID) for later setString(hChatContact, FACEBOOK_KEY_TID, fbc->thread_id.c_str()); for (auto &jt : fbc->participants) AddChatContact(fbc->thread_id.c_str(), jt.second, false); } if (!hChatContact) hChatContact = ChatIDToHContact(fbc->thread_id); if (!hChatContact) { // hopefully shouldn't happen, but who knows? debugLogW(L"!!! No hChatContact for %s", fbc->thread_id.c_str()); continue; } // We don't want to save (this) message ID for chatrooms // setString(hChatContact, FACEBOOK_KEY_MESSAGE_ID, msg.message_id.c_str()); setDword(FACEBOOK_KEY_LAST_ACTION_TS, msg.time); // Save TID setString(hChatContact, FACEBOOK_KEY_TID, msg.thread_id.c_str()); // Get name of this chat participant std::string name = msg.user_id; // fallback to numeric id { auto jt = fbc->participants.find(msg.user_id); if (jt != fbc->participants.end()) name = jt->second.nick; } switch (msg.type) { default: case MESSAGE: UpdateChat(fbc->thread_id.c_str(), msg.user_id.c_str(), name.c_str(), msg.message_text.c_str(), msg.time); break; case ADMIN_TEXT: UpdateChat(thread_id.c_str(), nullptr, nullptr, msg.message_text.c_str()); break; case UNSUBSCRIBE: case SUBSCRIBE: UpdateChat(thread_id.c_str(), nullptr, nullptr, msg.message_text.c_str()); { std::vector ids; utils::text::explode(msg.data, ";", &ids); for (auto &id : ids) { auto jt = fbc->participants.find(id); if (jt == fbc->participants.end()) { // We don't have this user there yet, so load info about him and then process event chatroom_participant participant; participant.is_former = (msg.type == UNSUBSCRIBE); participant.user_id = id; // FIXME: Load info about all participants at once fbc->participants.insert(std::make_pair(participant.user_id, participant)); LoadParticipantsNames(fbc); jt = fbc->participants.find(id); } if (jt != fbc->participants.end()) { if (msg.type == SUBSCRIBE) AddChatContact(thread_id.c_str(), jt->second, msg.isUnread); else { if (jt->second.user_id == facy.self_.user_id) { // we exited the thread Chat_Terminate(m_szModuleName, _A2T(fbc->thread_id.c_str()), true); facy.chat_rooms.erase(thread_id); delete fbc; } else RemoveChatContact(thread_id.c_str(), jt->second.user_id.c_str(), jt->second.nick.c_str()); } } } } break; case THREAD_NAME: UpdateChat(thread_id.c_str(), nullptr, nullptr, msg.message_text.c_str()); { std::string chatName = (!msg.data.empty() ? msg.data : GenerateChatName(fbc)); // proto->RenameChat(thread_id.c_str(), chatName.c_str()); // this don't work, why? setStringUtf(hChatContact, FACEBOOK_KEY_NICK, chatName.c_str()); } break; case THREAD_IMAGE: UpdateChat(thread_id.c_str(), nullptr, nullptr, msg.message_text.c_str()); break; } // Automatically mark message as read because chatroom doesn't support onRead event (yet) hChatContacts->insert(hChatContact); // std::set checks duplicates at insert automatically } else { // Single-user message debugLogA(" < Got message ID: %s", msg.message_id.c_str()); facebook_user fbu; fbu.user_id = msg.user_id; MCONTACT hContact = ContactIDToHContact(fbu.user_id); if (hContact == 0) { // In Naseem's spam mode we ignore outgoing messages sent from other instances if (naseemsSpamMode && !msg.isIncoming) continue; // We don't have this contact, lets load info about him LoadContactInfo(&fbu); hContact = AddToContactList(&fbu); } if (!hContact) { // hopefully shouldn't happen, but who knows? debugLogA("!!! No hContact for %s", msg.user_id.c_str()); continue; } // Save last (this) message ID setString(hContact, FACEBOOK_KEY_MESSAGE_ID, msg.message_id.c_str()); // Save TID setString(hContact, FACEBOOK_KEY_TID, msg.thread_id.c_str()); if (msg.isIncoming && msg.isUnread && msg.type == MESSAGE) { PROTORECVEVENT recv = { 0 }; recv.szMessage = const_cast(msg.message_text.c_str()); recv.timestamp = msg.time; ProtoChainRecvMsg(hContact, &recv); } else { DBEVENTINFO dbei = {}; if (msg.type == MESSAGE) dbei.eventType = EVENTTYPE_MESSAGE; else if (msg.type == VIDEO_CALL || msg.type == PHONE_CALL) dbei.eventType = FACEBOOK_EVENTTYPE_CALL; dbei.flags = DBEF_UTF; if (!msg.isIncoming) dbei.flags |= DBEF_SENT; if (!msg.isUnread) dbei.flags |= DBEF_READ; dbei.szModule = m_szModuleName; dbei.timestamp = msg.time; dbei.cbBlob = (DWORD)msg.message_text.length() + 1; dbei.pBlob = (PBYTE)msg.message_text.c_str(); db_event_add(hContact, &dbei); } // Reset the "message seen" info when we get any new message (doesn't matter if sent from other instance or received) if (msg.isUnread || !msg.isIncoming) facy.erase_reader(hContact); } } if (!hChatContacts->empty()) ForkThread(&FacebookProto::ReadMessageWorker, (void*)hChatContacts); else delete hChatContacts; } void FacebookProto::ShowNotifications() { mir_cslock s(facy.notifications_lock_); // Show popups for unseen notifications and/or write them to chatroom for (auto &it : facy.notifications) { facebook_notification *notification = it.second; if (notification != nullptr && !notification->seen) { debugLogA(" Showing popup for notification ID: %s", notification->id.c_str()); ptrW szText(mir_utf8decodeW(notification->text.c_str())); MCONTACT hContact = (notification->user_id.empty() ? 0 : ContactIDToHContact(notification->user_id)); notification->hWndPopup = NotifyEvent(m_tszUserName, szText, hContact, EVENT_NOTIFICATION, ¬ification->link, ¬ification->id, notification->icon); notification->seen = true; } } } void FacebookProto::ProcessNotifications(void *p) { if (isOffline()) return; bool manuallyTriggered = (p == MANUALLY_TRIGGERED); if (manuallyTriggered) facy.info_notify(TranslateT("Loading notifications...")); facy.handle_entry("notifications"); // Get notifications http::response resp = facy.sendRequest(facy.getNotificationsRequest(FACEBOOK_NOTIFICATIONS_LOAD_COUNT)); if (resp.code != HTTP_CODE_OK) { facy.handle_error("notifications"); return; } // Process notifications debugLogA("*** Starting processing notifications"); size_t numNotifications = facy.notifications.size(); if (ParseNotifications(&resp.data, &facy.notifications) == EXIT_SUCCESS) { if (manuallyTriggered) { numNotifications = facy.notifications.size() - numNotifications; facy.info_notify(CMStringW(FORMAT, TranslateT("Found %d notifications."), numNotifications)); } ShowNotifications(); debugLogA("*** Notifications processed"); } else debugLogA("*** Error processing notifications"); } void FacebookProto::ProcessFriendRequests(void *p) { if (isOffline()) return; bool manuallyTriggered = (p == MANUALLY_TRIGGERED); if (manuallyTriggered) facy.info_notify(TranslateT("Loading friendship requests...")); facy.handle_entry("friendRequests"); // Get load friendships http::response resp = facy.sendRequest(facy.getFriendshipsRequest()); // Workaround not working "mbasic." website for some people if (!resp.isValid()) { // Remember it didn't worked and try it again (internally it will use "m." this time) facy.mbasicWorks = false; resp = facy.sendRequest(facy.getFriendshipsRequest()); } if (resp.code != HTTP_CODE_OK) { facy.handle_error("friendRequests"); return; } int numRequestsNew = 0; int numRequestsOld = 0; // Parse it std::string reqs = utils::text::source_get_value(&resp.data, 3, "id=\"friends_center_main\"", "", "/friends/center/suggestions/"); std::string::size_type pos = 0; std::string::size_type pos2 = 0; bool last = (reqs.find("seenrequesttime=") == std::string::npos); // false when there are some requests while (!last && !reqs.empty()) { std::string req; if ((pos2 = reqs.find("", pos)) != std::string::npos) { req = reqs.substr(pos, pos2 - pos); pos = pos2 + 8; } else { req = reqs.substr(pos); last = true; } std::string get = utils::text::source_get_value(&req, 2, "notifications.php?", "\""); std::string time = utils::text::source_get_value2(&get, "seenrequesttime=", "&\""); std::string reason = utils::text::remove_html(utils::text::source_get_value(&req, 4, "", "", "")); facebook_user fbu; fbu.real_name = utils::text::remove_html(utils::text::source_get_value(&req, 3, "", "")); fbu.user_id = utils::text::source_get_value2(&get, "confirm=", "&\""); fbu.type = CONTACT_APPROVE; if (!fbu.user_id.empty() && !fbu.real_name.empty()) { MCONTACT hContact = AddToContactList(&fbu); setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_APPROVE); bool isNew = false; ptrA oldTime(getStringA(hContact, "RequestTime")); if (oldTime == nullptr || mir_strcmp(oldTime, time.c_str())) { // This is new request isNew = true; setString(hContact, "RequestTime", time.c_str()); DB_AUTH_BLOB blob(hContact, fbu.real_name.c_str(), nullptr, nullptr, fbu.user_id.c_str(), reason.c_str()); DBEVENTINFO dbei = {}; dbei.szModule = m_szModuleName; dbei.timestamp = ::time(0); dbei.flags = DBEF_UTF; dbei.eventType = EVENTTYPE_AUTHREQUEST; dbei.cbBlob = blob.size(); dbei.pBlob = blob; db_event_add(0, &dbei); } debugLogA(" < (%s) Friendship request [%s]", (isNew ? "New" : "Old"), time.c_str()); if (isNew) numRequestsNew++; else numRequestsOld++; } else debugLogA("!!! Wrong friendship request:\n%s", req.c_str()); } if (manuallyTriggered) { CMStringW text; if (numRequestsOld > 0) text.AppendFormat(TranslateT("Found %d friendship requests (%d seen)."), numRequestsNew, numRequestsOld); else text.AppendFormat(TranslateT("Found %d friendship requests."), numRequestsNew); facy.info_notify(text); } facy.handle_success("friendRequests"); } void FacebookProto::ProcessFeeds(void *p) { if (!isOnline()) return; bool manuallyTriggered = (p == MANUALLY_TRIGGERED); if (manuallyTriggered) facy.info_notify(TranslateT("Loading wall posts...")); facy.handle_entry("feeds"); // Get feeds http::response resp = facy.sendRequest(facy.newsfeedRequest()); if (resp.code != HTTP_CODE_OK || resp.data.empty()) { facy.handle_error("feeds"); return; } std::vector news; DWORD new_time = facy.last_feeds_update_; bool filterAds = getBool(FACEBOOK_KEY_FILTER_ADS, DEFAULT_FILTER_ADS); parseFeeds(resp.data, news, new_time, filterAds); if (!news.empty()) Skin_PlaySound("NewsFeed"); if (manuallyTriggered) facy.info_notify(CMStringW(FORMAT, TranslateT("Found %d wall posts."), news.size())); for (std::vector::size_type i = 0; i < news.size(); i++) { ptrW tszTitle(mir_utf8decodeW(news[i]->title.c_str())); ptrW tszText(mir_utf8decodeW(news[i]->text.c_str())); MCONTACT hContact = ContactIDToHContact(news[i]->user_id); NotifyEvent(tszTitle, tszText, hContact, EVENT_NEWSFEED, &news[i]->link); delete news[i]; } news.clear(); // Set time of last update to time of newest post this->facy.last_feeds_update_ = new_time; facy.handle_success("feeds"); } void FacebookProto::ProcessPages(void*) { if (isOffline() || !getByte(FACEBOOK_KEY_LOAD_PAGES, DEFAULT_LOAD_PAGES)) return; facy.handle_entry("load_pages"); // Get feeds http::response resp = facy.sendRequest(facy.getPagesRequest()); if (resp.code != HTTP_CODE_OK) { facy.handle_error("load_pages"); return; } std::string content = utils::text::source_get_value(&resp.data, 2, "id=\"bookmarksSeeAllSection\"", ""); std::string::size_type start, end; start = content.find(""); start = content.find(""); std::string::size_type pos = 0; std::string::size_type pos2 = 0; while ((pos = items.find("", pos) - pos); pos++; count++; std::string id; std::string type; // Type of search result: 69=group, 274=page, 844=event, 2048=contact std::string name = utils::text::source_get_value(&item, 3, "", "", "", ""); std::string url = utils::text::source_get_value(&item, 3, ", not in as people common = utils::text::source_get_value(&item, 4, "", "", ""); } } else if (type == "2048") { // people // When searching for people we try to parse nick and split first/last name if ((pos2 = name.find("")) != std::string::npos) { nick = name.substr(pos2 + 30, name.length() - pos2 - 31); // also remove brackets around nickname name = name.substr(0, pos2 - 1); } if ((pos2 = name.find(" ")) != std::string::npos) { surname = name.substr(pos2 + 1, name.length() - pos2 - 1); name = name.substr(0, pos2); } } else // This is group or event, let's ignore that continue; // ignore self contact and empty ids if (id.empty() || id == facy.self_.user_id) continue; ptrW tid(mir_utf8decodeW(id.c_str())); ptrW tname(mir_utf8decodeW(utils::text::html_entities_decode(name).c_str())); ptrW tsurname(mir_utf8decodeW(utils::text::html_entities_decode(surname).c_str())); ptrW tnick(mir_utf8decodeW(utils::text::html_entities_decode(nick).c_str())); ptrW tcommon(mir_utf8decodeW(utils::text::html_entities_decode(common).c_str())); PROTOSEARCHRESULT psr = { 0 }; psr.cbSize = sizeof(psr); psr.flags = PSR_UNICODE; psr.id.w = tid; psr.nick.w = tnick; psr.firstName.w = tname; psr.lastName.w = tsurname; psr.email.w = tcommon; ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, targ, (LPARAM)&psr); } ssid = utils::text::source_get_value(&resp.data, 3, "id=\"more_objects\"", "ssid=", "&"); pn++; // increment page number if (ssid.empty()) break; // No more results } else break; } ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, targ, 0); facy.handle_success("searchAckThread"); mir_free(targ); } void FacebookProto::SearchIdAckThread(void *targ) { facy.handle_entry("searchIdAckThread"); std::string search = T2Utf((wchar_t*)targ).str(); if (search.find(FACEBOOK_SERVER_DOMAIN "/") != std::string::npos) { // User entered URL, let's extract id/username from it std::string id = utils::text::source_get_value2(&search, "/profile.php?id=", "&#", true); if (id.empty()) { // This link probably contains username (if user entered proper profile url) id = utils::text::source_get_value2(&search, FACEBOOK_SERVER_DOMAIN "/", "?&#", true); } search = id; } search = utils::url::encode(search); if (!isOffline() && !search.empty()) { http::response resp = facy.sendRequest(facy.profileRequest(search.c_str())); if (resp.code == HTTP_CODE_FOUND && resp.headers.find("Location") != resp.headers.end()) { search = utils::text::source_get_value(&resp.headers["Location"], 2, FACEBOOK_SERVER_MBASIC"/", "_rdr"); // Use only valid username redirects if (search.find("home.php") == std::string::npos) resp = facy.sendRequest(facy.profileRequest(search.c_str())); } if (resp.code == HTTP_CODE_OK) { std::string about = utils::text::source_get_value(&resp.data, 2, "id=\"root\"", ""); std::string id = utils::text::source_get_value2(&about, ";id=", "&\""); if (id.empty()) id = utils::text::source_get_value2(&about, "?bid=", "&\""); std::string name = utils::text::source_get_value(&about, 3, "", "", ""); std::string surname; std::string::size_type pos; if ((pos = name.find(" ")) != std::string::npos) { surname = name.substr(pos + 1, name.length() - pos - 1); name = name.substr(0, pos); } // ignore self contact and empty ids if (!id.empty() && id != facy.self_.user_id) { ptrW tid(mir_utf8decodeW(id.c_str())); ptrW tname(mir_utf8decodeW(name.c_str())); ptrW tsurname(mir_utf8decodeW(surname.c_str())); PROTOSEARCHRESULT psr = { 0 }; psr.cbSize = sizeof(psr); psr.flags = PSR_UNICODE; psr.id.w = tid; psr.firstName.w = tname; psr.lastName.w = tsurname; ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, targ, (LPARAM)&psr); } } } ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, targ, 0); facy.handle_success("searchIdAckThread"); mir_free(targ); }