GoogleAppsScript(GAS)で効率化する時になんだかんだよく使うコードスニペット10選

この記事は[株式会社サイバー・バズ Advent calendar 2021](https://qiita.com/advent-calendar/2021/cyberbuzz)の記事です。


## 前置き

GASって便利ですよね。
ちょっとした業務改善に、サイバー・バズでもよく使われています。
claspを使えばTypeScriptで書けるしGitHubで管理できるので、保守性も高いです。

ただ、GASで何回も同じコードを使ってるなあと思ったので
汎用的に役に立ちそうなコード片をまとめてみることにしました。

1. 外部APIから値取得
1. Slackへ投稿
1. メールの送信
1. シートの取得または作成
1. シートへの値追加
1. カスタムメニューの追加
1. トリガーの追加
1. フォーム回答内容取得
1. ファイルの取得
1. プロパティの取得

## 1. 外部APIから値取得

おそらくGASで最も使われていてもおかしくない、UrlFetchAppでのデータ取得です。
headersはobject型、dataはany型でこのままでは使いづらいので、
好みに応じて型情報を設定して使うのが良いでしょう。

```typescript
export const fetch = (url: string, accessToken: String) => {
  const res: GoogleAppsScript.URL_Fetch.HTTPResponse = UrlFetchApp.fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` }
  })
  const data = JSON.parse(res.getContentText())
  const headers = res.getHeaders()
  return {
    headers,
    data
  }
}
```

## 2. Slackへ投稿

何らかのトリガーの実行後、成功/失敗の通知などにSlackを活用することが多いです。
attachmentsも駆使することで、より綺麗で見やすいフォーマットで送信可能です。

```typescript
export type SlackAttachment = {
  title: string
  title_link: string
  fields: SlackAttachmentField[]
}
export type SlackAttachmentField = {
  title: string
  value: string
  short: boolean
}
export type SlackUser = {
  username: String
  icon_url?: string
}
export const postToSlack = (params: {
  channel: string
  webhookUrl: string
  user: SlackUser
  attachments: SlackAttachment[]
}): string => {
  const res = UrlFetchApp.fetch(params.webhookUrl, {
    method: 'post',
    headers: { 'Content-Type': 'application/json' },
    payload: JSON.stringify({
      channel: params.channel,
      username: params.user.username,
      icon_url: params.user.icon_url,
      attachments: params.attachments.length > 0 ? params.attachments : undefined
    })
  })
  return res.getContentText()
}
```

## 3. メールの送信

定番中の定番ですが。toやcc,bccにはカンマ区切りで好きな値を設定できます。
システムから送られるメールとして、noReply設定は重宝します。

```typescript
const to = 'to1@gmail.com,to2@gmail.com'
const cc = 'cc1@gmail.com,cc2@gmail.com'
const subject = '件名件名件名件名件名件名件名'
const body = `
お世話になっております。
テストメールです。
`

GmailApp.sendEmail(to, subject, body, {
  cc,
  noReply: true
})
```

## 4. シートの取得または作成

シート名とテンプレート名を指定して新規作成、もしくは当てはまる名前のシートを取得します。

```typescript
export const createSheetIfNotExists = (
  sheetName: string,
  templateName?: string
): GoogleAppsScript.Spreadsheet.Sheet => {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
  const existSheet = spreadsheet.getSheetByName(sheetName)
  const sheet = existSheet
    ? existSheet
    : templateName
    ? spreadsheet.insertSheet(sheetName, { template: spreadsheet.getSheetByName(templateName) })
    : spreadsheet.insertSheet(sheetName)
  return sheet.showSheet()
}
```

## 5. シートへのデータ挿入

取得したデータをとにかくシートの末尾に一括挿入する。
APIからの取得データをrawデータとしてシートに貯めておく際に使ったりします。

```typescript
export const insertRows = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => (data: Object[][]) => {
  const lastIndex = sheet.getLastRow()
  const insertRange = sheet.getRange(
    lastIndex + 1,
    1,
    data.length,
    Math.max(...data.map(d => d.length))
  )
  return insertRange.setValues(data)
}
```

## 6. カスタムメニューの追加

スプレッドシートやフォーム、ドキュメントのメニューを追加したいときに使う。

```typescript
export type MenuParam = { title?: string; items: MenuListItem[] }
export type MenuItemParam = { label: string; functionName: string }
export type MenuListItem = MenuParam | MenuItemParam

