/*
Facebook plugin for Miranda Instant Messenger
_____________________________________________
Copyright © 2009-11 Michal Zelinka, 2011-13 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 "common.h"
FacebookProto::FacebookProto(const char* proto_name,const TCHAR* username) :
PROTO(proto_name, username)
{
facy.parent = this;
signon_lock_ = CreateMutex(NULL, FALSE, NULL);
avatar_lock_ = CreateMutex(NULL, FALSE, NULL);
log_lock_ = CreateMutex(NULL, FALSE, NULL);
update_loop_lock_ = CreateEvent(NULL, FALSE, FALSE, NULL);
facy.buddies_lock_ = CreateMutex(NULL, FALSE, NULL);
facy.send_message_lock_ = CreateMutex(NULL, FALSE, NULL);
facy.fcb_conn_lock_ = CreateMutex(NULL, FALSE, NULL);
CreateService(PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI);
CreateService(PS_GETMYAWAYMSG, &FacebookProto::GetMyAwayMsg);
CreateService(PS_GETMYAVATART, &FacebookProto::GetMyAvatar);
CreateService(PS_GETAVATARINFOT, &FacebookProto::GetAvatarInfo);
CreateService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps);
CreateService(PS_JOINCHAT, &FacebookProto::OnJoinChat);
CreateService(PS_LEAVECHAT, &FacebookProto::OnLeaveChat);
CreateService("/Mind", &FacebookProto::OnMind);
HookEvent(ME_CLIST_PREBUILDSTATUSMENU, &FacebookProto::OnBuildStatusMenu);
HookEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit);
HookEvent(ME_GC_EVENT, &FacebookProto::OnChatOutgoing);
HookEvent(ME_IDLE_CHANGED, &FacebookProto::OnIdleChanged);
HookEvent(ME_TTB_MODULELOADED, &FacebookProto::OnToolbarInit);
char module[512];
mir_snprintf(module, sizeof(module), "%s/Mind", m_szModuleName);
HOTKEYDESC hkd = { sizeof(hkd) };
hkd.dwFlags = HKD_TCHAR;
hkd.ptszDescription = LPGENT("Show Mind Window");
hkd.pszName = "ShowMindWnd";
hkd.ptszSection = m_tszUserName;
hkd.pszService = module;
hkd.DefHotKey = HOTKEYCODE(HOTKEYF_ALT|HOTKEYF_EXT, 'F');
Hotkey_Register(&hkd);
// Create standard network connection
TCHAR descr[512];
NETLIBUSER nlu = {sizeof(nlu)};
nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_TCHAR;
nlu.szSettingsModule = m_szModuleName;
mir_snprintf(module, SIZEOF(module), "%s", m_szModuleName);
nlu.szSettingsModule = module;
mir_sntprintf(descr, SIZEOF(descr), TranslateT("%s server connection"), m_tszUserName);
nlu.ptszDescriptiveName = descr;
m_hNetlibUser = (HANDLE)CallService(MS_NETLIB_REGISTERUSER, 0, (LPARAM)&nlu);
if (m_hNetlibUser == NULL)
MessageBox(NULL, TranslateT("Unable to get Netlib connection for Facebook"), m_tszUserName, MB_OK);
facy.set_handle(m_hNetlibUser);
SkinAddNewSoundExT("Notification", m_tszUserName, LPGENT("Notification"));
SkinAddNewSoundExT("NewsFeed", m_tszUserName, LPGENT("News Feed"));
SkinAddNewSoundExT("OtherEvent", m_tszUserName, LPGENT("Other Event"));
mir_sntprintf(descr, SIZEOF(descr), _T("%%miranda_avatarcache%%\\%s"), m_tszUserName);
hAvatarFolder_ = FoldersRegisterCustomPathT(LPGEN("Avatars"), m_szModuleName, descr, m_tszUserName);
// Set all contacts offline -- in case we crashed
SetAllContactStatuses(ID_STATUS_OFFLINE, true);
}
FacebookProto::~FacebookProto()
{
Netlib_CloseHandle(m_hNetlibUser);
WaitForSingleObject(signon_lock_, IGNORE);
WaitForSingleObject(avatar_lock_, IGNORE);
WaitForSingleObject(log_lock_, IGNORE);
WaitForSingleObject(facy.buddies_lock_, IGNORE);
WaitForSingleObject(facy.send_message_lock_, IGNORE);
CloseHandle(signon_lock_);
CloseHandle(avatar_lock_);
CloseHandle(log_lock_);
CloseHandle(update_loop_lock_);
CloseHandle(facy.buddies_lock_);
CloseHandle(facy.send_message_lock_);
CloseHandle(facy.fcb_conn_lock_);
}
//////////////////////////////////////////////////////////////////////////////
DWORD_PTR FacebookProto::GetCaps(int type, HANDLE hContact)
{
switch(type)
{
case PFLAGNUM_1:
{
DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ | /*PF1_ADDED |*/ PF1_BASICSEARCH | PF1_SEARCHBYEMAIL | PF1_SEARCHBYNAME | PF1_ADDSEARCHRES; // | PF1_VISLIST | PF1_INVISLIST;
if (getByte(FACEBOOK_KEY_SET_MIRANDA_STATUS, 0))
return flags |= PF1_MODEMSG;
else
return flags |= PF1_MODEMSGRECV;
}
case PFLAGNUM_2:
return PF2_ONLINE | PF2_INVISIBLE | PF2_ONTHEPHONE | PF2_IDLE; // | PF2_SHORTAWAY;
case PFLAGNUM_3:
if (getByte(FACEBOOK_KEY_SET_MIRANDA_STATUS, 0))
return PF2_ONLINE; // | PF2_SHORTAWAY;
else
return 0;
case PFLAGNUM_4:
return PF4_NOCUSTOMAUTH | PF4_FORCEADDED | PF4_IMSENDUTF | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE;
case PFLAGNUM_5:
return PF2_ONTHEPHONE;
case PFLAG_MAXLENOFMESSAGE:
return FACEBOOK_MESSAGE_LIMIT;
case PFLAG_UNIQUEIDTEXT:
return (DWORD_PTR) "Facebook ID";
case PFLAG_UNIQUEIDSETTING:
return (DWORD_PTR) FACEBOOK_KEY_ID;
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////
int FacebookProto::SetStatus(int new_status)
{
LOG("===== Beginning SetStatus process");
// Routing statuses not supported by Facebook
switch (new_status)
{
case ID_STATUS_INVISIBLE:
case ID_STATUS_OFFLINE:
m_iDesiredStatus = new_status;
break;
case ID_STATUS_IDLE:
default:
m_iDesiredStatus = ID_STATUS_INVISIBLE;
if (getByte(FACEBOOK_KEY_MAP_STATUSES, DEFAULT_MAP_STATUSES))
break;
case ID_STATUS_ONLINE:
case ID_STATUS_FREECHAT:
m_iDesiredStatus = ID_STATUS_ONLINE;
break;
}
if (new_status != ID_STATUS_OFFLINE && m_iStatus == ID_STATUS_CONNECTING) {
LOG("===== Status is already connecting, no change");
return 0;
}
if (m_iStatus == m_iDesiredStatus) {
LOG("===== Statuses are same, no change");
return 0;
}
facy.invisible_ = (new_status == ID_STATUS_INVISIBLE);
ForkThread(&FacebookProto::ChangeStatus, this);
return 0;
}
int FacebookProto::SetAwayMsg(int status, const PROTOCHAR *msg)
{
if (!msg) {
last_status_msg_.clear();
return 0;
}
char *narrow = mir_utf8encodeT(msg);
if (last_status_msg_ != narrow) last_status_msg_ = narrow;
utils::mem::detract(narrow);
if (isOnline() && getByte(FACEBOOK_KEY_SET_MIRANDA_STATUS, DEFAULT_SET_MIRANDA_STATUS))
ForkThread(&FacebookProto::SetAwayMsgWorker, NULL);
return 0;
}
void FacebookProto::SetAwayMsgWorker(void *)
{
if (!last_status_msg_.empty())
facy.set_status(last_status_msg_);
}
HANDLE FacebookProto::SearchBasic(const PROTOCHAR* id)
{
if (isOffline())
return 0;
TCHAR *tid = mir_tstrdup(id);
ForkThread(&FacebookProto::SearchIdAckThread, tid);
return tid;
}
HANDLE FacebookProto::SearchByEmail(const PROTOCHAR* email)
{
if (isOffline())
return 0;
TCHAR *temail = mir_tstrdup(email);
ForkThread(&FacebookProto::SearchAckThread, temail);
return temail;
}
HANDLE FacebookProto::SearchByName(const PROTOCHAR* nick, const PROTOCHAR* firstName, const PROTOCHAR* lastName)
{
TCHAR arg[200];
_sntprintf (arg, SIZEOF(arg), _T("%s %s %s"), nick, firstName, lastName);
return SearchByEmail(arg); // Facebook is using one search method for everything (except IDs)
}
HANDLE FacebookProto::AddToList(int flags, PROTOSEARCHRESULT* psr)
{
ptrA id = mir_t2a_cp(psr->id, CP_UTF8);
ptrA name = mir_t2a_cp(psr->firstName, CP_UTF8);
ptrA surname = mir_t2a_cp(psr->lastName, CP_UTF8);
facebook_user fbu;
fbu.user_id = id;
if (name != NULL)
fbu.real_name = name;
if (surname != NULL) {
fbu.real_name += " ";
fbu.real_name += surname;
}
if (fbu.user_id.find_first_not_of("0123456789") != std::string::npos) {
MessageBox(0, TranslateT("Facebook ID must be numeric value."), m_tszUserName, MB_ICONERROR | MB_OK);
return NULL;
}
HANDLE hContact = AddToContactList(&fbu, CONTACT_NONE);
if (hContact) {
if (flags & PALF_TEMPORARY) {
db_set_b(hContact, "Clist", "Hidden", 1);
db_set_b(hContact, "Clist", "NotOnList", 1);
}
else if (db_get_b(hContact, "CList", "NotOnList", 0)) {
db_unset(hContact, "CList", "Hidden");
db_unset(hContact, "CList", "NotOnList");
}
}
return hContact;
}
int FacebookProto::AuthRequest(HANDLE hContact,const PROTOCHAR *message)
{
return RequestFriendship((WPARAM)hContact, NULL);
}
int FacebookProto::Authorize(HANDLE hDbEvent)
{
if (!hDbEvent || isOffline())
return 1;
HANDLE hContact = HContactFromAuthEvent(hDbEvent);
if (hContact == INVALID_HANDLE_VALUE)
return 1;
return ApproveFriendship((WPARAM)hContact, NULL);
}
int FacebookProto::AuthDeny(HANDLE hDbEvent, const PROTOCHAR *reason)
{
if (!hDbEvent || isOffline())
return 1;
HANDLE hContact = HContactFromAuthEvent(hDbEvent);
if (hContact == INVALID_HANDLE_VALUE)
return 1;
// TODO: hide from facebook requests list
if (db_get_b(hContact, "CList", "NotOnList", 0))
CallService(MS_DB_CONTACT_DELETE, (WPARAM)hContact, 0);
return 0;
}
//////////////////////////////////////////////////////////////////////////////
// SERVICES
INT_PTR FacebookProto::GetMyAwayMsg(WPARAM wParam, LPARAM lParam)
{
DBVARIANT dbv = { DBVT_TCHAR };
if (!getTString("StatusMsg", &dbv) && lstrlen(dbv.ptszVal) != 0)
{
int res = (lParam & SGMA_UNICODE) ? (INT_PTR)mir_t2u(dbv.ptszVal) : (INT_PTR)mir_t2a(dbv.ptszVal);
db_free(&dbv);
return res;
} else {
return 0;
}
}
int FacebookProto::OnIdleChanged(WPARAM wParam, LPARAM lParam)
{
if (m_iStatus == ID_STATUS_INVISIBLE || m_iStatus <= ID_STATUS_OFFLINE)
return 0;
bool bIdle = (lParam & IDF_ISIDLE) != 0;
bool bPrivacy = (lParam & IDF_PRIVACY) != 0;
if (facy.is_idle_ && !bIdle)
{
facy.is_idle_ = false;
SetStatus(m_iDesiredStatus);
}
else if (!facy.is_idle_ && bIdle && !bPrivacy && m_iDesiredStatus != ID_STATUS_INVISIBLE)
{
facy.is_idle_ = true;
SetStatus(ID_STATUS_IDLE);
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////
int FacebookProto::OnEvent(PROTOEVENTTYPE event, WPARAM wParam, LPARAM lParam)
{
switch(event)
{
case EV_PROTO_ONLOAD:
return OnModulesLoaded(wParam,lParam);
case EV_PROTO_ONEXIT:
return OnPreShutdown(wParam,lParam);
case EV_PROTO_ONOPTIONS:
return OnOptionsInit(wParam,lParam);
case EV_PROTO_ONCONTACTDELETED:
return OnContactDeleted(wParam,lParam);
}
return 1;
}
//////////////////////////////////////////////////////////////////////////////
// EVENTS
INT_PTR FacebookProto::SvcCreateAccMgrUI(WPARAM wParam, LPARAM lParam)
{
return (int)CreateDialogParam(g_hInstance,MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT),
(HWND)lParam, FBAccountProc, (LPARAM)this);
}
int FacebookProto::OnModulesLoaded(WPARAM wParam, LPARAM lParam)
{
// Register group chat
GCREGISTER gcr = {sizeof(gcr)};
gcr.dwFlags = 0; //GC_ACKMSG;
gcr.pszModule = m_szModuleName;
gcr.pszModuleDispName = m_szModuleName;
gcr.iMaxText = FACEBOOK_MESSAGE_LIMIT;
gcr.nColors = 0;
gcr.pColors = NULL;
CallService(MS_GC_REGISTER,0,reinterpret_cast(&gcr));
return 0;
}
int FacebookProto::OnPreShutdown(WPARAM wParam, LPARAM lParam)
{
SetStatus(ID_STATUS_OFFLINE);
return 0;
}
int FacebookProto::OnOptionsInit(WPARAM wParam, LPARAM lParam)
{
OPTIONSDIALOGPAGE odp = {sizeof(odp)};
odp.hInstance = g_hInstance;
odp.ptszTitle = m_tszUserName;
odp.dwInitParam = LPARAM(this);
odp.flags = ODPF_BOLDGROUPS | ODPF_TCHAR | ODPF_DONTTRANSLATE;
odp.position = 271828;
odp.ptszGroup = LPGENT("Network");
odp.ptszTab = LPGENT("Account");
odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS);
odp.pfnDlgProc = FBOptionsProc;
Options_AddPage(wParam, &odp);
odp.position = 271829;
odp.ptszTab = LPGENT("Advanced");
odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS_ADVANCED);
odp.pfnDlgProc = FBOptionsAdvancedProc;
Options_AddPage(wParam, &odp);
odp.position = 271830;
if (ServiceExists(MS_POPUP_ADDPOPUPT))
odp.ptszGroup = LPGENT("Popups");
odp.ptszTab = LPGENT("Events");
odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS_EVENTS);
odp.pfnDlgProc = FBEventsProc;
Options_AddPage(wParam, &odp);
return 0;
}
int FacebookProto::OnToolbarInit(WPARAM, LPARAM)
{
TTBButton ttb = { sizeof(ttb) };
ttb.dwFlags = TTBBF_SHOWTOOLTIP | TTBBF_VISIBLE;
char service[100];
mir_snprintf(service, sizeof(service), "%s%s", m_szModuleName, "/Mind");
ttb.pszService = service;
ttb.pszTooltipUp = ttb.name = LPGEN("What's on your mind?");
ttb.hIconHandleUp = Skin_GetIconByHandle(GetIconHandle("mind"));
TopToolbar_AddButton(&ttb);
return 0;
}
INT_PTR FacebookProto::OnMind(WPARAM, LPARAM)
{
if (isOnline()) {
HWND hDlg = CreateDialogParam(g_hInstance, MAKEINTRESOURCE(IDD_MIND), (HWND)0, FBMindProc, reinterpret_cast(this));
ShowWindow(hDlg, SW_SHOW);
}
return 0;
}
INT_PTR FacebookProto::CheckNewsfeeds(WPARAM, LPARAM)
{
if (!isOffline()) {
facy.client_notify(TranslateT("Loading newsfeeds..."));
facy.feeds();
}
return 0;
}
INT_PTR FacebookProto::CheckFriendRequests(WPARAM, LPARAM)
{
if (!isOffline()) {
facy.client_notify(TranslateT("Checking friend requests..."));
ProcessFriendRequests(NULL);
}
return 0;
}
INT_PTR FacebookProto::RefreshBuddyList(WPARAM, LPARAM)
{
if (!isOffline()) {
facy.client_notify(TranslateT("Refreshing buddy list..."));
facy.buddy_list();
}
return 0;
}
INT_PTR FacebookProto::VisitProfile(WPARAM wParam,LPARAM lParam)
{
HANDLE hContact = reinterpret_cast(wParam);
// TODO: why isn't wParam == 0 when is status menu moved to main menu?
if (wParam != 0 && !IsMyContact(hContact))
return 1;
std::string url = FACEBOOK_URL_PROFILE;
ptrA val( getStringA(hContact, "Homepage"));
if (val != NULL) {
// Homepage link already present, get it
url = val;
} else {
// No homepage link, create and save it
val = getStringA(hContact, FACEBOOK_KEY_ID);
url += val;
setString(hContact, "Homepage", url.c_str());
}
OpenUrl(url);
return 0;
}
INT_PTR FacebookProto::VisitFriendship(WPARAM wParam,LPARAM lParam)
{
HANDLE hContact = reinterpret_cast(wParam);
if (wParam == 0 || !IsMyContact(hContact))
return 1;
ptrA id( getStringA(hContact, FACEBOOK_KEY_ID));
std::string url = FACEBOOK_URL_PROFILE;
url += facy.self_.user_id;
url += "&and=" + std::string(id);
OpenUrl(url);
return 0;
}
INT_PTR FacebookProto::Poke(WPARAM wParam,LPARAM lParam)
{
if (wParam == NULL || isOffline())
return 1;
HANDLE hContact = reinterpret_cast(wParam);
ptrA id(getStringA(hContact, FACEBOOK_KEY_ID));
if (id == NULL)
return 1;
ForkThread(&FacebookProto::SendPokeWorker, new std::string(id));
return 0;
}
INT_PTR FacebookProto::CancelFriendship(WPARAM wParam,LPARAM lParam)
{
if (wParam == NULL || isOffline())
return 1;
bool deleting = (lParam == 1);
HANDLE hContact = reinterpret_cast(wParam);
// Ignore groupchats and, if deleting, also not-friends
if (getByte(hContact, "ChatRoom", 0)
|| (deleting && getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE, 0) != CONTACT_FRIEND))
return 0;
ptrT tname = db_get_tsa(hContact, m_szModuleName, FACEBOOK_KEY_NAME);
if (tname == NULL)
tname = db_get_tsa(hContact, m_szModuleName, FACEBOOK_KEY_ID);
TCHAR tstr[256];
mir_sntprintf(tstr,SIZEOF(tstr),TranslateT("Do you want to cancel your friendship with '%s'?"), tname);
if (MessageBox(0, tstr, m_tszUserName, MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2) == IDYES) {
ptrA id( getStringA(hContact, FACEBOOK_KEY_ID));
if (id == NULL)
return 1;
std::string *val = new std::string(id);
if (deleting) {
facebook_user *fbu = facy.buddies.find(*val);
if (fbu != NULL)
fbu->handle = NULL;
}
ForkThread(&FacebookProto::DeleteContactFromServer, val);
}
return 0;
}
INT_PTR FacebookProto::RequestFriendship(WPARAM wParam,LPARAM lParam)
{
if (wParam == NULL || isOffline())
return 1;
HANDLE hContact = reinterpret_cast(wParam);
ptrA id( getStringA(hContact, FACEBOOK_KEY_ID));
if (id == NULL)
return 1;
ForkThread(&FacebookProto::AddContactToServer, new std::string(id));
return 0;
}
INT_PTR FacebookProto::ApproveFriendship(WPARAM wParam,LPARAM lParam)
{
if (wParam == NULL || isOffline())
return 1;
HANDLE *hContact = new HANDLE(reinterpret_cast(wParam));
ForkThread(&FacebookProto::ApproveContactToServer, hContact);
return 0;
}
INT_PTR FacebookProto::OnCancelFriendshipRequest(WPARAM wParam,LPARAM lParam)
{
if (wParam == NULL || isOffline())
return 1;
HANDLE *hContact = new HANDLE(reinterpret_cast(wParam));
ForkThread(&FacebookProto::CancelFriendsRequest, hContact);
return 0;
}
HANDLE FacebookProto::HContactFromAuthEvent(HANDLE hEvent)
{
DWORD body[2];
DBEVENTINFO dbei = { sizeof(dbei) };
dbei.cbBlob = sizeof(DWORD)*2;
dbei.pBlob = (PBYTE)&body;
if (db_event_get(hEvent, &dbei))
return INVALID_HANDLE_VALUE;
if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
return INVALID_HANDLE_VALUE;
if (strcmp(dbei.szModule, m_szModuleName))
return INVALID_HANDLE_VALUE;
return DbGetAuthEventContact(&dbei);
}
void FacebookProto::OpenUrl(std::string url)
{
std::string facebookDomain = "facebook.com";
std::string::size_type pos = url.find(facebookDomain);
bool isFacebookUrl = (pos != std::string::npos);
bool isRelativeUrl = (url.substr(0, 4) != "http");
if (isFacebookUrl || isRelativeUrl) {
// Make realtive url
if (!isRelativeUrl) {
url = url.substr(pos + facebookDomain.length());
// Strip eventual port
pos = url.find("/");
if (pos != std::string::npos && pos != 0)
url = url.substr(pos);
}
// Make absolute url
bool useHttps = getByte(FACEBOOK_KEY_FORCE_HTTPS, 1) > 0;
url = (useHttps ? HTTP_PROTO_SECURE : HTTP_PROTO_REGULAR) + facy.get_server_type() + url;
}
ptrT data = mir_utf8decodeT(url.c_str());
CallService(MS_UTILS_OPENURL, (WPARAM)OUF_TCHAR, (LPARAM)data);
}
void FacebookProto::ReadNotificationWorker(void *p)
{
if (p == NULL)
return;
std::string *id = static_cast(p);
std::string data = "seen=0&asyncSignal=&__dyn=&__req=a&alert_ids%5B0%5D=" + *id;
data += "&fb_dtsg=" + (facy.dtsg_.length() ? facy.dtsg_ : "0");
data += "&__user=" + facy.self_.user_id;
facy.flap(REQUEST_NOTIFICATIONS_READ, NULL, &data);
delete p;
}