/*
Copyright © 2012-24 Miranda NG team
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 "stdafx.h"
void CTwitterProto::OnLoggedIn()
{
ptrW wszGroup(getWStringA(TWITTER_KEY_GROUP));
if (wszGroup)
Clist_GroupCreate(0, wszGroup);
setAllContactStatuses(ID_STATUS_ONLINE);
SetChatStatus(ID_STATUS_ONLINE);
int old_status = m_iStatus;
// m_szMyId = root["id_str"].as_mstring(); !!!!!!!!!!!!!!
m_iStatus = m_iDesiredStatus;
ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus);
}
void CTwitterProto::OnLoggedFail()
{
ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE);
m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
setAllContactStatuses(ID_STATUS_OFFLINE, false);
}
void CTwitterProto::BeginConnection()
{
if (!m_si && getByte(TWITTER_KEY_CHATFEED))
OnJoinChat(0, true);
if (!m_hWorkerThreadId)
ForkThread(&CTwitterProto::ServerThread);
debugLogA("***** Negotiating connection with Twitter");
// saving the current status to a temp var
CMStringA szOauthToken = getMStringA(TWITTER_KEY_OAUTH_TOK);
CMStringA szOauthTokenSecret = getMStringA(TWITTER_KEY_OAUTH_TOK_SEC);
m_szUserName = getMStringA(TWITTER_KEY_NICK);
if (m_szUserName.IsEmpty())
m_szUserName = getMStringA(TWITTER_KEY_UN);
if (szOauthToken.IsEmpty() || szOauthTokenSecret.IsEmpty()) {
// first, reset all the keys so we can start fresh
ResetOauthKeys();
m_szUserName.Empty();
debugLogA("**NegotiateConnection - Requesting oauthTokens");
RequestOauthAuth();
return;
}
szOauthToken = getMStringA(TWITTER_KEY_OAUTH_ACCESS_TOK);
szOauthTokenSecret = getMStringA(TWITTER_KEY_OAUTH_ACCESS_SEC);
debugLogA("**NegotiateConnection - Successfully retrieved Access Tokens");
StringPairs accessTokenParameters = ParseQueryString("");
m_szAccessToken = accessTokenParameters[L"oauth_token"];
m_szAccessTokenSecret = accessTokenParameters[L"oauth_token_secret"];
m_szUserName = accessTokenParameters[L"screen_name"];
debugLogA("**NegotiateConnection - screen name is %s", m_szUserName.c_str());
// save em
setUString(TWITTER_KEY_OAUTH_ACCESS_TOK, m_szAccessToken);
setUString(TWITTER_KEY_OAUTH_ACCESS_SEC, m_szAccessTokenSecret);
setUString(TWITTER_KEY_NICK, m_szUserName);
setUString(TWITTER_KEY_UN, m_szUserName);
m_szAccessToken = szOauthToken;
m_szAccessTokenSecret = szOauthTokenSecret;
}
void CTwitterProto::MessageLoop(void*)
{
debugLogA("***** Entering Twitter::MessageLoop");
since_id_ = getId(TWITTER_KEY_SINCEID);
dm_since_id_ = getId(TWITTER_KEY_DMSINCEID);
bool new_account = getByte(TWITTER_KEY_NEW, 1) != 0;
bool popups = getByte(TWITTER_KEY_POPUP_SIGNON, 1) != 0;
// if this isn't set, it will automatically not turn a tweet into a msg. probably should make the default that it does turn a tweet into a message
bool tweetToMsg = getByte(TWITTER_KEY_TWEET_TO_MSG) != 0;
int poll_rate = getDword(TWITTER_KEY_POLLRATE, 80);
for (unsigned int i = 0;; i++) {
if (m_iStatus != ID_STATUS_ONLINE)
break;
if (i % 10 == 0)
UpdateFriends();
if (m_iStatus != ID_STATUS_ONLINE)
break;
UpdateStatuses(new_account, popups, tweetToMsg);
if (m_iStatus != ID_STATUS_ONLINE)
break;
UpdateMessages(new_account);
if (new_account) { // Not anymore!
new_account = false;
setByte(TWITTER_KEY_NEW, 0);
}
if (m_iStatus != ID_STATUS_ONLINE)
break;
debugLogA("***** CTwitterProto::MessageLoop going to sleep...");
if (SleepEx(poll_rate * 1000, true) == WAIT_IO_COMPLETION)
break;
debugLogA("***** CTwitterProto::MessageLoop waking up...");
popups = true;
}
debugLogA("***** Exiting CTwitterProto::MessageLoop");
}
struct update_avatar
{
update_avatar(MCONTACT hContact, const CMStringA &url) : hContact(hContact), url(url) {}
MCONTACT hContact;
CMStringA url;
};
/* void *p should always be a struct of type update_avatar */
void CTwitterProto::UpdateAvatarWorker(void *p)
{
if (p == nullptr)
return;
std::unique_ptr data((update_avatar*)p);
// db_get_s returns 0 when it suceeds, so if this suceeds it will return 0, or false.
// therefore if it returns 1, or true, we want to return as there is no such user.
// as a side effect, dbv now has the username in it i think
CMStringA username(getMStringA(data->hContact, TWITTER_KEY_UN));
if (username.IsEmpty())
return;
CMStringA ext = data->url.Mid(data->url.ReverseFind('.')); // finds the filetype of the avatar
CMStringW filename(FORMAT, L"%s\\%S%S", GetAvatarFolder().c_str(), username.c_str(), ext.c_str()); // local filename and path
PROTO_AVATAR_INFORMATION ai = { 0 };
ai.hContact = data->hContact;
ai.format = ProtoGetAvatarFormat(filename.c_str());
if (ai.format == PA_FORMAT_UNKNOWN) {
debugLogA("***** Update avatar: Terminated for this contact, extension format unknown for %s", data->url.c_str());
return; // lets just ignore unknown formats... if not it crashes miranda. should probably speak to borkra about this.
}
wcsncpy(ai.filename, filename.c_str(), MAX_PATH); // puts the local file name in the avatar struct, to a max of 260 chars (as of now)
debugLogA("***** Updating avatar: %s", data->url.c_str());
mir_cslock lck(avatar_lock_);
if (Miranda_IsTerminated()) { // if miranda is shutting down...
debugLogA("***** Terminating avatar update early: %s", data->url.c_str());
return;
}
if (save_url(hAvatarNetlib_, data->url, filename)) {
setString(data->hContact, TWITTER_KEY_AV_URL, data->url.c_str());
ProtoBroadcastAck(data->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai);
}
else ProtoBroadcastAck(data->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, &ai);
debugLogA("***** Done avatar: %s", data->url.c_str());
}
void CTwitterProto::UpdateAvatar(MCONTACT hContact, const CMStringA &url, bool force)
{
DBVARIANT dbv = { 0 };
if (!force && (!getString(hContact, TWITTER_KEY_AV_URL, &dbv) && url == dbv.pszVal)) {
debugLogA("***** Avatar already up-to-date: %s", url.c_str());
}
else {
// TODO: more defaults (configurable?)
if (url == "http://static.twitter.com/images/default_profile_normal.png") {
PROTO_AVATAR_INFORMATION ai = { 0 };
ai.hContact = hContact;
setString(hContact, TWITTER_KEY_AV_URL, url.c_str());
ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai);
}
else {
ForkThread(&CTwitterProto::UpdateAvatarWorker, new update_avatar(hContact, url));
}
}
db_free(&dbv);
}
void CTwitterProto::UpdateFriends()
{
/*
auto *req = new AsyncHttpRequest(REQUEST_GET, CMStringA(FORMAT, "/2/users/%s/followers", m_szMyId.c_str()));
http::response resp = Execute(req);
if (resp.code != 200) {
debugLogA("Friend list reading failed");
return;
}
JSONNode root = JSONNode::parse(resp.data.c_str());
if (!root) {
debugLogA("unable to parse response");
return;
}
for (auto &one : root["users"]) {
std::string username = one["screen_name"].as_string();
if (m_szUserName == username.c_str())
continue;
std::string id = one["id_str"].as_string();
std::string real_name = one["name"].as_string();
std::string profile_image_url = one["profile_image_url"].as_string();
std::string status_text = one["status"]["text"].as_string();
MCONTACT hContact = AddToClientList(username.c_str(), status_text.c_str());
setString(hContact, TWITTER_KEY_ID, id.c_str());
setUString(hContact, "Nick", real_name.c_str());
UpdateAvatar(hContact, profile_image_url.c_str());
}
*/
debugLogA("***** Friends list updated");
}
LRESULT CALLBACK PopupWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
CMStringA *url;
switch (message) {
case WM_COMMAND:
// Get the plugin data (we need the Popup service to do it)
url = (CMStringA *)PUGetPluginData(hwnd);
if (url != nullptr)
Utils_OpenUrl(url->c_str());
// Intentionally no break here
case WM_CONTEXTMENU:
// After a click, destroy popup
PUDeletePopup(hwnd);
return TRUE;
case UM_FREEPLUGINDATA:
// After close, free
url = (CMStringA *)PUGetPluginData(hwnd);
delete url;
return FALSE;
}
return DefWindowProc(hwnd, message, wParam, lParam);
};
void CTwitterProto::ShowContactPopup(MCONTACT hContact, const CMStringA &text, const CMStringA *url)
{
if (!getByte(TWITTER_KEY_POPUP_SHOW))
return;
POPUPDATAW popup = {};
popup.lchContact = hContact;
popup.iSeconds = getDword(TWITTER_KEY_POPUP_TIMEOUT);
popup.colorBack = getDword(TWITTER_KEY_POPUP_COLBACK);
if (popup.colorBack == 0xFFFFFFFF)
popup.colorBack = GetSysColor(COLOR_WINDOW);
popup.colorText = getDword(TWITTER_KEY_POPUP_COLTEXT);
if (popup.colorBack == 0xFFFFFFFF)
popup.colorBack = GetSysColor(COLOR_WINDOWTEXT);
DBVARIANT dbv;
if (!db_get_ws(hContact, "CList", "MyHandle", &dbv) || !getWString(hContact, TWITTER_KEY_UN, &dbv)) {
wcsncpy(popup.lpwzContactName, dbv.pwszVal, MAX_CONTACTNAME);
db_free(&dbv);
}
if (url != nullptr) {
popup.PluginWindowProc = PopupWindowProc;
popup.PluginData = (void *)url;
}
wcsncpy_s(popup.lpwzText, Utf2T(text), MAX_SECONDLINE);
PUAddPopupW(&popup);
}
void CTwitterProto::UpdateStatuses(bool pre_read, bool popups, bool tweetToMsg)
{
/*
auto *req = new AsyncHttpRequest(REQUEST_GET, "/statuses/home_timeline.json");
req << INT_PARAM("count", 200);
if (since_id_ != 0)
req << INT64_PARAM("since_id", since_id_);
http::response resp = Execute(req);
if (resp.code != 200) {
debugLogA("Status update failed with error %d", resp.code);
return;
}
JSONNode root = JSONNode::parse(resp.data.c_str());
if (!root) {
debugLogA("Status update failed: unable to parse response");
return;
}
OBJLIST messages(10);
for (auto &one : root) {
const JSONNode &pUser = one["user"];
auto *u = new twitter_user();
u->username = pUser["screen_name"].as_string();
u->real_name = pUser["name"].as_string();
u->profile_image_url = pUser["profile_image_url"].as_string();
CMStringA retweeteesName, retweetText;
// the tweet will be truncated unless we take action. i hate you CTwitterProto API
const JSONNode &pStatus = one["retweeted_status"];
if (pStatus) {
// here we grab the "retweeted_status" um.. section? it's in here that all the info we need is
// at this point the user will get no tweets and an error popup if the tweet happens to be exactly 140 chars, start with
// "RT @", end in " ...", and notactually be a real retweet. it's possible but unlikely, wish i knew how to get
// the retweet_count variable to work :(
const JSONNode &pRetweet = one["retweeted_status"],
&pUser2 = pRetweet["user"];
retweeteesName = pUser2["screen_name"].as_string().c_str(); // the user that is being retweeted
retweetText = pRetweet["text"].as_string().c_str(); // their tweet in all it's untruncated glory
// fix html entities in the text
htmlEntitiesDecode(retweetText);
u->status.text = "RT @" + retweeteesName + " " + retweetText; // mash it together in some format people will understand
}
else {
// if it's not truncated, then the CTwitterProto API returns the native RT correctly anyway,
CMStringA rawText = one["text"].as_string().c_str();
// fix html entities in the text
htmlEntitiesDecode(rawText);
u->status.text = rawText;
}
u->status.id = _atoi64(one["id"].as_string().c_str());
if (u->status.id > since_id_)
since_id_ = u->status.id;
std::string timestr = one["created_at"].as_string();
u->status.time = parse_time(timestr.c_str());
messages.insert(u);
}
for (auto &u : messages.rev_iter()) {
if (!pre_read && m_si)
UpdateChat(*u);
if (u->username == m_szUserName.c_str())
continue;
MCONTACT hContact = AddToClientList(u->username.c_str(), "");
UpdateAvatar(hContact, u->profile_image_url.c_str());
// if we send twits as messages, add an unread event
if (tweetToMsg) {
DBEVENTINFO dbei = {};
dbei.pBlob = (uint8_t*)(u->status.text.c_str());
dbei.cbBlob = (int)u->status.text.length() + 1;
dbei.eventType = TWITTER_DB_EVENT_TYPE_TWEET;
dbei.flags = DBEF_UTF;
dbei.timestamp = static_cast(u->status.time);
dbei.szModule = m_szModuleName;
db_event_add(hContact, &dbei);
}
else db_set_utf(hContact, "CList", "StatusMsg", u->status.text.c_str());
if (!pre_read && popups) {
Skin_PlaySound("TwitterNew");
ShowContactPopup(hContact, u->status.text.c_str(), new CMStringA(FORMAT, "https://twitter.com/%s/status/%lld", u->username.c_str(), u->status.id));
}
}
*/
setId(TWITTER_KEY_SINCEID, since_id_);
debugLogA("***** Status messages updated");
}
void CTwitterProto::UpdateMessages(bool pre_read)
{
/*
auto *req = new AsyncHttpRequest(REQUEST_GET, "/direct_messages/events/list.json");
req << INT_PARAM("count", 50);
if (dm_since_id_ != 0)
req << INT64_PARAM("since_id", dm_since_id_);
http::response resp = Execute(req);
if (resp.code != 200) {
debugLogA("Message request failed, error %d", resp.code);
return;
}
JSONNode root = JSONNode::parse(resp.data.c_str());
if (!root) {
debugLogA("unable to parse response");
return;
}
for (auto &one : root["events"]) {
std::string type = one["type"].as_string();
if (type != "message_create")
continue;
bool bIsMe = false;
auto &msgCreate = one["message_create"];
std::string sender = msgCreate["sender_id"].as_string();
if (m_szMyId == sender.c_str()) {
bIsMe = true;
sender = msgCreate["target"]["recipient_id"].as_string();
}
MCONTACT hContact = FindContactById(sender.c_str());
if (hContact == INVALID_CONTACT_ID) {
hContact = AddToClientList(sender.c_str(), "");
Contact::RemoveFromList(hContact);
}
std::string text = msgCreate["message_data"]["text"].as_string();
__time64_t time = _atoi64(one["created_timestamp"].as_string().c_str()) / 1000;
std::string msgid = one["id"].as_string();
if (db_event_getById(m_szModuleName, msgid.c_str()))
continue;
twitter_id id = _atoi64(msgid.c_str());
if (id > dm_since_id_)
dm_since_id_ = id;
PROTORECVEVENT recv = {};
if (pre_read)
recv.flags |= PREF_CREATEREAD;
if (bIsMe)
recv.flags |= PREF_SENT;
recv.szMessage = const_cast(text.c_str());
recv.timestamp = static_cast(time);
recv.szMsgId = msgid.c_str();
MEVENT hDbEVent = (MEVENT)ProtoChainRecvMsg(hContact, &recv);
if (!msgid.empty())
m_arChatMarks.insert(new CChatMark(hDbEVent, msgid.c_str()));
}
*/
setId(TWITTER_KEY_DMSINCEID, dm_since_id_);
debugLogA("***** Direct messages updated");
}
void CTwitterProto::ResetOauthKeys()
{
delSetting(TWITTER_KEY_OAUTH_ACCESS_TOK);
delSetting(TWITTER_KEY_OAUTH_ACCESS_SEC);
delSetting(TWITTER_KEY_OAUTH_TOK);
delSetting(TWITTER_KEY_OAUTH_TOK_SEC);
delSetting(TWITTER_KEY_OAUTH_PIN);
}