PowerShellでコラボフローの組織更新をしてみよう！

Pythonでの画像解析を覚え燃え上がっている naruo です！
[コラボフロー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/collaboflow) の18日目は、PowerShellでコラボフローをより便利にしちゃおうという内容です！

今年も懲りず※にPowerShellでいきますよ〜💪🐚
※昨年のAdvent Calendarでは、REST APIをPowerShellで実行する内容でした。
今回に関わってくる部分もありますので、よろしければ参考になさってください。

https://qiita.com/narubo/items/1239cfe519f7cb63f28b

## コラボフローの組織更新？

コラボフローにはCSVでユーザー、グループ、役職を更新する機能がありますが、現在のところスケジュール登録などができませんが、これをPowerShellとWindowsのタスクスケジューラを使ってスケジュール実行できるようにしちゃおう！という内容です！

では早速、いきますよ〜💪

## APIの実行方法を確認する

まずは、以下のサイトにアクセスして、必要なAPIの実行方法を確認しましょう。

http://docs.collaboflow.com/

今回利用するAPIは次の3つになります。

- POST /v1/bulk/users
- POST /v1/bulk/groups
- POST /v1/bulk/titles

いずれも、bulkAPIを使って一括登録を行います。

コラボフローには現在のユーザー、グループ、役職情報をまとめてCSVでダウンロードする機能がありますが、これをつかってダウンロード・編集しておいたCSVファイルを読み取って、各APIで更新していくという想定です。

## 注意点について

まず初めに、これからの内容の注意点を先にお話ししておきます。
- 今回は、CSV内の削除フラグについては対応していません
- APIを実行する順番には意図があります
- 本番環境でいきなり実行は危ないので、ご注意ください
- 簡易的に動作テストは行なっておりますが、自己責任でお願いします
- 公式でのサポートはおこなっておりませんので、ご注意ください
- 新しく作成されるグループへの権限設定はできません。事前にグループを作成して権限を指定しておくなどの工夫が必要です
- PowerShellのバージョンは5系を想定しています。6〜では正常に動作しない場合があります
    - Import-CSVの-Encodingオプションで、次のように指定すると6〜でもCSVの取り込みで文字化けしなくなります
    `Import-CSV -Encoding ([System.Text.Encoding]::GetEncoding("shift_jis"))`

では早速、PowerShellのスクリプトを作成していきましょう。

## PowerShellを書いていく

さて、再利用できるように＆スケジュール実行できるように、スクリプトファイルを書いていきます。

次の流れで説明していきます！

- 認証＆API実行関数の実装
- ユーザーAPI実行関数の実装
- グループAPI実行関数の実装
- 役職API実行関数の実装
- 上記の繋ぎ込み

### 認証＆API実行関数の実装

まずは、すべてのAPI呼び出しで共通して利用する認証とAPI実行部分を作成しましょう。

API実行（HTTPリクエスト）で使えるコマンドレットはInvoke-WebRequestまたはInvoke-RestAPIですが、今回はWebRequestの方を利用します。

はじめに、スクリプト名を「Update-CollaboflowUser.ps1」とし、以下のコードブロックをコピーして貼り付けていきましょう。
保存する際のテキストエンコーディングは、BOM付きのUTF-8を指定してください。

設定（のようなもの）を変数にまとめました。
ここは、実行環境の状況に合わせて変更してください。
この辺りは、APIのトークンなどセキュリティ上重要な情報が記録されますので、できればコード内にハードコードするのではなく、外部ファイルに読み込んだものを利用するなど、対策を行なっていただくといいでしょう。

```powershell
$API_TOKEN = "ここにAPIキーを入力"
$BASE_URL = "https://cloud.collaboflow.com/{{instance_name}}/api/index.cfm"
$INSTANCE_NAME = "ここにコラボフローインスタンス名を入力"
$LOGIN_ID = "ここにAPIを実行するコラボフローユーザーのIDを入力"
```

