/*
Copyright © 2009 Jim Porter
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 "proto.h"
#include "utility.h"
#include "theme.h"
#include "ui.h"
#include "oauth.dev.h"
#include "m_folders.h"
#include
#include
#include
#include
static volatile LONG g_msgid = 1;
TwitterProto::TwitterProto(const char *proto_name,const TCHAR *username) :
PROTO(proto_name, username)
{
CreateProtoService(PS_CREATEACCMGRUI, &TwitterProto::SvcCreateAccMgrUI);
CreateProtoService(PS_GETNAME, &TwitterProto::GetName);
CreateProtoService(PS_GETSTATUS, &TwitterProto::GetStatus);
CreateProtoService(PS_JOINCHAT, &TwitterProto::OnJoinChat);
CreateProtoService(PS_LEAVECHAT, &TwitterProto::OnLeaveChat);
CreateProtoService(PS_GETMYAVATAR, &TwitterProto::GetAvatar);
CreateProtoService(PS_SETMYAVATAR, &TwitterProto::SetAvatar);
HookProtoEvent(ME_DB_CONTACT_DELETED, &TwitterProto::OnContactDeleted);
HookProtoEvent(ME_CLIST_PREBUILDSTATUSMENU, &TwitterProto::OnBuildStatusMenu);
HookProtoEvent(ME_OPT_INITIALISE, &TwitterProto::OnOptionsInit);
// Initialize hotkeys
char text[512];
mir_snprintf(text,SIZEOF(text),"%s/Tweet",m_szModuleName);
HOTKEYDESC hkd = {sizeof(hkd)};
hkd.pszName = text;
hkd.pszService = text;
hkd.pszSection = m_szModuleName; // Section title; TODO: use username?
hkd.pszDescription = "Send Tweet";
Hotkey_Register(&hkd);
signon_lock_ = CreateMutex(0,false,0);
avatar_lock_ = CreateMutex(0,false,0);
twitter_lock_ = CreateMutex(0,false,0);
// set Tokens and stuff
//mirandas keys
ConsumerKey = OAUTH_CONSUMER_KEY;
ConsumerSecret = OAUTH_CONSUMER_SECRET;
AuthorizeUrl = _T("http://api.twitter.com/oauth/authorize?oauth_token=%s");
}
TwitterProto::~TwitterProto()
{
twit_.Disconnect();
if(m_hNetlibUser)
Netlib_CloseHandle(m_hNetlibUser);
if(hAvatarNetlib_)
Netlib_CloseHandle(hAvatarNetlib_);
CloseHandle(twitter_lock_);
CloseHandle(avatar_lock_);
CloseHandle(signon_lock_);
}
// *************************
DWORD_PTR TwitterProto::GetCaps(int type, MCONTACT)
{
switch(type)
{
case PFLAGNUM_1:
return PF1_IM | PF1_MODEMSGRECV | PF1_BASICSEARCH | PF1_SEARCHBYEMAIL |
PF1_SERVERCLIST | PF1_CHANGEINFO;
case PFLAGNUM_2:
return PF2_ONLINE;
case PFLAGNUM_3:
return PF2_ONLINE;
case PFLAGNUM_4:
return PF4_NOCUSTOMAUTH | PF4_IMSENDUTF | PF4_AVATARS;
case PFLAG_MAXLENOFMESSAGE:
return 159; // 140 + + 4 ("RT @"). this allows for the new style retweets
case PFLAG_UNIQUEIDTEXT:
return (DWORD_PTR) "Username";
case PFLAG_UNIQUEIDSETTING:
return (DWORD_PTR) TWITTER_KEY_UN;
}
return 0;
}
// *************************
int TwitterProto::RecvMsg(MCONTACT hContact,PROTORECVEVENT *pre)
{
Proto_RecvMessage(hContact, pre);
return 0;
}
// *************************
struct send_direct
{
__inline send_direct(MCONTACT _hContact, const std::string &_msg, int _msgid) :
hContact(_hContact), msg(_msg), msgid(_msgid)
{}
MCONTACT hContact;
std::string msg;
int msgid;
};
void TwitterProto::SendSuccess(void *p)
{
if(p == 0)
return;
send_direct *data = static_cast(p);
DBVARIANT dbv;
if( !db_get_s(data->hContact,m_szModuleName,TWITTER_KEY_UN,&dbv))
{
ScopedLock s(twitter_lock_);
twit_.send_direct(dbv.pszVal,data->msg);
ProtoBroadcastAck(data->hContact,ACKTYPE_MESSAGE,ACKRESULT_SUCCESS,(HANDLE)data->msgid,0);
db_free(&dbv);
}
delete data;
}
int TwitterProto::SendMsg(MCONTACT hContact,int flags,const char *msg)
{
if(m_iStatus != ID_STATUS_ONLINE)
return 0;
TCHAR* tszMsg;
if ( flags & PREF_UTF )
tszMsg = mir_utf8decodeT( msg );
else if ( flags & PREF_UNICODE )
tszMsg = mir_u2t(( wchar_t* )&msg[ strlen( msg )+1 ] );
else
tszMsg = mir_a2t( msg );
int seq = InterlockedIncrement(&g_msgid);
ForkThread(&TwitterProto::SendSuccess, new send_direct(hContact, msg, seq));
return seq;
}
// *************************
int TwitterProto::SetStatus(int new_status)
{
int old_status = m_iStatus;
if(new_status == m_iStatus)
return 0;
m_iDesiredStatus = new_status;
// 40072 - 40080 are the "online" statuses, basically every status except offline. see statusmodes.h
if(new_status >= 40072 && new_status <= 40080)
{
m_iDesiredStatus = ID_STATUS_ONLINE; //i think i have to set this so it forces the twitter proto to be online (and not away, DND, etc)
// if we're already connecting and they want to go online, BAIL! we're already trying to connect you dumbass
if(old_status == ID_STATUS_CONNECTING)
return 0;
// if we're already connected, and we change to another connected status, don't try and reconnect!
if (old_status >= 40072 && old_status <= 40080)
return 0;
// i think here we tell the proto interface struct that we're connecting, just so it knows
m_iStatus = ID_STATUS_CONNECTING;
// ok.. here i think we're telling the core that this protocol something.. but why?
ProtoBroadcastAck(0,ACKTYPE_STATUS,ACKRESULT_SUCCESS,(HANDLE)old_status,m_iStatus);
ForkThread(&TwitterProto::SignOn,this);
}
else if(new_status == ID_STATUS_OFFLINE)
{
twit_.Disconnect();
m_iStatus = m_iDesiredStatus;
SetAllContactStatuses(ID_STATUS_OFFLINE);
ProtoBroadcastAck(0,ACKTYPE_STATUS,ACKRESULT_SUCCESS,(HANDLE)old_status,m_iStatus);
}
return 0;
}
// *************************
int TwitterProto::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);
}
return 1;
}
// *************************
INT_PTR TwitterProto::SvcCreateAccMgrUI(WPARAM,LPARAM lParam)
{
return (int)CreateDialogParam(g_hInstance,MAKEINTRESOURCE(IDD_TWITTERACCOUNT),(HWND)lParam, first_run_dialog, (LPARAM)this );
}
INT_PTR TwitterProto::GetName(WPARAM wParam,LPARAM lParam)
{
lstrcpynA(reinterpret_cast(lParam), m_szModuleName, (int)wParam);
return 0;
}
INT_PTR TwitterProto::GetStatus(WPARAM,LPARAM)
{
return m_iStatus;
}
INT_PTR TwitterProto::ReplyToTweet(WPARAM hContact, LPARAM)
{
// TODO: support replying to tweets instead of just users
HWND hDlg = CreateDialogParam(g_hInstance,MAKEINTRESOURCE(IDD_TWEET),0,tweet_proc,reinterpret_cast(this));
DBVARIANT dbv;
if(!db_get_s(hContact,m_szModuleName,TWITTER_KEY_UN,&dbv))
{
SendMessage(hDlg,WM_SETREPLY,reinterpret_cast(dbv.pszVal),0);
db_free(&dbv);
}
ShowWindow(hDlg,SW_SHOW);
return 0;
}
INT_PTR TwitterProto::VisitHomepage(WPARAM hContact, LPARAM)
{
DBVARIANT dbv;
// TODO: remove this
if( !db_get_s(hContact,m_szModuleName,TWITTER_KEY_UN,&dbv))
{
std::string url = profile_base_url("https://twitter.com/") + http::url_encode(dbv.pszVal);
db_set_s(hContact,m_szModuleName,"Homepage",url.c_str());
CallService(MS_UTILS_OPENURL,1,reinterpret_cast(url.c_str()));
db_free(&dbv);
}
return 0;
}
// *************************
int TwitterProto::OnBuildStatusMenu(WPARAM,LPARAM)
{
HGENMENU hRoot = pcli->pfnGetProtocolMenu(m_szModuleName);
if (hRoot == NULL)
return 0;
char text[200];
strcpy(text,m_szModuleName);
char *tDest = text+strlen(text);
CLISTMENUITEM mi = { sizeof(mi) };
mi.pszService = text;
mi.hParentMenu = hRoot;
mi.flags = CMIF_ROOTHANDLE | CMIF_TCHAR;
mi.position = 1001;
HANDLE m_hMenuRoot = Menu_AddStatusMenuItem(&mi);
// TODO: Disable this menu item when offline
// "Send Tweet..."
CreateProtoService("/Tweet", &TwitterProto::OnTweet);
strcpy(tDest,"/Tweet");
mi.ptszName = LPGENT("Send Tweet...");
mi.popupPosition = 200001;
mi.icolibItem = GetIconHandle("tweet");
HANDLE m_hMenuBookmarks = Menu_AddStatusMenuItem(&mi);
return 0;
}
int TwitterProto::OnOptionsInit(WPARAM wParam,LPARAM)
{
OPTIONSDIALOGPAGE odp = { sizeof(odp) };
odp.position = 271828;
odp.hInstance = g_hInstance;
odp.ptszGroup = LPGENT("Network");
odp.ptszTitle = m_tszUserName;
odp.dwInitParam = LPARAM(this);
odp.flags = ODPF_BOLDGROUPS | ODPF_TCHAR;
odp.ptszTab = LPGENT("Basic");
odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS);
odp.pfnDlgProc = options_proc;
Options_AddPage(wParam, &odp);
if (ServiceExists(MS_POPUP_ADDPOPUPT)) {
odp.ptszTab = LPGENT("Popups");
odp.pszTemplate = MAKEINTRESOURCEA(IDD_OPTIONS_POPUPS);
odp.pfnDlgProc = popup_options_proc;
Options_AddPage(wParam, &odp);
}
return 0;
}
INT_PTR TwitterProto::OnTweet(WPARAM,LPARAM)
{
if(m_iStatus != ID_STATUS_ONLINE)
return 1;
HWND hDlg = CreateDialogParam(g_hInstance,MAKEINTRESOURCE(IDD_TWEET), 0,tweet_proc,reinterpret_cast(this));
ShowWindow(hDlg,SW_SHOW);
return 0;
}
int TwitterProto::OnModulesLoaded(WPARAM,LPARAM)
{
TCHAR descr[512];
NETLIBUSER nlu = {sizeof(nlu)};
nlu.flags = NUF_OUTGOING | NUF_INCOMING | NUF_HTTPCONNS | NUF_TCHAR;
nlu.szSettingsModule = m_szModuleName;
// Create standard network connection
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 == 0)
MessageBox(0, TranslateT("Unable to get Netlib connection for Twitter"), TranslateT("Twitter"), 0);
// Create avatar network connection (TODO: probably remove this)
char module[512];
mir_snprintf(module,SIZEOF(module),"%sAv",m_szModuleName);
nlu.szSettingsModule = module;
mir_sntprintf(descr,SIZEOF(descr),TranslateT("%s avatar connection"),m_tszUserName);
nlu.ptszDescriptiveName = descr;
hAvatarNetlib_ = (HANDLE)CallService(MS_NETLIB_REGISTERUSER,0,(LPARAM)&nlu);
if(hAvatarNetlib_ == 0)
MessageBox(0, TranslateT("Unable to get avatar Netlib connection for Twitter"), TranslateT("Twitter"), 0);
twit_.set_handle(this, m_hNetlibUser);
GCREGISTER gcr = { sizeof(gcr) };
gcr.pszModule = m_szModuleName;
gcr.ptszDispName = m_tszUserName;
gcr.iMaxText = 159;
CallService(MS_GC_REGISTER,0,reinterpret_cast(&gcr));
DBEVENTTYPEDESCR evt = { sizeof(evt) };
evt.eventType = TWITTER_DB_EVENT_TYPE_TWEET;
evt.module = m_szModuleName;
evt.descr = "Tweet";
evt.flags = DETF_HISTORY | DETF_MSGWINDOW;
CallService(MS_DB_EVENT_REGISTERTYPE,0,reinterpret_cast(&evt));
SetAllContactStatuses(ID_STATUS_OFFLINE); // In case we crashed last time
return 0;
}
int TwitterProto::OnPreShutdown(WPARAM,LPARAM)
{
Netlib_Shutdown(m_hNetlibUser);
Netlib_Shutdown(hAvatarNetlib_);
return 0;
}
int TwitterProto::OnPrebuildContactMenu(WPARAM hContact, LPARAM)
{
if(IsMyContact(hContact))
ShowContactMenus(true);
return 0;
}
int TwitterProto::ShowPinDialog()
{
HWND hDlg = (HWND)DialogBoxParam(g_hInstance,MAKEINTRESOURCE(IDD_TWITTERPIN),0,pin_proc,reinterpret_cast(this));
ShowWindow(hDlg,SW_SHOW);
return 0;
}
void TwitterProto::ShowPopup(const wchar_t *text, int Error)
{
POPUPDATAT popup = {};
mir_sntprintf(popup.lptzContactName,MAX_CONTACTNAME,TranslateT("%s Protocol"),m_tszUserName);
wcs_to_tcs(CP_UTF8,text,popup.lptzText,MAX_SECONDLINE);
if (Error) {
popup.iSeconds = -1;
popup.colorBack = 0x000000FF;
popup.colorText = 0x00FFFFFF;
}
if(ServiceExists(MS_POPUP_ADDPOPUPT))
PUAddPopupT(&popup);
else
MessageBox(0,popup.lptzText,popup.lptzContactName,0);
}
void TwitterProto::ShowPopup(const char *text, int Error)
{
POPUPDATAT popup = {};
mir_sntprintf(popup.lptzContactName,MAX_CONTACTNAME,TranslateT("%s Protocol"),m_tszUserName);
mbcs_to_tcs(CP_UTF8,text,popup.lptzText,MAX_SECONDLINE);
if (Error) {
popup.iSeconds = -1;
popup.colorBack = 0x000000FF;
popup.colorText = 0x00FFFFFF;
}
if(ServiceExists(MS_POPUP_ADDPOPUPT))
PUAddPopupT(&popup);
else
MessageBox(0,popup.lptzText,popup.lptzContactName,0);
}
void TwitterProto::debugLogA(TCHAR *fmt,...)
{
if (!m_hNetlibUser)
return;
va_list va;
va_start(va,fmt);
TCHAR text[1024];
mir_vsntprintf(text,SIZEOF(text),fmt,va);
va_end(va);
CallService(MS_NETLIB_LOGW, (WPARAM)m_hNetlibUser, (LPARAM)text);
}
// TODO: the more I think about it, the more I think all twit.* methods should
// be in MessageLoop
void TwitterProto::SendTweetWorker(void *p)
{
if (p == 0)
return;
char *text = static_cast(p);
if (strlen(text) > 140) { // looks like the chat max outgoing msg thing doesn't work, so i'll do it here.
TCHAR errorPopup[280];
mir_sntprintf(errorPopup,SIZEOF(errorPopup), _T("Don't be crazy! Everyone knows the max tweet size is 140, and you're trying to fit %d chars in there?"), strlen(text));
ShowPopup(errorPopup, 1);
return;
}
ScopedLock s(twitter_lock_);
twit_.set_status(text);
mir_free(text);
}
void TwitterProto::UpdateSettings()
{
if(db_get_b(0,m_szModuleName,TWITTER_KEY_CHATFEED,0))
{
if(!in_chat_)
OnJoinChat(0,0);
}
else
{
if(in_chat_)
OnLeaveChat(0,0);
for(MCONTACT hContact = db_find_first(m_szModuleName); hContact; ) {
MCONTACT hNext = db_find_next(hContact, m_szModuleName);
if(isChatRoom(hContact))
CallService(MS_DB_CONTACT_DELETE, WPARAM(hContact), 0);
hContact = hNext;
}
}
}
std::tstring TwitterProto::GetAvatarFolder()
{
TCHAR path[MAX_PATH];
mir_sntprintf(path, SIZEOF(path), _T("%s\\%s"), VARST( _T("%miranda_avatarcache%")), m_tszUserName);
return path;
}
INT_PTR TwitterProto::GetAvatar(WPARAM,LPARAM)
{
return 0;
}
INT_PTR TwitterProto::SetAvatar(WPARAM,LPARAM)
{
return 0;
}