Slackの投稿1週間分を要約・メンションで知らせてもらう

Google Apps Script（GAS）を使用して、Slackの特定のチャンネルから1週間分の情報を取得し、それをGoogleスプレッドシートにまとめ、各ユーザーごとに1週間の発言内容の要約をGPT APIで行った後各ユーザーにリプライするスクリプトです。

【用意するもの】
・Slack APIのアクセストークン
・SlackのチャンネルID
・GPT APIのAPIキー
・google Spreadsheet

GASのメニュー「プロジェクトの設定」→「スクリプト プロパティ」にアクセストークンやAPIキーを保管しておきます。
・GPT_APIKEY
・SLACK_TOKEN

初期設定
~~~javascript
var slackToken = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN');
var apiKey = PropertiesService.getScriptProperties().getProperty('GPT_APIKEY');
//Slackのチャンネル指定
var CHANNEL_ID = 'CHANNELID'
~~~

SlackのAPIにアクセスする
~~~javascript
function callSlackAPI(path, params) {

  // メッセージ取得系のSlackAPIの制限が1分間で50回までなので、超えないようにスリープする
  Utilities.sleep(1100);

  if (params === void 0) { params = {}; }
  var url = "https://slack.com/api/" + path + "?";
  var qparams = [];
  for (var k in params) {
      qparams.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
  }
  url += qparams.join('&');
  console.log(url);
  var headers = {
    'Authorization': 'Bearer '+ slackToken
  };
  var options = {
    'headers': headers
  };
  var resp = UrlFetchApp.fetch(url, options);
  var data = JSON.parse(resp.getContentText());
  if (data.error) {
    throw "Error " + path + ": " + data.error;
  }
  return data;
}
~~~
メッセージの取得
~~~javascript
function getMessages(ch_id, timestamp_from, timestamp_to, messages, next_cursor) {
  if (!messages) {
    messages = [];
  }

  var param = [];
  param['limit'] = 1000;
  param['channel'] = ch_id;
  param['oldest'] = timestamp_from;
  param['latest'] = timestamp_to;
  if (next_cursor) {
    param['cursor'] = next_cursor;
  }

  var resp = callSlackAPI('conversations.history', param);
  messages = messages.concat(resp.messages);
  if (resp.response_metadata && resp.response_metadata.next_cursor) {
    messages = getMessages(ch_id, timestamp_from, timestamp_to, messages, resp.response_metadata.next_cursor);
  }
  return messages;
}
~~~
リプライ・スレッドの取得
~~~javascript
function getReplies(ch_id, thread_ts, timestamp_from, timestamp_to, messages, next_cursor) {
  if (!messages) {
    messages = [];
  }

  var param = [];
  param['limit'] = 1000;
  param['channel'] = ch_id;
  param['ts'] = thread_ts;
  param['oldest'] = timestamp_from;
  param['latest'] = timestamp_to;
  if (next_cursor) {
    param['cursor'] = next_cursor;
  }

  var resp = callSlackAPI('conversations.replies', param);
  messages = messages.concat(resp.messages);
  if (resp.response_metadata && resp.response_metadata.next_cursor) {
    messages = getReplies(ch_id, thread_ts, timestamp_from, timestamp_to, messages, resp.response_metadata.next_cursor);
  }
  return messages;
}
~~~

タイムスタンプの計算用
~~~javascript
function getTimestamp(date) {
  var ms = new Date(date).getTime();
  var s = ms / 1000; // ms→sに変換
  return s;
}
~~~

メッセージをスプシに集約
タイムスタンプとユーザーが同じならば記録しない
~~~javascript
function messageAnalysis(message) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var timestamp = new Date(parseFloat(message.ts) * 1000);
  var userId = message.user;
  var text = message.text;

  // タイムスタンプとユーザーIDで重複をフィルター
  var range = sheet.getDataRange();
  var values = range.getValues();
  var isDuplicate = values.some(function(row) {
    var existingTimestamp = row[0];
    var existingUserId = row[1];
    return existingTimestamp.getTime() === timestamp.getTime() && existingUserId === userId;
  });

  // 重複がない場合のみ新しい行を追加
  if (!isDuplicate) {
    sheet.appendRow([timestamp, userId, text]);
  }
}
~~~
1日分のログを取得
~~~javascript
function run() {
  var now = new Date();
  var oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);

  var timestamp_to = getTimestamp(now);
  var timestamp_from = getTimestamp(oneDayAgo);

  var messages = getMessages(CHANNEL_ID, timestamp_from, timestamp_to);

  messages.forEach(function(message) {
    // スレッドのあるメッセージの場合
    if (message.thread_ts) {
      if (message.subtype == 'thread_broadcast') {
        return;
      }
      var thread_messages = getReplies(CHANNEL_ID, message.thread_ts, timestamp_from, timestamp_to);
      thread_messages.forEach(function(thread_message) {
        messageAnalysis(thread_message);
      });
    } else {
      messageAnalysis(message);
    }
  });
}

~~~

GTP APIを使用してログの要約をする
~~~javascript
function getSummaryFromGPT(messages) {
  var apiKey = PropertiesService.getScriptProperties().getProperty('GPT_APIKEY');
  var apiEndpoint = 'https://api.openai.com/v1/chat/completions';

  // プロンプトを作成
  var formattedMessages = messages.map(function(message) {
    return {
      role: "user",
      content: message
    };
  });

  formattedMessages.push({
    role: "system",
    content: "以下のメッセージを要約してユーザーの動向を教えてください。約束事が含まれていればリマインドをお願いします。"
  });


  var payload = {
    model: "gpt-3.5-turbo-1106",
    messages: formattedMessages,
    max_tokens: 2048,
    temperature: 0.9
  };

  var options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + apiKey
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  try {
    var response = UrlFetchApp.fetch(apiEndpoint, options);
    var jsonResponse = JSON.parse(response.getContentText());

    if (jsonResponse.choices && jsonResponse.choices.length > 0) {
      return jsonResponse.choices[0].message.content.trim();
    } else {
      Logger.log('No valid response or empty choices array.');
      return '';
    }
  } catch (error) {
    Logger.log('Error fetching summary: ' + error);
    return '';
  }
}
~~~


Slackにメッセージを送る
~~~javascript
function sendSlackMessage(channelId, userId, message, token) {
  var apiUrl = 'https://slack.com/api/chat.postMessage';
  var payload = {
    channel: channelId,
    text: `<@${userId}>: ${message}`,
    as_user: true
  };

  var options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + token
    },
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch(apiUrl, options);
}
~~~

1週間分のログを集約
~~~javascript
function summarizeWeeklyLogs() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var oneWeekAgo = new Date();
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

  var range = sheet.getDataRange();
  var data = range.getValues();
  var userMessages = {};

  data.forEach(function(row) {
    var timestamp = row[0];
    var userId = row[1];
    var text = row[2];

    if (timestamp >= oneWeekAgo) {
      if (!userMessages[userId]) {
        userMessages[userId] = [];
      }
      userMessages[userId].push(text);
    }
  });
  for (var userId in userMessages) {
    var messages = userMessages[userId];
    var summary = getSummaryFromGPT(userMessages[userId]);

    // Slackチャンネルに要約をリプライ
    sendSlackMessage(CHANNEL_ID, userId, summary, slackToken);
  }
}
~~~

ログは一定期間経つと消えたほうが良いかも

実行
GASのトリガー設定でrun()を1日に一回実行
summarizeWeeklyLogs()を1週間に一回実行