概要
新しいSlack Platformを使いワークフローからNotionのページを作成します。
notionのAPIを使う部分はCustom Function(カスタムステップ)を使います。
Notion APIの使い方についてはこの記事では扱いません。
https://developers.notion.com/ を参考にしてからAPI用のシークレットトークンとデータベースのIDを取得しておいてください。
はじめに
クイックスタートからサンプルのアプリ実行まで紹介しているので、一通り実行するとわかりやすいと思います。
https://api.slack.com/automation/quickstart
-
Slack CLIの導入, 承認
https://api.slack.com/automation/quickstart#install-cli
詳しくは省略します。slack auth list
でワークスペースの情報が表示されるまで確認してください。 -
スターターテンプレートと共にアプリ作成
$ slack create slack-to-notion --template https://github.com/slack-samples/deno-starter-template
作成するとfunctions, triggers, workflowsのフォルダがあると思いますが、これら3つがSlack アプリの主な構成要素となります。
各機能のざっくりとした説明は下記の通り
functions
Custom functions: https://api.slack.com/automation/functions/custom
自作のfunctionを作成する際はこちらのディレクトリの下で作成します。
Inputs(入力), outputs(出力)を定義して、SlackFunction内でやりたいことを実装します。
ワークフローのカスタムステップを作成する場合はこのCustom functionsを利用します。
triggers
https://api.slack.com/automation/triggers
ワークフローの起動方法をこちらで決めます。
サンプルはLink triggerでパプリックチャンネルからワークフローを起動します。
日付指定で実行する Scheduled triggerもあります。
workflows
https://api.slack.com/automation/workflows
functionを組み合わせて順次実行するワークフローを作成します。
カスタムステップを使う場合もこのワークフローにステップを追加します。
他の機能
datastores
https://api.slack.com/automation/datastores
アプリ内のデータを保存するデータベース。
今回は使用しません。
環境変数
https://api.slack.com/automation/environment-variables
.envがデフォルトで使えるためnotionのシークレットトークンとデータベースのIDを記述しておきます。
DATABASE_ID=XXX
NOTION_TOKEN=XXX
デプロイ
slack deploy
でデプロイ
環境変数を使っている場合下記のように追加する必要がある。
$ slack env add MY_ENV_VAR asdf1234
ローカル実行
slack run
でローカル環境の動作が確認できます。
ワークスペースを選択したら、ショートカット用のURLが表示されるので、slackチャンネルの上部の「+ 関連ページを追加する」から表示されたリンクにURLを入力します。
https://slack.com/shortcuts/XXXX
作成したもの
ワークフロー上でモーダルを表示してステータス、タイトル、概要、内容を入力します。
ボタンを押すとnotionのDBにページが作成され、成功したらSlackのチャンネルにメッセージを送信します。
実際のコード
notion_workflow.ts
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { NotionFunctionDefinition } from "../functions/notion_function.ts";
import Status from "../status.ts";
/// Workflow定義
const notionWorkflow = DefineWorkflow({
callback_id: "request_workflow",
title: "notionページ作成",
description: "notionページ作成",
input_parameters: {
properties: {
interactivity: {
type: Schema.slack.types.interactivity,
},
channel_id: {
type: Schema.slack.types.channel_id,
},
user_id: {
type: Schema.slack.types.user_id,
},
},
required: ["user_id", "interactivity"],
},
});
const inputForm = notionWorkflow.addStep(
// モーダル表示
Schema.slack.functions.OpenForm,
{
title: "notionへの投稿",
interactivity: notionWorkflow.inputs.interactivity,
submit_label: "作成",
fields: {
elements: [
{
name: "status",
title: "ステータス",
type: Schema.types.string,
enum: Object.values(Status),
choices: [
{
value: Status.New,
title: Status.New,
description: "",
},
{
value: Status.InProgress,
title: Status.InProgress,
description: "",
},
{
value: Status.Closed,
title: Status.Closed,
description: "",
},
],
},
{
name: "title",
title: "タイトル",
type: Schema.types.string,
description: "記事のタイトル",
},
{
name: "summary",
title: "概要",
type: Schema.types.string,
long: true,
description: "記事の概要",
},
{
name: "content",
title: "内容",
type: Schema.types.string,
long: true,
description: "記事の内容",
},
],
required: ["status", "title"],
},
},
);
/// notionに送信
const requestFunctionStep = notionWorkflow.addStep(NotionFunctionDefinition, {
user_id: notionWorkflow.inputs.user_id,
status: inputForm.outputs.fields.status,
title: inputForm.outputs.fields.title,
summary: inputForm.outputs.fields.summary,
content: inputForm.outputs.fields.content,
});
// Slackメッセージ送信
notionWorkflow.addStep(
Schema.slack.functions.SendMessage,
{
channel_id: notionWorkflow.inputs.channel_id,
message: requestFunctionStep.outputs.result_message,
},
);
export default notionWorkflow;
タイトルのプロパティはnotionのDBのtitleと同じプロパティ名にする必要があります
notion_trigger.ts
import { Trigger } from "deno-slack-sdk/types.ts";
import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";
import notionWorkflow from "../workflows/notion_workflow.ts";
const notionTrigger: Trigger<typeof notionWorkflow.definition> = {
type: TriggerTypes.Shortcut,
name: "notionのページ作成",
description: "notionのページ作成トリガー",
workflow: `#/workflows/${notionWorkflow.definition.callback_id}`,
inputs: {
interactivity: {
value: TriggerContextData.Shortcut.interactivity,
},
channel_id: {
value: TriggerContextData.Shortcut.channel_id,
},
user_id: {
value: TriggerContextData.Shortcut.user_id,
},
},
};
export default notionTrigger;
notion_function.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
export const NotionFunctionDefinition = DefineFunction({
callback_id: "notion_function",
title: "notionへの投稿",
description: "notionへの投稿",
source_file: "functions/notion_function.ts",
input_parameters: {
properties: {
user_id: {
type: Schema.slack.types.user_id,
description: "ユーザーID",
},
status: {
type: Schema.types.string,
description: "ステータス",
},
title: {
type: Schema.types.string,
description: "タイトル",
},
summary: {
type: Schema.types.string,
description: "概要",
long: true,
},
content: {
type: Schema.types.string,
description: "内容",
},
},
required: ["user_id", "status", "title"],
},
output_parameters: {
properties: {
notion_page_id: {
type: Schema.types.string,
description: "作成したnotionページのID",
},
result_message: {
type: Schema.types.string,
description: "結果のメッセージ",
},
},
required: ["result_message"],
},
});
function postNotion(notionToken: string, blockJson: string) {
const url = "https://api.notion.com/v1/pages";
const options = {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + notionToken,
"Notion-Version": "2022-06-28",
},
"body": blockJson,
};
return fetch(url, options);
}
/// notionページに反映する内容
interface Request {
userName: string;
status: string;
title: string;
summary?: string;
content?: string;
}
type SlackUser = {
id: string;
real_name: string;
};
type NotionResponse = {
id: string;
url: string;
};
export default SlackFunction(
NotionFunctionDefinition,
async ({ inputs, env, client }) => {
let userName: string;
// Slackユーザー情報取得
try {
const info = await client.users.info({
user: inputs.user_id,
});
if (!info.ok) {
throw new Error(info.error);
}
// Slackユーザー名設定
const user = info.user as SlackUser;
userName = user.real_name;
} catch (error) {
const resultMessage =
`Slack APIからのエラーメッセージ:\n${error.message}`;
return { outputs: { result_message: resultMessage } };
}
const request: Request = {
userName: userName,
status: inputs.status,
title: inputs.title,
summary: inputs.summary,
content: inputs.content,
};
const blockJson = createBlockObjects(env["DATABASE_ID"], request);
let resultMessage: string;
try {
// notionへの投稿
const response = await postNotion(env["NOTION_TOKEN"], blockJson);
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json() as NotionResponse;
resultMessage = `notionのページ: ${json.url}`;
} catch (error) {
resultMessage = `notionAPIからのエラーメッセージ:\n${error.message}`;
}
return {
outputs: { result_message: resultMessage },
};
},
);
function createBlockObjects(databaseId: string, request: Request): string {
const propertyBlock = (
propertyName: string,
type: string,
content: string,
) => {
return {
[propertyName]: {
[type]: [
{
"text": {
"content": content,
},
},
],
},
};
};
const objectBlock = (type: string, content: string) => {
return {
"object": "block",
"type": type,
[type]: {
"rich_text": [
{
"type": "text",
"text": {
"content": content,
},
},
],
},
};
};
const payload = {
"parent": {
"type": "database_id",
"database_id": databaseId,
},
"properties": {
...propertyBlock("タイトル", "title", request.title),
"ステータス": {
"select": {
"name": request.status,
},
},
},
"children": [
objectBlock("heading_2", "概要"),
objectBlock("paragraph", request.summary ?? ""),
objectBlock("heading_3", "内容"),
objectBlock("paragraph", request.content ?? ""),
],
};
return JSON.stringify(payload);
}
manifest.ts
import { Manifest } from "deno-slack-sdk/mod.ts";
import notionWorkflow from "./workflows/notion_workflow.ts";
export default Manifest({
name: "slack-to-notion",
description: "Slackからnotionページ作成",
icon: "assets/default_new_app_icon.png",
workflows: [notionWorkflow],
outgoingDomains: ["api.notion.com"],
botScopes: [
"commands",
"chat:write",
"chat:write.public",
"users:read",
],
});
outgoingDomainsに"api.notion.com"を指定しないとnotionのAPIにアクセスできないので注意
status.ts
enum Status {
New = "新規",
InProgress = "進行中",
Closed = "完了",
}
export default Status
動作について
モーダル
Slackメッセージ
作成したnotionページ
Custom funtionについて
上記の例ではワークフローごと作成して、最後にSlackメッセージを送信するものまで作成しましたが
Custom funtionではnotionの接続部分だけ作成することもでき、自作のワークフローに組み込むことができます。