4

We've just changed to Twitter api 1.1, and now Tweeting doesn't work & returns an error "The remote server returned an error: (400) Bad Request." Researching on SO about this suggests that it's something to do with authentication, but we are sending the accessToken & secret which we've just got from the login page. It all worked fine with api 1.0. The code is -

    public void Tweet(Action<string> response, string message)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("POST&");
        sb.Append(Uri.EscapeDataString(_postUrl));
        sb.Append("&");

        string oauthNonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));
        string timeStamp = MakeTimestamp();

        var dict = new SortedDictionary<string, string>
        {
            { "oauth_consumer_key", _oAuthConfig.ConsumerKey },
            { "oauth_nonce", oauthNonce },
            { "oauth_signature_method", "HMAC-SHA1" },
            { "oauth_timestamp", timeStamp },
            { "oauth_token", _accessToken },
            { "oauth_version", "1.0" },
        };

        foreach (var keyValuePair in dict)
        {
            sb.Append(Uri.EscapeDataString(string.Format("{0}={1}&", keyValuePair.Key, keyValuePair.Value)));
        }

        string encodedMessage = EscapeAdditionalChars(Uri.EscapeDataString(message));
        sb.Append(Uri.EscapeDataString("status=" + encodedMessage));

        string signatureBaseString = sb.ToString();


        // create the signature

        string signatureKey = Uri.EscapeDataString(_oAuthConfig.ConsumerSecret) + "&" + Uri.EscapeDataString(_accessTokenSecret);

        var hmacsha1 = new HMACSHA1(new ASCIIEncoding().GetBytes(signatureKey));

        string signatureString = Convert.ToBase64String(hmacsha1.ComputeHash(new ASCIIEncoding().GetBytes(signatureBaseString)));


        // create the headers

        string authorizationHeaderParams = String.Empty;

        authorizationHeaderParams += "OAuth ";
        authorizationHeaderParams += "oauth_consumer_key=\"" + _oAuthConfig.ConsumerKey + "\", ";
        authorizationHeaderParams += "oauth_nonce=\"" + oauthNonce + "\", ";
        authorizationHeaderParams += "oauth_signature=\"" + Uri.EscapeDataString(signatureString) + "\", ";
        authorizationHeaderParams += "oauth_signature_method=\"" + "HMAC-SHA1" + "\", ";
        authorizationHeaderParams += "oauth_timestamp=\"" + timeStamp + "\", ";
        authorizationHeaderParams += "oauth_token=\"" + _accessToken + "\", ";
        authorizationHeaderParams += "oauth_version=\"" + "1.0" + "\"";

        string messageToPost = EscapeAdditionalChars(SpacesToPlusSigns(message));


        // initialise the WebClient

        WebClient client = new WebClient();

        client.Headers [HttpRequestHeader.Authorization] = authorizationHeaderParams;

        client.UploadDataCompleted += (s, eArgs) =>
        {
            if (eArgs.Error == null)
                response(DefaultSuccessMessage());
            else
                response(eArgs.Error.Message);
        };

        try
        {
            Uri uri = new Uri(_postUrl);
            try
            {
                client.UploadDataAsync(uri, "POST", Encoding.UTF8.GetBytes("status=" + messageToPost));
            }
            catch (WebException e)
            {
                Log.Info("TwitterService->Tweet web error: " + e.Message);
                response(DefaultErrorMessage());
            }
            catch (Exception e)
            {
                // Can happen if we had already favorited this status
                Log.Info("TwitterService->Tweet error: " + e.Message);
                response(DefaultErrorMessage());
            }
        }
        catch (WebException e)
        {
            Log.Info("TwitterService->Tweet web error 2: " + e.Message);
            response(DefaultErrorMessage());
        }
        catch (Exception e)
        {
            Log.Info("TwitterService->Tweet error 2: " + e.Message);
            response(DefaultErrorMessage());
        }
    }

Basically, I'd like to be able to Tweet without using any 3rd party libraries such as Twitterizer (even TweetStation seems to be broken with api 1.1) - surely it can't be that difficult!

