PowerShell + O365 付帯の Power Automate で実現するデータのダウンロードと通知の自動化

このブログは、[Office 365 Advent Calendar 2021](https://adventar.org/calendars/6679) の 25 日目の記事です。  
久々ではありますが Advent Calendar への参加をきっかけに、ゆるりとブログを再開したいと思います。  
なおブログ内で掲載する内容は、個人の見解に基づくものです。所属組織を代表するものではありません。予めご了承ください。

# はじめに
私は仕事柄ライセンスガイドという公開ドキュメント (PDF) をよく読むのですが、最新版が公開されたか否かは自分で都度ダウンロードして確認するしかありませんでした。これは、私以外のチームメンバも同じでした。  
私は「ちょっと面倒だし、効率悪いなー。公開されたら自動的に関係者がアクセス可能な場所にダウンロードされ、Update されたことが Teams に通知されたらいいののにな」という、通称「めんどくさがり」を発動し実装してみることに。  
せっかく実装し運用を始めたので、実装について備忘録も兼ねて解説したいと思います。

私と同じように、データの更新を定期確認・ダウンロード・通知といった一連の流れを自動化したいと検討されている方の参考になれば幸いです。

# Agenda
このような構成で、解説していきたいと思います。

- 前提条件 / アーキテクチャ
    - 必要なツール・ライセンス
    - 構成
- 実装詳細
    - Teams 編
    - PowerShell 編
    - Power Automate 編

# 前提条件 / アーキテクチャ
## 必要なツール・ライセンス
この記事で紹介する内容を実行するために、必要なツール類は以下の通りです。

- Microsoft 365 (法人向けライセンスを想定しています)
  - OneDrive for Business
  - Microsoft Teams
  - Power Automate (Microsoft 365 付帯のもので結構です)
- Windows 11 (10 でも問題ありませんが、せっかくだから 11 にしよ ?)
  - タスクスケジューラ (OS に標準で用意されているものです)
  - PowerShell (PowerShell Script を実行できる環境であれば、問題ありません)
  - PowerShell の実行ポリシーは、RemoteSigned になっていることを前提としています。


## 構成
今回は、このようなアーキテクチャで実装しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/b0825878-5fc0-13c0-4a1e-06de4657438e.png)

# 実装詳細
ではさっそく、実装の詳細を見ていきましょう。

## Teams 編
これからチームを組織する場合は、Teams でチームとチャネルを作ります。  
チームやチャネルを作る権限が無い ? それなら、管理者さんにお願いしましょ !

### チームを作る
これから新たにチームを作る場合は、次の手順でチームを作成します。

1. [チームに参加、またはチームを...] から [チームを作成] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/bbdf4427-3638-3310-1824-a62ff15aadc2.png)

1. [最初から] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/c51591de-f1c2-329f-ab74-ceb14ecf5a12.png)

1. [パブリック] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/afd61f15-83d3-14e0-1986-bfe00714db0a.png)

1. チーム名を入力後 [作成] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/f70f5947-f158-d362-933f-a51b629cabef.png)

### 通知用のチャネルを作る
これから通知先のチャネルを作る場合は、次の手順でチャネルを作成します。

1. 作成したチャネルの […] から [チャネルを追加] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/f53d78d1-ce67-a525-6eb9-ee9f23d86d5b.png)

1. チャネル名を入力後 [追加] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/796f5b9e-a52f-3b0b-8c35-630db1ee5eac.png)

### データ格納用フォルダを PC と同期する
この手順は、PC に OneDrife for Business のクライアントを導入しアカウント設定が導入している必要があります。

1. 先ほど作成したチャネルの [Files] タブを開き、[同期] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/a5a81986-fe1a-760e-36ec-7d1d18664b19.png)

1. OneDrife for Business クライアントが同期を始めると、同期中のポップアップが表示されるため [閉じる] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/5d43eacd-cc2f-f5b4-620b-1703557b9b8d.png)

1. エクスプローラーを開き、[チーム名 - チャネル名] のフォルダが同期状態になっていることを確認します。  
後の工程でこのフォルダのパスを利用するため、コピーしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/1eb6578c-364a-33f7-5a10-f4ff9941b357.png)

## PowerShell 編
### PowerShell Script を作成
詳細は後述しますが、まずは下記 Script をコピーし LicenseGuideDownloader.ps1 というファイル名で保存します。 PowerShell ISE で作成すると編集しやすいのでおススメです。

**LicenseGuideDownloader.ps1 サンプル**

