もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

APIGatewayとDynamoDBとSlash CommandsでブログSlackのブログ管理

はじめに

こんにちは。

連続でGo言語頑張っています。

今回は、APIGateway、DynamoDBとSlackのSlashCommandを駆使して、ブログのSlackチームの手動管理をなくす対応を行いました。

やってみた

APIGateway

ブログ登録用のAPIを作成します。

API作成

特に説明すること無いんですが、事前に用意しているLambdaと連携してます。

f:id:kojirooooocks:20181102001533p:plain

ステージは、1こだけです。

f:id:kojirooooocks:20181102001546p:plain

使用量プラン

使用量プランをつくります。

そんなにガンっとユーザーが増えるわけはないと思うんですが、念の為です。

先程作ったステージと紐づけます。

f:id:kojirooooocks:20181102001558p:plain

とりあえず、これでアクセス出来ます。

f:id:kojirooooocks:20181102001611p:plain

次は、ブログを登録するようのパラメータのマッピングテンプレートを登録します。

APIGateWayはこれで終わり。

DynamoDB

テーブルだけ登録しました。

idはstringでslackのUserIDを保存するようにしておきます。

f:id:kojirooooocks:20181102001628p:plain

Lambda

現状実装しているものに加えて、今回作成した、ブログ登録用のlambdaを作成しました。

  1. 水・金・日のリマインダー
  2. ブログの登録

1. 水・金・日のリマインダー

こちらは、すでに作っていたので、処理の流れは基本的には変えてないのですが、tomlファイルで管理していた各ユーザーとRSSフィードURLのマッピングをDynamoDBから取得するように変更しました。

package database

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/guregu/dynamo"
    config "../config"
)

type WriteBlogEveryWeek struct {
    UserID       string `dynamo:"user_id"`
    UserName     string `dynamo:"user_name"`
    FeedURL      string `dynamo:"feed_url"`
    RequireCount int    `dynamo:"require_count"`
}

/**
 * DynamoDBからデータを全取得する
 */
func FindAll(configData config.ConfigData) []WriteBlogEveryWeek {
    var writeBlogEveryWeek []WriteBlogEveryWeek
    table := getTableObject(configData)
    err := table.Scan().All(&writeBlogEveryWeek)
    if err != nil {
        panic("データの読み込みエラー => " + err.Error())
    }

    return writeBlogEveryWeek
}


/**
 * DynamoDBのテーブルオブジェクトを取得する
 */
func getTableObject(configData config.ConfigData) dynamo.Table {
    credential := credentials.NewStaticCredentials(configData.AWS.AccessKey, configData.AWS.SecretKey, "")
    db := dynamo.New(session.New(), &aws.Config{
        Credentials: credential,
        Region:      aws.String(configData.AWS.Region),
    })

    table := db.Table(configData.AWS.DataBase)

    return table
}

参考サイトがゴロゴロ転がっていたので、そこまで迷うことはありませんでした!

ユーザーデータはDynamoDBからとるようになったので、tomlファイルは、AWSとかSlackの設定だけを管理するようになり、スッキリしました。

2. ブログの登録

前述したとおり、今までは tomlファイルで各メンバーのブログを管理していました。

なので、メンバーが増えるごとに自分がtomlファイルを修正していました。

現在メンバーが20人超えしていて、流石に辛くなったんで、この管理方法をやめるために、各ユーザーで勝手に追加してもらうように対応しました。

使ったのは、 Slash Commands です。

Slash Command -> API Gateway -> Lambda -> DynamoDB登録 という流れです。

こちらの記事をほぼほぼ使わせていただきました。

ありがたい・・・

slack.go

package slack

import (
    "errors"
    "net/url"
    "strings"
    config "../config"
)

/**
 * Slackからのパラメータ格納構造体
 */
type SlackParams struct {
    Token    string
    UserID   string
    UserName string
    Text     string
}

/**
 * Slackから送られたパラメータをパースする
 */