Any help much appreciated, as it feels a bit like a brick wall at the moment - I'm also fairly new to c#, which doesn't help...

Edited to show code which wasn't clear previously.

SomaMan
  • 4,127
  • 1
  • 34
  • 45
  • Are you sure you're not being rate limited? Since this will also cause a 400 / Bad request. – Edwin de Koning Jul 01 '13 at 07:17
  • Definitely not a rate limit issue, and the error changes according to tweaks I make to the code, it can be either 400 / Bad Request or 401 / Unauthorized. I finally figured it out - see my answer – SomaMan Jul 02 '13 at 12:04

4 Answers4

5

Finally found the solution, as usual with most of these things, it was pretty simple. Code below -

    public void Tweet(Action<string> response, string message)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat ("status={0}", PercentEncode(message));

        string content = sb.ToString();


        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_postUrl);

        request.Headers.Add("Authorization", AuthorizeRequest(_accessToken, _accessTokenSecret, "POST", new Uri(_postUrl), content));
        request.ContentType = "application/x-www-form-urlencoded";
        request.ServicePoint.Expect100Continue = false;
        request.Method = "POST";


        try
        {
            try
            {
                using (Stream stream = request.GetRequestStream())
                {
                    Byte[] streamContent = Encoding.UTF8.GetBytes("status=" + PercentEncode(message));
                    stream.Write(streamContent, 0, streamContent.Length);
                }


                HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse();

                string contents = "";
                using (Stream stream = webResponse.GetResponseStream())
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        contents = reader.ReadToEnd();
                    }

                Console.WriteLine("Twitter response: " + contents);

                response(DefaultSuccessMessage());

            }
            catch (WebException e)
            {
                Log.Info("TwitterService->Tweet web error: " + e.Message);
                response(DefaultErrorMessage());
            }
            catch (Exception e)
            {
                // Can happen if we had already favorited this status
                Log.Info("TwitterService->Tweet error: " + e.Message);
                response(DefaultErrorMessage());
            }
        }
        catch (WebException e)
        {
            Log.Info("TwitterService->Tweet web error 2: " + e.Message);
            response(DefaultErrorMessage());
        }
        catch (Exception e)
        {
            Log.Info("TwitterService->Tweet error 2: " + e.Message);
            response(DefaultErrorMessage());
        }
    }


    private string AuthorizeRequest(string oauthToken, string oauthTokenSecret, string method, Uri uri, string data)
    {
        string oauthNonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));

        var headers = new Dictionary<string, string>()
        {
            { "oauth_consumer_key", _oAuthConfig.ConsumerKey },
            { "oauth_nonce", oauthNonce },
            { "oauth_signature_method", "HMAC-SHA1" },
            { "oauth_timestamp", MakeTimestamp() },
            { "oauth_token", oauthToken },
            { "oauth_verifier", PercentEncode(_authorizationVerifier) },
            { "oauth_version", "1.0A" }
        };
        var signatureHeaders = new Dictionary<string,string>(headers);

        // Add the data and URL query string to the copy of the headers for computing the signature
        if (data != null && data != "")
        {
            var parsed = HttpUtility.ParseQueryString(data);
            foreach (string k in parsed.Keys)
            {
                signatureHeaders.Add(k, PercentEncode(parsed [k]));
            }
        }

        var nvc = HttpUtility.ParseQueryString(uri.Query);
        foreach (string key in nvc)
        {
            if (key != null)
                signatureHeaders.Add(key, PercentEncode(nvc [key]));
        }

        string signature = MakeSignature (method, uri.GetLeftPart(UriPartial.Path), signatureHeaders);
        string compositeSigningKey = MakeSigningKey(_oAuthConfig.ConsumerSecret, oauthTokenSecret);
        string oauth_signature = MakeOAuthSignature(compositeSigningKey, signature);

        headers.Add ("oauth_signature", PercentEncode(oauth_signature));


        return HeadersToOAuth(headers);
    }


    private static string PercentEncode (string s)
    {
        var sb = new StringBuilder ();

        foreach (byte c in Encoding.UTF8.GetBytes (s))
        {
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~')
                sb.Append ((char) c);
            else
            {
                sb.AppendFormat ("%{0:X2}", c);
            }
        }
        return sb.ToString ();
    }


    private static string MakeTimestamp ()
    {
        return ((long) (DateTime.UtcNow - _unixBaseTime).TotalSeconds).ToString ();
    }

    private static string MakeSignature (string method, string base_uri, Dictionary<string,string> headers)
    {
        var items = from k in headers.Keys orderby k 
            select k + "%3D" + PercentEncode (headers [k]);

        return method + "&" + PercentEncode (base_uri) + "&" + 
            string.Join ("%26", items.ToArray ());
    }

    private static string MakeSigningKey (string consumerSecret, string oauthTokenSecret)
    {
        return PercentEncode (consumerSecret) + "&" + (oauthTokenSecret != null ? PercentEncode (oauthTokenSecret) : "");
    }

    private static string MakeOAuthSignature (string compositeSigningKey, string signatureBase)
    {
        var sha1 = new HMACSHA1 (Encoding.UTF8.GetBytes (compositeSigningKey));

        return Convert.ToBase64String (sha1.ComputeHash (Encoding.UTF8.GetBytes (signatureBase)));
    }

    private static string HeadersToOAuth (Dictionary<string,string> headers)
    {
        return "OAuth " + String.Join (",", (from x in headers.Keys select String.Format ("{0}=\"{1}\"", x, headers [x])).ToArray ());
    }

