Slack API の予約投稿機能とスプレッドシートを駆使して定期連絡を自動化した

Dec 1st, 2023 lifehack node.js

この記事はジーズアカデミー 技術記事書いてみた編 Advent Calendar 2023の1日めです.

概要

定期投稿の多い Slack の連絡を自動化した.自動化には Slack API とスプレッドシートを用い,Node.js で作成したアプリケーションを定期的に動作させて実現した.

本記事では実現した内容と解決策の考え方,実装したコードの一部を紹介する.

背景

筆者はプログラミング講師を務めており,各講義の前後で受講生に連絡を行うことが多い.連絡には Slack を使用しており,講義後の補足や次回講義の案内などを投稿している.

講義は半年1セットで行うため,Slack に投稿する内容は各セットで同じ内容となる.文面を毎回コピペする手間と Slack のデータ保存期限(無料だと90日)を鑑み,自動化に踏み切った次第である.

使用した技術と考え方

使用技術は以下の通り.

大まかな流れは以下の通りである.

  1. あらかじめ Slack のアプリを作成しておき,アプリ経由で投稿ができるよう token などを用意しておく.
  2. 投稿する内容はスプレッドシートで管理する.
  3. スプレッドには投稿内容と投稿日時を用意しておき,Google の API を用いて Node.js でデータを取得する.
  4. Node.js は毎日 1:00 に動作するよう設定し,取得した内容(投稿内容や日時)を走査して該当日の投稿のみを抽出する.
  5. 3 で抽出したデータに含まれている投稿日時で Slack に予約投稿する.予約投稿は Slack API に機能がある.
  6. 設定された時間に Slack に自動的に投稿される.

指定した時間に自動投稿を行うためには様々な方法があるが,Node.js を動かす回数(今回は 1 回 / 日)などを勘案すると予約投稿が最適という結論に達した.

以下,流れに沿って詳細を解説する.Slack API を試すときには,まず開発用の Workspace を用意して実験すると良いだろう.

Slack アプリの設定

アプリの作成は下記の UR Lから行える.

https://api.slack.com/apps

様々な設定項目があるが,自動投稿する程度であれば権限と token の発行だけでOK.設定は「OAuth & Permissions」部分から行う.

自動投稿を行う Bot に必要となる token には「User OAuth Token(ユーザ自身が自動投稿を行うパターン)」と「Bot User OAuth Token(設定した Bot が自動投稿を行うパターン)」が存在するが,オススメは前者である.

今回の場合,「自動投稿(講義の連絡など)の通知は不要」「自動投稿に対する返信は通知要」であるため,自分が投稿した扱いになる方が都合が良い.

あとは OAuth & Permissions 内の「Scopes」項目を「chat:write」に設定しておけば自動で投稿ができるようになる.設定が完了したら,Slack 側の各チャンネル詳細「Integrations」→「Add an App」で作成したアプリをインストールする(チャンネルごとにインストールが必要).

スプレッドシートでのデータ管理

スプレッドシートには下記のようなデータを記載している.token は上記で取得したもの,channel は自動投稿先のチャンネルである.

token year month day hour minute seconds channel text
xoxp-99999999hogefygapiyo 2023 12 01 19 00 0 C01HOGEFUGA 明日の講義は準備必須!
xoxp-99999999hogefygapiyo 2023 12 24 17 30 0 C01FOOBAR 本日課題の締切!

工夫した点としては以下.

データ取得の処理(Node.js)

データを取得する部分はスプレッドシートの API を使用している.範囲を設定してデータを取得し,空のデータや欠損データを除外して次のステップで使用する形に整形する.予約投稿時には「投稿するチャンネル」「投稿するテキスト」「日時」が必要なのでこれに合わせてデータを作成することで実装が容易になる.

ドキュメントに従って実装すれば特にハマる部分はない.スプレッドシートからデータの取得さえできればあとは自分の望む形にデータを整形すれば良いだけである.

import dotenv from 'dotenv';
dotenv.config();

import { google, } from 'googleapis';

const sheets = google.sheets('v4');

export const execAPI = async (spreadsheetId, range) => {
  const auth = await google.auth.getClient({
    scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
  });

  const apiOptions = {
    auth,
    spreadsheetId,
    range,
  };

  const result = await sheets.spreadsheets.values.get(apiOptions);
  return result.data.values;

}

// 空文字含む行を判定
const hasEmpty = (array) => array.every((x) => x != '');

