GraphQLガイド - ファイルアップロード

GraphQLでファイルアップロードを実装する場合、いくつかの方法があります。GraphQL自体はバイナリデータを直接扱う仕様にはなっていませんが、工夫次第で実現可能です。

この記事では、GraphQLでのファイルアップロード実装について詳しく解説し、適切な方法を選択できるようにします。

バイナリデータの取り扱いについて

GraphQLの標準プロトコルはJSONのみをサポートしており、JSONはバイナリデータを直接扱えません。しかし、工夫次第でバイナリデータの送受信は可能です。主な方法は以下の2つです。

1. Base64エンコードで送信

  • バイナリデータをBase64文字列に変換し、GraphQLのString型で送受信します
  • 小さなファイルや簡易的な用途には使えますが、ファイルサイズが大きいと効率が悪くなります
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// クライアント側
const file = document.getElementById('fileInput').files[0];
const reader = new FileReader();
reader.onload = function() {
const base64 = reader.result.split(',')[1]; // data:image/jpeg;base64, の部分を除去

const mutation = `
mutation UploadFile($file: String!) {
uploadFile(file: $file) {
id
url
filename
}
}
`;

// GraphQLリクエストを送信
fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: mutation,
variables: { file: base64 }
})
});
};
reader.readAsDataURL(file);

2. GraphQL multipart request specification(ファイルアップロード)

  • Apollo Serverやgraphql-uploadなどのライブラリを使うことで、multipart/form-data形式でファイルアップロードが可能です
  • これはHTTPの拡張で、GraphQLのmutationと一緒にバイナリファイルを送信できます
  • サーバー側でgraphql-uploadなどのミドルウェアが必要です
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// クライアント側
const file = document.getElementById('fileInput').files[0];
const formData = new FormData();

formData.append('operations', JSON.stringify({
query: `
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
id
url
filename
}
}
`,
variables: { file: null }
}));

formData.append('map', JSON.stringify({
"0": ["variables.file"]
}));

formData.append('0', file);

fetch('/graphql', {
method: 'POST',
body: formData
});

multipart/form-dataとは

multipart/form-dataは、HTTPリクエストでテキストデータとバイナリデータ(ファイル)を同時に送信するためのMIMEタイプです。HTMLの<form>要素でファイルアップロードを行う際に使用されます。

基本的な構造

1. Content-Typeヘッダー

1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
  • boundaryは、データの境界を区切る文字列です
  • ブラウザやクライアントが自動生成します

2. リクエストボディの構造

1
2
3
4
5
6
7
8
9
10
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="text_field"

Hello World
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg

[バイナリデータ]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

3. 実際のHTTPリクエスト例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="operations"

{"query":"mutation UploadFile($file: Upload!) { uploadFile(file: $file) { id url filename } }","variables":{"file":null}}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="map"

{"0":["variables.file"]}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="0"; filename="document.pdf"
Content-Type: application/pdf

[PDFファイルのバイナリデータ]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

4. GraphQL multipart request specificationの構造

  1. operations: GraphQLクエリのJSON
  2. map: ファイルとGraphQL変数のマッピング
  3. 0, 1, 2…: 実際のファイルデータ

Base64エンコードとバイナリの比較

項目 Base64エンコード バイナリ(multipart/form-data)
データサイズ 元サイズ + 33% 元サイズのまま
メモリ使用量 約33%増加 元サイズのまま
転送時間 約33%増加 元サイズのまま
CPU使用量 エンコード/デコード処理が必要 処理不要
実装の複雑さ 簡単 やや複雑
適している用途 小さなデータ(QRコード、アイコン) 大きなファイル(画像、動画、PDF)

セキュリティ

ファイルアップロード処理における、基本的なセキュリティ対策を紹介します。

1. ファイルサイズの制限

サーバー側でファイルサイズを検証します。

2. ファイルタイプの検証

ホワイトリスト方式で .jpg, .png, .pdf のように明示的に許可された拡張子のみ受付します。
また、クライアントから送られたContent-Typeではなく、サーバー側でMIMEタイプを再検査します。

