見出し画像

ChatGPTと一緒にプログラミングの実際 - 後編

中編では、ChatGPTと格闘して、Googleの新しい認証方式に対応したGmail送信プログラムを作る作業をしてきました。しかし、エラーが頻発する過程でChatGPTが再び古い認証方式を使うように指示したりするなど、一筋縄ではいかない状況が続いていました。

この後編では、そのような困難にもめげず、ChatGPTと共に目的とするプログラムを完成させるために奮闘する様子をお伝えします。果たして、私たちは無事に目標を達成することができるのでしょうか? 後編でのドラマチックなやりとりをお楽しみください!

ChatGPT & Toshi Akazawa

この記事を書くにあたっての背景は、前編の「1. プログラム開発の背景」をご覧ください。

以下で、「Qで始まる段落」は私からChatGPTへの質問やコメントです。「Aで始まる段落」は、ChatGPTからの出力の概要です。出力の実際の中身はスクリーンショットで示します。《》内は、私の感想、感情あるいは説明です。

スクリーンショットの中身をしっかり読む必要はありません。「Aで始まる段落」で出力の概要を示しますので、スクリーンショットは参考までで結構です。

1. ChatGPTとの対話

Q49.
あたなはプログラミングができる人として振る舞ってください。
《これまでの経緯から、ChatGPTに目的を踏まえて完璧なコードを書いてもらうのは無理と判断した。しかし、これまでのやりとりから何を知るべきかについてはわかったので、その方向で質問することにした》

A49.
同意。

Q50.
Google Identity Services を使って、Bearerタイプのアクセストークンを取得したいです。JavaScriptを使ってどのようなコードを書いたらいいでしょうか。
《何をすべきか専門用語を使い単刀直入に質問した》

A50.
コードを提示。

Q51.
コード中の response.credential と client_secret は違うものですか。
《コード中の疑問について質問》

A51.
解説してくれた。
client_secretの取り扱いについて注意点が示された。

Q52.
client_secret の取り扱いについて理解できました。今回は動作を理解するために使いますので、ご安心ください。

A52.
安心した旨の回答

Q53.
redirect_uri は何のために必要でしょうか。今回は別のURIへリダイレクトする必要はないのですが。
《コードについて質問した》

A53.
解説してくれた。
Google Identity Servicesを使う場合は設定不要とのこと。

Q54.
では、今回は redirect_uri を指定しないでおきますね。
《「redirect_uriを使わない」ことにChatGPTが同意するかどうかを確認する》

A54.
同意した。

