/*
Facebook plugin for Miranda Instant Messenger
_____________________________________________
Copyright � 2009-11 Michal Zelinka, 2011-15 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 "stdafx.h"
#define MAX_NEWSFEED_LEN 500
/**
* Helper function for loading name from database (or use default one specified as parameter), used for title of few notifications.
*/
std::string getContactName(FacebookProto *proto, MCONTACT hContact, const char *defaultName)
{
std::string name = defaultName;
DBVARIANT dbv;
if (!proto->getStringUtf(hContact, FACEBOOK_KEY_NICK, &dbv)) {
name = dbv.pszVal;
db_free(&dbv);
}
return name;
}
void FacebookProto::ProcessBuddyList(void*)
{
ScopedLock s(facy.buddies_lock_);
if (isOffline())
return;
facy.handle_entry("ProcessBuddyList");
// Prepare update data
std::string post_data = "user=" + facy.self_.user_id + "&fetch_mobile=true&fb_dtsg=" + facy.dtsg_ + "&__user=" + facy.self_.user_id + "&cached_user_info_ids=";
int counter = 0;
for (List::Item< facebook_user >* i = facy.buddies.begin(); i != NULL; i = i->next, counter++)
{
post_data += i->data->user_id + "%2C";
}
post_data += "&phstamp=" + facy.phstamp(post_data);
// Get buddy list
http::response resp = facy.flap(REQUEST_BUDDY_LIST, &post_data);
if (resp.code != HTTP_CODE_OK) {
facy.handle_error("buddy_list");
return;
}
debugLogA("*** Starting processing buddy list");
CODE_BLOCK_TRY
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_buddy_list(&resp.data, &facy.buddies);
delete p;
for (List::Item< facebook_user >* i = facy.buddies.begin(); i != NULL;)
{
facebook_user* fbu = i->data;
if (!fbu->deleted) {
if (!fbu->handle) // just been added
fbu->handle = AddToContactList(fbu, CONTACT_FRIEND);
ptrT client(getTStringA(fbu->handle, "MirVer"));
if (!client || mir_tstrcmp(client, fbu->getMirVer()))
setTString(fbu->handle, "MirVer", fbu->getMirVer());
if (getDword(fbu->handle, "IdleTS", 0) != fbu->last_active) {
if ((fbu->idle || fbu->status_id == ID_STATUS_OFFLINE) && fbu->last_active > 0)
setDword(fbu->handle, "IdleTS", fbu->last_active);
else
delSetting(fbu->handle, "IdleTS");
}
}
if (fbu->status_id == ID_STATUS_OFFLINE || fbu->deleted) {
if (fbu->handle)
setWord(fbu->handle, "Status", ID_STATUS_OFFLINE);
std::string to_delete(i->key);
i = i->next;
facy.buddies.erase(to_delete);
} else {
i = i->next;
if (!fbu->handle) // just been added
fbu->handle = AddToContactList(fbu, CONTACT_FRIEND);
if (getWord(fbu->handle, "Status", 0) != (int)fbu->status_id)
setWord(fbu->handle, "Status", fbu->status_id);
if (getDword(fbu->handle, "LastActiveTS", 0) != fbu->last_active) {
if (fbu->last_active > 0)
setDword(fbu->handle, "LastActiveTS", fbu->last_active);
else
delSetting(fbu->handle, "LastActiveTS");
}
if (getByte(fbu->handle, FACEBOOK_KEY_CONTACT_TYPE, 0) != CONTACT_FRIEND) {
setByte(fbu->handle, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_FRIEND);
// TODO: remove that popup and use "Contact added you" event?
}
// Wasn't contact removed from "server-list" someday?
if (getDword(fbu->handle, FACEBOOK_KEY_DELETED, 0)) {
delSetting(fbu->handle, FACEBOOK_KEY_DELETED);
std::string url = FACEBOOK_URL_PROFILE + fbu->user_id;
std::string contactname = getContactName(this, fbu->handle, !fbu->real_name.empty() ? fbu->real_name.c_str() : fbu->user_id.c_str());
ptrT szTitle(mir_utf8decodeT(contactname.c_str()));
NotifyEvent(szTitle, TranslateT("Contact is back on server-list."), fbu->handle, FACEBOOK_EVENT_FRIENDSHIP, &url);
}
// Check avatar change
CheckAvatarChange(fbu->handle, fbu->image_url);
}
}
debugLogA("*** Buddy list processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing buddy list: %s", e.what());
CODE_BLOCK_END
}
void FacebookProto::ProcessFriendList(void*)
{
ScopedLock s(facy.buddies_lock_);
if (isOffline())
return;
facy.handle_entry("load_friends");
// Get buddy list
http::response resp = facy.flap(REQUEST_USER_INFO_ALL);
if (resp.code != HTTP_CODE_OK) {
facy.handle_error("load_friends");
return;
}
debugLogA("*** Starting processing friend list");
CODE_BLOCK_TRY
std::map friends;
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_friends(&resp.data, &friends);
delete p;
// Check and update old contacts
for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
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);
}
facebook_user *fbu;
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (id != NULL) {
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
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
if (getByte(hContact, "Gender", 0) != (int)fbu->gender)
setByte(hContact, "Gender", fbu->gender);
// TODO: remove this in some future version?
// Remove old useless "RealName" field
ptrA realname(getStringA(hContact, "RealName"));
if (realname != NULL) {
delSetting(hContact, "RealName");
}
// Update real name and nick
if (!fbu->real_name.empty()) {
SaveName(hContact, fbu);
}
// Update username
ptrA username(getStringA(hContact, FACEBOOK_KEY_USERNAME));
if (!username || mir_strcmp(username, fbu->username.c_str())) {
if (!fbu->username.empty())
setString(hContact, FACEBOOK_KEY_USERNAME, fbu->username.c_str());
else
delSetting(hContact, FACEBOOK_KEY_USERNAME);
}
// Update contact type
if (getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, 0) != CONTACT_FRIEND) {
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_FRIEND);
// TODO: remove that popup and use "Contact added you" event?
}
// Wasn't contact removed from "server-list" someday?
if (getDword(hContact, FACEBOOK_KEY_DELETED, 0)) {
delSetting(hContact, FACEBOOK_KEY_DELETED);
std::string url = FACEBOOK_URL_PROFILE + fbu->user_id;
std::string contactname = getContactName(this, hContact, !fbu->real_name.empty() ? fbu->real_name.c_str() : fbu->user_id.c_str());
ptrT szTitle(mir_utf8decodeT(contactname.c_str()));
NotifyEvent(szTitle, TranslateT("Contact is back on server-list."), hContact, FACEBOOK_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 was removed from "server-list", notify it
// Wasnt we already been notified about this contact? And was this real friend?
if (!getDword(hContact, FACEBOOK_KEY_DELETED, 0) && getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, 0) == CONTACT_FRIEND) {
setDword(hContact, FACEBOOK_KEY_DELETED, ::time(NULL));
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE);
std::string url = FACEBOOK_URL_PROFILE + std::string(id);
std::string contactname = getContactName(this, hContact, id);
ptrT szTitle(mir_utf8decodeT(contactname.c_str()));
NotifyEvent(szTitle, TranslateT("Contact is no longer on server-list."), hContact, FACEBOOK_EVENT_FRIENDSHIP, &url);
}
}
}
}
// Check remaining contacts in map and add them to contact list
for (std::map< std::string, facebook_user* >::iterator it = friends.begin(); it != friends.end();) {
if (!it->second->deleted)
AddToContactList(it->second, CONTACT_FRIEND, true); // we know this contact doesn't exists, so we force add it
delete it->second;
it = friends.erase(it);
}
friends.clear();
debugLogA("*** Friend list processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing friend list: %s", e.what());
CODE_BLOCK_END
}
void FacebookProto::ProcessUnreadMessages(void*)
{
if (isOffline())
return;
facy.handle_entry("ProcessUnreadMessages");
// receive messages from all folders by default, use hidden setting to receive only inbox messages
bool inboxOnly = getBool(FACEBOOK_KEY_INBOX_ONLY, 0);
std::string data = "folders[0]=inbox";
if (!inboxOnly)
data += "&folders[1]=other";
data += "&client=mercury";
data += "__user=" + facy.self_.user_id;
data += "&fb_dtsg=" + facy.dtsg_;
data += "&__a=1&__dyn=&__req=&ttstamp=" + facy.ttstamp();
http::response resp = facy.flap(REQUEST_UNREAD_THREADS, &data);
if (resp.code != HTTP_CODE_OK) {
facy.handle_error("ProcessUnreadMessages");
return;
}
CODE_BLOCK_TRY
std::vector threads;
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_unread_threads(&resp.data, &threads, inboxOnly);
delete p;
ForkThread(&FacebookProto::ProcessUnreadMessage, new std::vector(threads));
debugLogA("*** Unread threads list processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing unread threads list: %s", e.what());
CODE_BLOCK_END
facy.handle_success("ProcessUnreadMessages");
}
void FacebookProto::ProcessUnreadMessage(void *data)
{
if (data == NULL)
return;
std::vector *threads = (std::vector*)data;
if (isOffline()) {
delete threads;
return;
}
facy.handle_entry("ProcessUnreadMessage");
int offset = 0;
int limit = 21;
// receive messages from all folders by default, use hidden setting to receive only inbox messages
bool inboxOnly = getBool(FACEBOOK_KEY_INBOX_ONLY, 0);
http::response resp;
while (!threads->empty()) {
std::string data = "client=mercury";
data += "&__user=" + facy.self_.user_id;
data += "&fb_dtsg=" + facy.dtsg_;
data += "&__a=1&__dyn=&__req=&ttstamp=" + facy.ttstamp();
for (std::vector::size_type i = 0; i < threads->size(); i++) {
std::string thread_id = utils::url::encode(threads->at(i));
// request messages from thread
data += "&messages[thread_ids][" + thread_id;
data += "][offset]=" + utils::conversion::to_string(&offset, UTILS_CONV_SIGNED_NUMBER);
data += "&messages[thread_ids][" + thread_id;
data += "][limit]=" + utils::conversion::to_string(&limit, UTILS_CONV_SIGNED_NUMBER);
// request info about thread
data += "&threads[thread_ids][" + utils::conversion::to_string(&i, UTILS_CONV_UNSIGNED_NUMBER);
data += "]=" + thread_id;
}
resp = facy.flap(REQUEST_THREAD_INFO, &data);
if (resp.code == HTTP_CODE_OK) {
CODE_BLOCK_TRY
std::vector messages;
std::map chatrooms;
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_thread_messages(&resp.data, &messages, &chatrooms, false, inboxOnly);
delete p;
for (std::map::iterator it = chatrooms.begin(); it != chatrooms.end();) {
// TODO: refactor this too!
// TODO: have all chatrooms in facy, in memory, and then handle them as needed... somehow think about it...
/* facebook_chatroom *room = it->second;
MCONTACT hChatContact = NULL;
ptrA users(GetChatUsers(room->thread_id.c_str()));
if (users == NULL) {
AddChat(room->thread_id.c_str(), room->chat_name.c_str());
hChatContact = ChatIDToHContact(room->thread_id);
// Set thread id (TID) for later
setTString(hChatContact, FACEBOOK_KEY_TID, room->thread_id.c_str());
for (std::map::iterator jt = room->participants.begin(); jt != room->participants.end(); ) {
AddChatContact(room->thread_id.c_str(), jt->first.c_str(), jt->second.c_str());
++jt;
}
}
if (!hChatContact)
hChatContact = ChatIDToHContact(room->thread_id);
ForkThread(&FacebookProto::ReadMessageWorker, (void*)hChatContact);*/
delete it->second;
it = chatrooms.erase(it);
}
chatrooms.clear();
ReceiveMessages(messages, true);
debugLogA("*** Unread messages processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing unread messages: %s", e.what());
CODE_BLOCK_END
facy.handle_success("ProcessUnreadMessage");
}
else {
facy.handle_error("ProcessUnreadMessage");
}
offset += limit;
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 *p)
{
if (p == NULL)
return;
if (isOffline()) {
delete (MCONTACT*)p;
return;
}
facy.handle_entry("LoadLastMessages");
MCONTACT hContact = *(MCONTACT*)p;
delete (MCONTACT*)p;
if (!isOnline())
return;
std::string data = "client=mercury";
data += "&__user=" + facy.self_.user_id;
data += "&fb_dtsg=" + facy.dtsg_;
data += "&__a=1&__dyn=&__req=&ttstamp=" + facy.ttstamp();
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 == NULL) {
debugLogA("!!! LoadLastMessages(): Contact has no TID/ID");
return;
}
std::string id = utils::url::encode(std::string(item_id));
std::string type = isChat ? "thread_ids" : "user_ids";
int count = getByte(FACEBOOK_KEY_MESSAGES_ON_OPEN_COUNT, DEFAULT_MESSAGES_ON_OPEN_COUNT);
count = min(count, FACEBOOK_MESSAGES_ON_OPEN_LIMIT);
// request messages from thread
data += "&messages[" + type + "][" + id;
data += "][offset]=0";
data += "&messages[" + type + "][" + id;
data += "][limit]=" + utils::conversion::to_string(&count, UTILS_CONV_UNSIGNED_NUMBER);
// request info about thread
data += "&threads[" + type + "][0]=" + id;
http::response resp = facy.flap(REQUEST_THREAD_INFO, &data);
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);
CODE_BLOCK_TRY
std::vector messages;
std::map chatrooms;
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_thread_messages(&resp.data, &messages, &chatrooms, false, false);
delete p;
// TODO: do something with this, chat is loading somewhere else... (in receiveMessages method right now)
/*for (std::map::iterator it = chatrooms.begin(); it != chatrooms.end();) {
facebook_chatroom *room = it->second;
MCONTACT hChatContact = NULL;
ptrA users(GetChatUsers(room->thread_id.c_str()));
if (users == NULL) {
AddChat(room->thread_id.c_str(), room->chat_name.c_str());
hChatContact = ChatIDToHContact(room->thread_id);
// Set thread id (TID) for later
setTString(hChatContact, FACEBOOK_KEY_TID, room->thread_id.c_str());
for (std::map::iterator jt = room->participants.begin(); jt != room->participants.end();) {
AddChatContact(room->thread_id.c_str(), jt->first.c_str(), jt->second.c_str());
++jt;
}
}
if (!hChatContact)
hChatContact = ChatIDToHContact(room->thread_id);
ForkThread(&FacebookProto::ReadMessageWorker, (void*)hChatContact);
delete it->second;
it = chatrooms.erase(it);
}
chatrooms.clear();*/
ReceiveMessages(messages, true);
debugLogA("*** Thread messages processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing thread messages: %s", e.what());
CODE_BLOCK_END
facy.handle_success("LoadLastMessages");
// Enable marking messages as read for this contact
facy.ignore_read.erase(hContact);
// And force mark read
OnDbEventRead(hContact, NULL);
}
void FacebookProto::SyncThreads(void*)
{
facy.handle_entry("SyncThreads");
if (isOffline())
return;
// Always load unread messages because syncthreads request is not reliable (probably doesn't load multi user chat messages at all)
ProcessUnreadMessages(NULL);
// Get timestamp of last action (message or other event)
time_t timestamp = getDword(FACEBOOK_KEY_LAST_ACTION_TS, 0);
// If last event is older than 2 day, we force sync to be max. 2 day old
time_t daysBefore = ::time(NULL) - 24 * 60 * 60 * 2;
if (timestamp < daysBefore) {
debugLogA(" Last action timestamp is too old: %d, use 24 hours old instead: %d", timestamp, daysBefore);
timestamp = daysBefore;
}
// Receive messages from all folders by default, use hidden setting to receive only inbox messages
bool inboxOnly = getBool(FACEBOOK_KEY_INBOX_ONLY, 0);
// Get milli timestamp string for Facebook
std::string time = utils::conversion::to_string((void*)×tamp, UTILS_CONV_TIME_T) + "000";
std::string data = "client=mercury";
data += "&last_action_timestamp=" + time;
data += "&__user=" + facy.self_.user_id;
data += "&fb_dtsg=" + facy.dtsg_;
data += "&folders[0]=inbox";
if (!inboxOnly)
data += "&folders[1]=other";
data += "&__req=7&__a=1&__dyn=&__req=&__rev=&ttstamp=" + facy.ttstamp();
debugLogA(" Facebook's milli timestamp for sync: %s", time.c_str());
http::response resp = facy.flap(REQUEST_THREAD_SYNC, &data);
if (resp.code != HTTP_CODE_OK || resp.data.empty()) {
facy.handle_error("LoadLastMessages");
return;
}
CODE_BLOCK_TRY
std::vector messages;
std::map chatrooms;
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_thread_messages(&resp.data, &messages, &chatrooms, false, false);
delete p;
ReceiveMessages(messages, true);
debugLogA("*** Thread messages processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing thread messages: %s", e.what());
CODE_BLOCK_END
facy.handle_success("SyncThreads");
}
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("