``` LicenseGuideDownloader.ps1
<#
==============================================================
 Script Name    : License Guide Download Script
 Script File    : "C:\_work\LicenseGuideDownloader.ps1"
 Script Version : 1.0
 ExectionPolicy : RemoteSigned
 LastUpdate     : 2021/11/24

 History :
  2021/11/24 Create New
==============================================================

--------------------------------------------------------------
 Common Parameter
   savePath : Directory path synchronized from SharePoint Documents Library by OneDrive Client
   logFilePath : File path for log data
   arrayUris : Array of redirection URL ; JP Power Platform, EN Power Platform, JP Dynamics 365, EN Dynamics 365
--------------------------------------------------------------
#>
$savePath = "C:\Users\{Account Name}\{Organization Name}\Biz Apps Team - License Channel\"
$logFilePath = "C:\_work\LicenseGuideDonwloadAutomation.log"
$arrayUris = @("https://go.microsoft.com/fwlink/?LinkId=2085130&clcid=0x411","https://go.microsoft.com/fwlink/?LinkId=2085130&clcid=0x409","https://go.microsoft.com/fwlink/?LinkId=866544&clcid=0x411","https://go.microsoft.com/fwlink/?LinkId=866544&clcid=0x409")

<#
--------------------------------------------------------------
 Loop function for file download
   respCom : Response of Invoke-WebRequest commandlet
   respFileName : File Name of donwload target PDF File
   saveFilePath : Full Path of downloaded file
--------------------------------------------------------------
#>
function fncFileDownLoad{
    for($i=0; $i -lt $arrayUris.count; $i++){
        $respCom = Invoke-WebRequest -Uri $arrayUris[$i]
        $respFileName = $respCom.BaseResponse.ResponseUri.LocalPath -replace ".*/",""
        $saveFilePath = $savePath + $respFileName
        If(-not (Test-Path ($saveFilePath))){
            Invoke-WebRequest -Uri $arrayUris[$i] -OutFile $saveFilePath
            fncLogging("Download Successed : " + $respFileName)
        } else {
            fncLogging("No-Update : " + $respFileName)
        }
    }
}

<#
--------------------------------------------------------------
 function for logging
   respCom : Response of Invoke-WebRequest commandlet
   respFileName : File Name of donwload target PDF File
   saveFilePath : Full Path of downloaded file
--------------------------------------------------------------
#>
function fncLogging ($LogMsg) {
    If (!(Test-Path -Path $logFilePath)){
        New-Item -Path $logFilePath -ItemType File -Force
    }
    Add-Content $logFilePath ((Get-Date -Format o) + " : " + $LogMsg)
}

<#
--------------------------------------------------------------
 Call Function
--------------------------------------------------------------
#>
try{
    fncFileDownload
} catch{
    fncLogging("Failed to File Download Function")
}
```

**LicenseGuideDownloader.ps1 - First Block 解説**

- savePath に、先ほど同期したフォルダのパスを指定します。最後に \ を忘れないようにしましょう。
- logFilePath に、ログファイルのパスを指定します。更新チェック・ダウンロードをログに記録するためのものなので、不要な場合は行頭に # を入れてコメントアウトしてください。
  - arrayUris は、ドキュメントが公開されている URL をコンマ区切りで指定します。複数の URL を巡回できるよう、配列として URL を格納しています。

``` LicenseGuideDownloader.ps1
$savePath = "C:\Users\{Account Name}\{Organization Name}\Biz Apps Team - License Channel\"
$logFilePath = "C:\_work\LicenseGuideDonwloadAutomation.log"
$arrayUris = @("https://go.microsoft.com/fwlink/?LinkId=2085130&clcid=0x411","https://go.microsoft.com/fwlink/?LinkId=2085130&clcid=0x409","https://go.microsoft.com/fwlink/?LinkId=866544&clcid=0x411","https://go.microsoft.com/fwlink/?LinkId=866544&clcid=0x409")
```

**LicenseGuideDownloader.ps1**

- 1-2 行目 : arrayUris 内の配列数繰り返し処理を function 化しています。
- 3-4 行目 : Invoke-WebRequest で情報を取得し、ファイル名を抽出しています。(ファイル名より前の URL 部分を置換処理で削除しています。)
- 5 行目 : 格納するデータのフルパスを生成しています。
- 6 -10 行目 : 5 行目で生成したフルパス (既存データ) の存在を確認し、存在しなければデータをダウンロードしダウンロードをログに記録しています。存在した場合は、更新が無かった旨をログに記録します。ログに記録する必要がない場合は、行頭に # を入れてコメントアウトしてください。

