使用括号和 åäö 的 Twitter 搜索失败

Twitter search using parenthesis and åäö fails

我正在使用下面的代码来搜索 Twitter。大多数搜索工作正常,但有几个搜索不工作。请参阅下面的列表。这是我这边的问题(签名?)还是可以认为是 API 中的错误?

我得到的错误:

{"errors":[{"message":"Could not authenticate you","code":32}]}

我尝试过的搜索:

"(Malmö OR Lund) (Sverige OR Skåne)" // Fails
"(Malmo OR lund) (Skane OR Sweden)" // Works, returns correct result
"Malmö" // Works

我的代码:

public async Task SearchTwitter(string query)
{
    var oauth_token = "xxxxxxxxxxxx";
    var oauth_token_secret = "xxxxxxxxxxxx";
    var oauth_consumer_key = "xxxxxxxxxxxx";
    var oauth_consumer_secret = "xxxxxxxxxxxx";

    var baseUrl = "https://api.twitter.com/1.1/search/tweets.json";

    var parameters = new Dictionary<string, string>
    {
        {"tweet_mode", "extended"},
        {"result_type", "recent"},
        {"count", 100.ToString()},
        {"q", query},

        {"oauth_consumer_key", oauth_consumer_key},
        {"oauth_timestamp", DateTime.UtcNow.ToUnixStringFromDateTime()},
        {"oauth_nonce", Guid.NewGuid().ToString("N")},
        {"oauth_version", "1.0"},
        {"oauth_signature_method", "HMAC-SHA1"},
        {"oauth_token", oauth_token}
    };

    var sortedParameterString =
        string.Join("&",
            (from parm in parameters
             orderby parm.Key
             select Uri.EscapeDataString(parm.Key) + "=" + Uri.EscapeDataString(parameters[parm.Key]))
                .ToArray());

    var signatureBaseString = "GET&" + Uri.EscapeDataString(baseUrl) + "&" + Uri.EscapeDataString(sortedParameterString);

    var signingKey = Uri.EscapeDataString(oauth_consumer_secret) + "&" + Uri.EscapeDataString(oauth_token_secret);

    string oauth_signature;
    using (HMACSHA1 hasher = new HMACSHA1(ASCIIEncoding.ASCII.GetBytes(signingKey)))
    {
        oauth_signature = Convert.ToBase64String(hasher.ComputeHash(ASCIIEncoding.ASCII.GetBytes(signatureBaseString)));
    }

    var headerString = "OAuth " + string.Join(", ", parameters.Where(kv => kv.Key.StartsWith("oauth")).Select(kv => kv.Key + "=\"" + Uri.EscapeDataString(kv.Value) + "\"")) + ", oauth_signature=\"" + Uri.EscapeDataString(oauth_signature) + "\"";

    var uri = new Uri(baseUrl + $"?count={Uri.EscapeDataString(100.ToString())}&q={Uri.EscapeDataString(query)}&result_type={Uri.EscapeDataString("recent")}&tweet_mode={Uri.EscapeDataString("extended")}");

    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
    httpRequestMessage.Headers.Add("Authorization", headerString);
    httpRequestMessage.Headers.ExpectContinue = false;

    var httpResponseMessage = await GetHttpClient().SendAsync(httpRequestMessage).ConfigureAwait(false);
    var resultString = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
    if (httpResponseMessage.IsSuccessStatusCode)
    {
        // Request succeeded
    }
    else {
        // Request failed
    }
}

private static HttpClient GetHttpClient()
{
    var handler = new HttpClientHandler();
    if (handler.SupportsAutomaticDecompression)
    {
        handler.AutomaticDecompression = DecompressionMethods.GZip;
    }

    return new HttpClient(handler);
}

要转义 URI 字符串上的查询数据,您必须使用 Uri.EscapeUriString 而不是 Uri.EscapeDataString,这两个函数非常相似,但在某些情况下有所不同,例如 space char wich一个表示为 + 而另一个表示为 %20.

因此,结果证明 Uri 编码和解码 urls 的方式有所不同,具体取决于 unicode 字符与否:https://github.com/dotnet/corefx/issues/15865.

我的解决方案是解析 Uri.AbsoluteUri 的内容(以相同的、不一致的方式对 url 进行编码)并在计算身份验证签名时使用它。而不是像我以前那样使用 Uri.EscapeDataString。

public async Task SearchTwitter(string query)
    {
        var oauth_token = "xxxxxx";
        var oauth_token_secret = "xxxxxx";
        var oauth_consumer_key = "xxxxxx";
        var oauth_consumer_secret = "xxxxxx";

        var baseUrl = "https://api.twitter.com/1.1/search/tweets.json";

        var oauthParameters = new Dictionary<string, string>
        {
            {"oauth_consumer_key", Uri.EscapeDataString(oauth_consumer_key)},
            {"oauth_timestamp", Uri.EscapeDataString(DateTime.UtcNow.ToUnixStringFromDateTime())},
            {"oauth_nonce", Uri.EscapeDataString(Guid.NewGuid().ToString("N"))},
            {"oauth_version", Uri.EscapeDataString("1.0")},
            {"oauth_signature_method", Uri.EscapeDataString("HMAC-SHA1")},
            {"oauth_token", Uri.EscapeDataString(oauth_token)}
        };

        var uri = new Uri(baseUrl + $"?count={Uri.EscapeDataString(100.ToString())}&q={Uri.EscapeDataString(query)}&result_type={Uri.EscapeDataString("recent")}&tweet_mode={Uri.EscapeDataString("extended")}");

        var queryString = uri.AbsoluteUri;
        queryString = queryString.Split('?')[1];
        var queryStringParameters = queryString.Split('&');
        var queryStringParameterDictionary = queryStringParameters.Select(queryStringParameter => queryStringParameter.Split('=')).ToDictionary(keyValue => keyValue[0], keyValue => keyValue[1]);

        var allParameters = queryStringParameterDictionary.Concat(oauthParameters).GroupBy(d => d.Key).ToDictionary(d => d.Key, d => d.First().Value);
        var sortedParameterString =
            string.Join("&",
                (from parm in allParameters
                 orderby parm.Key
                 select parm.Key + "=" + parm.Value)
                    .ToArray());

        var signatureBaseString = "GET&" + Uri.EscapeDataString(baseUrl) + "&" + Uri.EscapeDataString(sortedParameterString);

        var signingKey = Uri.EscapeDataString(oauth_consumer_secret) + "&" + Uri.EscapeDataString(oauth_token_secret);

        string oauth_signature;
        using (HMACSHA1 hasher = new HMACSHA1(Encoding.ASCII.GetBytes(signingKey)))
        {
            oauth_signature = Convert.ToBase64String(hasher.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString)));
        }

        var headerString = "OAuth " + string.Join(", ", oauthParameters.Select(kv => kv.Key + "=\"" + kv.Value + "\"")) + ", oauth_signature=\"" + Uri.EscapeDataString(oauth_signature) + "\"";


        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
        httpRequestMessage.Headers.Add("Authorization", headerString);
        httpRequestMessage.Headers.ExpectContinue = false;

        var httpResponseMessage = await GetHttpClient().SendAsync(httpRequestMessage);

        var resultString = await httpResponseMessage.Content.ReadAsStringAsync();
    }