Q55.
作成したHTMLファイルをwebサーバにアップロードし、webブラウザでHTMLファイルにアクセスしました。そして、私のGoogleアカウントでログインしました。id_tokenの値が、ディベロッパーツールのConsoleに表示されました。
しかし、コードの fetch("https://www.googleapis.com/oauth2/v4/token" の行で次のエラーが発生しました。どうしたらいいでしょうか。
https://www.googleapis.com/oauth2/v4/token 400
《新たなエラーが発生した》

A55.
リクエストの構文間違いかリクエストの不正が原因。
エラーレスポンスの内容を確認するよう指示された。
エラーを確認するための修正版コードも提示してくれた。
《これまでデバッグはしてくれないんだなという印象を持っていた。ここで初めてエラーを確認するよう指示があり、少しはデバッグについても学習しているのかなと感じた》

Q56.
body: formBody, の行の formBody の中身はどこで定義していますか。
《コード中の未定義の変数について問いただした》

A56.
「formBodyを定義する部分を省略してしまいました。」とあっさり説明不十分を認めた。
追加のコードを提示された。
《最初から十分なコードを書くことはできないのかな。少しあきれる》

Q57.
HTMLファイル内の JavaScriptコードを修正して、webサーバにアップロードしました。webブラウザでアクセスしたときの、Consoleに表示されたエラーは次のとおりで3行あります。
https://www.googleapis.com/oauth2/v4/token 400
Error during token exchange: {error: 'invalid_grant', error_description: 'Malformed auth code.'}
Error: {error: 'invalid_grant', error_description: 'Malformed auth code.'}
《エラー内容を伝えた》

A57.
認証コードが無効か正しくフォーマットされていないと回答があった。
確認すべき事項について列挙された。
《問題の本質に迫りつつあると感じた。しかし、指摘が十分とは思えない。》

Q58.
credentialプロパティ自体が正しいかどうかは確認のしようがないと思いますが。

A58.
credentialプロパティ自体が正しいかどうかは確認が難しいと認めた。ただし、他の部分を確認するように求められた。

Q59.
responseのcredentialプロパティの値をここでお示しするのは適切でないので控えますが、とても長い文字列となっています。
クライアントIDとクライアントシークレットはともに、https://console.cloud.google.com/ で作成したときのものになっています。
「承認済みリダイレクトURI」には、HTMLファイルのアップロードしたwebサーバのHTTPオリジンが正しくセットされています。

ところで、exchangeAuthCodeForAccessToken関数とは何でしょうか。
《指摘された箇所は正しいと伝えた。不明の関数があったので質問した。》

A59.
exchangeAuthCodeForAccessToken関数が存在しないコードであることを認めた。
他の部分についての確認を求められた。

Q60.
codeパラメータは次の一行で設定しています。
params.append("code", response.credential);

リダイレクトURIとは「承認済みのリダイレクト URI」のことですね。すみません、先ほどの回答は間違っていました。まだこのURIを設定していませんでした。これから設定し、設定が終わったらお知らせします。

A60.
承知した旨の回答。

Q61.
リダイレクトURIを設定しました。しかし、まだ同じエラーが発生しています。
Google Cloud Consoleには「注: 設定が有効になるまで 5 分から数時間かかることがあります」とありますので、数時間待つことにします。それでいいでしょうか。

A61.
数時間待つように指示あり。

Q62.
ちょっと待ってください。URLSearchParamsコンストラクタは、URL のクエリー文字列を作るためのものですよね。そうやって作った文字列を、https://www.googleapis.com/oauth2/v4/token に渡すオブジェクトのbodyプロパティにセットするのは、無意味ではありませんか。
《コードについて不審な点があり質問した》

A62.
私が不審に感じた点は、正しいコードであることを詳しく説明してくれた。
《これまで質問者に迎合しているかのような印象があったけど、自信があるところはちゃんと指摘してくれるんだなと一定の信用がおけると感じた。》

Q63.
なるほどそういうことですか。さすがですね。
《感心した旨を伝えた》

A63.
謙虚な回答。

Q64.
ところで、もう2時間が経ちました。webサーバ上のHTMLファイルにブラウザでアクセスしても前と同じように以下のエラーが出たままです。
https://www.googleapis.com/oauth2/v4/token 400
Error during token exchange: {error: 'invalid_grant', error_description: 'Malformed auth code.'}
Error: {error: 'invalid_grant', error_description: 'Malformed auth code.'}
《数時間待つ意味はなかった》

A64.
認証コードが無効か、正しくないことを指摘された。
確認点を提示される。
《既に確認済なのだが・・・》

Q65.《ここで、仕切り直し以降のことを忘れている気がしたので、コード全体を提示して解決策を求めた》
それらついて確認しましたが、問題ありませんでした。エラーメッセージに1つ前に示したとおりです。現在のHTMLファイルは以下のとおりです。ただし、クライアントIDとクライアントシークレットは伏せ字にしてあります。解決策をお待ちします。

<!DOCTYPE html>
<html>
<head>
	<title>Google Identity Services - Access Token Example</title>
	<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
	function handleCredentialResponse(response) {
		console.log(response);
		var id_client = 'MY_CLIENT_ID.apps.googleusercontent.com';
		var secret = 'MY_CLIENT_SECRET';
		const params = new URLSearchParams();
		params.append("grant_type", "authorization_code");
		params.append("client_id", id_client);
		params.append("client_secret", secret);
		params.append("code", response.credential);
		const formBody = params.toString();
		// アクセストークンを取得するためにIDトークンを使用します
		fetch("https://www.googleapis.com/oauth2/v4/token", {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded",
				},
			body: formBody,
		})
		.then((response) => {
			if (!response.ok) {
			return response.json().then((error) => {
				console.error("Error during token exchange:", error);
				throw error;
			});
			}
			return response.json();
		})
		.then((data) => {
			const accessToken = data.access_token;
			console.log("Access Token:", accessToken);
		})
		.catch((error) => {
			console.error("Error:", error);
		});
	}