```LicenseGuideDownloader.ps1
function fncFileDownLoad{
    for($i=0; $i -lt $arrayUris.count; $i++){
        $respCom = Invoke-WebRequest -Uri $arrayUris[$i]
        $respFileName = $respCom.BaseResponse.ResponseUri.LocalPath -replace ".*/",""
        $saveFilePath = $savePath + $respFileName
        If(-not (Test-Path ($saveFilePath))){
            Invoke-WebRequest -Uri $arrayUris[$i] -OutFile $saveFilePath
            fncLogging("Download Successed : " + $respFileName)
        } else {
            fncLogging("No-Update : " + $respFileName)
        }
    }
}
```

**LicenseGuideDownloader.ps1 - Third Block 解説**

- ログに記録する処理を function 化しています。ログに記録する必要がない場合は、このブロックを削除して構いません。
- 2-3 行目 : ログファイルの有無を確認し、存在していなければファイルを作成しています。
- 5 行目 : 日付とログメッセージ (引数) をログファイルに追記しています。

```LicenseGuideDownloader.ps1
function fncLogging ($LogMsg) {
    If (!(Test-Path -Path $logFilePath)){
        New-Item -Path $logFilePath -ItemType File -Force
    }
    Add-Content $logFilePath ((Get-Date -Format o) + " : " + $LogMsg)
}
```

**LicenseGuideDownloader.ps1 - Fourth Block 解説**
- 各機能を呼び出し、実処理を行っています。
- 1-2 行目 : First Block を呼び出し、実処理を行っています。
- 3-4 行目 : First Block の呼び出し時、エラーとなった際にログにエラーを記録しています。

```LicenseGuideDownloader.ps1
try{
    fncFileDownload
} catch{
    fncLogging("Failed to File Download Function")
}
```

### タスクスケジューラで定期実行する
PowerShell Script が完成したらタスクスケジューラで定期実行し、更新があれば指定したフォルダにダウンロードします。

1. [Windows] + [R] でファイル名を指定して実行を開き、[taskschd.msc] と入力しタスクスケジューラを起動します。

1. [タスク スケジューラ ライブラリ] を右クリックし、[タスクの作成] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/a75e0aae-bb6b-f3aa-fe7a-3e0941602310.png)

1. 全般タブで名前を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/4684f1e0-2a0d-d4aa-91f1-e2dd75033067.png)

1. トリガータブで実行タイミング指定します。今回は、4 回 / 日 で動くように設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/786a7c04-f12e-0460-fc83-d76c1ced55e5.png)

1. 操作タブで、先ほどの PowerShell Script を指定します。各項目は、次のように指定しています。
    - プログラム/スクリプトに、[C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe] と入力しています。
    - 引数の追加に、[-Command ".\LicenseGuideDownloader.ps1"] と入力しています。
    - 開始に、[C:\_work] と入力しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/8c166a6d-423e-387a-b3df-2816fbadf238.png)

:::note info
作成したタスクスケジュールを右クリックし、[実行する] を選択し動作確認をしておくことをお勧めします。  
正常動作すると、同期フォルダにデータがダウンロードされていることが確認できます。  
Power Automate の動作確認に向け、動作確認が終わったらデータを削除しておきましょう。
:::

## Power Automate 編
### 通知用クラウドフローを作成
Power Automate のクラウドフローを作成し、Teams に通知する設定を行います。これから作成するクラウドフローの全体像は、このようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/d627a35e-89d1-977f-0a71-d0336da54c74.png)

1. [Power Automate](https://flow.microsoft.com/) にアクセスし、[作成] - [自動化したクラウドフロー] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/fcbab3c5-5dc3-b5c6-7d2f-fa1675209c70.png)

1. フロー名を指定後、[フォルダー内にファイルが作成されたとき SharePoint] を選択し [作成] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/b7ac95c2-963f-a55f-cf9e-893db54a5769.png)

1. トリガーと最初のアクションを設定します。  
    - サイトのアドレスは、先ほど作成したチームが表示されていたら、それを選択します。  
      表示されていない場合は [カスタム値の入力] を選択し、 SharePoint サイトの URL を入力します。
    - フォルダー ID は、ダウンロードデータを保存するフォルダを選択します。
    - [新しいステップ] を選択し、最初のアクションとして [スイッチ] を選択します。
    - オンは、トリガーで得られる [Content Type] を動的なコンテンツから選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/cbc882df-6746-9364-49a5-d110ec394f24.png)