3. ファイル名のリネーム

アップロードされたファイルをランダム名に変えて保存(例:upload_48cd1e2a.jpg)します。

まとめ

GraphQLでのファイルアップロードは、Base64エンコード方式とmultipart/form-data方式の2つのアプローチがあります。

  • Base64エンコード: 小さなファイルや簡易的な用途に適している
  • multipart/form-data: 大きなファイルや本格的なファイルアップロード機能に適している

用途に応じて適切な方法を選択し、ファイルサイズ制限やファイルタイプ検証などのセキュリティ対策も忘れずに実装することが重要です。

GraphQLガイド - GraphQLのプロトコルと特徴

GraphQLは、Facebook(現Meta)が2012年に開発し、2015年に公開したクエリ言語およびAPI仕様です。REST APIの代替として設計され、クライアントがサーバーから必要なデータを正確に取得できるようにすることを目的としています。

この記事では、GraphQLのプロトコルと特徴について解説し、理解を深めていきます。

GraphQLとは

GraphQLは、クライアントがサーバーに対して必要なデータを正確に指定して取得できるクエリ言語です。従来のREST APIでは、サーバーが決めたエンドポイントから固定のデータ構造を取得する必要がありましたが、GraphQLではクライアントが自由にデータの形を指定できます。

主な特徴

1. 単一エンドポイント

GraphQLでは通常、/graphqlのような単一のエンドポイントを使用します。すべてのクエリ、ミューテーション、サブスクリプションがこのエンドポイントを通じて処理されます。

1
2
// すべての操作が同じエンドポイントを使用
POST /graphql

2. クエリ言語

GraphQLは独自のクエリ言語を提供し、クライアントが必要なデータを正確に指定できます。

1
2
3
4
5
6
7
8
9
10
query {
user(id: "123") {
name
email
posts {
title
content
}
}
}

3. 型システム

GraphQLは強力な型システムを持ち、APIの仕様が明確になります。また、イントロスペクション機能により、スキーマ情報を動的に取得できます。

4. リアルタイム通信

Subscriptions機能により、WebSocketを使用したリアルタイム通信をサポートしています。

プロトコルの詳細

HTTPプロトコルでの実装

GraphQLは主にHTTPプロトコル上で動作します。リクエストボディはJSONです。
以下が基本的なリクエスト形式です。

1
2
3
4
5
6
7
8
POST /graphql
Content-Type: application/json

{
"query": "query { user(id: \"123\") { name email } }",
"variables": {},
"operationName": "GetUser"
}

レスポンス形式

レスポンス形式はJSONです。

1
2
3
4
5
6
7
8
9
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com"
}
},
"errors": null
}

WebSocketプロトコルでの実装

リアルタイム通信が必要な場合は、WebSocketプロトコルを使用します。

接続確立

1
2
3
4
5
GET /graphql HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <key>
Sec-WebSocket-Protocol: graphql-ws

サブスクリプション例

1
2
3
4
5
6
7
subscription {
userUpdated(userId: "123") {
id
name
email
}
}

GraphQLプロトコルレイヤー

GraphQLは以下のプロトコルレイヤーで動作します。

  1. HTTP/HTTPS: 主なトランスポートプロトコル
  2. WebSocket: Subscriptions用のリアルタイム通信
  3. GraphQL: アプリケーションレベルのクエリ言語

プロトコルスタック

1
2
3
4
5
6
7
┌─────────────────┐
│ GraphQL │ ← クエリ言語
├─────────────────┤
│ HTTP/HTTPS │ ← トランスポート
├─────────────────┤
│ TCP/IP │ ← ネットワーク
└─────────────────┘

実装例

基本的なクエリ

データの取得にはクエリを利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// リクエスト
{
"query": `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`,
"variables": {
"id": "123"
}
}

ミューテーション

データの作成、更新にはミューテーションを利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// リクエスト
{
"query": `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`,
"variables": {
"input": {
"name": "Jane Doe",
"email": "jane@example.com"
}
}
}

