橋本翼(ツバサムス)

メタバースプログラマー。
UnityとUnreal Engineを専門的に扱う。
メタバース系スタートアップ企業に所属。

詳細はこちら

【作品紹介(GAS)】TSUBASAMUSU-COPILOT(Slack App)

Google Apps Script

はじめに

概要

作品名TSUBASAMUSU-COPILOT
制作期間2023.07.09
(1日)
制作形式自主制作

使用動画

内容

コード

OnMentioned.gs
let promptBody = null;

let userIdBody = null;

let mentionedUserId = null;

let channelId = null;

function doPost(e) 
{
  const BOT_ID = PropertiesService.getScriptProperties().getProperty("BOT_ID");

  const MENTION_TEXT = "<@" + BOT_ID + ">";

  const PARAMS = JSON.parse(e.postData.getDataAsString());

  channelId = PARAMS.event.channel;

  mentionedUserId = PARAMS.event.user;

  setUpUserIdDocument();

  userIdBody.appendParagraph(mentionedUserId);

  const MENTIONED_MESSAGE = PARAMS.event.text;

  const PROMPT = MENTIONED_MESSAGE.replace(MENTION_TEXT,"");

  setUpPromptDocument();

  promptBody.appendParagraph(PROMPT);

  sendMessageWithButtonsToSlack();
    
  return ContentService.createTextOutput(PARAMS.challenge);
}

function setUpUserIdDocument()
{
  const DOCUMENT_ID = PropertiesService.getScriptProperties().getProperty("USER_DOCUMENT_ID");

  const DOCUMENT = DocumentApp.openById(DOCUMENT_ID);

  userIdBody = DOCUMENT.getBody();

  userIdBody.clear();
}

function setUpPromptDocument()
{
  const DOCUMENT_ID = PropertiesService.getScriptProperties().getProperty("PROMPT_DOCUMENT_ID");

  const DOCUMENT = DocumentApp.openById(DOCUMENT_ID);

  promptBody = DOCUMENT.getBody();

  promptBody.clear();
}

function sendMessageToSlack(text) 
{
  const TOKEN = PropertiesService.getScriptProperties().getProperty("TOKEN");

  const SLACK_APP = SlackApp.create(TOKEN);

  SLACK_APP.postMessage(channelId, text); 
}

function sendMessageWithButtonsToSlack() 
{
  const TOKEN = PropertiesService.getScriptProperties().getProperty("TOKEN");

	const API_URL = "https://slack.com/api/chat.postMessage";

  const MESSAGE = "<@" +  mentionedUserId + ">\n" + "そのプロンプトで生成したいものはどちらですか?";

  const BLOCKS = 
  [
    {
      "type": "section",
      "text": 
      {
        "type": "mrkdwn",
        "text": MESSAGE
      }
    },
    {
      "type": "actions",
      "elements": 
      [
        {
          "type": "button",
          "text": 
          {
            "type": "plain_text",
            "text": "文章"
          },
          "style": "primary",
          "value": "text"
        },
        {
          "type": "button",
          "text": 
          {
            "type": "plain_text",
            "text": "画像"
          },
          "style": "danger",
          "value": "image"
        }
      ]
    }
  ];

  const PAYLOAD = 
  {
    token: TOKEN,
    channel: channelId,
    text: MESSAGE,
    blocks: BLOCKS
  };

  const HEADERS = 
  {
    "Authorization": "Bearer " + TOKEN
  };

  const OPTIONS = 
  {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(PAYLOAD),
    headers: HEADERS
  };

  UrlFetchApp.fetch(API_URL, OPTIONS);
}
OnButtonClicked.gs
let channelId = null;

function doPost(e) 
{
  const PAYLOAD = JSON.parse(e.parameter.payload);

  channelId = PAYLOAD.channel.id;

  deleteSlackMessage(PAYLOAD);

  const VALUE = PAYLOAD.actions[0].value;

  const PROMPT = getPromptFromDocument();

  switch(VALUE)
  {
    case "text":
      const MESSAGE = getMentionText() + "\n" + getChatGptAnswer(PROMPT);
      sendMessageToSlack(MESSAGE);
      break;

    case "image":
      const IMAGE_FILE_URL = getDalleAnswer(PROMPT);
      sendFileToSlack(IMAGE_FILE_URL);
      break;
  }
}

function deleteSlackMessage(payload) 
{
  const TOKEN =  PropertiesService.getScriptProperties().getProperty("TOKEN");

  const API_URL = "https://slack.com/api/chat.delete";
  
  const PAYLOAD = 
  {
    token: TOKEN,
    channel: channelId,
    ts: payload.message.ts
  };

  const OPTIONS = 
  {
    method: "post",
    payload: PAYLOAD
  };
    
  UrlFetchApp.fetch(API_URL, OPTIONS);
}

function getPromptFromDocument()
{
  const DOCUMENT_ID = PropertiesService.getScriptProperties().getProperty("PROMPT_DOCUMENT_ID");

  const DOCUMENT = DocumentApp.openById(DOCUMENT_ID);

  const BODY = DOCUMENT.getBody();

  return BODY.getText();
}

