【Google Apps Script】Webブラウザ上からBloggerに複数記事をまとめて投稿する

# 経緯
以前から個人的な利用目的で、Webブラウザ上から複数記事をまとめてBloggerに投稿する、というアプリケーションをPythonで開発し、Herokuで運用していました。

が、Herokuの無料プラン廃止の発表に伴い、引越し先を探す必要が出てきました。

そんなときに、別の目的で調べていたGoogle Apps Scriptでも同じ機能を持ったアプリを作れるのでは？と思い、検証のために簡単なアプリを開発してみた記録です。

GoogleAppsScriptでのプロジェクトの作成方法やデプロイの仕方など基本中の基本の部分は省いてあるのでご了承下さい。

# 準備するもの
* Googleのアカウント
* GoogleCloudPlatForm上のプロジェクト(認証やAPIキー発行のため)
* Bloggerで開設したブログ
* コーディングにはブラウザ上からGAS公式のエディターを使う

# 制作物の概要
以下のような入力フォームから情報を入力(例:「2022」年「9」月「1」日から「4」日まで)すると、
![2022-09-10 16.27.34 script.google.com 5680de69078d.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/3984424d-3406-745d-0552-ea87095ed3fb.jpeg)
Blogger上に「2022年9月1日の日記」「2022年9月2日の日記」「2022年9月3日の日記」「2022年9月4日の日記」といった題名の記事が作成される。

※あくまで、GASからBloggerAPIを介して記事を投稿できるかを検証することが目的なので、入力値のバリデーションの機能は設けません。

# プロジェクトの設定
## 権限を自分のみに設定する
プロジェクト作成直後に右上のボタンからアクセスできるユーザーが自分だけになっていることを確認します。
## 「appsscript.json」をエディタで編集する
[プロジェクトの設定] の 「「appsscript.json」マニフェスト ファイルをエディタで表示する」をチェックしてappsscript.jsonを編集できるようにします。

エディタにappsscript.jsonが表示されるので、以下を追加しOAuth認証を使えるようにします。
```
"oauthScopes": [
    "https://www.googleapis.com/auth/blogger",
    "https://www.googleapis.com/auth/script.external_request"
]
```
## GCPのプロジェクト番号を設定する
[プロジェクトの設定]の「Google Cloud Platform（GCP）プロジェクト」から認証やAPIキーの発行に使うGCPプロジェクトのプロジェクト番号を設定します。
## スクリプト プロパティ
[プロジェクトの設定]の「スクリプト プロパティ」にBloggerのAPIキーを"API_KEY"、ブログのIDを"BLOG_ID"のプロパティ名で設定します。
後述する.gsスクリプトファイル内で簡単に呼び出すことができます。

# 実際に作成したコード
## HTMLテンプレート
### ソースコード
``` index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <title>Blogger Example</title>
  </head>
  <body>
    <? if (msg != "") {?>
      <div style="white-space: pre-line;">結果:<br><?= msg ?></div>
    <? }?>
    <form action="https://script.google.com/macros/s/<アプリの固有値>/exec" method="post">
      <div>
        <input type="text" name="year" required>年
        <input type="text" name="month" required>月
        <input type="text" name="from" required>日から
        <input type="text" name="to" required>日までの日記
      </div>
      <div>
        <input type="submit" value="投稿">
      </div>
    </form>
  </body>
</html>
```
### 実際の画面
![2022-09-10 16.52.58 script.google.com f794c9029ceb.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/72dc0fe7-0ab4-48ee-bde4-73602408ebf0.jpeg)

#### ポイント
* 実際に生成されるページは上記のHTMLをiframe内に表示しているという形になるので、`<base target="_top">` で、フレームの中でリンクが開くという風ならないようにしています(※Apps ScriptのエディタでHTMLを新規作成すると初期状態で指定されている)。
* `<? if ～ ?>` で msgに値が入ったいる時のみ結果のメッセージが埋め込まれるようにします。
* formの送信先はアプリのURLを指定します。

