37cohina
@37cohina

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

TwitterAPIで画像のバイナリをアップロードしたい

課題

TwitterAPIを使って画像ファイルをアップロードしたいのですが、Base64エンコードすると上手くいくのですが、生のバイナリではアップロードすることができません。

何がいけないのでしょうか?

ソースコード

main.gs
function upload_test() {

  const consumerAPIKey = /*ConsumerAPIKey*/,
  consumerAPIKeySecret = /*ConsumerAPIKeySecret*/,
  accessToken = /*AccessToken*/

  const media = DriveApp.getFileById( "1Tf46niiF5C2HbzOlhfNdDYinD7PrKD9S" ).getBlob()

  const media_data = Utilities.base64Encode( media.getBytes() )

  const response = new TwitterMediaUploader( consumerAPIKey, consumerAPIKeySecret, accessToken ).upload( { media } )

  console.log( response )

}
TwitterMediaUploader.gs
class Util {

  /**
   * @param {string} str
   * @return {string}
   */
  static encode( str ) {

    const replaceTargetPattern = this.replaceTargetPattern_
    const replaceMap = this.replaceMap_

    return encodeURIComponent( str )
    .replace(
      replaceTargetPattern,
      char => replaceMap[ char ]
    )

  }

}

Util.replaceTargetPattern_ = new RegExp( `[${ [ ...Object.keys( replaceMap_ ) ].join("") }]`, "g" )

Util.replaceMap_ = Object.freeze( {
  "!": "%21",
  "'": "%27",
  "(": "%28",
  ")": "%29",
  "*": "%2A",
} )

class TwitterMediaUploader {

  constructor( consumerAPIKey, consumerAPIKeySecret, accessToken ) {
    this.consumerAPIKey_ = consumerAPIKey
    this.consumerAPIKeySecret_ = consumerAPIKeySecret
    this.accessToken_ = accessToken
  }

  upload( { media = null, media_data = null } ) {

    const consumerAPIKey = this.consumerAPIKey_
    const consumerAPIKeySecret = this.consumerAPIKeySecret_
    const accessToken = this.accessToken_

    const url = "https://upload.twitter.com/1.1/media/upload.json"

    if ( media != null && media_data != null ) throw new TypeError( "too much media" )

    const params = {
      method: "POST",
      payload: media != null ? { media } : { media_data },
      muteHttpExceptions: true,
    }
  
    const oauth_data = {
      oauth_consumer_key: consumerAPIKey,
      oauth_signature_method: "HMAC-SHA1",
      oauth_version: "1.0",
      oauth_nonce: "",
      oauth_timestamp: Math.trunc( Date.now() / 1000 ).toString( 10 ),
      oauth_token: accessToken.oauth_token,
    }

    const oauth_token_secret = accessToken.oauth_token_secret

    const nonce_length = 32
    for ( let i = 0; i < nonce_length; i++ ) {
      const word_characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
      oauth_data.oauth_nonce += word_characters[ Math.random() * word_characters.length | 0 ]
    }
    
    for ( const key in params.payload ) oauth_data[ key ] = params.payload[ key ]

    const [ baseUrl, urlParams ] = url.split( "?" )

    if ( urlParams != null ) {

      for ( const item of urlParams.replace( /\+/g, " " ).split( "&" ) ) {

        const [ key, value ] = item.split( "=" )
        oauth_data[ key ] = decodeURIComponent( value )

      }

    }

    const sortedOauthData = Object.entries( oauth_data ).sort(
      ( [ aKey ], [ bKey ] ) => aKey == bKey ? 0 : aKey < bKey ? -1 : 1
    )

    const baseString = `${ params.method }&${ Util.encode( baseUrl ) }&${
      Util.encode(
        sortedOauthData.reduce(
          ( data, [ key, value ] ) => {
            return data += `&${ Util.encode( key ) }=${ Util.encode( value ) }`
          },
          ""
        ).substring( 1 )
      )
    }`

    const signingKey = `${ Util.encode( consumerAPIKeySecret ) }&${ oauth_token_secret == null ? "" : Util.encode( oauth_token_secret ) }`

    const hmacSig = Utilities.computeHmacSignature( Utilities.MacAlgorithm.HMAC_SHA_1, baseString, signingKey )

    const targetKey = "oauth_signature"
    let targetIndex = 0
    for ( const [ key ] of sortedOauthData )
      if ( key < targetKey ) targetIndex++
      else break

    sortedOauthData.splice( targetIndex, 0, [ targetKey, Utilities.base64Encode( hmacSig ) ] )

    const Authorization = sortedOauthData.reduce( ( data, [ key, value ], index ) => {

      if ( key != "realm" && !key.includes( "oauth_" ) ) return data

      const key_ = Util.encode( key )
      const value_ = Util.encode( value )
      return data += `${ ( index > 0 ) ? ", " : "" }${ key_ }="${ value_ }"`

    }, "OAuth ")

    params.headers = { Authorization, }

    const response = UrlFetchApp.fetch( url, params )

    return JSON.parse( response.getContentText() )

  }

}

TwitterAPIのレスポンス

media利用時
{ errors: [ { code: 32, message: 'Could not authenticate you.' } ] }
media_data利用時
{ media_id: /*media_id*/,
  media_id_string: /*media_id_string*/,
  size: 823,
  expires_after_secs: 86400,
  image: { image_type: 'image/png', w: 48, h: 36 } }

#調査

Content-Typemultipart/form-dataがセットされていないのかと思い、リクエストの向き先をhttpbin.orgに変更してテストしてみました。

##httpbin.orgのレスポンス

※一部の値を省略しています。

media利用時
{ args: {},
  data: '',
  files: { media: /*data:image/png;base64,から始まるDATA URI*/ },
  form: {},
  headers: 
   { 'Accept-Encoding': 'gzip,deflate,br',
     Authorization: 'OAuth , oauth_consumer_key="", oauth_nonce="kD9zrPWVR2DoF3WS1UM0DkdgYylWgkJw", oauth_signature="%2FQTmr1fFDr%2B7b5IWzl5xGmdumaM%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1608281186", oauth_token="undefined", oauth_version="1.0"',
     'Content-Length': '1004',
     'Content-Type': 'multipart/form-data; boundary="-----yJxVgVR1G1KYjE3MYN42j3PtmC2yaCuXQ38ZHKmVvWefa3Ay5i"',
     Host: 'httpbin.org',
   },
  json: null,
  url: 'https://httpbin.org/post' }
media_data利用時
{ args: {},
  data: '',
  files: {},
  form: { media_data: /*Base64エンコードされたバイナリ*/ },
  headers: 
   { 'Accept-Encoding': 'gzip,deflate,br',
     Authorization: 'OAuth , oauth_consumer_key="", oauth_nonce="pUv5zONf2N9BofVTyxE2niuvvVHNvSs5", oauth_signature="pcIaNfG8AsckyYQnZ8%2BZp%2FyQjs0%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1608281187", oauth_token="undefined", oauth_version="1.0"',
     'Content-Length': '1127',
     'Content-Type': 'application/x-www-form-urlencoded',
     Host: 'httpbin.org',
 },
  json: null,
  url: 'https://httpbin.org/post' }

上記の結果を見る限りではmultipart/form-dataがセットされているようなので、問題は別の箇所にあると考えています。

oauth_signatureの値かなと思っていますが、他の言語のサンプルコードなどを見てもよくわかりませんでした。

ご教授よろしくお願いします。

0

No Answers yet.

Your answer might help someone💌