All Articles

Go で slack slash command を作る

このページについて

タイトルの通りの作業を行った作業ログ。serverless framework を使っている。

slack に slash command 登録する

今は manifest ファイルを編集して登録させるのがいい模様。GUI からポチポチが何かうまく行かなかったのでこちらにトライした。

_metadata:
  major_version: 1
  minor_version: 1
display_information:
  name: <command-name>
features:
  app_home:
    home_tab_enabled: true
    messages_tab_enabled: true
    messages_tab_read_only_enabled: false
  bot_user:
    display_name: <command-name>
    always_online: true
  slash_commands:
    - command: /<your-command-name>
      url: <serverless framework で作成した api endpoint>
      description: <description>
      usage_hint: /<your-command-name> help
      should_escape: true
oauth_config:
  scopes:
    bot:
      - commands
      - chat:write
      - chat:write.public
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  is_hosted: false
  token_rotation_enabled: false

これを書いたあとは、Install Apps リンクからアプリケーションのインストールを実行させる。
これで、Slack 内で /<your-command-name> が叩けるようになる。

api の実装

ひとまずlambda までリクエストが到達したら呼ばれたチャンネルへテキスト返すまで。SlackRequest に ResponseURL があり(期限付き)、そこに対して Post を行うとメッセージが返せる。Slash Command の応答は 3秒以内だが、この値を使うと一次受けの lambda では後続の処理に投げたあと素早く 200 を返すようなこともできる。

slash command から送られてくる request body は url encode の形なので github.com/hetiansu5/urlquery を使って unmarshal してみた。
Text は space 区切りで文字列が入るので、split して、0 番目の値によって処理を分けて上げればよさそう。

package main

import (
    "bytes"
    "net/http"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
    "github.com/google/uuid"
    "github.com/hetiansu5/urlquery"
    "go.uber.org/zap"

    "encoding/json"
    "fmt"
    "os"
)

type Item struct {
    Id      string `json:"id,omitempty"`
    Title   string `json:"title"`
    Details string `json:"details"`
}

// https://api.slack.com/interactivity/slash-commands#app_command_handling
type SlackRequest struct {
    Token          string `query:"token"`
    TeamID         string `query:"team_id"`
    TeamDomain     string `query:"team_domain"`
    EnterpriseID   string `query:"enterprise_id"`
    EnterpriseName string `query:"enterprise_name"`
    ChannelID      string `query:"channel_id"`
    ChannelName    string `query:"channel_name"`
    UserID         string `query:"user_id"`
    Command        string `query:"command"`
    Text           string `query:"text"`
    ResponseURL    string `query:"response_url"`
    TriggerID      string `query:"trigger_id"`
    APIAppID       string `query:"api_app_id"`
}

type SlackResponse struct {
    Text string `json:"text"`
}

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    zl, _ := zap.NewProduction()

    v := &SlackRequest{}
    if err := urlquery.Unmarshal([]byte(request.Body), v); err != nil {
        zl.Error("", zap.Error(err))
        return events.APIGatewayProxyResponse{StatusCode: 400}, nil
    }

    bss, _ := json.MarshalIndent(v, "", "   ")
    fmt.Printf("%v\n", string(bss))

    bss, _ = json.MarshalIndent(request.Headers, "", "  ")
    fmt.Printf("%v\n", string(bss))

    // Unmarshal to Item to access object properties
    itemString := request.Body
    itemStruct := Item{}
    json.Unmarshal([]byte(itemString), &itemStruct)

    res := SlackResponse{
        Text: helpText,
    }
    bs, _ := json.Marshal(res)

    req, err := http.NewRequest(
        "POST",
        v.ResponseURL,
        bytes.NewBuffer(bs),
    )
    if err != nil {
        return events.APIGatewayProxyResponse{StatusCode: 400}, nil
    }

    // Content-Type 設定
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return events.APIGatewayProxyResponse{StatusCode: 400}, nil
    }
    defer resp.Body.Close()

    return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}