GAS × Redmine APIで添付ファイル付きチケットの作成

## この記事について

- Google App Script (以下GAS) からRedmine APIを利用して、Redmineプロジェクト下に添付ファイル付きのチケットを作成するサンプルコードを紹介しています
(英語と日本語の両方でググっても見つかりませんでした…！)
- 今回はGoogleドライブ上のファイルを添付したRedmineチケットを作成しています
- GASやRedmine自体についての詳しい説明はございません

**チケットイメージ**
![Screen Shot 2022-10-05 at 13.07.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/95a5fb6e-b7ff-fccb-6901-6e764109a1e9.png)

## Redmine APIについて

- プロジェクトやチケット情報の取得、作成や削除といった操作を行うことができるAPIです
- APIの利用にはRedmineの管理メニューから設定が必要ですので、未設定であれば管理者さんに依頼をしてください
- RedmineバージョンによってAPIの対応状況が異なるので、詳しくは[公式Wiki](https://www.redmine.org/projects/redmine/wiki/rest_api)などで事前に確認ください

## 準備するもの

- GAS開発環境 (Googleアカウントとブラウザ)
- Redmine (Redmine URLと、プロジェクトに参加しているRedmine個人アカウント)
- Redmine APIアクセスキー (Redmine個人アカウントごとに付与される文字列)
- RedmineプロジェクトのID (取得方法は別途お調べください)

## 添付ファイル付きチケット作成までの手順

添付ファイル付きのチケットの作成にはRedmine APIを2度 (以上) 実行する必要があります
1. Redmine API (1度目) を利用し、Redmineにファイルをアップロード
    - ファイルの`token`を含むをレスポンスを取得する
    - ファイルが複数ある場合は、アップロードもその数だけ実行する
2. Redmine API (2度目) を利用し、ファイルの`token`を紐つけたチケットを作成

## 詳しい手順

### 1. ファイルをアップロードする関数の作成

- チケット作成前にファイルだけをアップロードする関数のサンプルコードです
- 下記サンプルコードのファイル名を`main.gs`とするとQiitaのハイライトが働かなかったため、ここでは拡張子を`.js`としていますが、実際には`.gs`のファイルです
- Blob形式のファイルを引数として関数に渡す実装にしています
- 元のファイルは画像やテキスト、その他の形式も送信可能です
- `upload_url`と`api_key`はGASのスクリプトプロパティから呼び出す実装をしています
(ハードコーディングでも動作します)
- 関数の戻り値は、チケットに紐つける際にそのまま使える辞書型にしています

```main.js
/**
 * ファイルのRedmineにアップロード
 * チケットへの紐付けは別途行う
 *
 * @param {blob} blob - Blob型ファイルデータ
 * @return {object} token, filename, content_typeのキーを持つ連想配列
 */
function redmineBlobUpload(blob) {
    // Redmine URLとAPIアクセスキーをスクリプトプロパティから呼び出し
    const upload_url = PropertiesService.getScriptProperties().getProperty('REDMINE_URL') + 'uploads.json';
    const api_key = PropertiesService.getScriptProperties().getProperty('REDMINE_API_KEY');

    // headers
    const headers = {
        'X-Redmine-API-Key': api_key,
        'Content-Type': 'application/json',
    };

    // options
    const options = {
        'method': 'POST',
        'contentType': 'application/octet-stream',
        'headers': headers,
        'payload': blob,
    };

    // post実行
    const response = UrlFetchApp.fetch(upload_url, options);
    return {
        "token": JSON.parse(response).upload.token,
        "filename": blob.getName(),
        "content_type": "application/octet-stream",
    }
}
```

### 2. チケットを作成する関数の作成

- 上記「1. ファイルをアップロード」であげたファイルを紐つけたチケットを作成する関数のサンプルコードです
- 下記サンプルコードは拡張子を`.js`としていますが、実際には`.gs`のファイルです
- `upload_url`と`api_key`はスクリプトプロパティから呼び出す実装をしていますが、ハードコーディングでも動作します
- 引数でプロジェクト、チケットのタイトル、本文、添付ファイルを渡す実装をしています
(添付ファイルは必須ではありません)
- トラッカーやステータスの初期値を別の値にする、または引数化するなど、適宜変更してください

```main.js
/**
 * 添付ファイル付きチケット作成
 *
 * @param {number} project_id - RedmineプロジェクトID
 * @param {string} subject - チケットタイトル
 * @param {string} message - チケットの本文
 * @uploads {object} uploads - 添付ファイル配列、redmineBlobUpload()関数の戻り値が要素
 * @return {string} Redmine APIレスポンス
 */
function creatIssueWithAttachment(project_id, subject, message, uploads) {
    // Redmine URLとAPIアクセスキーをスクリプトプロパティから呼び出し
    const redmine_url = PropertiesService.getScriptProperties().getProperty('REDMINE_URL') + 'issues.json';
    const api_key = PropertiesService.getScriptProperties().getProperty('REDMINE_API_KEY');

    // uploads(添付ファイル)有無チェック
    if (!uploads) {
        uploads = []; // なければ添付しない
    }

    // headers
    const headers = {
        'X-Redmine-API-Key': api_key,
        'Content-Type': 'application/json',
    };

    // Redmineチケット情報
    const issue = {
        'subject': subject,
        'description': message,
        'tracker_id': 1,
        'status_id': 1,
        'uploads': uploads
    };

    // payload
    const payload = JSON.stringify(
        {
            'project_id': project_id,
            'issue': issue,
        }
    );

    // options
    const options = {
        'method': 'POST',
        'contentType': 'application/json',
        'headers': headers,
        'payload': payload,
    };

    // post実行
    const response = UrlFetchApp.fetch(redmine_url, options);
    return response.getContentText;
}
```

### 3. 作成した関数を実行

- 1と2の関数を順に実行すれば、添付ファイル付きチケット作成の完了です
- 今回はGoogleドライブの指定のフォルダ内のファイルを全てチケットに添付するサンプルコードを紹介します
- 下記サンプルコードは拡張子を`.js`としていますが、実際には`.gs`のファイルです

```main.js
/**
 * 添付ファイル付きチケット作成関数サンプル
 *
 * @return {string} Redmine APIレスポンス
 */
function driveFileUpload() {
    //Googleドライブ フォルダID (フォルダURLの最後の'/'以降の文字列)
    const drive_folder_id = '1234567890abcdefghijklmnopqrstuvw';

    // RedmineプロジェクトID
    const project_id = 123;

    // チケット情報
    const subject = 'Googleドライブファイルの確認';
    const message = 'Googleドライブ内のファイルを全て添付していますので、ご確認してください。';

    // 添付ファイル格納用配列
    const uploadList = [];

    // ---------- ファイルの取得とアップロード ----------

    // フォルダ内のファイルを取得
    const driveFolder = DriveApp.getFolderById(drive_folder_id);
    const files = driveFolder.getFiles();

    while (files.hasNext()) {
        // ファイルをBlobに変換
        const file = files.next();
        const blob = file.getBlob();

        // Redmineにアップロード
        const upload = redmineBlobUpload(blob);
        // 戻り値を配列に格納
        uploadList.push(upload)
    }

    // ---------- Redmineチケット作成 ----------

    // チケット投稿＆レスポンス取得
    const responce = creatIssueWithAttachment(project_id, subject, message, uploadList);
    return ContentService.createTextOutput(responce);
}
```

## おわりに

REST API経由でGASを実行することで、APIアクセスキーを隠蔽した形でRedmine APIの一部を外部アプリに利用させるといった事も可能だと思います。

その他の活用方法や、気になる点やご質問などあればコメントください、お待ちしております。
いいねとストックも励みになるのでぜひ！