認証キーの作成方法は先ほどの　[コラボフロー REST APIドキュメント](http://docs.collaboflow.com/)　の「APIキー認証」部分を参考にします。関数を組み立てます。

```powershell
function ExecuteAPI($method, $endpoint, $body) {
	$authText = "${LOGIN_ID}/apikey:${API_TOKEN}"
	$base64AuthText = ToBase64 $authText

	$headers = @{
		"X-Collaboflow-Authorization" = "Basic ${base64AuthText}"
	}

	$contentType = "application/json; charset=utf-8"
	$url = "$($BASE_URL.Replace('{{instance_name}}', $INSTANCE_NAME))${endpoint}"

	try {
		# APIを実行する。400以上が返った場合はエラー扱いで、catch部が実行される。
		$response = Invoke-WebRequest -Uri $url -Header $headers -Body $body -Method $method -ContentType $contentType
		return $response
	}
	catch {
		Write-Error "API実行でエラーが発生しました。($($_.Exception.Message))"
		throw $_.Exception
	}
}

function ToBase64($text){
	$byte = ([System.Text.Encoding]::Default).GetBytes($text)
	return [Convert]::ToBase64String($byte)
}
```

２つの関数を作成しました。
順を追って説明します。

ExecuteAPI関数は、実際にAPIを実行するための準備と実行部分です。
Invoke-WebRequestが実際にHTTPリクエストを実行します。成功した場合、レスポンス情報を関数呼び出し側に戻すよう記述してあります。
例外が発生した場合は、例外をrethrowします。

ToBase64関数は、APIキー認証を行う上で、認証情報をBASE64で符号化する処理です。

さて、これでAPI実行を行う準備が整いました。
それでは、Title, Group, Userの３つを実行できるよう、こちらも関数化していきましょう。

## ユーザー、グループ、役職を更新する処理を記述する

更新時、注意事項があります。

それは、更新する順番です。

ユーザーCSVは、グループや役職をもとに構築する必要があります。
そのため、役職→グループ→ユーザーの順で更新する必要があります。

### 役職の更新処理を記述する

以下を参考に関数を作成します。

http://docs.collaboflow.com/api-docs/#/Title/postTitles

```powershell
function UpdateTitles($titleCSVPath) {
	$titles = Import-Csv -Path $titleCSVPath -Encoding Default
	$convertedTitles = @()
	foreach ($title in $titles) {
		if ($title."削除" -eq "1") {
			continue
		}

		$convertedTitles += @{
			"code"        = $title."役職コード"
			"name"        = $title."役職名"
			"description" = $title."役職の説明"
			"level"       = [Int32]::Parse($title."役職権限レベル")
		}
	}

	$body = @{
		"records" = $convertedTitles
	}
	$jsonBody = $body | ConvertTo-JSON 
	ExecuteAPI "POST" "/v1/bulk/titles" $jsonBody
}
```

CSVファイルの内容をAPIで送信するbody（JSON）のキーにマッピングして、組み立てます。
今回、削除フラグが立っているものは更新対象から除外しました。
CSVで削除フラグを立てていた場合でもレコードは削除されませんので、削除が必要な場合は削除APIを呼び出すようにしてみてください。

### グループの更新処理を記述する

先ほどと同じように、関数を作成します。

```powershell
function UpdateGroups($groupCSVPath) {
	$groups = Import-Csv -Path $groupCSVPath -Encoding Default

	$convertedGroups = @()
	foreach ($group in $groups) {
		if ($group."削除" -eq 1) {
			continue
		}

		$convertedGroups += @{
			"code"         = $group."グループコード"
			"parent_code"  = $group."親グループコード"
			"name"         = $group."グループ名"
			"display_name" = $group."グループ表示名"
			"description"  = $group."グループの説明"
			"order"        = [int32]::Parse($group."同一階層での並び順")
		}
	}

	$body = @{
		"records" = $convertedGroups
	}
	$jsonBody = $body | ConvertTo-JSON
	ExecuteAPI "POST" "/v1/bulk/groups" $jsonBody
}

```

### ユーザーの更新処理を記述する

http://docs.collaboflow.com/api-docs/#/Title/postTitles

グループ、役職と比べると、ユーザーのCSVファイルからAPI実行時に送信するBody部の生成処理はやや複雑です。

```powershell
function UpdateUsers($userCSVPath) {
	$users = Import-Csv -Path $userCSVPath -Encoding Default
	$convertedUsers = @()
	foreach ($user in $users) {
		$userHashTable = @{
			"userid"                   = $user."ユーザーID"
			"name"                     = $user."氏名"
			"name_reading"             = $user."氏名（ふりがな）"
			"employee_code"            = $user."社員コード"
			"phone"                    = $user."電話番号"
			"phone_mobile"             = $user."携帯番号"
			"fax"                      = $user."FAX"
			"email"                    = $user."メールアドレス（PC）"
			"email_mobile"             = $user."メールアドレス（携帯）"
			"extra1"                   = $user."独自項目1"
			"extra2"                   = $user."独自項目2"
			"extra3"                   = $user."独自項目3"
			"extra4"                   = $user."独自項目4"
			"extra5"                   = $user."独自項目5"
			"password_change_required" = ToBoolean $user."ログイン時パスワード変更"
			"lockout"                  = ToBoolean $user."ロックアカウントフラグ"
			"admin"                    = ToBoolean $user."システム管理者フラグ"
			"groups"                   = @(ToArray $user."グループコード")
			"titles"                   = @(ToArray $user."グループ指定役職コード")
			"order"                    = [int32]::Parse($user."表示順")
		}

		# パスワードは、指定されている場合のみキーを追加する
		if ($user."パスワード".Length -ne 0) {
			$userHashTable.add("password", $user."パスワード")
		}

		$convertedUsers += $userHashTable
	}

	$body = @{
		"records" = $convertedUsers
	}

	$jsonBody = $body | ConvertTo-JSON -Depth 3
	ExecuteAPI "POST" "/v1/bulk/users" $jsonBody

}

function ToArray($text) {
	if ($text -eq "") {
		return @()
	}
	if ($text.IndexOf(",") -eq -1) {
		return @($text)
	}
	return $text -Split ","
}

# CSVファイル上の真偽値（真＝1、偽＝0）をBoolean（真＝$True、偽＝$False）に変換します
function ToBoolean($csvBooleanValue) {
	$result = $False
	if ($csvBooleanValue -eq 1) {
		$result = $True
	}
	return $result
}

```

## これまでで作成した関数を呼び出す処理を追加する

さて、これまででユーザー、グループ、役職の実行準備は整いました。
記述した関数を呼び出す処理を追加していきましょう。

```powershell
$groupCSV = "./グループ.csv" #　更新対象のCSVファイルです。状況に応じて変更してください。
$titleCSV = "./役職.csv"    #　更新対象のCSVファイルです。状況に応じて変更してください。
$userCSV = "./ユーザー.csv"  #　更新対象のCSVファイルです。状況に応じて変更してください。

try {
	# 役職情報の更新
	$result = UpdateTitles $titleCSV
	$result.Content

	# グループ情報の更新
	$result = UpdateGroups $groupCSV
	$result.Content

	# ユーザー情報の更新
	$result = UpdateUsers $userCSV
	$result.Content
}
catch {
	Write-Host "エラーが発生しています。処理は中断されました。"
	$_.Exception
}
```

## では、ターミナルから実行してみましょう

注意：ここの手順を実施すると、APIが実行され実際に変更されます。テスト環境などでお試しください。

1. スクリプトファイルを保存したディレクトリに、コラボフローからダウンロードしたユーザー一覧CSV、グループ一覧CSV、役職一覧CSVを次の名前で保存します。これが、今回の更新対象になります。
    1. ユーザー.csv
    1. グループ.csv
    1. 役職.csv
1. PowerShellのターミナルを起動します。
1. スクリプトファイルを保存したディレクトリに移動します。
    1. 例）`cd C:¥work¥`
1. 次のコマンドを入力し、Enterキーを押下します。（注意：実行されます）
    1. `./Update-CollaboflowUser.ps1`

正常に実行できましたでしょうか？
赤い字が一つでも表示されていたらなんらかのエラーが起きていますので、内容を確認して修正してください。

正常に完了すると、各APIのレスポンスであるJSONが表示されます。

## スケジュールで実行できるようにする

Windowsではタスクスケジューラを利用して、指定した日時に実行することができます。
次のように設定しましょう！

1. タスクスケジューラを起動します。
    1. スタートボタン > タスクスケジューラと入力して、リストされたアイコンをクリックします。
1. タスクスケジューラ ライブラリをクリックし、右のパネルで「基本タスクの作成」をクリックします。
1. 基本タスクの作成ウィザードが開きますので、次のとおり入力していきます。
    1. 基本タスクの作成画面
        1. 名前：任意の名前を入力します。今回は「コラボフロー組織情報更新」のように入力します。
        1. 説明：任意の説明文を入力します。今回は「テスト実行」と入力します。
    1. タスク トリガー画面
        1. いつタスクを開始しますか？では、一回限りを選択します。
    1. 1回　画面
        1. 開始：任意の日時を設定します。
    1. 操作画面
        1. タスクでどの操作を実行しますか？では、プログラムの開始にします。
    1. プログラムの開始
        1. プログラム／スクリプト欄：以下のように入力します。
            1. `%Systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe`
        1. 引数の追加（オプション）：以下のように入力します。
            1. `-ExecutionPolicy Bypass <スクリプトファイルの絶対パス>`
        1. 開始（オプション）：以下のように入力します。
            1. `<スクリプトファイルを置いたフォルダの絶対パス>`
    1. 完了
        1. 入力内容が正しいか確認し、完了ボタンを押下します。
1. 作成したタスクを右クリックし、プロパティを開きます。
1. 全般タブのセキュリティオプションは、環境に合わせて変更してください。ログインしていないときでも実行できるように変更しておかない場合、実行されません。

さて、テスト実行では、時間になってしばらく（時間ぴったりとは限りませんが、概ね1分以内には実行されるはずです。）たったらタスク一覧の画面で何もないところを右クリックし、最新の情報に更新をクリックしてみてください。
前回の実行結果で「この操作を正しく終了しました。(0x0）となっていれば成功です！

## おわりに

いかがでしたでしょうか。
削除などにも対応した完全なものではありませんが、少しPowerShell触れるからやってみようかな！と思っていただけたら幸いです。

19日目は、[@sumichan](https://qiita.com/sumichan) さんです！お楽しみに！