/*
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"
void updateStringUtf(FacebookProto *proto, MCONTACT hContact, const char *key, const std::string &value) {
bool update_required = true;
DBVARIANT dbv;
if (!proto->getStringUtf(hContact, key, &dbv)) {
update_required = mir_strcmp(dbv.pszVal, value.c_str()) != 0;
db_free(&dbv);
}
if (update_required) {
proto->setStringUtf(hContact, key, value.c_str());
}
}
void FacebookProto::SaveName(MCONTACT hContact, const facebook_user *fbu)
{
// Save nick
std::string nick = fbu->real_name;
if (!getBool(FACEBOOK_KEY_NAME_AS_NICK, 1) && !fbu->nick.empty())
nick = fbu->nick;
updateStringUtf(this, hContact, FACEBOOK_KEY_NICK, nick);
// Explode whole name into first, second and last name
std::vector names;
utils::text::explode(fbu->real_name, " ", &names);
updateStringUtf(this, hContact, FACEBOOK_KEY_FIRST_NAME, names.size() > 0 ? names.front().c_str() : "");
updateStringUtf(this, hContact, FACEBOOK_KEY_LAST_NAME, names.size() > 1 ? names.back().c_str() : "");
std::string middle;
if (names.size() > 2) {
for (std::string::size_type i = 1; i < names.size() - 1; i++) {
if (!middle.empty())
middle += " ";
middle += names.at(i);
}
}
updateStringUtf(this, hContact, FACEBOOK_KEY_SECOND_NAME, middle);
}
bool FacebookProto::IsMyContact(MCONTACT hContact, bool include_chat)
{
const char *proto = GetContactProto(hContact);
if (proto && !mir_strcmp(m_szModuleName, proto)) {
if (include_chat)
return true;
return !isChatRoom(hContact);
}
return false;
}
MCONTACT FacebookProto::ChatIDToHContact(const std::string &chat_id)
{
if (chat_id.empty()) {
debugLogA("!!! Calling ChatIDToContactID() with empty chat_id");
return 0;
}
// First check cache
auto it = facy.chat_id_to_hcontact.find(chat_id);
if (it != facy.chat_id_to_hcontact.end()) {
// Check if contact is still valid
if (CallService(MS_DB_CONTACT_IS, (WPARAM)it->second) == 1)
return it->second;
else
facy.chat_id_to_hcontact.erase(it);
}
// Go through all local contacts
for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
if (!IsMyContact(hContact, true))
continue;
ptrA id(getStringA(hContact, "ChatRoomID"));
if (id && !mir_strcmp(id, chat_id.c_str())) {
facy.chat_id_to_hcontact.insert(std::make_pair(chat_id, hContact));
return hContact;
}
}
return 0;
}
MCONTACT FacebookProto::ContactIDToHContact(const std::string &user_id)
{
if (user_id.empty()) {
debugLogA("!!! Calling ContactIDToHContact() with empty user_id");
return 0;
}
// First check cache
std::map::iterator it = facy.user_id_to_hcontact.find(user_id);
if (it != facy.user_id_to_hcontact.end()) {
// Check if contact is still valid
if (CallService(MS_DB_CONTACT_IS, (WPARAM)it->second) == 1)
return it->second;
else
facy.user_id_to_hcontact.erase(it);
}
// Go through all local contacts
for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
if (isChatRoom(hContact))
continue;
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (id && !mir_strcmp(id, user_id.c_str())) {
facy.user_id_to_hcontact.insert(std::make_pair(user_id, hContact));
return hContact;
}
}
return 0;
}
std::string FacebookProto::ThreadIDToContactID(const std::string &thread_id)
{
if (thread_id.empty()) {
debugLogA("!!! Calling ThreadIDToContactID() with empty thread_id");
return "";
}
// First check cache
std::map::iterator it = facy.thread_id_to_user_id.find(thread_id);
if (it != facy.thread_id_to_user_id.end()) {
return it->second;
}
// Go through all local contacts
for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
if (!IsMyContact(hContact))
continue;
ptrA tid(getStringA(hContact, FACEBOOK_KEY_TID));
if (tid && !mir_strcmp(tid, thread_id.c_str())) {
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
std::string user_id = (id ? id : "");
if (!user_id.empty()) {
facy.thread_id_to_user_id.insert(std::make_pair(thread_id, user_id));
return user_id;
}
break; // this shouldn't happen unless user manually deletes ID from FB contact in DB
}
}
// We don't have any contact with this thread_id cached, we must ask server
if (isOffline())
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_;
data += "&threads[thread_ids][0]=" + utils::url::encode(thread_id);
std::string user_id;
http::response resp = facy.flap(REQUEST_THREAD_INFO, &data);
if (resp.code == HTTP_CODE_OK) {
CODE_BLOCK_TRY
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_thread_info(&resp.data, &user_id);
delete p;
if (!user_id.empty())
facy.thread_id_to_user_id.insert(std::make_pair(thread_id, user_id));
debugLogA("*** Thread info processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing thread info: %s", e.what());
CODE_BLOCK_END
}
return user_id;
}
void FacebookProto::LoadContactInfo(facebook_user* fbu)
{
if (isOffline())
return;
// TODO: support for more friends at once
std::string get_query = "&ids[0]=" + utils::url::encode(fbu->user_id);
http::response resp = facy.flap(REQUEST_USER_INFO, NULL, &get_query);
if (resp.code == HTTP_CODE_OK) {
CODE_BLOCK_TRY
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_user_info(&resp.data, fbu);
delete p;
debugLogA("*** Contact thread info processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing contact thread info: %s", e.what());
CODE_BLOCK_END
}
}
void FacebookProto::LoadParticipantsNames(facebook_chatroom *fbc)
{
for (std::map::iterator it = fbc->participants.begin(); it != fbc->participants.end(); ++it) {
if (it->second.empty()) {
if (!mir_strcmp(it->first.c_str(), facy.self_.user_id.c_str()))
it->second = facy.self_.real_name;
else {
MCONTACT hContact = ContactIDToHContact(it->first.c_str());
if (hContact != NULL) {
DBVARIANT dbv;
if (!getStringUtf(hContact, FACEBOOK_KEY_NICK, &dbv)) {
it->second = dbv.pszVal;
db_free(&dbv);
}
// TODO: set correct role (friend/user) for this contact here - need rework participants map to
}
// TODO: load unknown contact's names from server
if (it->second.empty())
it->second = it->first;
//if (isOffline())
// return;
}
}
}
}
void FacebookProto::LoadChatInfo(facebook_chatroom *fbc)
{
if (isOffline())
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_;
std::string thread_id = utils::url::encode(fbc->thread_id);
// request info about thread
data += "&threads[thread_ids][0]=" + thread_id;
http::response resp = facy.flap(REQUEST_THREAD_INFO, &data);
if (resp.code != HTTP_CODE_OK) {
facy.handle_error("LoadChatInfo");
return;
}
CODE_BLOCK_TRY
facebook_json_parser* p = new facebook_json_parser(this);
p->parse_chat_info(&resp.data, fbc);
delete p;
// Load missing participants names
LoadParticipantsNames(fbc);
// If chat has no name, create name from participants list
if (fbc->chat_name.empty()) {
unsigned int namesUsed = 0;
for (std::map::iterator it = fbc->participants.begin(); it != fbc->participants.end(); ++it) {
std::string participant = it->second;
// Ignore empty and numeric only participant names
if (participant.empty() || participant.find_first_not_of("0123456789") == std::string::npos)
continue;
if (namesUsed > 0)
fbc->chat_name += _T(", ");
std::tstring tname = _A2T(participant.c_str(), CP_UTF8);
fbc->chat_name += utils::text::prepare_name(tname, false);
if (++namesUsed >= FACEBOOK_CHATROOM_NAMES_COUNT)
break;
}
if (fbc->participants.size() > namesUsed) {
TCHAR more[200];
mir_sntprintf(more, TranslateT("%s and more (%d)"), fbc->chat_name.c_str(), fbc->participants.size() - namesUsed);
fbc->chat_name = more;
}
// If there are no participants to create a name from, use just thread_id
if (fbc->chat_name.empty())
fbc->chat_name = std::tstring(_A2T(fbc->thread_id.c_str())); // TODO: is this needed? Isn't it showed automatically as id if there is no name?
}
//ReceiveMessages(messages, true); // don't let it fall into infinite cycle, solve it somehow...
debugLogA("*** Chat thread info processed");
CODE_BLOCK_CATCH
debugLogA("*** Error processing chat thread info: %s", e.what());
CODE_BLOCK_END
facy.handle_success("LoadChatInfo");
}
MCONTACT FacebookProto::AddToContactList(facebook_user* fbu, ContactType type, bool force_add, bool add_temporarily)
{
// Ignore self user completely
if (fbu->user_id == facy.self_.user_id)
return NULL;
// First, check if this contact exists (and if does, just return it)
if (!force_add) {
MCONTACT hContact = ContactIDToHContact(fbu->user_id);
if (hContact)
return hContact;
}
// Try to make a new contact
MCONTACT hContact = (MCONTACT)CallService(MS_DB_CONTACT_ADD);
if (hContact && Proto_AddToContact(hContact, m_szModuleName) != 0) {
CallService(MS_DB_CONTACT_DELETE, hContact);
hContact = NULL;
}
// If we have some contact, we'll save its data
if (hContact) {
// Save these values only when adding new contact, not when updating existing
if (add_temporarily) {
db_set_b(hContact, "CList", "Hidden", 1);
db_set_b(hContact, "CList", "NotOnList", 1);
}
setString(hContact, FACEBOOK_KEY_ID, fbu->user_id.c_str());
std::string homepage = FACEBOOK_URL_PROFILE + fbu->user_id;
setString(hContact, "Homepage", homepage.c_str());
setTString(hContact, "MirVer", fbu->getMirVer());
db_unset(hContact, "CList", "MyHandle");
if (m_tszDefaultGroup)
db_set_ts(hContact, "CList", "Group", m_tszDefaultGroup);
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, type);
if (getByte(FACEBOOK_KEY_DISABLE_STATUS_NOTIFY, 0))
CallService(MS_IGNORE_IGNORE, hContact, (LPARAM)IGNOREEVENT_USERONLINE);
if (!fbu->real_name.empty())
SaveName(hContact, fbu);
if (!fbu->username.empty())
setString(hContact, FACEBOOK_KEY_USERNAME, fbu->username.c_str());
if (fbu->gender)
setByte(hContact, "Gender", fbu->gender);
// CheckAvatarChange(hContact, fbu->image_url);
}
return hContact;
}
void FacebookProto::SetAllContactStatuses(int status)
{
for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) {
if (isChatRoom(hContact))
continue;
if (getWord(hContact, "Status", 0) != status)
setWord(hContact, "Status", status);
}
}
void FacebookProto::DeleteContactFromServer(void *data)
{
facy.handle_entry("DeleteContactFromServer");
if (data == NULL)
return;
std::string id = *(std::string*)data;
delete (std::string*)data;
if (isOffline())
return;
std::string query = "norefresh=true&unref=button_dropdown&confirmed=1&__a=1";
query += "&fb_dtsg=" + facy.dtsg_;
query += "&uid=" + id;
query += "&__user=" + facy.self_.user_id;
query += "&ttstamp=" + facy.ttstamp_;
std::string get_query = "norefresh=true&unref=button_dropdown&uid=" + id;
// Get unread inbox threads
http::response resp = facy.flap(REQUEST_DELETE_FRIEND, &query, &get_query);
if (resp.data.find("\"payload\":null", 0) != std::string::npos)
{
facebook_user* fbu = facy.buddies.find(id);
if (fbu != NULL)
fbu->deleted = true;
MCONTACT hContact = ContactIDToHContact(id);
// If contact wasn't deleted from database
if (hContact != NULL) {
setWord(hContact, "Status", ID_STATUS_OFFLINE);
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE);
setDword(hContact, FACEBOOK_KEY_DELETED, ::time(NULL));
}
NotifyEvent(m_tszUserName, TranslateT("Contact was removed from your server list."), NULL, FACEBOOK_EVENT_FRIENDSHIP);
}
else {
facy.client_notify(TranslateT("Error occurred when removing contact from server."));
}
if (resp.code != HTTP_CODE_OK)
facy.handle_error("DeleteContactFromServer");
}
void FacebookProto::AddContactToServer(void *data)
{
facy.handle_entry("AddContactToServer");
if (data == NULL)
return;
std::string id = *(std::string*)data;
delete (std::string*)data;
if (isOffline())
return;
std::string query = "action=add_friend&how_found=profile_button&ref_param=ts&outgoing_id=&unwanted=&logging_location=&no_flyout_on_click=false&ego_log_data=&lsd=";
query += "&fb_dtsg=" + facy.dtsg_;
query += "&to_friend=" + id;
query += "&__user=" + facy.self_.user_id;
// Get unread inbox threads
http::response resp = facy.flap(REQUEST_ADD_FRIEND, &query);
if (resp.data.find("\"success\":true", 0) != std::string::npos) {
MCONTACT hContact = ContactIDToHContact(id);
// If contact wasn't deleted from database
if (hContact != NULL)
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_REQUEST);
NotifyEvent(m_tszUserName, TranslateT("Request for friendship was sent."), NULL, FACEBOOK_EVENT_FRIENDSHIP);
}
else facy.client_notify(TranslateT("Error occurred when requesting friendship."));
if (resp.code != HTTP_CODE_OK)
facy.handle_error("AddContactToServer");
}
void FacebookProto::ApproveContactToServer(void *data)
{
facy.handle_entry("ApproveContactToServer");
if (data == NULL)
return;
MCONTACT hContact = *(MCONTACT*)data;
delete (MCONTACT*)data;
if (isOffline())
return;
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (!id)
return;
std::string query = "action=confirm";
query += "&id=" + std::string(id);
query += "&__user=" + facy.self_.user_id;
query += "&fb_dtsg=" + facy.dtsg_;
// Ignore friendship request
http::response resp = facy.flap(REQUEST_FRIENDSHIP, &query);
if (resp.data.find("\"success\":true") != std::string::npos)
{
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_FRIEND);
NotifyEvent(m_tszUserName, TranslateT("Request for friendship was accepted."), NULL, FACEBOOK_EVENT_FRIENDSHIP);
}
else facy.client_notify(TranslateT("Error occurred when accepting friendship request."));
if (resp.code != HTTP_CODE_OK)
facy.handle_error("ApproveContactToServer");
}
void FacebookProto::CancelFriendsRequest(void *data)
{
facy.handle_entry("CancelFriendsRequest");
if (data == NULL)
return;
MCONTACT hContact = *(MCONTACT*)data;
delete (MCONTACT*)data;
if (isOffline())
return;
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (!id)
return;
std::string query = "confirmed=1";
query += "&fb_dtsg=" + facy.dtsg_;
query += "&__user=" + facy.self_.user_id;
query += "&friend=" + std::string(id);
// Cancel (our) friendship request
http::response resp = facy.flap(REQUEST_CANCEL_FRIENDSHIP, &query);
if (resp.data.find("\"payload\":null", 0) != std::string::npos)
{
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE);
NotifyEvent(m_tszUserName, TranslateT("Request for friendship was canceled."), NULL, FACEBOOK_EVENT_FRIENDSHIP);
}
else facy.client_notify(TranslateT("Error occurred when canceling friendship request."));
if (resp.code != HTTP_CODE_OK)
facy.handle_error("CancelFriendsRequest");
}
void FacebookProto::IgnoreFriendshipRequest(void *data)
{
facy.handle_entry("IgnoreFriendshipRequest");
if (data == NULL)
return;
MCONTACT hContact = *(MCONTACT*)data;
delete (MCONTACT*)data;
if (isOffline())
return;
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (!id)
return;
std::string query = "action=reject";
query += "&id=" + std::string(id);
query += "&__user=" + facy.self_.user_id;
query += "&fb_dtsg=" + facy.dtsg_;
// Ignore friendship request
http::response resp = facy.flap(REQUEST_FRIENDSHIP, &query);
if (resp.data.find("\"success\":true") != std::string::npos)
{
setByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, CONTACT_NONE);
NotifyEvent(m_tszUserName, TranslateT("Request for friendship was ignored."), NULL, FACEBOOK_EVENT_FRIENDSHIP);
// Delete this contact, if he's temporary
if (db_get_b(hContact, "CList", "NotOnList", 0))
CallService(MS_DB_CONTACT_DELETE, hContact);
}
else facy.client_notify(TranslateT("Error occurred when ignoring friendship request."));
if (resp.code != HTTP_CODE_OK)
facy.handle_error("IgnoreFriendshipRequest");
}
void FacebookProto::SendPokeWorker(void *p)
{
facy.handle_entry("SendPokeWorker");
if (p == NULL)
return;
std::string *id = (std::string*)p;
if (isOffline()) {
delete id;
return;
}
std::string data = "poke_target=" + *id;
data += "&do_confirm=0";
data += "&fb_dtsg=" + facy.dtsg_;
data += "&__user=" + facy.self_.user_id;
data += "&ttstamp=" + facy.ttstamp_;
// Send poke
http::response resp = facy.flap(REQUEST_POKE, &data);
if (resp.data.find("\"payload\":null", 0) != std::string::npos) {
resp.data = utils::text::slashu_to_utf8(
utils::text::source_get_value(&resp.data, 2, "__html\":\"", "\"}"));
std::string message = utils::text::source_get_value(&resp.data, 4, "", "<\\/div>");
if (message.empty()) // message has different format, show whole message
message = resp.data;
message = utils::text::html_entities_decode(
utils::text::remove_html(message));
ptrT tmessage(mir_utf8decodeT(message.c_str()));
NotifyEvent(m_tszUserName, tmessage, NULL, FACEBOOK_EVENT_OTHER);
}
facy.handle_success("SendPokeWorker");
delete id;
}
HANDLE FacebookProto::GetAwayMsg(MCONTACT)
{
return 0; // Status messages are disabled
}
int FacebookProto::OnContactDeleted(WPARAM wParam, LPARAM)
{
MCONTACT hContact = (MCONTACT)wParam;
// Remove this contact from caches
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (id)
facy.user_id_to_hcontact.erase(std::string(id));
ptrA tid(getStringA(hContact, FACEBOOK_KEY_TID));
if (tid)
facy.thread_id_to_user_id.erase(std::string(tid));
if (isChatRoom(hContact)) {
ptrA chat_id(getStringA(hContact, "ChatRoomID"));
if (chat_id)
facy.chat_id_to_hcontact.erase(std::string(chat_id));
}
// Cancel friendship (with confirmation)
CancelFriendship(hContact, 1);
return 0;
}
void FacebookProto::StartTyping(MCONTACT hContact) {
// ignore if contact is already typing
if (facy.typers.find(hContact) != facy.typers.end())
return;
// show notification and insert into typing set
CallService(MS_PROTO_CONTACTISTYPING, hContact, (LPARAM)FACEBOOK_TYPING_TIME);
facy.typers.insert(hContact);
}
void FacebookProto::StopTyping(MCONTACT hContact) {
// ignore if contact is not typing
if (facy.typers.find(hContact) == facy.typers.end())
return;
// show notification and remove from typing set
CallService(MS_PROTO_CONTACTISTYPING, hContact, (LPARAM)PROTOTYPE_CONTACTTYPING_OFF);
facy.typers.erase(hContact);
}