Twitter请求令牌Wahts错误的HTTP?

时间:2014-02-20 00:29:47

标签: javascript twitter oauth firefox-addon twitter-oauth

我确定我正在做的事情,我的oauth关键是正确的,nonce等等,但是我正在未经授权。我正在尝试从twitter api request_token请求令牌。

/***********REQUEST HEADERS*********/
https://api.twitter.com/oauth/request_token

POST /oauth/request_token HTTP/1.1
Host: api.twitter.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
OAuth: oauth_callback="http%3A%2F%2Fwww.floppers.comyr", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1392864982", oauth_version="1.0", oauth_consumer_key="AjONvgAdbD8YWCtRn5U9yA", oauth_signature="bGlm1jpJN6vHzoMwDxHBuv%2BJrts%3D", oauth_nonce="4L1mWWD5"
Cookie: guest_id=v1%3A139284158178241372; _twitter_sess=BAh7CToPY3JlYXRlZF9hdGwrCJEb1UtEASIKZmxhc2hJQzonQWN0aW9uQ29u%250AdHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7ADoMY3NyZl9p%250AZCIlMzM5OTJjNTVlN2FjODY1MjUwZDk1NDc0ZjE3OWM0NmQ6B2lkIiVlZTVm%250AMjg2MjA4MjdmMjY4MjUwMzkzZDVjNzcxMGRhMA%253D%253D--a4fad573f9311178e3d998cda64c388d697d2d36; __utma=43838368.710631160.1392843854.1392843854.1392843854.1; __utmc=43838368; __utmz=43838368.1392843854.1.1.utmcsr=loginradius.com|utmccn=(referral)|utmcmd=referral|utmcct=/; pid="v3:1392861388625837524567745"
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Content-Length: 0




/***********RESPONSE HEADERS*********/
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache, no-store, must-revalidate, pre-check=0, post-check=0
Content-Encoding: deflate
Content-Length: 56
Content-Type: text/html; charset=utf-8
Date: Thu, 20 Feb 2014 02:56:22 GMT
Expires: Tue, 31 Mar 1981 05:00:00 GMT
Last-Modified: Thu, 20 Feb 2014 02:56:22 GMT
Pragma: no-cache
Server: tfe
Set-Cookie: _twitter_sess=BAh7CToPY3JlYXRlZF9hdGwrCJEb1UtEAToMY3NyZl9pZCIlMzM5OTJjNTVl%250AN2FjODY1MjUwZDk1NDc0ZjE3OWM0NmQ6B2lkIiVlZTVmMjg2MjA4MjdmMjY4%250AMjUwMzkzZDVjNzcxMGRhMCIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6%250ARmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA%253D%253D--eec641e68861e03b4806c30892b1e5be1ac3cba7; domain=.twitter.com; path=/; secure; HttpOnly
Status: 401 Unauthorized
Strict-Transport-Security: max-age=631138519
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN
x-mid: 824a51486310f68e801a698727a57539909af7c3
X-Runtime: 0.00876
x-transaction: 1e075c3b5eb3911c
X-UA-Compatible: IE=10,chrome=1
X-XSS-Protection: 1; mode=block
X-Firefox-Spdy: 3.1

这是javascript代码。可以将它复制并粘贴到firefox scratchpadset环境到浏览器并运行。

initSHA1(this); //onload must run this once

var HTTPMethod = 'POST';
var requestURL = 'https://api.twitter.com/oauth/request_token';
var APIKey = 'AjONvgAdbD8YWCtRn5U9yA'; //also known as oauth_consumer_key //from your app page on twitter dev site
var ConsumerSecret = 'jrcJKxvJ92NeeV48RL1lotN9PigbxCCbqUkKj237yio'; //from your app page on twitter dev site
var OAuthTokenSecret = '';

var param = {
    oauth_callback: rawurlencode('http://www.floppers.comyr'),
    oauth_signature_method: 'HMAC-SHA1',
    oauth_timestamp: 0, //running setTimestampNonceSignature() will update this
    oauth_version: '1.0',
    oauth_consumer_key: APIKey, //api key u get from ur app page on twiitter
    oauth_signature: 0, //running setTimestampNonceSignature() will update this //to make the function i followed steps here to create my gen function: https://dev.twitter.com/docs/auth/creating-signature
    oauth_nonce: 0 //running setTimestampNonceSignature() will update this
}
//then first run this func
setTimestampNonceSignature();
//then can run post
var labelHTTP = 'twit api requesting token';