With Twitter api 1.0, I used a WebClient to post, that doesn't work with api 1.1, and it seems that the reason for this is that you can't set the ContentType or the ServicePoint.Expect100Continue properties - without these set as I've set them, the request is sent back as (401) unauthorized. Nothing to do with encoding problems in the end.

Thanks to others for the various helper methods.

SomaMan
  • 4,127
  • 1
  • 34
  • 45
  • I have spent the best part of a whole day trying to get the authentication working. It's a pain in the a$$. This was pretty much a copy+paste job. Thanks. I couldn't take guessing what was wrong any longer! – Dave Oct 21 '15 at 19:08
3

I had exactly the same problem:

This is exactly what you need to do here:

Authenticate and request a user's timeline with Twitter API 1.1 oAuth

I have created a project for this at : https://github.com/andyhutch77/oAuthTwitterTimeline

It also includes an MVC, Web app and console demo.

Community
  • 1
  • 1
hutchonoid
  • 32,982
  • 15
  • 99
  • 104
  • Thanks for posting, but I'd already figured it out & posted my solution! Can't think how I didn't come across your post when I was searching, and I searched for a long time... – SomaMan Jul 03 '13 at 07:43
  • No problem, it was probably before I posted this too. – hutchonoid Jul 03 '13 at 07:54
0

I ran into this problem, or at least one striking similiar (from my noob perspective), recently for an app I am building. What seemed to solve it for me (after looking at the tool at dev.twitter.com) was simply to get rid of the quotes around the parameter names, so that (in your case):

I notice that you do in fact not have quotes around your parameter names. However, it confuses me that you send authentication details twice (hence my wrongheaded post.) It works for me without doing this, and I googled it briefly and found: https://dev.twitter.com/discussions/12322#comment-27120, which confirms this can be a problem generating an Authetication Error.

  • Thanks for your suggestion - I've finally found the solution, & I'll post an answer which details everything. Actually nothing to do with quotes in the end, but worth considering - Twitter's docs didn't really help (maybe fair enough, as it ended up being a c# issue). – SomaMan Jul 02 '13 at 09:09
-1

400 means you are not authenticated. I recommend getting user context.

https://dev.twitter.com/docs/auth/oauth#user-context

VDALLCO
  • 168
  • 9
  • I don't think authentication is the problem, as I've just signed in & been given the accessToken & accessTokenSecret. I think it could well be something to do with the encodings used - I'm investigating it. – SomaMan Jul 01 '13 at 16:30