</script>
</head>
<body>
	<div id="g_id_onload"
	   data-client_id="MY_CLIENT_ID.apps.googleusercontent.com"
	   data-callback="handleCredentialResponse">
	</div>
	<div class="g_id_signin"
	   data-type="standard"
	   data-shape="rectangular"
	   data-theme="outline"
	   data-text="sign_in_with"
	   data-size="large"
	   data-logo_alignment="left">
	</div>
</body>
</html>

A65.
修正点を指摘の上、修正後のコードを提示してくれた。

Q66.
console.log(response); の出力結果を見ると、codeというプロパティはありません。ですので、params.append("code", response.code); と書くことはできません。
《コードの不備を指摘》

A66.
別のプロパティ id_token を使ってアクセストークンを取得するように提案。
修正コードを提示。
《id_tokenなんて無いのに・・・なんでそんなプロパティを提示してくるんだろう》

Q67.《配慮しつつ、id_tokenプロパティは存在しない旨を指摘した》
ご指摘ありがとうございます。しかし、responseオブジェクトに存在するプロパティとその値は次のとおりです。ただし、クライアントIDと credentialプロパティの値は伏せ字にしました。
{
clientId: "MY_CLIENT_ID",
client_id: "MY_CLIENT_ID",
credential: "MY_SECRET_CREDENTIAL",
select_by: "user"
}

このように、id_tokenプロパティは存在しませんので、params.append("id_token", response.id_token); とは書けないのです。

A67.《方向転換の予感》
あっさり説明不十分を認めた。
新たに Google One Tap なるものを持ち出してきた。
《Googleのドキュメントに書いてあったやつだ = Google One Tap》

Q68.
HTMLファイルを修正にwebサーバにアップロードしました。ブラウザでHTMLファイルにアクセスしたところ、Consoleに responseの中身が表示されました。しかし、id_tokenプロパティはありません。また、Consoleに次のエラーが発生しました。
https://www.googleapis.com/oauth2/v4/token 400
Error during token exchange: {error: 'invalid_request', error_description: 'Missing required parameter: assertion'}

A68.
Google One Tapの仕様変更があったようだと回答。
《Googleのせいにしてるけど本当かな》

Q69.
そのようにしてみましたが、症状は前と同じです。アクセストークンは取得できませんでした。

A69.
Google One Tapのレスポンスを正しく理解していませんでしたと理解不十分を吐露。
修正内容を提示。
《古い認証方式 Google API Client Library for JavaScript(gapi) を使ってアクセストークンを取得できるという。古い方式はやめて新しい方に移行したいのに、また趣旨からずれている》

Q70.
Google API Client Library for JavaScript(gapi)は非推奨となってしまいましたので使うことができません。このため今回の提案は採用できないのです。
《非推奨形式は使えない旨を伝える》

A70.
非推奨になっていることに気付かなかったとのこと。
Google One Tapを使ってアクセストークンを取得するように要請される。
そのための手順とコードを提示された。
《プログラム断片だけを見せられて理解不能。また迷宮入りしそうな雰囲気なのでしばらく時間をおくことにする。自分でGoogleのドキュメントを調べて動作確認してみることにした》

自分で調べた内容
「Google の ID サービスへの移行」
このサイトの「暗黙的フローの例」に アクセストークンを取得した上で Google Calendar を表示するコードが書いてあった。Google Calendar をGmail APIに置き換えれば、新しい認証方式で Gmail API経由でメールを送信できる予感がした。

