Qiita の限定共有記事を予約投稿機能として使えるようにした話

1 日 1 Qiita 運動中。株式会社オズビジョンのユッコ (@terra_yucco) です。

さて、エモく「1 日 1 Qiita やる！」なんて言ったものの、無理したら続かないので、続ける工夫を考えようと思いました。
旧タイトル：1 日 1 Qiita 活動を無理なくやっていこう

# 1 日 1 Qiita
## 課題

1 日 1 Qiita と銘打っているからには、可能ならば毎日投稿したい。

ただし Qiita は Blog ではないので、公開日を変更することはできません。
※限定公開にしておいて公開にする、などしても、公開日は限定公開した日になるようです。

- [Qiitaの限定公開の悲しみ ~公開日には気をつけろ~](https://qiita.com/kinpira/items/0eee55bcfc89391d200c)

なら、下書きで置いておいて公開すればいいのでは？とかちょっと思いました。
Qiita API 使ったらこのへんできるのではないかと。

## 解決トライ
### 要件

- ~~下書き保存した投稿を指定日に公開可能~~
    - 仕様確認時、下書きは使えないことが分かったので方針変更
- 公開日を指定していないものは公開対象外
- 公開日を投稿日に設定
- 専用のサーバ構築不要
- 公開投稿したら Twitter に Tweet
- // 公開投稿したら Slack に通知

### 仕様

- Cron などにより定期的に投稿可能
- Qiita API v2 で記事の公開公開投稿を実施
    - 【変更】下書きを取得する API が見当たらないので、限定共有投稿から記事を作り直す
    - 【付随】元となった限定共有投稿は削除

### 与件

- 予約の指定は日単位
- 実行環境は Google Spreadsheet のトリガ

### 実装
#### アクセストークン取得

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95030/dd463f7c-04ac-2fc4-be54-1b956042d77e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95030/a46cb7d2-0407-5f71-ef29-c33bba35d11b.png)

#### プロパティストア設定

※今までなんちゃって VBA もどきで GAS を書いていたので、プロパティストアを初めて知りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95030/4901be76-85ad-3647-c91b-5c2cac1ab8fc.png)

#### コード


```js
/**
 * Usage
 *
 * (1) Qiita に 限定共有投稿しておく
 * (2) 2019-05-30 の GAS のトリガ起動で限定共有投稿は削除され、同内容で公開投稿される
 *
 *   {限定共有投稿} [2019-05-30]Qiita 記事のタイトル
 *   {公開投稿}     Qiita 記事のタイトル
 */
// 予約用の記事タイトル日付ヘッダフォーマット
var reservationDateFormat = '[yyyy-MM-dd]';
// 投稿時 Tweet しないなら false
var tweet = true;

/**
 * トリガ起動で Qiita に記事投稿
 */
function postQiita() {
  var runDate = Utilities.formatDate(new Date(), 'Asia/Tokyo', reservationDateFormat);
  
  var response = callQiitaAPI('/api/v2/authenticated_user/items?page=1&per_page=20', 'GET', '');
  var data = JSON.parse(response.getContentText());
  
  for (i = 0; i < data.length; i++) {
    if (!isTargetArticle(data[i], runDate)) {
      continue;
    }
    postNewArticle(data[i], tweet);
    deleteOldArticle(data[i].id);
  }
}

/**
 * Qiita 投稿対象記事かの判断
 *
 * @param {object} 記事
 * @param {string} スクリプト起動日
 * @return {boolean} 投稿処理対象なら true
 */
function isTargetArticle(datum, runDate) {
  if (!datum.private) {
    return false;
  }
  var titleDate = datum.title.slice(0, reservationDateFormat.length);
  if (titleDate != runDate) {
    return false;
  }
  return true;
}

/**
 * 取得した限定共有投稿の内容を利用して新規記事投稿
 * タイトルを変更
 *
 * @param {object} 限定共有投稿内容
 * @param {boolean} 投稿時に Tweet するか
 * @return {object} Qiita 投稿 API の response
 */
function postNewArticle(draftData, isTweet) {
  var postData = {};
  postData.body = draftData.body;
  // only for qiita team: coedting
  // only for qiita team: group_url_name
  postData.private = false;
  postData.tags = draftData.tags;
  postData.title = getNewTitle(draftData.title);
  postData.tweet = isTweet;
  return callQiitaAPI('/api/v2/items', 'POST', JSON.stringify(postData));
}

/**
 * 投稿元の限定公開記事を削除
 *
 * @param {string} 削除する Qiita 記事の id
 * @return {object} 呼び出し API の response
 */
function deleteOldArticle(id) {
  return callQiitaAPI('/api/v2/items/' + id, 'DELETE', '');
}

/**
 * 公開記事用タイトル取得
 *
 * @param {string} 元記事のタイトル
 * @return {string} 予約投稿用文字列を取り除いたタイトル
 */
function getNewTitle(originalTitle) {
  return originalTitle.slice(reservationDateFormat.length);
}

/**
 * Google Apps ScriptからQiita APIを使って、Qiita:Teamの情報を得る方法（ユーザー一覧の入手）
 * https://qiita.com/takoratta/items/4a0f3a5f9bb75c0646dc
 *
 * @param {string} 呼び出す Qiita の API URL
 * @param {string} 呼び出し時 method
 * @param {object} API 呼び出し時に指定する payload
 * @return {object} 呼び出し API の response
 */
function callQiitaAPI(endPoint, method, payload) {
  var scriptProperties = PropertiesService.getScriptProperties();
  const QIITA_API_DOMAIN = scriptProperties.getProperty('QIITA_API_DOMAIN');
  const QIITA_TOKEN = scriptProperties.getProperty('QIITA_TOKEN');

  var headers = {
    'Authorization': 'Bearer '+ QIITA_TOKEN,
    'Content-Type' : 'application/json',
  };
  var options = {
    'method': method,
    'headers': headers,
    'payload': payload,
  };

  apiEndPoint = QIITA_API_DOMAIN + endPoint;
  return UrlFetchApp.fetch(apiEndPoint, options);
}
```