HTTP(HTTPMethod, requestURL, {
    body: '',
    returnHeaders: true,
    headers: {
        OAuth: OAuthHeaderFromParam(param)
    },
    onSuccess: function(status, responseXML, responseText, headers, statusText) {

            Cu.reportError('SUCCESS: ' + labelHTTP + '\n\nstatusText:' + statusText + '\nresponseText:' + responseText + '\nheaders:' + uneval(headers));

        },
        onFailure: function(status, responseXML, responseText, headers, statusText) {

            Cu.reportError('FAIL: ' + labelHTTP + '\n\nstatusText:' + statusText + '\nresponseText:' + responseText + '\nheaders:' + uneval(headers));
            //var response = JSON.parse(responseText);
        }
});


function OAuthHeaderFromParam(param) {
    //param is an object of key value pairs of post data
    //this returns a string of it
    ////key1="val1", key2="val2", key3="val3"....
    var header = [];
    for (var p in param) {
        header.push(p + '="' + param[p] + '"');
    }
    return header.join(', ');
}

function setTimestampNonceSignature() {
    param.oauth_timestamp = Math.floor(new Date().getTime()/1000);
    param.oauth_nonce = _nonce();

    //start gen signature
    //param.oauth_signature = ''; //no need for thsi line as we overwrite sig val
    var combine = [];
    var keys = [];
    for (var p in param) {
        if (p == 'oauth_signature') {
            continue;
        }
        keys.push(p);
    }
    keys.sort(); //sorts it lexographically

    //NOTE: important: values in param must not be urlencoded
    for (var i=0; i<keys.length; i++) {
        combine.push(rawurlencode(keys[i]) + '=' + rawurlencode(param[keys[i]]));
    }

    var ParameterString = combine.join('&');
    //Cu.reportError('ParameterString = ' + ParameterString);

    var combine =  [HTTPMethod.toUpperCase(), rawurlencode(requestURL), rawurlencode(ParameterString)];
    var SignatureBaseString = combine.join('&');

    //Cu.reportError('SignatureBaseString = ' + SignatureBaseString);

    var SigningKey = rawurlencode(ConsumerSecret) + '&';
    if (OAuthTokenSecret && OAuthTokenSecret.length > 0) {
        SigningKey += rawurlencode(OAuthTokenSecret);
    }

    //Cu.reportError('SigningKey = ' + SigningKey);

    //signature done
    param.oauth_signature = calcHMAC(SignatureBaseString, SigningKey);

    //Cu.reportError('param.oauth_signature = ' + param.oauth_signature);
    param.oauth_signature = rawurlencode(param.oauth_signature);
}




/*************************************************************/
/////LIBRARY FUNCTIONS BELOW NOT IMPORTANT FOR STACKOVERFLOW HELP
/*************************************************************/
















//https://gist.github.com/Noitidart/9087938
    function _nonce(length) {
        if (typeof length === "undefined") {
            length = 8;
        }
        if (length < 1) {
            console.warn("Invalid nonce length.");
        }
        var nonce = "";
        for (var i = 0; i < length; i++) {
            var character = Math.floor(Math.random() * 61);
            nonce += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".substring(character, character + 1);
        }
        return nonce;
    };