サブスクリプション

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WebSocket接続後のメッセージ
{
"type": "start",
"id": "1",
"payload": {
"query": `
subscription {
userUpdated {
id
name
email
}
}
`
}
}

エラーハンドリング

GraphQLでは、エラーが発生した場合でも部分的なデータを返すことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"data": {
"user": {
"name": "John Doe",
"email": null // エラーにより取得できなかった
}
},
"errors": [
{
"message": "Cannot return null for non-nullable field User.email",
"locations": [
{
"line": 3,
"column": 7
}
],
"path": ["user", "email"]
}
]
}

パフォーマンス最適化

1. クエリの最適化

必要なフィールドのみを指定することで、ネットワーク転送量を削減できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 良い例:必要なフィールドのみ
query {
user(id: "123") {
name
email
}
}

# 悪い例:不要なフィールドも含む
query {
user(id: "123") {
name
email
posts {
title
content
comments {
text
author {
name
}
}
}
}
}

2. バッチ処理

複数のクエリを一度に実行することで、ネットワークリクエスト数を削減できます。

1
2
3
4
5
6
7
8
9
{
"query": `
query {
user1: user(id: "1") { name email }
user2: user(id: "2") { name email }
user3: user(id: "3") { name email }
}
`
}

セキュリティ

1. 認証・認可

Authorizationヘッダーに認証・認可トークンを付与します。

1
2
3
4
5
6
7
8
// ヘッダーにトークンを含める
{
"query": "...",
"variables": {},
"headers": {
"Authorization": "Bearer <token>"
}
}

2. クエリの複雑度制限

悪意のあるクエリによる攻撃を防ぐため、クエリの複雑度を制限することもできます。

1
2
3
4
5
6
7
// サーバー側での実装例
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(7)]
});

まとめ

GraphQLは、モダンなWebアプリケーション開発において、REST APIの代替として広く採用されており、REST APIと比較して通信量の削減が見込めます。
今後もGraphQLの普及は進むことが予想され、API設計の標準的なアプローチの一つとして確立されていくと思われます。

Gitで日本語ファイル名が正しく表示されない問題の解決方法

はじめに

Gitで日本語ファイル名が正しく表示されず、エンコードされた文字列として表示されることがあります。
エンコードされた文字は一見何が書いてあるかはわからないし、ものすごく表示の幅をとるのでなんとかしたい。

問題の原因

Gitはデフォルトで、非ASCII文字(日本語など)を含むファイル名を引用符で囲んで表示したり、適切なエンコーディングで処理しない場合があります。これにより、日本語ファイル名が正しく表示されない問題が発生します。

解決方法

日本語ファイル名を正しく表示するための設定

以下の3つの設定を変更することで、日本語ファイル名が正しく表示されるようになります。

1
2
3
4
5
6
7
8
# 日本語などの非ASCII文字を含むファイル名を引用符で囲まずに表示
git config --global core.quotepath false

# Gitのログ出力をUTF-8エンコーディングで表示
git config --global i18n.logoutputencoding utf-8

# コミットメッセージをUTF-8エンコーディングで処理
git config --global i18n.commitencoding utf-8

設定を変更した後、git status コマンドなどで日本語ファイルを表示してみると、正しく表示されるはずです。

設定内容の詳細説明

設定した内容は以下の通りです。

core.quotepath false

  • 目的: 日本語などの非ASCII文字を含むファイル名を引用符で囲まずに表示
  • 効果: ファイル名が "日本語ファイル名.txt" ではなく 日本語ファイル名.txt として表示される

i18n.logoutputencoding utf-8

  • 目的: Gitのログ出力をUTF-8エンコーディングで表示
  • 効果: git loggit status などの出力で日本語が正しく表示される

i18n.commitencoding utf-8

  • 目的: コミットメッセージをUTF-8エンコーディングで処理
  • 効果: 日本語のコミットメッセージが正しく保存・表示される(この設定は直接は関係ないが、設定しておいてよいと思う)

Slack Block Kitの制約とデザインパターンガイド

はじめに

