この記事では、Google Apps Scriptを使って、Google SpreadsheetのグラフをSlackにアップロードする方法を説明する。 Slack Appを作ってBot APIトークンを発行する手順や、OAuthスコープを付与する手順については記載しない。
SlackのファイルアップロードAPI(V2)
Slackのfiles.upload APIは、2025年3月11日をもって廃止されている。
そのため、Slackのチャンネルにファイルをアップロードするには、次の手順を踏む必要がある。
- ファイルをアップロードするためのURLを取得する
files.getUploadURLExternal - 1.で取得したURLを使ってファイルをアップロードする
- アップロードしたファイルとSlackチャンネルを関連付けて投稿する files.completeUploadExternal
これらのAPIに必要なOAuthスコープは、files:write
である。
Google Apps Scriptを使ってSlackにファイルをアップロードする
この記事で紹介するコードは、claspを使って、Google Apps Scriptのプロジェクトを作成し、TypeScriptで開発することを前提としている。
claspのインストール方法や、claspを使ってGoogle Apps Scriptのプロジェクトを作成、デプロイする方法は次の記事を参考にしてほしい。
プロジェクトの作成
Spreadsheetのグラフをアップロードするため、プロジェクトをSpreadsheetに紐づけておく。
Spreadsheetがない場合には、プロジェクトの作成時にspreadsheetのプロジェクトとして作成する。
後からGoogle Apps ScriptのプロジェクトをSpreadsheetに紐づける場合には、.clasp.json
のparentId
にSpreadsheetのIDを指定する。
{
"scriptId": "スクリプトID",
// 省略
"parentId": ["SpreadsheetのID"]
}
スクリプトプロパティの設定
Google Apps Scriptのスクリプトプロパティには、次の値を設定する。
SLACK_API_TOKEN
:SlackのBot APIトークンとチャンネルID、スプレッドシートのシート名を設定しておく。SLACK_CHANNEL_ID
:SlackのチャンネルIDSHEET_NAME
:スプレッドシートのシート名
ソースコード:Slackクラスの作成
この記事で紹介するコードは、Slackの処理とメイン処理を、slack.ts
とindex.ts
に分けている。
まずはSlackの処理を行うSlack
クラスをslack.ts
に作成する。
このクラスでは、次の関数を実装している。
- ファイルのアップロード用URLを取得するAPIを実行する関数
- アップロード用で取得したURLを使ってファイルをアップロードする関数
- アップロードしたファイルとSlackチャンネルを関連付けて投稿する関数
export class Slack {
private token: string;
private channelId: string;
constructor(token: string, channelId: string) {
this.token = token;
this.channelId = channelId;
}
/**
* Make a POST request
* @param url - The API endpoint URL
* @param payload - The data to send
* @returns The parsed JSON response
*/
private post(url: string, payload: object): any {
const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json",
},
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (!result.ok) {
throw new Error(`Request failed: ${result.error} - ${JSON.stringify(result)}`);
}
return result;
}
/**
* Make a form-urlencoded POST request
* @param url - The API endpoint URL
* @param payload - The form data to send
* @returns The parsed JSON response
*/
private postData(url: string, payload: object): any {
const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
},
payload: payload,
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (!result.ok) {
throw new Error(`Request failed: ${result.error}`);
}
return result;
}
/**
* Get upload URL from Slack API
* @param filename - Name of the file to upload
* @param fileSize - Size of the file in bytes
* @returns Object containing uploadUrl and fileId
*/
getUploadUrl(filename: string, fileSize: number): { uploadUrl: string; fileId: string } {
const url = `${this.endpoint}/files.getUploadURLExternal`;
const payload = {
filename: filename,
length: fileSize.toString(),
};
const result = this.postData(url, payload);
return {
uploadUrl: result.upload_url,
fileId: result.file_id,
};
}
/**
* Upload a file to the Slack URL
* @param uploadUrl - URL to upload the file to
* @param blob - File blob to upload
* @param filename - Name of the file
*/
uploadFile(
uploadUrl: string,
blob: GoogleAppsScript.Base.Blob,
filename: string
): void {
const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
payload: {
file: blob,
filename: filename,
},
muteHttpExceptions: true,
};
const response = UrlFetchApp.fetch(uploadUrl, options);
if (response.getResponseCode() !== 200) {
throw new Error(`Failed to upload file: ${response.getContentText()}`);
}
}
/**
* Complete the upload process by associating the file with a channel
* @param fileId - ID of the uploaded file
*/
completeUpload(fileId: string): void {
const url = `${this.endpoint}/files.completeUploadExternal`;
const payload = {
files: [
{
id: fileId,
title: fileId,
},
],
channels: this.channelId,
};
this.post(url, payload);
}
}
ソースコード:メイン処理
SpreadsheetのグラフをSlackにアップロードするメイン処理をindex.ts
に実装する。
import { Slack } from "./slack";
const scriptProperties = PropertiesService.getScriptProperties();
const token = scriptProperties.getProperty("SLACK_API_TOKEN");
const channelId = scriptProperties.getProperty("SLACK_CHANNEL_ID");
const sheetName = scriptProperties.getProperty("SHEET_NAME");
function main() {
if (!token || !channelId || !sheetName) {
throw new Error(
"Please set SLACK_API_TOKEN, SLACK_CHANNEL_ID, and SHEET_NAME in script properties."
);
}
try {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName(this.sheetName);
if (!sheet) {
throw new Error(`Not found sheet: ${sheetName}`);
}
const charts = sheet.getCharts();
if (charts.length === 0) {
console.log(`No charts found in the sheet "${sheetName}"`);
return;
}
const client = new Slack(token, channelId);
charts.forEach((chart, index) => {
const chartBlob = chart.getAs("image/png");
const chartName = `chart_${index + 1}_${new Date().getTime()}.png`;
const fileSize = chartBlob.getBytes().length;
const { uploadUrl, fileId } = client.getUploadUrl(chartName, fileSize);
client.uploadFile(uploadUrl, chartBlob, chartName);
client.completeUpload(fileId);
console.log(`Uploaded: ${index + 1}/${charts.length}`);
});
} catch (error) {
console.error("Failed to upload image:", error);
}
}
実行結果
プロジェクトをデプロイしmain
関数を実行すると、Slackにグラフがアップロードされる。