const isMenuItem = (value): value is MenuItemParam => 'label' in value && 'functionName' in value

export const createMemu = (
  { title = 'カスタムメニュー', items }: MenuParam,
  ui: GoogleAppsScript.Base.Ui = SpreadsheetApp.getUi()
): GoogleAppsScript.Base.Menu => {
  const menu = items.reduce((m, item) => {
    return isMenuItem(item)
      ? m.addItem(item.label, item.functionName)
      : m.addSubMenu(createMemu(item, ui))
  }, ui.createMenu(title))
  menu.addToUi()
  return menu
}

// Usage
global.onOpen = () => {
  createMemu({
    items: [
      { label: '実行する', functionName: 'myFunction1' },
      { title: 'サブメニュー', items: [{ label: '実行する', functionName: 'myFunction2' }] }
    ]
  })
}
```

## 7. トリガーの更新

特に時間ベーストリガーを自分ではない誰かの権限で設定したい時。
担当者に設定方法を案内するよりも、設定する関数を書いて、メニュー等から設定してもらいましょう。
トリガーの設定は冪等になるようにしておくと尚良いです。

```typescript
export const updateTrigger = (functionName: string): GoogleAppsScript.Script.Trigger => {
  ScriptApp.getProjectTriggers()
    .filter((_) => _.getHandlerFunction() === functionName)
    .forEach((_) => ScriptApp.deleteTrigger(_))
  // トリガー設定値は要調整
  return ScriptApp.newTrigger(functionName).timeBased().everyDays(1).atHour(9).create()
}

```

## 8. フォーム回答内容取得

フォームの回答のタイミングで、フォームの回答内容を取得して何らかの処理を行う。
フォームの回答TriggerのハンドラとしてonSubmit関数を設定して使いましょう。

```typescript
type FormSubmitEvent = {
  authMode: GoogleAppsScript.Script.AuthMode
  response: GoogleAppsScript.Forms.FormResponse
  source: GoogleAppsScript.Forms.Form
  triggerUid: string
}
export const onSubmit = (event: FormSubmitEvent) => {
  const sender = event.response.getRespondentEmail()
  const itemResponses = event.response.getItemResponses()
  const qAndA = itemResponses.map((r) => ({
    question: r.getItem().getTitle(),
    answer: r.getResponse().toString()
  }))
}

```

## 9. ファイルの取得

HTMLメールのテンプレート取得であったり、
指定フォルダ内の該当ファイル取得であったり

```typescript
const id: string = 'idididid'

const folder = DriveApp.getFolderById(id)
const file = DriveApp.getFileById(id)

const name: string = 'namename'

const folders = DriveApp.getFoldersByName(name)
const files = DriveApp.getFilesByName(name)
```

## 10. プロパティの取得

`ScriptProperty`はユーザー間で共有されつつ、GASからのみ利用可能なので使い勝手が良いです。

```typescript
export const properties = (
  prop: GoogleAppsScript.Properties.Properties = PropertiesService.getScriptProperties()
) => {
  return {
    set: (key: string, value: string) => prop.setProperty(key, value),
    get: (key: string) => prop.getProperty(key),
    delete: (key: string) => prop.deleteProperty(key)
  }
}

// Usage
const prop = properties()
prop.set('EXAMPLE_KEY', 'hoge')
prop.get('EXAMPLE_KEY') // => 'hoge' 
prop.delete('EXAMPLE_KEY')
```

---

## 終わりに

GASの効率化のパワーは侮れません。
うまく使って、快適な業務環境を作りましょう :muscle: 
初学者にも手軽に試せて、TypeScriptの練習にもなりますね！