前回の記事「GoでSlack通知を実装する方法」では、Slack通知の基本的な実装方法とBlock Kitの初歩的な使い方について解説しました。

今回は、Slack Block Kitをより深く掘り下げて、その特性、制約、そして効果的なデザインパターンについて詳しく説明します。特に、Block Kitの「見た目があまり変えられない」という特性と、その制約の中でいかに美しく機能的なメッセージを作成するかに焦点を当てます。

Slack Block Kitの特性と制約

1. デザインの統一性と制約

Slack Block Kitの最も大きな特徴は、デザインの自由度が意図的に制限されていることです。これにはいくつかの理由があります。

  • 一貫性の確保: すべてのアプリからのメッセージが統一された見た目になる
  • 可読性の向上: Slackの標準UIパターンに従うことで、ユーザーが迷わない
  • アクセシビリティ: スクリーンリーダーやキーボードナビゲーションに配慮

2. 制約の具体例

以下のような点でカスタマイズが制限されています。

  • 色の変更: テキストやブロックの背景色は基本的に変更不可
  • フォントサイズ: 固定のフォントサイズ体系
  • 余白・レイアウト: ブロック間の余白やレイアウトは Slack側で制御
  • アニメーション: 動的な効果は実装不可

これらの制約があるからこそ、コンテンツの構成とブロックの組み合わせが重要になります。

Block Kitの主要ブロックタイプ解説

実際のサンプルコードを基に、各ブロックタイプの特性と使用例を詳しく見てみましょう。

1. Header ブロック

1
2
3
4
5
6
7
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Slack Block Kitデザインシステム見本"
}
}

特徴:

  • メッセージの最上部に配置される大きなタイトル
  • plain_textのみ対応(Markdownは使用不可)
  • 絵文字を使用することで視覚的なアクセントを追加可能

使用場面:

  • 通知のタイトル

2. Section ブロック

1
2
3
4
5
6
7
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*1. 基本的なセクション*\nこれは基本的なセクションブロックです。*太字*や_斜体_、~取り消し線~、`コード`などのMarkdown書式が使えます。"
}
}

特徴:

  • 最も汎用的で使用頻度が高いブロック
  • Markdownによる豊富なテキスト装飾
  • accessoryフィールドで画像やボタンを右側に配置可能

応用例 - 画像付きセクション:

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "画像を右側に配置できます"
},
"accessory": {
"type": "image",
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png",
"alt_text": "かわいい犬の画像"
}
}

3. Context ブロック

1
2
3
4
5
6
7
8
9
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "👆 Contextブロックは小さいテキストで補足情報を表示するのに最適です"
}
]
}

特徴:

  • 小さなフォントサイズで表示
  • 補足情報や注釈に最適
  • 複数の要素を横並びで配置可能

使用場面:

  • タイムスタンプ
  • 作成者情報
  • 追加の説明文

4. Divider ブロック

1
2
3
{
"type": "divider"
}

特徴:

  • シンプルな水平線
  • セクション間の視覚的な区切り
  • パラメータ不要で最もシンプルなブロック

5. Actions ブロック

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Primary ボタン",
"emoji": true
},
"style": "primary",
"value": "primary_button",
"url": "https://example.com/primary"
}
]
}

特徴:

  • ボタンやその他のインタラクティブ要素を配置
  • 最大5つの要素まで横並び配置可能
  • 3つのボタンスタイル:default(境界線のみ)、primary(青色)、danger(赤色)

制約の中での効果的なデザインパターン

1. 疑似的な枠線の実装

Block Kitには明確な「枠線」がありませんが、以下のようなテクニックで視覚的なグループ化を実現できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{		
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*2. 擬似的な枠線付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```項目:値```"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```ステータス:完了```"
}
}

工夫のポイント:

  • Markdownを使用した疑似的な枠線

2. 状態表示のパターン

色が変更できない制約の中で、絵文字とテキストを組み合わせて状態を表現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/goals|目標の基本を学ぶ>*\n :alarm_clock: 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/personal-goals|個人目標設定の方法を学ぶ>*\n :warning: 2024年9月24日(期限超過)"
}
}