function getMentionText()
{
  const DOCUMENT_ID = PropertiesService.getScriptProperties().getProperty("USER_DOCUMENT_ID");

  const DOCUMENT = DocumentApp.openById(DOCUMENT_ID);

  const BODY = DOCUMENT.getBody();

  const MENTIONED_USER_ID = BODY.getText().trim();

  return "<@" + MENTIONED_USER_ID + ">";
}

function getChatGptAnswer(prompt)
{
  const API_KEY = PropertiesService.getScriptProperties().getProperty("OPEN_AI_API_KEY");

  const API_URL = 'https://api.openai.com/v1/chat/completions';

  const MESSAGE =
  {
    role: "user",
    content: prompt
  };

  const MESSAGES = [MESSAGE];

  const PAYLOAD = 
  {
    model: "gpt-3.5-turbo",
    temperature: 0.5,
    max_tokens: 1000,
    messages: MESSAGES
  }

  const HEADERS = 
  {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + API_KEY
  };

  const REQUEST = 
  {
    method: "POST",
    muteHttpExceptions : true,
    headers: HEADERS,
    payload: JSON.stringify(PAYLOAD)
  }

  try 
  {
    const RESPONSE = JSON.parse(UrlFetchApp.fetch(API_URL, REQUEST).getContentText());

    return RESPONSE.choices[0].message.content;
  }
  catch(e)
  {
    return e.toString();
  }
}

function getDalleAnswer(prompt) 
{
  const API_KEY = PropertiesService.getScriptProperties().getProperty("OPEN_AI_API_KEY");

  const API_URL = 'https://api.openai.com/v1/images/generations';

  const PAYLOAD =
  {
    n: 1,
    size : "256x256",
    prompt: prompt
  };
  
  const HEADERS = 
  {
    "Authorization":"Bearer " + API_KEY,
    "Content-type": "application/json",
    "X-Slack-No-Retry": 1
  };

  const OPTIONS = 
  {
    muteHttpExceptions : true,
    headers: HEADERS, 
    method: "POST",
    payload: JSON.stringify(PAYLOAD)
  };

  try
  {
    const RESPONSE = JSON.parse(UrlFetchApp.fetch(API_URL, OPTIONS).getContentText());

    return RESPONSE.data[0].url;
  }
  catch(e)
  {
    const ERROR_MESSAGE = e.toString();

    sendMessageToSlack(ERROR_MESSAGE);

    return null;
  }
}

function sendMessageToSlack(text) 
{
  const TOKEN = PropertiesService.getScriptProperties().getProperty("TOKEN");

  const SLACK_APP = SlackApp.create(TOKEN);

  SLACK_APP.postMessage(channelId, text);
}

function sendFileToSlack(fileUrl)
{
  const TOKEN = PropertiesService.getScriptProperties().getProperty("TOKEN");

  const API_URL = "https://slack.com/api/files.upload";

  let file = null;

  try
  {
    file = UrlFetchApp.fetch(fileUrl).getBlob();
  }
  catch(e)
  {
    const MESSAGE = getMentionText() + "\n画像ファイルの取得に失敗しました。";

    sendMessageToSlack(MESSAGE);

    return;
  }

  const PAYLOAD =
  {
    file: file,
    channels: channelId,
    initial_comment: getMentionText(),
    title: getPromptFromDocument()
  };

  const HEADERS =
  {
    "Authorization": "Bearer " + TOKEN
  };

  const OPTIONS =
  {
    method: "post",
    payload: PAYLOAD,
    headers: HEADERS
  };

  UrlFetchApp.fetch(API_URL,OPTIONS);
}

出来るようになった事

  • PropertiesService.getScriptProperties().getProperty()」を使用した、スクリプトプロパティの値の取得
  • e.postData.getDataAsString()」を使用した、ポストデータの文字列での取得
  • Body.appendParagraph()」を使用した、「Google ドキュメント」へのテキストの追加
  • string.replace()」を使用した、文字列の置換
  • ContentService.createTextOutput()」を使用した、プロジェクトの呼び出し側への返信
  • DocumentApp.openById()」を使用した、IDによる「Google ドキュメント」の取得
  • Document.getBody()」を使用した、「Body」クラスの取得
  • Body.clear()」を使用した、「Google ドキュメント」内のテキストの削除
  • SlackApp.create()」を使用した、ボット(App)の作成
  • SlackApp.postMessage()」を使用した、「Slack」へのメッセージの送信
  • Body.getText()」を使用した、ドキュメント内テキストの取得
  • try-catch」を使用した、例外が発生した際の処理の記述
  • UrlFetchApp.fetch().getBlob()」を使用した、URLからのファイルの取得

工夫した点

  • メンションしたユーザーのIDや、ユーザーが送信したプロンプト記憶しておく為に、「Google ドキュメント」にそれらの情報を記録した
  • リテラル表記を避ける為にスクリプトプロパティを使用した
  • 間違えて複数回、ボタンを押してしまう事の無いように、ボタンを押された直後にボタンを含むメッセージを削除するようにした
  • 生成した画像をURLで渡すのではなく、画像ファイルに変換して添付する事で、ユーザーがすぐにその画像を使用できるようにした
  • ボット(App)からメッセージを送信する際に、送信相手に通知が届くように送信相手をメンションする仕様にした

お問い合わせ

    コメント

    タイトルとURLをコピーしました