// カラム数が足りない行を判定
const hasColumn = (array, initialArray) => array.length === initialArray.length;

// 必要な行のみ抽出して整形
export const createFantasticData = (rawData) => rawData
  .filter((x, i, arr) => i !== 0 && hasEmpty(x) && hasColumn(x, arr[0]))
  .map((x, i) => Object.fromEntries(x.map((x, i) => [rawData[0][i], ['year', 'month', 'day', 'hour', 'minute', 'seconds'].includes(rawData[0][i]) ? Number(x) : x])));

export const getSheetData = async () => createFantasticData(await execAPI(process.env.SPREADSHEET_ID, 'data'));

自動投稿の処理

前項で整形したデータを作成したので,自動投稿の処理を行う.自動投稿自体は API のドキュメントに記載されているので難しくはない.

今回は本番環境で Google Cloud Run を使用したのだが,サーバ側の時間が日本時間ではなくグリニッジ標準時であった.そのため,はじめに動かしたときは当日のデータでも投稿されるものと投稿されないものが存在し,謎挙動を示した.したがって,実行時に本番環境の場合は時間を調整することで日本時間にあったものが漏れなく投稿されるよう実装を修正した.

Google Cloud Run は今回初採用で仕様がよくわかっていなかったこともあるが,サーバは何でも良いと思うし時間の設定もできると思うので慣れているものを選ぶのがオススメ.

import { WebClient, } from '@slack/web-api';
import dotenv from 'dotenv';
import { getUnixTime, format, addHours } from 'date-fns';
import { formatToTimeZone } from 'date-fns-timezone';
import { getSheetData } from '../repositories/spreadsheet.repository.js';

dotenv.config();

// メッセージ予約投稿する関数
const postToSlackScheduled = async (token, post_at, channel, text) => {
  const client = new WebClient(token);
  try {
    const result = await client.chat.scheduleMessage({ channel, text, post_at, });
    return result;
  }
  catch (error) {
    return error;
  }
}

// スケジュールを入力して本日のものだけ出力する関数
const getTodaySchedules = (schedules) => {
  const todayObject = Object.fromEntries(format(process.env.DEPLOY === 'production' ? addHours(new Date(), 9) : new Date(), 'yyyy-M-d').split('-').map((x, i) => [['year', 'month', 'day'][i], Number(x)]));
  return schedules.filter((x) => [x.year === todayObject.year, x.month === todayObject.month, x.day === todayObject.day].every(x => x));
}

// スケジュールを入力すると本日のものだけ送信する関数
const scheduleAll = (schedules) => {
  const todaysSchedules = getTodaySchedules(schedules);
  todaysSchedules.forEach((x) => postToSlackScheduled(x.token, process.env.DEPLOY === 'production' ? getUnixTime((new Date(x.year, x.month - 1, x.day, x.hour, x.minute, x.seconds))) - (60 * 60 * 9) : getUnixTime((new Date(x.year, x.month - 1, x.day, x.hour, x.minute, x.seconds))), x.channel, x.text));
  return
}

// 予約投稿処理
export const postScheduled = async () => {
  try {
    const schedules = await getSheetData();
    return scheduleAll(schedules);
  } catch (e) {
    throw Error('Error while posting message');
  }
};

スプレッドシートと Slack API 双方を実装したら全体をまとめて cron などで動作させる.毎日決まった時間に動いてくれれば OK なのでやり方は何でも良いと思う.

まとめ

今回はスプレッドシートと Slack API の予約投稿機能を用いて定期投稿の自動化を行った.スプレッドシートを用いることで投稿内容の管理・編集を行いやすくし,投稿内容以外にも token や日時の設定もスプレッドシートで完結できるよう工夫した.定期投稿はすべて自動化され,クラス開講時に講義日程の設定や Slack の token 取得などの設定を行うだけである.筆者の連絡漏れもゼロとなり「連絡しなければならない」精神的負担も皆無となった.

また,自動投稿を実現するに際し,どのようなやり方を採用するべきか決めるまでに時間がかかった.Slack の API の仕様を確認し,予約投稿の機能を見出したことで今回の方法で実現する足がかりを掴むことができた.何かをハックしようと試みる際には,仕様をよく確認することがとても大切である.

今後も自身が行う業務は自動化を進め,新しい技術のインプットやアウトプットに時間を割けるよう業務改善に努めたい.

以上だ( `・ω・)b