使用される表現方法:

  • :alarm_clock: - 通常の期限
  • :warning: - 期限超過や注意
  • :white_check_mark: - 完了
  • :x: - エラーやキャンセル

3. 複数ボタンのレイアウトパターン

Actionsブロックでは、最大5つまでのボタンを配置できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "確認",
"emoji": true
},
"style": "primary",
"value": "confirm"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "後で",
"emoji": true
},
"value": "later"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "キャンセル",
"emoji": true
},
"style": "danger",
"value": "cancel"
}
]
}

デザインの考慮点:

  • Primary(青色)は最重要アクション用
  • Default(境界線のみ)は通常アクション用
  • Danger(赤色)は削除や危険なアクション用

まとめ

Slack Block Kitは確かに「見た目があまり変えられない」という制約がありますが、これらの制約を理解し、適切にブロックを組み合わせることで、美しく機能的なメッセージを作成できます。

重要なのは以下の点です。

  1. 制約を受け入れる: デザインの自由度は制限されているが、その分一貫性と可読性が保たれる
  2. コンテンツ構成に集中: 色やレイアウトではなく、情報の構造化と優先順位付けに注力
  3. パターンの活用: 疑似的な枠線や絵文字による状態表現など、制約内でのテクニックを習得

Block Kitの詳細なリファレンスはSlack Block Kit Builderで実際に構築しながら確認できますので、ぜひご活用ください。

この記事で紹介したテクニックのサンプルもぜひご利用ください!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Slack Block Kitデザインシステム見本",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "こんにちは、鈴木 太郎さん\nこちらはデザインパターンの総合的な見本です。"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*1. 基本的なセクション*\nこれは基本的なセクションブロックです。 *太字* や _斜体_ 、 ~取り消し線~ 、 `コード` などのMarkdown書式が使えます。"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "👆 Contextブロックは小さいテキストで補足情報を表示するのに最適です"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*2. 擬似的な枠線付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```項目:値```"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```ステータス:完了```"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*3. リンク付きのテキスト*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/1|リンク付きタスク名>*\n 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• リンクなしタスク名\n 2024年9月25日"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*4. 画像付きセクション*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "画像を右側に配置できます"
},
"accessory": {
"type": "image",
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png",
"alt_text": "かわいい犬の画像"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*5. ボタンとアクション*"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Primary ボタン",
"emoji": true
},
"style": "primary",
"value": "primary_button",
"url": "https://example.com/primary"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Default ボタン(アウトライン)",
"emoji": true
},
"value": "default_button",
"url": "https://example.com/default"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Danger ボタン",
"emoji": true
},
"style": "danger",
"value": "danger_button",
"url": "https://example.com/danger"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*6. 通知カード*"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "┌──────────────────────────────────────────┐"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " *【オンボーディング】入社後1ヶ月間のTODO*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " *社員*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": " 佐藤 花子、田中 一郎、山田 太郎"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "└──────────────────────────────────────────┘"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "コースを確認",
"emoji": true
},
"style": "primary",
"value": "check_course",
"url": "https://example.com/onboarding/course"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*7. タスクリスト(期限付き)*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/goals|目標の基本を学ぶ>*\n :alarm_clock: 2024年9月24日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/management|代表的なマネジメントの型を知る>*\n :alarm_clock: 2024年9月25日"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "• *<https://example.com/task/personal-goals|個人目標設定の方法を学ぶ>*\n :warning: 2024年9月24日(期限超過)"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*8. 複数ボタン配置*"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "確認",
"emoji": true
},
"style": "primary",
"value": "confirm"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "後で",
"emoji": true
},
"value": "later"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "キャンセル",
"emoji": true
},
"style": "danger",
"value": "cancel"
}
]
},
{
"type": "divider"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "このメッセージはデザインシステムの参考用に自動生成されました"
}
]
}
]
}

GoでSlack通知を実装する方法

はじめに