1. ケースの設定と、次のアクションを設定します。
    - 次の値と等しいは、"application/pdf" と入力します。前項のスイッチアクションで指定した、コンテンツタイプが PDF か否かをここで判定しています。
    - [アクションの追加] を選択し、更に [スイッチ] を追加します。
    - オンは、動的なコンテンツの隣にある式から "split(triggerOutputs()?['headers/x-ms-file-name'],' ')[0]" と入力し [OK] を選択します。ここではファイル名の中からスペースを区切り文字とし、最初の文字列を取得しています。
    - [+] を選択し、ケースを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/9888738d-8573-95c4-ac6b-209ca3190d18.png)

1. ファイル名の最初の文字列に応じ、内容の異なるメッセージを Teams に投稿します。
    - ケースの次の値と等しいは、"Dynamics" と入力します。(Dynamics 365 ... という文字列の Dynamics の部分で判定しています)
    - [アクションの追加] を選択し、[チャットやチャネルにアダプティブカードを投稿する] を選択します。
    - 投稿者、投稿先、Team、Channel を画像のように選択します。
    - Adaptive Card は、後述の JSON と画像を参考に動的コンテンツを当てはめます。
    - ケース 2 も同様に、"Power" と入力します。(Power Apps ... という文字列の Power の部分で判定しています)
    - Dynamics の際と同様に、Teams のアクションを追加し設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/2536820b-1b19-1356-1bf5-67c5a3b5afe1.png)

1. 設定が完了したら、クラウドフローをテストします。
    - テストにあたり同期先フォルダが空であることを確認し、タスクスケジュールを即時実行します。
    - テストが成功すると図のような通知が投稿され、[Open PDF] をクリックするとブラウザで PDF を確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66349/e671cd76-d093-dbf0-a168-a3a8c79d645c.png)

**Adaptive Card の JSON について**

Adaptive Card を作成する際は、[Adaptive Card Designer](https://adaptivecards.io/designer/) で仕上がりをイメージしながら作成することが可能です。クラウドフローで動的コンテンツを利用する部分は取り除き、設定値を変更してデザインを確認してみてください。

Adaptive Card を簡単に作成いただけるよう、各 Adaptive Card の JSON をサンプルとして掲載しておきます。SharePoint の URL 等、変更が必要な個所にコメントを入れてあります。コピーして該当箇所をお試しいただく環境に合わせて変更しご活用ください。

**Adaptive Card - Dynamics JSON**

```
{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Large",
            "weight": "Bolder",
            "text": "License Guide Updated",
            "spacing": "None",
            "separator": true,
            "isSubtle": false
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "Dynamics 365",
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "spacing": "None",
                            "weight": "Bolder",
                            "text": "動的コンテンツから、x-ms-file-name-encoded を選択します。",
                            "isSubtle": true,
                            "wrap": true
                        }
                    ],
                    ],
                    "width": "auto"
                }
            ],
            "style": "default",
            "spacing": "None",
            "separator": true,
            "horizontalAlignment": "Left"
        }
    ],
    "actions": [
        {
            "type": "Action.OpenUrl",
            "title": "Open PDF",
            "weight": "Bolder",
            "url": "トリガーで指定した URL を貼り付け、続けて動的コンテンツから x-ms-file-path-encoded を選択します。"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3",
    "verticalContentAlignment": "Center"
}
```

**Adaptive Card - Power JSON**

```
{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Large",
            "weight": "Bolder",
            "text": "License Guide Updated",
            "spacing": "None",
            "separator": true,
            "isSubtle": false
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "Power Apps",
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "spacing": "None",
                            "weight": "Bolder",
                            "text": "動的コンテンツから、x-ms-file-name-encoded を選択します。",
                            "isSubtle": true,
                            "wrap": true
                        }
                    ],
                    "width": "auto"
                }
            ],
            "style": "default",
            "spacing": "None",
            "separator": true,
            "horizontalAlignment": "Left"
        }
    ],
    "actions": [
        {
            "type": "Action.OpenUrl",
            "title": "Open PDF",
            "weight": "Bolder",
            "url": "トリガーで指定した URL を貼り付け、続けて動的コンテンツから x-ms-file-path-encoded を選択します。"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3",
    "verticalContentAlignment": "Center"
}
```

# さいごに
いかがでしたでしょうか。PowerShell や Adaptive Card の部分は、馴染みが無いと難しく感じてしまった方もいらっしゃるでしょうか。今回は誰もがコピペで始められるよう、なるべく詳細に説明を入れました。もし不明点等があれば、[Twitter](https://twitter.com/nya193) までご連絡ください。可能な限りフォローさせていただきます。