//from: https://github.com/kvz/phpjs/blob/master/functions/url/rawurlencode.js
//also found at: http://phpjs.org/functions/rawurlencode/
function rawurlencode(str) {
  //       discuss at: http://phpjs.org/functions/rawurlencode/
  //      original by: Brett Zamir (http://brett-zamir.me)
  //         input by: travc
  //         input by: Brett Zamir (http://brett-zamir.me)
  //         input by: Michael Grier
  //         input by: Ratheous
  //      bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  //      bugfixed by: Brett Zamir (http://brett-zamir.me)
  //      bugfixed by: Joris
  // reimplemented by: Brett Zamir (http://brett-zamir.me)
  // reimplemented by: Brett Zamir (http://brett-zamir.me)
  //             note: This reflects PHP 5.3/6.0+ behavior
  //             note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on
  //             note: pages served as UTF-8
  //        example 1: rawurlencode('Kevin van Zonneveld!');
  //        returns 1: 'Kevin%20van%20Zonneveld%21'
  //        example 2: rawurlencode('http://kevin.vanzonneveld.net/');
  //        returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F'
  //        example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
  //        returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'

  str = (str + '')
    .toString();

  // Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current
  // PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following.
  return encodeURIComponent(str)
    .replace(/!/g, '%21')
    .replace(/'/g, '%27')
    .replace(/\(/g, '%28')
    .
  replace(/\)/g, '%29')
    .replace(/\*/g, '%2A');
}

//HTTPWRapper from https://gist.github.com/Noitidart/9088113
/** 
 * The following keys can be sent:
 * onSuccess (required)  a function called when the response is 2xx
 * onFailure             a function called when the response is not 2xx
 * username              The username for basic auth
 * password              The password for basic auth
 * overrideMimeType      The mime type to use for non-XML response mime types
 * timeout               A timeout value in milliseconds for the response
 * onTimeout             A function to call if the request times out.
 * body                  A string containing the entity body of the request
 * contentType           The content type of the entity body of the request
 * headers               A hash of optional headers
 * returnHeaders         Set to true if want headers returned in the "headers" var of the onSuccess etc
 */
function HTTP(method,url,options)
{
   var requester = new XMLHttpRequest();

   var timeout = null;
   if (!options.synchronizedRequest) {

      requester.onreadystatechange = function() {
         switch (requester.readyState) {
            case 0:
               if (options.onUnsent) {
                  options.onUnsent(requester);
               }
            break;
            case 1:
               if (options.onOpened) {
                  options.onOpened(requester);
               }
            break;
            case 2:
               if (options.onHeaders) {
                  options.onHeaders(requester);
               }
            break;
            case 3:
               if (options.onLoading) {
                  options.onLoading(requester);
               }
            break;
            case 4:
               if (timeout) {
                  clearTimeout(timeout);
               }
               if (requester.status==0 || (requester.status>=200 && requester.status<300)) {
                  options.onSuccess(
                     requester.status,
                     requester.responseXML,
                     requester.responseText,
                     options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null,
                     requester.statusText
                  );
               } else {
                  if (options.onFailure) {
                     options.onFailure(
                        requester.status,
                        requester.responseXML,
                        requester.responseText,
                        options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null,
                        requester.statusText
                     );
                  }
               }
            break;
         }
      }
   }

   if (options.overrideMimeType) {
      requester.overrideMimeType(options.overrideMimeType);
   }   
   if (options.username) {
      requester.open(method,url,!options.synchronizedRequest,options.username,options.password);
   } else {
      requester.open(method,url,!options.synchronizedRequest);
   }

   if (options.timeout && !options.synchronizedRequest) {
      timeout = setTimeout(
          function() {
             var callback = options.onTimeout ? options.onTimeout : options.onFailure;
             callback(0,"Operation timeout.");
          },
          options.timeout
      );
   }
   if (options.headers) {
      for (var name in options.headers) {
         requester.setRequestHeader(name,options.headers[name]);
      }
   }
   if (options.sendAsBinary) {
        Cu.reportError('sending as binary');
       requester.sendAsBinary(options.body);
   } else if (options.body) {
      requester.setRequestHeader("Content-Type",options.contentType);
      requester.send(options.body);
   } else {
      requester.send(null);
   }
   if (options.synchronizedRequest) {
      if (requester.status==0 || (requester.status>=200 && requester.status<300)) {
         options.onSuccess(
            requester.status,
            requester.responseXML,
            requester.responseText,
            options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null,
            requester.statusText
         );
      } else {
         if (options.onFailure) {
            options.onFailure(
               requester.status,
               requester.responseXML,
               requester.responseText,
               options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null,
               requester.statusText
            );
         }
      }
      return {
         abort: function() {
         }
      };
   } else {
      return {
         abort: function() {
            clearTimeout(timeout);
            requester.abort();
         }
      };
   }
}

var _HTTP_HEADER_NAME = new RegExp("^([a-zA-Z0-9_-]+):");
function _HTTP_parseHeaders(headerText)
{
   var headers = {};
   if (headerText) {
      var eol = headerText.indexOf("\n");
      while (eol>=0) {
         var line = headerText.substring(0,eol);
         headerText = headerText.substring(eol+1);
         while (headerText.length>0 && !headerText.match(_HTTP_HEADER_NAME)) {
            eol = headerText.indexOf("\n");
            var nextLine = eol<0 ? headerText : headerText.substring(0,eol);
            line = line+' '+nextLine;
            headerText = eol<0 ? "" : headerText.substring(eol+1);
         }
         // Parse the name value pair
         var colon = line.indexOf(':');
         var name = line.substring(0,colon);
         var value = line.substring(colon+1);
         headers[name] = value;
         eol = headerText.indexOf("\n");
      }
      if (headerText.length>0) {
         var colon = headerText.indexOf(':');
         var name = headerText.substring(0,colon);
         var value = headerText.substring(colon+1);
         headers[name] = value;
      }
   }
   return headers;
}




function calcHMAC(input, inputKey) { //MUST place this function below the block of yucky code above
    //currently set up to take inputText and inputKey as text and give output as SHA-1 in base64
    //var inputText = 'stuff you want to convert'; //must be text, if you use Base64 or HEX then change hmacInputType on line 34
    //var inputKey = 'key to use in conversion'; //must be text, if you use Base64 or HEX then change hmacKeyInputType on line 35
    try {
        var hmacInputType = 'TEXT'; //other values: B64, HEX
        var hmacKeyInputType = 'TEXT'; //other values: B64, HEX
        var hmacVariant = 'SHA-1'; //other values NOT SUPPORTED because js for it was stripped out of the src code for optimization: SHA-224, SHA-256, SHA-384, SHA-512
        var hmacOutputType = 'B64';
        var hmacObj = new jsSHA(input, hmacInputType);

        return hmacObj.getHMAC(
            inputKey,
            hmacKeyInputType,
            hmacVariant,
            hmacOutputType
        );
    } catch(e) {
        return e
    }
}

/*
 A JavaScript implementation of the SHA-1 ONLY hash, as
 defined in FIPS PUB 180-2 as well as the corresponding HMAC implementation
 as defined in FIPS PUB 198a

 Copyright Brian Turek 2008-2013
 Distributed under the BSD License
 See http://caligatio.github.com/jsSHA/ for more information

 Several functions taken from Paul Johnston
*/
function initSHA1(A){function q(a,d,b){var f=0,e=[0],c="",g=null,c=b||"UTF8";if("UTF8"!==c&&"UTF16"!==c)throw"encoding must be UTF8 or UTF16";if("HEX"===d){if(0!==a.length%2)throw"srcString of HEX type must be in byte increments";g=t(a);f=g.binLen;e=g.value}else if("ASCII"===d||"TEXT"===d)g=v(a,c),f=g.binLen,e=g.value;else if("B64"===d)g=w(a),f=g.binLen,e=g.value;else throw"inputFormat must be HEX, TEXT, ASCII, or B64";this.getHash=function(a,b,c,d){var g=null,h=e.slice(),k=f,m;3===arguments.length?"number"!==
typeof c&&(d=c,c=1):2===arguments.length&&(c=1);if(c!==parseInt(c,10)||1>c)throw"numRounds must a integer >= 1";switch(b){case "HEX":g=x;break;case "B64":g=y;break;default:throw"format must be HEX or B64";}if("SHA-1"===a)for(m=0;m<c;m++)h=s(h,k),k=160;else throw"Chosen SHA variant is not supported";return g(h,z(d))};this.getHMAC=function(a,b,d,g,q){var h,k,m,l,r=[],u=[];h=null;switch(g){case "HEX":g=x;break;case "B64":g=y;break;default:throw"outputFormat must be HEX or B64";}if("SHA-1"===d)k=64,l=
160;else throw"Chosen SHA variant is not supported";if("HEX"===b)h=t(a),m=h.binLen,h=h.value;else if("ASCII"===b||"TEXT"===b)h=v(a,c),m=h.binLen,h=h.value;else if("B64"===b)h=w(a),m=h.binLen,h=h.value;else throw"inputFormat must be HEX, TEXT, ASCII, or B64";a=8*k;b=k/4-1;if(k<m/8){if("SHA-1"===d)h=s(h,m);else throw"Unexpected error in HMAC implementation";h[b]&=4294967040}else k>m/8&&(h[b]&=4294967040);for(k=0;k<=b;k+=1)r[k]=h[k]^909522486,u[k]=h[k]^1549556828;if("SHA-1"===d)d=s(u.concat(s(r.concat(e),
a+f)),a+l);else throw"Unexpected error in HMAC implementation";return g(d,z(q))}}function v(a,d){var b=[],f,e=[],c=0,g;if("UTF8"===d)for(g=0;g<a.length;g+=1)for(f=a.charCodeAt(g),e=[],2048<f?(e[0]=224|(f&61440)>>>12,e[1]=128|(f&4032)>>>6,e[2]=128|f&63):128<f?(e[0]=192|(f&1984)>>>6,e[1]=128|f&63):e[0]=f,f=0;f<e.length;f+=1)b[c>>>2]|=e[f]<<24-c%4*8,c+=1;else if("UTF16"===d)for(g=0;g<a.length;g+=1)b[c>>>2]|=a.charCodeAt(g)<<16-c%4*8,c+=2;return{value:b,binLen:8*c}}function t(a){var d=[],b=a.length,f,
e;if(0!==b%2)throw"String of HEX type must be in byte increments";for(f=0;f<b;f+=2){e=parseInt(a.substr(f,2),16);if(isNaN(e))throw"String of HEX type contains invalid characters";d[f>>>3]|=e<<24-f%8*4}return{value:d,binLen:4*b}}function w(a){var d=[],b=0,f,e,c,g,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw"Invalid character in base-64 string";f=a.indexOf("=");a=a.replace(/\=/g,"");if(-1!==f&&f<a.length)throw"Invalid '=' found in base-64 string";for(e=0;e<a.length;e+=4){p=a.substr(e,4);for(c=g=0;c<
p.length;c+=1)f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(p[c]),g|=f<<18-6*c;for(c=0;c<p.length-1;c+=1)d[b>>2]|=(g>>>16-8*c&255)<<24-b%4*8,b+=1}return{value:d,binLen:8*b}}function x(a,d){var b="",f=4*a.length,e,c;for(e=0;e<f;e+=1)c=a[e>>>2]>>>8*(3-e%4),b+="0123456789abcdef".charAt(c>>>4&15)+"0123456789abcdef".charAt(c&15);return d.outputUpper?b.toUpperCase():b}function y(a,d){var b="",f=4*a.length,e,c,g;for(e=0;e<f;e+=3)for(g=(a[e>>>2]>>>8*(3-e%4)&255)<<16|(a[e+1>>>
2]>>>8*(3-(e+1)%4)&255)<<8|a[e+2>>>2]>>>8*(3-(e+2)%4)&255,c=0;4>c;c+=1)b=8*e+6*c<=32*a.length?b+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(g>>>6*(3-c)&63):b+d.b64Pad;return b}function z(a){var d={outputUpper:!1,b64Pad:"="};try{a.hasOwnProperty("outputUpper")&&(d.outputUpper=a.outputUpper),a.hasOwnProperty("b64Pad")&&(d.b64Pad=a.b64Pad)}catch(b){}if("boolean"!==typeof d.outputUpper)throw"Invalid outputUpper formatting option";if("string"!==typeof d.b64Pad)throw"Invalid b64Pad formatting option";
return d}function B(a,d){return a<<d|a>>>32-d}function C(a,d,b){return a^d^b}function D(a,d,b){return a&d^~a&b}function E(a,d,b){return a&d^a&b^d&b}function F(a,d){var b=(a&65535)+(d&65535);return((a>>>16)+(d>>>16)+(b>>>16)&65535)<<16|b&65535}function G(a,d,b,f,e){var c=(a&65535)+(d&65535)+(b&65535)+(f&65535)+(e&65535);return((a>>>16)+(d>>>16)+(b>>>16)+(f>>>16)+(e>>>16)+(c>>>16)&65535)<<16|c&65535}function s(a,d){var b=[],f,e,c,g,p,q,s=D,t=C,v=E,h=B,k=F,m,l,r=G,u,n=[1732584193,4023233417,2562383102,
271733878,3285377520];a[d>>>5]|=128<<24-d%32;a[(d+65>>>9<<4)+15]=d;u=a.length;for(m=0;m<u;m+=16){f=n[0];e=n[1];c=n[2];g=n[3];p=n[4];for(l=0;80>l;l+=1)b[l]=16>l?a[l+m]:h(b[l-3]^b[l-8]^b[l-14]^b[l-16],1),q=20>l?r(h(f,5),s(e,c,g),p,1518500249,b[l]):40>l?r(h(f,5),t(e,c,g),p,1859775393,b[l]):60>l?r(h(f,5),v(e,c,g),p,2400959708,b[l]):r(h(f,5),t(e,c,g),p,3395469782,b[l]),p=g,g=c,c=h(e,30),e=f,f=q;n[0]=k(f,n[0]);n[1]=k(e,n[1]);n[2]=k(c,n[2]);n[3]=k(g,n[3]);n[4]=k(p,n[4])}return n}"function"===typeof define&&
typeof define.amd?define(function(){return q}):"undefined"!==typeof exports?"undefined"!==typeof module&&module.exports?module.exports=exports=q:exports=q:A.jsSHA=q};

2 个答案:

答案 0 :(得分:1)

假设您的签名是正确的(我没有检查过),Noit是正确的,以至于Authorization标头错误。它应该是以下形式:

Authorization: OAuth oauth_callback="http%3A%2F%2Fwww.floppers.comyr" ...

您需要更改标头生成代码,以便以上述格式生成授权标头。

如果仍然遇到问题,可以使用The LinkedIn OAuth Test Console等工具检查签名。

答案 1 :(得分:0)

如果你做了明文那会有用吗?也许关键是错误的。

什么时候oauth_signature_method =“PLAINTEXT”

你的oauth_signature然后成为消费者秘密,然后是一个&符号用于request_token

所以根据你的代码

oauth_signature = rawurlencode(ConsumerSecret +'&amp;')

编辑:从不介意测试,不工作,我不知道我不是推特专家