Twitter OAuth in C++ for Win32 - Part 3 - Updating Twitter Status

Part 1 covered the OAuth process at a high level, and Part 2 went over the example code in detail.  Here in Part 3, we’ll finish things off by adding support for doing OAuth POST requests, enabling you to update your Twitter status.

The example project and source files are available on Google Code github. C++ code for Win32, with a Visual Studio 2008 project file.

OAuth POST Requests

Doing a POST request isn’t much different from doing a GET request, but there are still a few important things that you need to get right and, like all things OAuth, if there is a single character out of place somewhere it won’t work.

First one simple cosmetic change for clarity, I renamed the OAuthParameters type to HTTPParameters to reflect its general nature. I’m using it for holding GET and POST query parameters as well as those used for OAuth.

typedef std::map<wstring, wstring> HTTPParameters;

This name change is reflected throughout the code. Next we add a new URL constant for accessing the status update API on twitter:

const wstring UserStatusUpdateUrl = L"http://twitter.com/statuses/update.xml";

I added a quick interface to prompt the user for their status update after retrieving the user’s timeline:

wprintf(L"Enter your status update (Enter to skip): ");

wchar_t buf[500] = {L""};
_getws_s(buf, SIZEOF(buf));

wstring input = buf;
if(input.size() > 0)
{
    wprintf(L"\r\nUpdating status: %s\r\n", input.c_str());

    HTTPParameters postParams;
    postParams[L"status"] = UrlEncode(input);

    wstring submitResult = OAuthWebRequestSubmit(UserStatusUpdateUrl, L"POST", &postParams, ConsumerKey, ConsumerSecret,
        oauthAccessToken, oauthAccessTokenSecret);

    wprintf(L"\r\nUpdate Result:\r\n%s\r\n", submitResult.c_str());
}

After checking if the user has entered an update and not an empty string, the first step is to build an HTTPParameters object and assign the user’s text to the 'status' parameter. There are a number of other possible parameters which could also be added to postParams, as documented on the Twitter API statuses/update reference page.

Note that the status text is URL Encoded immediately, before it is put into the parameters object. I originally had made the mistake of leaving this until later, when I build the POST query string to send over HTTP. This is incorrect (as I’ve implemented things), because when we generate the OAuth signature later, we need to sign the POST query values as they are sent to the server. In this case, that means URL Encoded, and since I was only URL encoding the data as I sent it in the POST request, the signed data didn’t match (it wasn’t URL encoded). One alternative would have been to do the URL encoding in multiple places, when I generate the OAuth signature, and when generating the POST form data. Doing it once removes decreases your chances of forgetting or doing things differently in multiple places later.

There are two other changes of note here, one is that we are passing "POST" as the HTTP request method, indicating that we are passing parameters as the request body, and not as part of the URL as in GET requests. The second is I’ve added a new parameter to OAuthWebRequestSubmit, following the request method is an optional pointer to an HTTPParameters list, here we provide postParams which contains the status value.

This second change is also reflected throughout the rest of the example code, where all of the other calls are using the "GET" HTTP request method I am simply passing NULL for the unused postParameters value.

The updated OAuthWebRequestSubmit:

wstring OAuthWebRequestSubmit(
    const wstring& url,
    const wstring& httpMethod,
    const HTTPParameters* postParameters,
    const wstring& consumerKey,
    const wstring& consumerSecret,
    const wstring& oauthToken = L"",
    const wstring& oauthTokenSecret = L"",
    const wstring& pin = L""
    )
{
    wstring query = UrlGetQuery(url);
    HTTPParameters getParameters = ParseQueryString(query);

    HTTPParameters oauthSignedParameters = BuildSignedOAuthParameters(
        getParameters,
        url,
        httpMethod,
        postParameters,
        consumerKey, consumerSecret,
        oauthToken, oauthTokenSecret,
        pin );
    return OAuthWebRequestSignedSubmit(oauthSignedParameters, url, httpMethod, postParameters);
}

The only difference is that we are now passing the new postParameters value to BuildSignedOAuthParameters and OAuthWebRequestSignedSubmit (formerly also named OAuthWebRequestSubmit, which wasn’t a great choice for clarity), as well as passing the httpMethod to OAuthWebRequestSignedSubmit which now takes the HTTP request method as an argument instead of hard coding it to "GET" internally.

