解決したいこと
- LINE広告APIドキュメントにある、パフォーマンスレポートを作成するPOSTリクエストをGAS(Google Apps Script)で動かしたい
- サンプルコードがPythonだったので、Pythonで書いてみたところ動いたが、これをGASでとなると思ったようにいかない
開発の背景
- LINE広告のパフォーマンスレポートをスプレッドシートに自動で書き出したい
- 他にGoogle広告やYahoo!広告のデータ抽出もすでにGASで実装しているため、できればGASで書きたい
発生しているエラー
Request failed for https://ads.line.me returned code 401. Truncated server response: {"errors":[{"reason":"Unauthorized","message":"Request digest is invalid."}]}
エラーコードをドキュメントで調べてみると、
401 The token specified in 'Authorization' header is invalid.
「Authorization ヘッダーで指定されたトークンが無効」だと言われます。
ただ、Authorization ヘッダーに渡しているtoken
という変数の中身は、PythonでもGASでも一致しているんです...。ここが今一番混乱しているところです。
以下が「レスポンスが返ってくるPythonのコード」と「エラーで返ってくるGASのコード」です。
変数名も全体の構成もなるべく同じにしています。
Python
Python
1import base64 2import datetime 3import hashlib 4import hmac 5from inspect import signature 6import json 7import urllib.request 8from datetime import datetime, timedelta 9 10def calc_sha256_digest(content: str) -> str: 11 sha256 = hashlib.new('sha256') 12 sha256.update(content.encode()) 13 return sha256.hexdigest() 14 15def encode_with_base64(value: bytes) -> str: 16 return base64.urlsafe_b64encode(value).decode() 17 18if __name__ == '__main__': 19 # Setting parameters for your request 20 today = datetime.now() 21 yesterday = (today - timedelta(1)).strftime('%Y-%m-%d') 22 accountId = "A00000000000" 23 24 access_key = "XXXXXXXXXXXXXXXX" 25 secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 26 method = "POST" 27 canonical_url = "/api/v3/adaccounts/" + accountId + "/pfreports" 28 url_parameters = "" 29 request_body = { 30 "level":"ADACCOUNT", 31 "since": yesterday, 32 "until": yesterday, 33 "filtering":{ 34 "idType":"ADACCOUNT", 35 "ids":[accountId] 36 }, 37 "fileFormat":"CSV" 38 } 39 has_request_body = request_body is not None 40 41 endpoint = 'https://ads.line.me' + canonical_url + url_parameters 42 request_body_json = json.dumps(request_body) if has_request_body else "" 43 content_type = 'application/json' if has_request_body else "" 44 45 jws_header = encode_with_base64( 46 json.dumps({ 47 "alg": "HS256", 48 "kid": access_key, 49 "typ": "text/plain" 50 }).encode() 51 ) 52 53 hex_digest = calc_sha256_digest(request_body_json) 54 payload_date = today.strftime('%Y%m%d') 55 payload = "%s\n%s\n%s\n%s" % (hex_digest, content_type, payload_date, canonical_url) 56 jws_payload = encode_with_base64(payload.encode()) 57 58 signing_input = "%s.%s" % (jws_header, jws_payload) 59 signature = hmac.new( 60 secret_key.encode(), 61 signing_input.encode(), 62 hashlib.sha256 63 ).digest() 64 encoded_signature = encode_with_base64(signature) 65 token = "%s.%s.%s" % (jws_header, jws_payload, encoded_signature) 66 print(token) 67 68 http_headers = { 69 "Date": today.strftime('%a, %d %b %Y %H:%M:%S GMT'), 70 "Authorization": "Bearer %s" % token 71 } 72 if has_request_body: 73 http_headers["Content-Type"] = content_type 74 req = urllib.request.Request(endpoint, data=request_body_json.encode(), headers=http_headers, method=method) 75 else: 76 req = urllib.request.Request(endpoint, headers=http_headers, method=method) 77 78 with urllib.request.urlopen(req) as res: 79 resp = res.read() 80 print(resp.decode())
GAS
JavaScript
1function main() { 2 const today = new Date(); 3 const yesterday = getYesterday(new Date()); 4 const accountId = "A00000000000"; 5 6 const access_key = "XXXXXXXXXXXXXXXX"; 7 const secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; 8 const method = "POST"; 9 const canonical_url = `/api/v3/adaccounts/${accountId}/pfreports`; 10 const url_parameters = ""; 11 const request_body = { 12 "level":"ADACCOUNT", 13 "since":yesterday, 14 "until":yesterday, 15 "filtering": { 16 "idType": "ADACCOUNT", 17 "ids": [accountId] 18 }, 19 "fileFormat": "CSV" 20 }; 21 const has_request_body = request_body 22 23 const endpoint = "https://ads.line.me" + canonical_url + url_parameters; 24 const request_body_json = JSON.stringify(request_body).replace(/:/g, ': ').replace(/,/g, ', '); 25 const content_type = "application/json"; 26 27 const jws_header = encode_with_base64(JSON.stringify({ 28 "alg":"HS256", 29 "kid":access_key, 30 "typ":"text/plain" 31 }).replace(/:/g, ': ').replace(/,/g, ', ')); 32 33 const hex_digest = calc_sha256_digest(request_body_json); 34 const payload_date = Utilities.formatDate(today, 'JST', 'yyyyMMdd'); 35 const payload = `${hex_digest}\n${content_type}\n${payload_date}\n${canonical_url}`; 36 const jws_payload = encode_with_base64(payload); 37 38 const signing_input = `${jws_header}.${jws_payload}`; 39 const encoded_signature = hmacSha256(secret_key, signing_input); 40 const token = `${jws_header}.${jws_payload}.${encoded_signature}`; 41 console.log(token); 42 43 const http_headers = { 44 "Date": Utilities.formatDate(today, 'GMT', 'E, dd MMM yyyy HH:mm:ss z'), 45 "Authorization": `Bearer ${token}`, 46 "Content-Type":content_type 47 } 48 const resp = UrlFetchApp.fetch(endpoint, { 49 "data": request_body_json, 50 "headers": http_headers, 51 "method": method, 52 }); 53 console.log(resp.getContentText()); 54} 55 56/** 57 * SHA 256でハッシュ化 58 * @param {string} input - ハッシュ化する文字列 59 * @return {string} - ハッシュ値 60 */ 61function calc_sha256_digest(input) { 62 const rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input, Utilities.Charset.UTF_8); 63 let txtHash = ''; 64 for (i = 0; i < rawHash.length; i++) { 65 let hashVal = rawHash[i]; 66 if (hashVal < 0) { 67 hashVal += 256; 68 } 69 if (hashVal.toString(16).length == 1) { 70 txtHash += '0'; 71 } 72 txtHash += hashVal.toString(16); 73 } 74 return txtHash 75} 76 77/** 78 * base64エンコード 79 * @param {string} input - エンコードしたい文字列 80 * @return {string} - base64エンコードされた文字列 81 */ 82function encode_with_base64(input) { 83 return Utilities.base64Encode(input, Utilities.Charset.UTF_8) 84} 85 86/** 87 * HMAC SHA 256でハッシュ化 88 * @param {string} key - ハッシュ化するシークレットキー 89 * @param {string} text - ハッシュ化する文字列 90 * @return {string} - ハッシュ値 91 */ 92function hmacSha256(key, text) { 93 const rowHash = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256, text, key); 94 return Utilities.base64Encode(rowHash); 95}; 96 97/** 98 * 昨日の日付を計算 99 * @param {date} date - 今日の日付 100 * @return {date} - 昨日の日付(YYYY-MM-DD形式) 101 */ 102function getYesterday(date) { 103 date.setDate(date.getDate() - 1); 104 const yesterday = Utilities.formatDate(date, 'JMT', 'yyyy-MM-dd'); 105 return yesterday 106}
エラーの原因っぽいところ
エラーメッセージ的にはtoken
が間違っているみたいなんですが...。
Pythonでprint(tokne)
とした結果と、GASでconsole.log(token)
とした結果は完全に一致していました。
だとすると最後のHTTPリクエストのところなのかなと...。
Pythonだと
Python
1resp = urllib.request.Request(endpoint, data=request_body_json.encode(), headers=http_headers, method=method)
と書いていて、GASでは
JavaScript
1const resp = UrlFetchApp.fetch(endpoint, { 2 "data": request_body_json, 3 "headers": http_headers, 4 "method": method, 5 });
と書いているのですが、このあたりは自信がないです。
「Pythonはrequest_body_json
をエンコードしてるけどこれってバイト列に変えてるってこと...?GAS(JavaScript)にバイト列ってあるっけ...?これ変換しないとだめなのか...?」となっています。
Pythonに関してはかなり知識不足なところがありますが、上記誤っている箇所がお分かりになる方いらっしゃいましたら、ご教示いただけるととても勉強になります...。よろしくお願いいたします...。
補足
この「LINE Ads APIのPython→GAS変換」は以前にも当サイトで質問させていただいたことがありました。
今回と直接関係するか分かりませんが、こちらも記載させていただきます。
回答1件
あなたの回答
tips
プレビュー