Slackは現代のチーム開発において欠かせないコミュニケーションツールです。システムからの通知やアラート、定期的なレポートなど、様々な情報をSlackに送信することで、チーム全体での情報共有を効率化できます。

この記事では、Goを使ってSlackに通知を送信する方法を、基本的なテキストメッセージから高度なBlock Kitを使ったリッチなメッセージまで、実際のコード例とともに解説します。

使用するライブラリ

今回は、github.com/slack-go/slackというGoの公式Slackクライアントライブラリを使用します。このライブラリは活発に開発されており、Slack APIの最新機能もサポートしています。

セットアップ

1. Slackアプリの作成とトークンの取得

まず、Slack APIでアプリを作成し、Bot User OAuth Tokenを取得する必要があります。

  1. Slack APIのページにアクセス
  2. “Create New App” → “From scratch”でアプリを作成
  3. “OAuth & Permissions”から必要な権限を設定
    • users:read - ユーザー一覧の取得
    • chat:write - メッセージの送信
    • im:write - ダイレクトメッセージの送信
  4. Bot User OAuth Tokenをコピー

2. 環境変数の設定

取得したトークンを環境変数として設定します。

1
export SLACK_BOT_TOKEN="xoxb-your-token-here"

基本的な実装

Slackクライアントの初期化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"os"
"github.com/slack-go/slack"
)

func main() {
// Slack APIトークンを環境変数から取得
token := os.Getenv("SLACK_BOT_TOKEN")
if token == "" {
log.Fatal("SLACK_BOT_TOKEN is not set")
}

// Slackクライアントの初期化
api := slack.New(token)
}

ユーザー一覧の取得

通知を送信する前に、まずワークスペース内のユーザー一覧を取得してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getUsers(api *slack.Client) {
// ユーザー一覧を取得
users, err := api.GetUsers()
if err != nil {
log.Fatalf("ユーザー一覧の取得に失敗しました: %v", err)
}

// ユーザー情報を表示
log.Printf("ワークスペース内のユーザー数: %d\n", len(users))
for i, user := range users {
// 必要な情報だけを表示(すべての情報を表示するとログが長くなりすぎるため)
log.Printf("%d: ID=%s, Name=%s, RealName=%s, IsBot=%v\n",
i+1, user.ID, user.Name, user.RealName, user.IsBot)
}
}

シンプルなダイレクトメッセージの送信

ユーザーIDを指定して、シンプルなテキストメッセージを送信する関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sendDirectMessage(api *slack.Client, userID, message string) error {
// ユーザーとのDMチャンネルを開く
channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{
Users: []string{userID},
})
if err != nil {
return fmt.Errorf("DMチャンネルを開けませんでした: %v", err)
}

// DMを送信
_, _, err = api.PostMessage(
channel.ID,
slack.MsgOptionText(message, false),
)
if err != nil {
return fmt.Errorf("DMの送信に失敗しました: %v", err)
}

log.Printf("ユーザー %s にDMを送信しました", userID)
return nil
}

Block Kitを使った高度なメッセージ

Slack Block Kitを使用すると、画像、ボタン、フォーマットされたテキストなどを含む、より視覚的にリッチなメッセージを作成できます。

Block Kitメッセージの構築