BuildSignedOAuthParameters actually does something useful with the new postParameters value:

// create a parameter list containing both oauth and original parameters
// this will be used to create the parameter signature
HTTPParameters allParameters = requestParameters;
if(Compare(httpMethod, L"POST", false) && postParameters)
{
    allParameters.insert(postParameters->begin(), postParameters->end());
}
allParameters.insert(oauthParameters.begin(), oauthParameters.end());

// prepare a signature base, a carefully formatted string containing
// all of the necessary information needed to generate a valid signature
wstring normalUrl = OAuthNormalizeUrl(url);
wstring normalizedParameters = OAuthNormalizeRequestParameters(allParameters);
wstring signatureBase = OAuthConcatenateRequestElements(httpMethod, normalUrl, normalizedParameters);

// obtain a signature and add it to header requestParameters
wstring signature = OAuthCreateSignature(signatureBase, consumerSecret, requestTokenSecret);
oauthParameters[L"oauth_signature"] = signature;

return oauthParameters;

Where before we were taking the request parameters (parsed from the URL) and adding in the OAuth parameters (that we just generated) and combining them into the same list, we are now adding the postParameters into the mix as well, but only if it’s a "POST" request, and there are any postParameters available to be added. As you can see, this master list is then sent to OAuthNormalizeRequestParameters whose return value is then concatenated and ultimately included in the OAuthCreateSignature process.

Finally, after the signature is generated we pass our two new parameters to OAuthWebRequestSignedSubmit where the actual working of doing the POSTing happens:

wstring OAuthWebRequestSignedSubmit(
    const HTTPParameters& oauthParameters,
    const wstring& url,
    const wstring& httpMethod,
    const HTTPParameters* postParameters
    )
{
    _TRACE("OAuthWebRequestSignedSubmit(%s)", url.c_str());

    wstring oauthHeader = L"Authorization: OAuth ";
    oauthHeader += OAuthBuildHeader(oauthParameters);
    oauthHeader += L"\r\n";

    _TRACE("%s", oauthHeader.c_str());

    wchar_t host[1024*4] = {};
    wchar_t path[1024*4] = {};

    URL_COMPONENTS components = { sizeof(URL_COMPONENTS) };

    components.lpszHostName = host;
    components.dwHostNameLength = SIZEOF(host);

    components.lpszUrlPath = path;
    components.dwUrlPathLength = SIZEOF(path);

    wstring normalUrl = url;

    BOOL crackUrlOk = InternetCrackUrl(url.c_str(), url.size(), 0, &components);
    _ASSERTE(crackUrlOk);

    wstring result;

    // TODO you'd probably want to InternetOpen only once at app initialization
    HINTERNET hINet = InternetOpen(L"tc2/1.0",
        INTERNET_OPEN_TYPE_PRECONFIG,
        NULL,
        NULL,
        0 );
    _ASSERTE( hINet != NULL );
    if ( hINet != NULL )
    {
        // TODO add support for HTTPS requests
        HINTERNET hConnection = InternetConnect(
            hINet,
            host,
            components.nPort,
            NULL,
            NULL,
            INTERNET_SERVICE_HTTP,
            0, 0 );
        _ASSERTE(hConnection != NULL);
        if ( hConnection != NULL)
        {
            HINTERNET hData = HttpOpenRequest( hConnection,
                httpMethod.c_str(),
                path,
                NULL,
                NULL,
                NULL,
                INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_RELOAD,
                0 );
            _ASSERTE(hData != NULL);
            if ( hData != NULL )
            {
                BOOL oauthHeaderOk = HttpAddRequestHeaders(hData,
                    oauthHeader.c_str(),
                    oauthHeader.size(),
                    0);
                _ASSERTE(oauthHeaderOk);

                // NOTE POST requests are supported, but the MIME type is hardcoded to application/x-www-form-urlencoded (aka. form data)
                // TODO implement support for posting image, raw or other data types
                string postDataUTF8;
                if(Compare(httpMethod, L"POST", false) && postParameters)
                {
                    wstring contentHeader = L"Content-Type: application/x-www-form-urlencoded\r\n";
                    BOOL contentHeaderOk = HttpAddRequestHeaders(hData,
                        contentHeader.c_str(),
                        contentHeader.size(),
                        0);
                    _ASSERTE(contentHeaderOk);

                    postDataUTF8 = WideToUTF8(BuildQueryString(*postParameters));
                    _TRACE("POST DATA: %S", postDataUTF8.c_str());
                }

                BOOL sendOk = HttpSendRequest( hData, NULL, 0, (LPVOID)(postDataUTF8.size() > 0 ? postDataUTF8.c_str() : NULL), postDataUTF8.size());
                _ASSERTE(sendOk);

                // TODO dynamically allocate return buffer
                BYTE buffer[1024*32] = {};
                DWORD dwRead = 0;
                while(
                    InternetReadFile( hData, buffer, SIZEOF(buffer) - 1, &dwRead ) &&
                    dwRead > 0
                    )
                {
                    buffer[dwRead] = 0;
                    result += UTF8ToWide((char*)buffer);
                }

                _TRACE("%s", result.c_str());

                InternetCloseHandle(hData);
            }
            InternetCloseHandle(hConnection);
        }
        InternetCloseHandle(hINet);
    }

    return result;
}

