TwitterAPIで画像のバイナリをアップロードしたい
Q&A
Closed
課題
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-Type
にmultipart/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