以下は、様々なBlock Kit要素を含むサンプルメッセージの構築例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func buildSampleBlockKit() slack.MsgOption {
// Block Kitを使ったメッセージの構築
headerText := slack.NewTextBlockObject("mrkdwn", "*ブロックキットのサンプル*", false, false)
headerSection := slack.NewSectionBlock(headerText, nil, nil)

// テキストブロック
textBlock := slack.NewTextBlockObject("mrkdwn", "これは `Block Kit` を使ったメッセージです。\n*太字* や _斜体_ などのMarkdownも使えます!", false, false)
textSection := slack.NewSectionBlock(textBlock, nil, nil)

// 画像ブロック
accessory := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/beagle.png", "犬の画像")
imageText := slack.NewTextBlockObject("mrkdwn", "こちらはかわいい犬の画像です :dog:", false, false)
imageSection := slack.NewSectionBlock(imageText, nil, slack.NewAccessory(accessory))

// ボタン要素
btnText := slack.NewTextBlockObject("plain_text", "クリックしてください", false, false)
btn := slack.NewButtonBlockElement("click_button", "button_clicked", btnText)
btnAccessory := slack.NewAccessory(btn)

// ボタンセクション
btnSectionText := slack.NewTextBlockObject("mrkdwn", "アクションを実行するには:", false, false)
btnSection := slack.NewSectionBlock(btnSectionText, nil, btnAccessory)

// 区切り線
divider := slack.NewDividerBlock()

// フッターブロック
footerText := slack.NewTextBlockObject("mrkdwn", "Block Kitの詳細は <https://api.slack.com/block-kit|こちら> をご覧ください", false, false)
footerSection := slack.NewSectionBlock(footerText, nil, nil)

// すべてのブロックを一つのメッセージにまとめる
return slack.MsgOptionBlocks(
headerSection,
divider,
textSection,
imageSection,
divider,
btnSection,
divider,
footerSection,
)
}

Block Kitメッセージの送信

構築したBlock Kitメッセージを送信する関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sendDirectMessageWithBlocks(api *slack.Client, userID string, blocks slack.MsgOption) error {
// ユーザーとのDMチャンネルを開く
channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{
Users: []string{userID},
})
if err != nil {
return fmt.Errorf("DMチャンネルを開けませんでした: %v", err)
}

// ブロックキットメッセージを送信
_, _, err = api.PostMessage(
channel.ID,
blocks,
)
if err != nil {
return fmt.Errorf("DMの送信に失敗しました: %v", err)
}

log.Printf("ユーザー %s にブロックキットDMを送信しました", userID)
return nil
}

完全なサンプルコード

以下が、これまでの機能をすべて含んだ完全なサンプルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
// Slack APIトークンを環境変数から取得
token := os.Getenv("SLACK_BOT_TOKEN")
if token == "" {
log.Fatal("SLACK_BOT_TOKEN is not set")
}

// Slackクライアントの初期化
api := slack.New(token)

// ユーザー一覧を取得して表示
getUsers(api)

// 特定のユーザーにDMを送信
userID := "U02K6JU8D" // 実際のユーザーIDに置き換えてください
err := sendDirectMessage(api, userID, "これはテストメッセージです!")
if err != nil {
log.Fatalf("DMの送信に失敗しました: %v", err)
}

// ブロックキットを使用したDMを送信
blocks := buildSampleBlockKit()
err = sendDirectMessageWithBlocks(api, userID, blocks)
if err != nil {
log.Fatalf("ブロックキットDMの送信に失敗しました: %v", err)
}
}

実行方法

  1. 依存関係をインストール:
1
go mod tidy
  1. 環境変数を設定:
1
export SLACK_BOT_TOKEN="xoxb-your-token"
  1. プログラムを実行:
1
go run main.go

注意事項

  1. レート制限: Slack APIにはレート制限があります。大量のメッセージを送信する場合は、適切な間隔を設けましょう。

  2. エラーハンドリング: 本番環境では、ネットワークエラーやAPI制限エラーに対する適切なリトライ機構を実装することを推奨します。

  3. ユーザーID: 実際の運用では、ユーザー名からユーザーIDを動的に取得する仕組みを構築することが一般的です。

  4. セキュリティ: Slack APIトークンは機密情報です。ソースコードに直接記述せず、必ず環境変数や設定ファイルから読み込むようにしてください。

まとめ

この記事では、Goを使ったSlack通知の実装方法を、基本的なテキストメッセージから高度なBlock Kitを使ったリッチなメッセージまで幅広く解説しました。

github.com/slack-go/slackライブラリを使用することで、Slack APIの豊富な機能を簡単に利用できます。システム監視、定期レポート、チーム内通知など、様々な用途でSlack通知を活用して、より効率的なチーム開発を実現してください。

Block Kitの詳細については、Slack Block Kit Builderで実際にブロックを構築しながら学習することをおすすめします。