func ParseSlackParams(rawParams interface{}) (result *SlackParams, err error) {
    result = &SlackParams{}

    tmp := rawParams.(map[string]interface{})
    if _, ok := tmp["body"]; !ok {
        err = errors.New("params body does not exists")
        return
    }
    rawQueryString := tmp["body"].(string)
    parsed, err := url.QueryUnescape(rawQueryString)
    if err != nil {
        err = errors.New("params body unescape failed. body: " + rawQueryString)
        return
    }
    params, err := url.ParseQuery(parsed)
    if err != nil {
        err = errors.New("params body parse failed. body: " + rawQueryString)
        return
    }

    result.Token = params["token"][0]
    result.UserID = params["user_id"][0]
    result.UserName = params["user_name"][0]
    result.Text = params["text"][0]

    // Slash CommandでURL形式を送ると <URL>という形式になるので、先頭と末尾をtrimする
    result.Text = strings.TrimLeft(result.Text, "<")
    result.Text = strings.TrimRight(result.Text, ">")
 
    return
}

database.go

package database

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/guregu/dynamo"

    config "../config"
    slack "../slack"
)

type WriteBlogEveryWeek struct {
    UserID       string `dynamo:"user_id"`
    UserName     string `dynamo:"user_name"`
    FeedURL      string `dynamo:"feed_url"`
    RequireCount int    `dynamo:"require_count"`
}

/**
 * Pkを指定して1件取得
 */
func FindByPK(configData config.ConfigData, pk string) WriteBlogEveryWeek {
    var writeBlogEveryWeek WriteBlogEveryWeek
    table := getTableObject(configData)
    table.Get("user_id", pk).One(&writeBlogEveryWeek)
    return writeBlogEveryWeek
}

/**
 * 新しいユーザーデータを作成する
 */
func CreateUser(configData config.ConfigData, slackParams *slack.SlackParams) {
    var writeBlogEveryWeek WriteBlogEveryWeek
    writeBlogEveryWeek.UserID = slackParams.UserID
    writeBlogEveryWeek.UserName = slackParams.UserName
    writeBlogEveryWeek.FeedURL = slackParams.Text
    writeBlogEveryWeek.RequireCount = 1
    table := getTableObject(configData)
    err := table.Put(writeBlogEveryWeek).Run()
    if err != nil {
        panic("登録エラー => " + err.Error())
    }
}

/**
 * DynamoDBのテーブルオブジェクトを取得する
 */
func getTableObject(configData config.ConfigData) dynamo.Table {
    credential := credentials.NewStaticCredentials(configData.AWS.AccessKey, configData.AWS.SecretKey, "")
    db := dynamo.New(session.New(), &aws.Config{
        Credentials: credential,
        Region:      aws.String(configData.AWS.Region),
    })

    table := db.Table(configData.AWS.DataBase)

    return table
}

config.go

package config

import "github.com/BurntSushi/toml"

type ConfigData struct {
    Slack Slack
    AWS   AWS
}

type Slack struct {
    SendAPIURL    string
    ChannelName   string
    RegsiterToken string
}

type AWS struct {
    AccessKey string
    SecretKey string
    Region    string
    DataBase  string
}

/**
 * 設定データを取得する
 */
func GetConfigData() ConfigData {
    var configData ConfigData
    _, err := toml.DecodeFile("config.toml", &configData)
    if err != nil {
        panic("tomlファイルを読み込めません")
    }

    return configData
}

main.go

/**
 * ブログの登録ロジックを実行
 */
func blogRegister(_ context.Context, rawParams interface{}) (interface{}, error) {
    configData := config.GetConfigData()
    envToken := os.Getenv("SLACK_TOKEN")
    params, err := slack.ParseSlackParams(rawParams)
    if err != nil {
        return "スラックのパラメータが取得できませんでした。 error: " + err.Error(), nil
    }
    if envToken != params.Token {
        return "トークンの不一致", nil
    }
    userData := database.FindByPK(configData, params.UserID)
    if userData.UserID != "" {
        return "あなたのブログはすでに登録済みです feedURL: " + userData.FeedURL, nil
    }
    database.CreateUser(configData, params)

    return "ブログを登録しました。これからは妥協は許しませんよ。", nil
}

実際に実行するとこんな感じです。

f:id:kojirooooocks:20181102001648g:plain

特にバリデートしてないのは許してください。

アイコンはこちらから一枚使わせていただいています。。。

これで、メンバー登録を自分の手動運用から離す事ができました。

終わりに

開いてる時間でやってたんですが、結構時間かかりました。

Go言語慣れてきたんですが、ポインターとか使ってなくて、全く使いこなせていません。

もうちょっと本格的に触らないとなー。と思っていたら、知り合いからGoの仕事のお誘いが来ました。

受けられるほどの技術はまだないんですが、とりあえず本当にやるとなった想定で、素振りをブンブンしておきます。