#### トリガ設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95030/1c5e9ea6-091d-e03a-9620-087b5c6a4d4a.png)

### コーディング時にお世話になった記事たち

久々に書いたので、忘れていることばかりで、Google 先生のお世話になりっぱなしでした。

- GAS から Qiita API を呼ぶための便利な共通処理
    - https://qiita.com/takoratta/items/4a0f3a5f9bb75c0646dc
- 簡単にログを出すには (console.log() ではなく Logger.log())
    - https://qiita.com/HeRo/items/946dff8c9161a730ec66
- 文字列から任意の場所を切り出すには
    - https://vba-gas.info/gas-slice
- 起動日を取得するには
    - https://qiita.com/yasuyuki_tsuda/items/11e882076e2dc9fdee16
- token 類を安全に利用するには (プロパティストア)
    - https://tonari-it.com/gas-get-trello-cards-list/#toc2
- 変数の型を知るには
    - https://tonari-it.com/gas-variable-data-type/
- GAS でドキュメンテーションコメントを書くには
    - https://tonari-it.com/gas-documentation-comment/

上記の記事を共有された皆様、ありがとうございました。

### デバッグ時にお世話になった記事

- Qiita API を呼ぶときに必要なヘッダ
    - https://qiita.com/Eai/items/1165d08dce9f183eac74

>HeadersにContent-Typeを指定し忘れると400エラーを返します。コードで書くときに忘れがちなので気をつけましょう。

こちら見事にハマりました。
この Qiita は Discord への投稿ですが、Qiita API でも適切な Content-Type は必要となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95030/f5dcea6e-ea19-e327-13f1-783bcfb7ec03.png)

### Qiita 仕様メモ

なお、この方法をテストするときに、過去に公開していてほとんど読まれていない記事を一度下書きに戻して試そうと思ったのですが戻らず。
どうも Qiita は一度公開すると下書きに戻せないようです。ちなみに限定公開にも変更はできず、できるのは削除だけのようです。知りませんでした。

## Next Action

この記事は上記のスクリプトを使って投稿しているので、公開されていたらスクリプトがうまく動いたということになります :four_leaf_clover: 

とはいえ、予約できるようになっても記事が無いと予約投稿してくれないので、次はちゃんと記事を書いていこうと思います。目指せ 3 日坊主卒業。

## [追記] Note

若干不具合があり、タイトルが思ったより更に 1 文字切り詰められて投稿されました。詰めの甘さ。
スクリプトは修正しているので、明日 5/11 の投稿で問題なければ一回完成になると思います。

gist へもコードを保存。
https://gist.github.com/terra-yucco/40cacd476d5febca85e2d403ff28a620