Q71.《アクセストークンを取得できたことを報告》
複雑になってきましたので、調べてみましたところ以下のコードを使ってアクセストークンを取得できるようになりました。参考にしたサイトは https://developers.google.com/identity/oauth2/web/guides/migration-to-gis?hl=ja です。「暗黙的フローの例」という項のコードがためになりました。

<!DOCTYPE html>
<html>
<head>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body onload="initClient()">
<script>
	var client;
	var access_token;

	function initClient() {
		client = google.accounts.oauth2.initTokenClient({
			client_id: 'MY_CLIEND_ID.apps.googleusercontent.com',
			scope: 'https://www.googleapis.com/auth/gmail.send',
			callback: (tokenResponse) => {
				access_token = tokenResponse.access_token;
				console.log(tokenResponse);
			},
		});
	}

	function getToken() {
		client.requestAccessToken();
	}

	function revokeToken() {
		google.accounts.oauth2.revoke(access_token, () => {
			console.log('access token revoked');
			console.log(access_token);
		});
	}
</script>
<h1>Google Identity Services Authorization Token model</h1>
<button onclick="getToken();">Get access token</button><br><br>
<button onclick="revokeToken();">Revoke token</button>
</body>
</html>

A71.
お力になれてうれしい旨の表明あり。
《うーん、君の力ではないんだけど。ボクが調べたんだから(苦笑)》

Q72.
では、このコードをもとに、ボタンをクリックしたらメールを送信できるように改造してください。
《最終コードを提示するよう要請》

A72.
コードを提示してくれた。

Q73.
はい、ちゃんとメールを送信できました。長い間付き合ってくれてどうもありがとう!
《Googleの新しい認証方式でメールを送信できた!》

A73.
喜びの声!

Q74.
「Get access token」ボタンを押す前に間違えて「Send Email」ボタンをクリックしたら、当然のことですが、メールの送信はできませんでした。しかし、ディベロッパーツールのConsoleには「Email sent successfully」と表示されてしまいました。エラー処理に問題があるのではないでしょうか。
《誤操作時のエラー処理に問題がある旨を指摘》

A74.
エラー処理を修正したコードを提示。

Q75.
どうもありがとう!

A75.
喜びの声!

Q76.
いや〜長い時間どうもありがとう。疲れたね〜

A76.
ねぎらいと謝意!
AIは疲れないとのこと《そう言うかなと思って聞いたんだけどね》

【完】

2. まとめ

私は、kintoneのアプリからGmailを経由してメールを送信するプラグイン「Gsender」を開発し顧客に提供しておりました(現時点で一般公開はしていません)。ところが、Googleのこれまでの認証方式が非推奨となり、新しい認証方式に切り替える必要が出てきました。

従来であれば、Googleのドキュメントを読んでプログラムを改修するところですが、ChatGPTが登場したタイミングでしたので、ChatGPTを使ってプログラムを改修することに挑戦しました。

ChatGPTをプログラミングに使うことは次のような点で有益と感じました。

  • コードを書く時間を短縮できる。

  • コードの中身に疑問がある場合、その場にすぐに詳しい解説を得られる。(内容によっては裏付け調査が必要)

一方、ChatGPTを使ってプログラミングをする際に、人は次の点を押さえておかなければいけないと感じました。

  • ChatGPTは必ずしも正しいコードを書いてくれるわけではない。

  • ChatGPTは過去のデータを学習しているせいなのか、古い書き方に固執する。(新しい内容についての学習量が少ないのかもしれない)

  • 人がうまくガイドしないといけない。

一連の作業を通し、私はChatGPTをガイドしつつも、私もChatGPTにガイドされつつ、解決の糸口を見つけ出すことに成功しました。

今回は、古い書き方から新しい書き方への切り替えでしたので、ChatGPTにとっては苦手な領域だったのかもしれません。これからもChatGPTとともに仕事をしていくことでしょう。

この記事が気に入ったらサポートをしてみませんか?