質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.38%
JWT(JSON Web Token)

JWT(JSON Web Token)とは、JSONをベースとしたアクセストークンの仕様。電子署名付きのURL safeなJSONのことを指します。電子署名が付いているため、改ざんをチェックできる点がメリットです。

HttpWebRequest

HttpWebRequestとは.NETにおけるクラスであり、WebRequestクラスをHTTPに導入するものです。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Google Analytics API

Google Analytics APIは、アクセス解析機能が行える API(Application Programming Interface)です。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

3887閲覧

【GAS】Pythonならきちんと動いてるLINE Ads APIのHTTPリクエストをGASでも動かしたい

退会済みユーザー

退会済みユーザー

総合スコア0

JWT(JSON Web Token)

JWT(JSON Web Token)とは、JSONをベースとしたアクセストークンの仕様。電子署名付きのURL safeなJSONのことを指します。電子署名が付いているため、改ざんをチェックできる点がメリットです。

HttpWebRequest

HttpWebRequestとは.NETにおけるクラスであり、WebRequestクラスをHTTPに導入するものです。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Google Analytics API

Google Analytics APIは、アクセス解析機能が行える API(Application Programming Interface)です。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

1クリップ

投稿2022/09/19 03:28

解決したいこと

開発の背景

  • 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変換」は以前にも当サイトで質問させていただいたことがありました。
今回と直接関係するか分かりませんが、こちらも記載させていただきます。

Pythonで書かれたLINE Ads APIのサンプルコードをGAS(JavaScript)に翻訳したい

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

odataiki

2022/09/19 06:42

一つずつ値をチェックしていくしかないと思います。 getYesterdayが想定している値になっているのでしょうか? JMT → GMTまたはJST? ``` function getYesterday(date) { date.setDate(date.getDate() - 1); const yesterday = Utilities.formatDate(date, 'JMT', 'yyyy-MM-dd'); return yesterday } ```
guest

回答1

0

ベストアンサー

下記のように data ではなく、payload プロパティに request_body_json を指定してみてはいかがでしょうか。

js

1 const resp = UrlFetchApp.fetch(endpoint, { 2 // "data": request_body_json, // 削除 3 "headers": http_headers, 4 "method": method, 5 "payload": request_body_json // 追加 6 });

python 側のコードでは、urllib.request.Request 関数の引数 data に、 「request_body_json をバイト列に変換したもの」が指定されています。

python

1 req = urllib.request.Request(endpoint, data=request_body_json.encode(), headers=http_headers, method=method)

これによって data に指定された内容が POST リクエストされます。
(request_body_json.encode() によって文字列をバイト列に変換している理由は、urllib.request.Request 関数の data には文字列を直接渡せないからだと考えられます。)

これと同様の機能を GAS の UrlFetchApp.fetch 関数で実現させる場合、
エンドポイントに渡すデータを payload に指定します。
(もちろん、POST リクエストを行うためには method:'POST' の指定も必要です。)

なお、GAS では payload に「文字列、バイト配列、blob、JavaScript オブジェクトのいずれかを使用可能」とされていますので、ここでバイト列に変換する必要はありません。

参照:
https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#advanced-parameters


自分のアプリケーションがどのようなリクエストを送っているかについては httpbin.org というサイトで調べることが可能です。
https://qiita.com/sameyasu/items/adacceb8a1bee893599b

httpbin.org に対して HTTP リクエストを送ると、自分の送った内容がそのままレスポンスとして返ってきます。

たとえば本件の場合、endpoint に「https://httpbin.org/post」 を指定してみることで、POSTリクエストの内容を検証することが可能です。

投稿2022/09/19 07:10

編集2022/09/19 07:37
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2022/09/19 12:33

ご回答いただきありがとうございます...! 今回もご指摘いただいた箇所の修正で、意図した通りに動かすことできました!! 過去投稿を掘り返してコメントまでしてしまい...おかげさまでした...。 > これと同様の機能を GAS の UrlFetchApp.fetch 関数で実現させる場合、エンドポイントに渡すデータを payload に指定します。 ここがとても盲点でした。 UrlFetchApp.fetch に使うpayloadは名称を変えたりしてはいけないということですよね...。 Pythonの書き方に引きづられていました。 いつもいつもありがとうございます。とても勉強になりました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.38%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問