## GSファイル
### ソースコード
``` script.gs
function doGet() {
  // 初期表示
  const htmlTemplate = HtmlService.createTemplateFromFile("index");
  htmlTemplate.msg = "";
  return htmlTemplate.evaluate();
}

function doPost(e) {
  // 入力値を受け取り、Bloggerに投稿し、結果をhtmlに埋め込んで表示
  const htmlTemplate = HtmlService.createTemplateFromFile("index");
  htmlTemplate.msg = insert(e);
  return htmlTemplate.evaluate();
}

function insert(e){
  // Bloggerに記事を投稿

  // APIの認証に必要な情報を取得
  const properties = PropertiesService.getScriptProperties(); // プロパティスクリプトから全ての値を取得
  const apiKey = properties.getProperty("API_KEY");
  const blogId = properties.getProperty("BLOG_ID");

  const accessToken = ScriptApp.getOAuthToken(); // OAuth2.0のトークンを取得

  // 投稿APIのURL
  const url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/?key=" + apiKey + "&isDraft=true"; // isDraft:下書きか否か

  
  // リクエスト内容の準備
  const headers = {
    "Authorization":"Bearer " + accessToken
  };
  let options = {
    "headers" : headers,
    "muteHttpExceptions" : true, // StatusCodeが200以外でもExceptionが発生しないようにする
    "method" : "POST",
    "contentType" : "application/json",
  };

  let response;
  let responseCode;
  let content;
  let messages = "";
  let title;
  
  let body = {};
  let from =  Number(e.parameter.from);
  let to = Number(e.parameter.to);

  // from日付からto日付までの記事を投稿
  for (let i = from; i < to + 1; i++) {
    body["title"] = e.parameter.year + "年" + e.parameter.month + "月" + i + "日の日記"; // 投稿記事タイトルを作成(例:2022年8月1日の日記)
    options["payload"] = JSON.stringify(body);
    
    // リクエスト送信
    response = UrlFetchApp.fetch(url,options);
    responseCode = response.getResponseCode();
    
    // 投稿に成功した場合、投稿した記事のタイトルを、そうではない場合失敗した旨を画面側に表示
    if (responseCode == 200) {
      content = JSON.parse(response.getContentText());
      title = content["title"];
      messages += title + "\n";
    } else {
      messages += "<記事の投稿に失敗しました。>\n";
    }
  }

  return messages;
}
```
#### ポイント
##### doGet
* 初期表示なので結果のメッセージが表示されないよう空文字を積み込んで、前述のテンプレートが表示されるようにします。
##### doPost
* 引数eが画面からの入力値です。
* ↑をinsert関数に渡して、その戻り値をメッセージとして積み込んで前述のテンプレートが表示されるようにします。
##### insert
* PropertiesService.getScriptProperties() で、上述のプロパティスクリプトからすべての値を取得し、その下の2行で個々の値を取得しています。
* ScriptApp.getOAuthToken() でOAuthのトークンを取得しています。初回実行時にOAuth認証の画面が出るかと思います。(※スクショ撮るのは忘れました)
* e.parameter.[formのnameの値]でフォームでの入力値を取得しています。
* ブログへの投稿に成功した場合は記事タイトルを、失敗した場合は失敗の旨を文字列に追加していって呼び出しもとにreturnします。
# 画面上での動作
## 入力
![2022-09-10 17.08.32 script.google.com 39491ab4c19a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/6962f6fe-f69a-c858-61f3-07c2636298ad.jpeg)
フォームに値を入力し、投稿ボタンをクリックすると…

## 結果画面
![2022-09-10 17.08.51 script.google.com 677f3158b8b4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/ace9f902-668b-4869-8634-acc825632585.jpeg)
投稿に成功した記事の題名と、失敗のメッセージが表示されました。
BloggerAPIには時間当たりの投稿数の制限などがあるため、連続して投稿すると制限に掛かって失敗する場合もあります。
実際に運用する場合は1記事ごとに1秒待機する、といったような処理があると良いかと思います。


## Blogger管理画面
![2022-09-10 17.10.23 www.blogger.com 0508ff59d07f.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/00552124-ca68-3580-9cc7-c1b402425f39.jpeg)
実際のブログ上にも記事が投稿されています。

# 最後に
以上で検証は終わりです。

今回の制作を通じて、以下の点にGASのメリットを感じました
* 元々Pythonで作成していたアプリでは、第三者による投稿を防ぐために、ログイン機能を実装していたが、GASを使うことで自分以外のアクセスを簡単に防ぐことが出来る。
* APIの認証周りも、BloggerがGoogle社のサービスだけあって容易に実装出来る。

個人的な利用目的で軽めのアプリを制作する場合、今後もGASが役立ってくれそうです。

# 参考サイト
[Blogger API 公式ドキュメント](https://developers.google.com/blogger)
[Google Apps Script 公式ドキュメント](https://developers.google.com/apps-script)