Another minor cosmetic change, I extracted the OAuth header parameter formatting out into its own function OAuthBuildHeader.

Farther down you can see in the call to HttpOpenRequest I am no longer blindly passing "GET", and instead am passing in the new httpMethod parameter value. Currently "GET" and "POST" are the only values likely to work.

And finally, just before calling HttpSendRequest, we again check to see if we’re processing a "POST" request, and if so, we take care of a couple of tasks.

First, we need to add a header indicating the MIME type of the data that we’ll be posting, in this case application/x-www-form-urlencoded which is the data format used for submitting forms on websites.

Next we need to format the POST parameters into the application/x-www-form-urlencoded data format, which is simple. It’s essentially the same as the query part of an URL used in a GET request (var1=blah&var2=foo&var3=john). This is taken care of by the helper function BuildQueryString:

// parameters must already be URL encoded before calling BuildQueryString
wstring BuildQueryString( const HTTPParameters &parameters )
{
    wstring query;

    for(HTTPParameters::const_iterator it = parameters.begin();
        it != parameters.end();
        ++it)
    {
        _TRACE("%s = %s", it->first.c_str(), it->second.c_str());

        if(it != parameters.begin())
        {
            query += L"&";
        }

        wstring pair;
        pair += it->first + L"=" + it->second + L"";
        query += pair;
    }
    return query;
}

The resulting query string must then be UTF8 encoded. When we send the query string as part of a GET request, the WinInet APIs convert the entire URL to UTF8 for us, parameters and all. Not so with POST data, which WinInet treats as raw binary and will pass along unchanged.

I forgot to do this the first time, and just sent my wide string data buffer as the POST data. This resulted in an invalid signature error from Twitter since the data was UTF8 encoded as part of the signing process and therefor the signature didn’t match the raw wide string data that I sent with the POST (and which wouldn’t have worked anyway since the twitter API would almost certainly only accept UTF8 encoded POST data).

Last but not least, we pass the UTF8 encoded form data string to HttpSendRequest, or pass NULL if there is no data to send.

Tweeted

And that should be it. Assuming everything has gone right so far, you have just sent a tweet out into the twitterverse. The return value from the call (if it succeeds) should be a copy of the full tweet in XML format. One thing to note while testing is that (according to the Twitter API docs) multiple tweets in a row with exactly the same text will be ignored, so make sure to change it up each time.

Since UrlEncode converts to UTF8 as an intermediate format as part of the encoding process (when we first added the status text to the HTTPParameters object), Unicode status text should work fine. I tested it with Japanese and had no problems, but I had to hard code the string as I wasn’t able to enter Japanese into my console window. A utility I found in order to do the test without saving my source file in Unicode is Richard Ishida’s Unicode Code Converter, which looks super handy for a variety of Unicode related tasks.