/*
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 "twitter.h"
//#include "tc2.h"
#include
#include
#include
#include
#include "tinyjson.hpp"
#include
//#include "boost/spirit/include/classic_core.hpp"
//#include "boost/spirit/include/classic_loops.hpp"
typedef json::grammar js;
// utility functions
template
static T cast_and_decode(boost::any &a,bool allow_null)
{
if(allow_null && a.type() == typeid(void))
return T();
return boost::any_cast(a);
}
template <>
static std::string cast_and_decode(boost::any &a,bool allow_null)
{
if(allow_null && a.type() == typeid(void))
return std::string();
std::string s = boost::any_cast(a);
// Twitter *only* encodes < and >, so decode them
size_t off;
while( (off = s.find("<")) != std::string::npos)
s.replace(off,4,"<");
while( (off = s.find(">")) != std::string::npos)
s.replace(off,4,">");
return s;
}
template
static T retrieve(const js::object &o,const std::string &key,bool allow_null = false)
{
using boost::any_cast;
js::object::const_iterator i = o.find(key);
if(i == o.end())
throw std::exception( ("unable to retrieve key '"+key+"'").c_str());
try
{
return cast_and_decode(*i->second,allow_null);
}
catch(const boost::bad_any_cast &)
{
throw std::exception( ("unable to cast key '"+key+"' to target type").c_str());
}
}
twitter::twitter() : base_url_("https://api.twitter.com/")
{}
bool twitter::set_credentials(const std::string &username, const std::wstring &consumerKey, const std::wstring &consumerSecret,
const std::wstring &oauthAccessToken, const std::wstring &oauthAccessTokenSecret, const std::wstring &pin, bool test)
{
username_ = username;
consumerKey_ = consumerKey;
consumerSecret_ = consumerSecret;
oauthAccessToken_ = oauthAccessToken;
oauthAccessTokenSecret_ = oauthAccessTokenSecret;
pin_ = pin;
if(test)
return slurp(base_url_+"1.1/account/verify_credentials.json",http::get).code == 200;
else
return true;
}
http::response twitter::request_token() {
return slurp("https://api.twitter.com/oauth/request_token",http::get);
}
http::response twitter::request_access_tokens() {
return slurp("https://api.twitter.com/oauth/access_token", http::get);
}
void twitter::set_base_url(const std::string &base_url)
{
base_url_ = base_url;
}
const std::string & twitter::get_username() const
{
return username_;
}
const std::string & twitter::get_base_url() const
{
return base_url_;
}
// this whole function is wrong i think. should be calling friends/ids, not followers
/*js::array twitter::buildFriendList() {
INT_PTR friendCursor = -1;
js::array IDs; // an array for the userIDs. i dunno if js::array is the right thing to use..?
js::array masterIDs; // the list that contains all the users that the user follows
std::vector friends;
while (friendCursor != 0) {
http::response resp = slurp(base_url_ + "/1.1/followers/ids.json?cursor=" + friendCursor + "&screen_name=" + username_,http::get);
if(resp.code != 200)
throw bad_response();
const js::variant var = json::parse( resp.data.begin(),resp.data.end() ); // pull the data out of the http response
if(var->type() == typeid(js::object)) // make sure the parsed data is of type js::object (??)
{
const js::object &friendIDs = boost::any_cast(*var); // cast the object into the type we can use
if(friendIDs.find("error") != friendIDs.end()) // don't really know why error should be at the end here?
throw std::exception("error while parsing friendIDs object from ids.json");
// ok need to find out how to convert all the IDs into an array. dunno if i can magically make it happen, or
// if i will have to parse it myself and add them one by one :(
IDs = retrieve(friendIDs,"ids");
for(js::array::const_iterator i=IDs.begin(); i!=IDs.end(); ++i) {
//LOG("friends ID: " + i);
// add array to master array
js::object one = boost::any_cast(**i);
masterIDs.push_back(one); // i don't understand this. how do we push into the array? should i just use C++ arrays (list?) and bail on boost?
}
// now we need to pick out the cursor stuff, and keep punching IDs into the array
}
else {
throw std::exception("in buildFriendList(), return type is not js::object");
}
}
}*/
std::vector twitter::get_friends()
{
// maybe once i have the buildFriendLIst() func working.. but for now let's just get twitter working.
//js::array friendArray = buildFriendList();
std::vector friends;
http::response resp = slurp(base_url_+"1.1/statuses/friends.json",http::get);
if(resp.code != 200)
throw bad_response();
const js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() != typeid(js::array))
throw std::exception("unable to parse response");
const js::array &list = boost::any_cast(*var);
for(js::array::const_iterator i=list.begin(); i!=list.end(); ++i)
{
if((*i)->type() == typeid(js::object))
{
const js::object &one = boost::any_cast(**i);
twitter_user user;
user.username = retrieve(one,"screen_name");
user.real_name = retrieve(one,"name",true);
user.profile_image_url = retrieve(one,"profile_image_url",true);
if(one.find("status") != one.end())
{
js::object &status = retrieve(one,"status");
user.status.text = retrieve(status,"text");
user.status.id = retrieve(status,"id");
std::string timestr = retrieve(status,"created_at");
user.status.time = parse_time(timestr);
}
friends.push_back(user);
}
}
return friends;
}
bool twitter::get_info(const std::string &name,twitter_user *info)
{
if(!info)
return false;
std::string url = base_url_+"1.1/users/show/"+http::url_encode(name)+".json";
http::response resp = slurp(url,http::get);
if(resp.code != 200)
throw bad_response();
const js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() == typeid(js::object))
{
const js::object &user_info = boost::any_cast(*var);
if(user_info.find("error") != user_info.end())
return false;
info->username = retrieve(user_info,"screen_name");
info->real_name = retrieve(user_info,"name",true);
info->profile_image_url = retrieve(user_info,"profile_image_url",true);
return true;
}
else
return false;
}
bool twitter::get_info_by_email(const std::string &email,twitter_user *info)
{
if(!info)
return false;
std::string url = base_url_+"1.1/users/show.json?email="+http::url_encode(email);
http::response resp = slurp(url,http::get);
if(resp.code != 200)
throw bad_response();
js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() == typeid(js::object))
{
const js::object &user_info = boost::any_cast(*var);
if(user_info.find("error") != user_info.end())
return false;
info->username = retrieve(user_info,"screen_name");
info->real_name = retrieve(user_info,"name",true);
info->profile_image_url = retrieve(user_info,"profile_image_url",true);
return true;
}
else
return false;
}
twitter_user twitter::add_friend(const std::string &name)
{
std::string url = base_url_+"1.1/friendships/create/"+http::url_encode(name)+".json";
twitter_user ret;
http::response resp = slurp(url,http::post);
if(resp.code != 200)
throw bad_response();
js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() != typeid(js::object))
throw std::exception("unable to parse response");
const js::object &user_info = boost::any_cast(*var);
ret.username = retrieve(user_info,"screen_name");
ret.real_name = retrieve(user_info,"name",true);
ret.profile_image_url = retrieve(user_info,"profile_image_url",true);
if(user_info.find("status") != user_info.end())
{
// TODO: fill in more fields
const js::object &status = retrieve(user_info,"status");
ret.status.text = retrieve(status,"text");
}
return ret;
}
void twitter::remove_friend(const std::string &name)
{
std::string url = base_url_+"1.1/friendships/destroy/"+http::url_encode(name)+".json";
slurp(url,http::post);
}
void twitter::set_status(const std::string &text)
{
if(text.size())
{
/* slurp(base_url_+"statuses/update.json",http::post,
"status="+http::url_encode(text)+
"&source=mirandaim");*/
//MessageBox(NULL, UTF8ToWide(text).c_str(), NULL, MB_OK);
std::wstring wTweet = UTF8ToWide(text);
OAuthParameters postParams;
postParams[L"status"] = UrlEncode(wTweet);
slurp(base_url_+"1.1/statuses/update.json",http::post, postParams);
}
}
void twitter::send_direct(const std::string &name,const std::string &text)
{
std::wstring temp = UTF8ToWide(text);
OAuthParameters postParams;
postParams[L"text"] = UrlEncode(temp);
postParams[L"screen_name"] = UTF8ToWide(name);
slurp(base_url_+"1.1/direct_messages/new.json", http::post, postParams);
}
std::vector twitter::get_statuses(int count,twitter_id id)
{
using boost::lexical_cast;
std::vector statuses;
std::string url = base_url_+"1.1/statuses/home_timeline.json?count="+
lexical_cast(count);
if(id != 0)
url += "&since_id="+boost::lexical_cast(id);
http::response resp = slurp(url,http::get);
if(resp.code != 200)
throw bad_response();
js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() != typeid(js::array))
throw std::exception("unable to parse response");
const js::array &list = boost::any_cast(*var);
for(js::array::const_iterator i=list.begin(); i!=list.end(); ++i)
{
if((*i)->type() == typeid(js::object))
{
const js::object &one = boost::any_cast(**i);
const js::object &user = retrieve(one,"user");
//size_t RTcount = retrieve(one,"retweet_count", true); // why doesn't this work?? it can't cast the output into an int even though twitter api says it's an int
twitter_user u;
u.username = retrieve(user,"screen_name");
std::string rawText = retrieve(one,"text");
bool foundTruncatedRT = false;
if (rawText.length() == 140) { // might be a truncated tweet
if (rawText.substr(0, 4) == "RT @") { // starting to look like a RT...
if (rawText.substr(136, 4) == " ...") { // ok this is the best I can do. it starts with "RT @", ends with " ...", and is the full 140 chars
//if (RTcount > 0) { // the tweet will be truncated unless we take action. i hate you twitter API
//MessageBox(NULL, L"retweeted: TRUE", L"long tweets", MB_OK);
// 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 js::object &Retweet = retrieve(one,"retweeted_status");
const js::object &RTUser = retrieve(Retweet,"user");
std::string retweeteesName = retrieve(RTUser,"screen_name"); // the user that is being retweeted
std::string retweetText = retrieve(Retweet,"text"); // their tweet in all it's untruncated glory
// fix "&" in the tweets :(
size_t pos = 0;
while((pos = retweetText.find("&", pos)) != std::string::npos) {
retweetText.replace(pos, 5, "&");
pos += 1;
}
u.status.text = "RT @" + retweeteesName + " " + retweetText; // mash it together in some format people will understand
foundTruncatedRT = true;
}
}
}
if (foundTruncatedRT == false) { // if it's not truncated, then the twitter API returns the native RT correctly anyway,
//std::string twt = retrieve(one,"text"); // no need to do this anymore, we already grabbed it above in rawText
// ok here i'm trying some way to fix all the "&" things that are showing up
// i dunno why it's happening, so i'll just find and replace each occurance :/
size_t pos = 0;
while((pos = rawText.find("&", pos)) != std::string::npos) {
rawText.replace(pos, 5, "&");
pos += 1;
}
u.status.text = rawText;
}
u.status.id = retrieve(one,"id");
std::string timestr = retrieve(one,"created_at");
u.status.time = parse_time(timestr);
statuses.push_back(u);
}
}
return statuses;
}
std::vector twitter::get_direct(twitter_id id)
{
std::vector messages;
std::string url = base_url_+"1.1/direct_messages.json";
if(id != 0)
url += "?since_id="+boost::lexical_cast(id);
http::response resp = slurp(url,http::get);
if(resp.code != 200)
throw bad_response();
js::variant var = json::parse( resp.data.begin(),resp.data.end());
if(var->type() != typeid(js::array))
throw std::exception("unable to parse response");
const js::array &list = boost::any_cast(*var);
for(js::array::const_iterator i=list.begin(); i!=list.end(); ++i)
{
if((*i)->type() == typeid(js::object))
{
const js::object &one = boost::any_cast(**i);
twitter_user u;
u.username = retrieve(one,"sender_screen_name");
u.status.text = retrieve(one,"text");
u.status.id = retrieve(one,"id");
std::string timestr = retrieve(one,"created_at");
u.status.time = parse_time(timestr);
messages.push_back(u);
}
}
return messages;
}
string twitter::urlencode(const string &c)
{
string escaped;
int max = c.length();
for(int i=0; i>4;
char dig2 = (dec&0x0F);
if ( 0<= dig1 && dig1<= 9) dig1+=48; //0,48 in ascii
if (10<= dig1 && dig1<=15) dig1+=65-10; //A,65 in ascii
if ( 0<= dig2 && dig2<= 9) dig2+=48;
if (10<= dig2 && dig2<=15) dig2+=65-10;
string r;
r.append( &dig1, 1);
r.append( &dig2, 1);
return r;
}
// Some Unices get this, now we do too!
time_t timegm(struct tm *t)
{
_tzset();
t->tm_sec -= _timezone;
t->tm_isdst = 0;
return mktime(t);
}
static char *month_names[] = { "Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec" };
int parse_month(const char *m)
{
for(size_t i=0; i<12; i++)
{
if(strcmp(month_names[i],m) == 0)
return i;
}
return -1;
}
time_t parse_time(const std::string &s)
{
struct tm t;
char day[4],month[4];
char plus;
int zone;
if(sscanf(s.c_str(),"%3s %3s %d %d:%d:%d %c%d %d",
day,month,&t.tm_mday,&t.tm_hour,&t.tm_min,&t.tm_sec,
&plus,&zone,&t.tm_year) == 9)
{
t.tm_year -= 1900;
t.tm_mon = parse_month(month);
if(t.tm_mon == -1)
return 0;
return timegm(&t);
}
return 0;
}