/*
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::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 pd = { sizeof(pd) };
pd.iSeconds = 5;
pd.lchContact = hContact;
pd.lchIcon = IcoLib_GetIconByHandle(GetIconHandle("conversation")); // TODO: Use better icon
wcsncpy(pd.lpwzContactName, m_tszUserName, MAX_CONTACTNAME);
wcsncpy(pd.lpwzText, TranslateT("Loading history started."), MAX_SECONDLINE);
HWND popupHwnd = nullptr;
if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (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 (ServiceExists(MS_POPUP_CHANGETEXTW) && popupHwnd) {
PUChangeTextW(popupHwnd, text);
}
else if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
wcsncpy(pd.lpwzText, text, MAX_SECONDLINE);
pd.iSeconds = 1;
popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (LPARAM)0);
}
// 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 (ServiceExists(MS_POPUP_CHANGETEXTW) && popupHwnd)
PUChangeTextW(popupHwnd, TranslateT("Loading history completed."));
else if (ServiceExists(MS_POPUP_ADDPOPUPW)) {
pd.iSeconds = 5;
wcsncpy(pd.lpwzText, TranslateT("Loading history completed."), MAX_SECONDLINE);
popupHwnd = (HWND)CallService(MS_POPUP_ADDPOPUPW, (WPARAM)&pd, (LPARAM)0);
}
}
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\"", ">", "