Skip to content

Commit

Permalink
Merge pull request #35 from traPtitech/v3
Browse files Browse the repository at this point in the history
V3
  • Loading branch information
motoki317 authored May 5, 2023
2 parents 1a26e71 + c175fcc commit e0225bc
Show file tree
Hide file tree
Showing 15 changed files with 1,011 additions and 911 deletions.
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
*.iml
/.idea/
./DevOpsBot
./log
./config.yaml
./dist

/DevOpsBot
/log
/config.yaml
/dist
/test.sh
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,35 @@ ENV GOARCH=$TARGETARCH
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go build -o /dev-ops-bot -ldflags="-s -w -X main.version=$VERSION" .

FROM --platform=$BUILDPLATFORM golang:1.20-alpine AS installer

ENV CGO_ENABLED 0
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH

RUN apk add --no-cache wget

RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_"$TARGETOS"_"$TARGETARCH" -O /yq && \
chmod +x /yq

RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go install sigs.k8s.io/kustomize/kustomize/v5@latest
# keep output directory the same between platforms; workaround for https://github.com/golang/go/issues/57485
RUN cp /go/bin/kustomize /kustomize || cp /go/bin/"$GOOS"_"$GOARCH"/kustomize /kustomize

FROM alpine:3

WORKDIR /work

# Install commands for deploy scripts
RUN apk add --no-cache git openssh
RUN mkdir -p /root/.ssh && ssh-keyscan github.com >> /root/.ssh/known_hosts

COPY --from=installer /yq /usr/local/bin/
COPY --from=installer /kustomize /usr/local/bin/

COPY --from=builder /dev-ops-bot ./

ENTRYPOINT ["/work/dev-ops-bot"]
160 changes: 47 additions & 113 deletions bot.go
Original file line number Diff line number Diff line change
@@ -1,154 +1,88 @@
package main

import (
"errors"
"context"
"fmt"
"github.com/dghubble/sling"
"github.com/go-chi/render"
"strings"

"github.com/kballard/go-shellquote"
"github.com/samber/lo"
"github.com/traPtitech/go-traq"
"github.com/traPtitech/traq-ws-bot/payload"
"go.uber.org/zap"
"net/http"
"time"
)

var traQClient *sling.Sling

type Map map[string]interface{}

// MessageCreatedPayload MESSAGE_CREATEDイベントペイロード
type MessageCreatedPayload struct {
EventTime time.Time `json:"eventTime"`
Message struct {
ID string `json:"id"`
User struct {
ID string `json:"id"`
Name string `json:"name"`
Bot bool `json:"bot"`
} `json:"user"`
ChannelID string `json:"channelId"`
Text string `json:"text"`
PlainText string `json:"plainText"`
CreatedAt string `json:"createdAt"`
} `json:"message"`
}

// BotEndPoint Botサーバーエンドポイント
func BotEndPoint(w http.ResponseWriter, r *http.Request) {
// トークン検証
if r.Header.Get("X-TRAQ-BOT-TOKEN") != config.VerificationToken {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// BotMessageReceived BOTのMESSAGE_CREATEDイベントハンドラ
func BotMessageReceived(p *payload.MessageCreated) {
ctx := context.Background()

switch r.Header.Get("X-TRAQ-BOT-EVENT") {
case "PING", "JOINED", "LEFT":
w.WriteHeader(http.StatusNoContent)
case "MESSAGE_CREATED":
var payload MessageCreatedPayload
if err := render.Decode(r, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
go BotMessageReceived(payload)
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
if p.Message.User.Bot {
return // Ignore bots
}
}

// BotMessageReceived BOTのMESSAGE_CREATEDイベントハンドラ
func BotMessageReceived(p MessageCreatedPayload) {
if p.Message.ChannelID != config.DevOpsChannelID {
if p.Message.ChannelID != config.ChannelID {
return // DevOpsチャンネル以外からのメッセージは無視
}

args, err := shellquote.Split(p.Message.PlainText)
if err != nil {
_ = SendTRAQMessage(p.Message.ChannelID, fmt.Sprintf("invalid syntax error\n%s", cite(p.Message.ID)))
_ = PushTRAQStamp(p.Message.ID, config.Stamps.BadCommand)
_ = SendTRAQMessage(ctx, p.Message.ChannelID, fmt.Sprintf("invalid syntax error\n%s", cite(p.Message.ID)))
_ = PushTRAQStamp(ctx, p.Message.ID, config.Stamps.BadCommand)
return
}
if len(args[0]) == 0 {
return // 空メッセージは無視
_, argStart, ok := lo.FindIndexOf(args, func(arg string) bool { return strings.HasPrefix(arg, config.Prefix) })
if !ok {
return
}
args = args[argStart:]
args[0] = strings.TrimPrefix(args[0], config.Prefix)

ctx := &Context{
P: &p,
Args: args,
cmdCtx := &Context{
Context: ctx,
P: p,
Args: args,
}
c, ok := commands[args[0]]
if !ok {
// コマンドが見つからない
_ = ctx.ReplyBad(fmt.Sprintf("Unknown command: `%s`", args[0]))
_ = cmdCtx.ReplyBad(fmt.Sprintf("Unknown command: `%s`", args[0]))
return
}
err = c.Execute(ctx)
err = c.Execute(cmdCtx)
if err != nil {
ctx.L().Error("failed to execute command", zap.Error(err))
cmdCtx.L().Error("failed to execute command", zap.Error(err))
}
}

// SendTRAQMessage traQにメッセージ送信
func SendTRAQMessage(channelID string, text string) error {
req, err := traQClient.New().
Post(fmt.Sprintf("api/v3/channels/%s/messages", channelID)).
BodyJSON(Map{"content": text}).
Request()
if err != nil {
return err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return errors.New(res.Status)
}
return nil
func SendTRAQMessage(ctx context.Context, channelID string, text string) error {
_, _, err := bot.API().
ChannelApi.
PostMessage(ctx, channelID).
PostMessageRequest(traq.PostMessageRequest{Content: text}).
Execute()
return err
}

// SendTRAQDirectMessage traQにダイレクトメッセージ送信
func SendTRAQDirectMessage(userID string, text string) error {
req, err := traQClient.New().
Post(fmt.Sprintf("api/v3/users/%s/messages", userID)).
BodyJSON(Map{"content": text}).
Request()
if err != nil {
return err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return errors.New(res.Status)
}
return nil
func SendTRAQDirectMessage(ctx context.Context, userID string, text string) error {
_, _, err := bot.API().
UserApi.
PostDirectMessage(ctx, userID).
PostMessageRequest(traq.PostMessageRequest{Content: text}).
Execute()
return err
}

// PushTRAQStamp traQのメッセージにスタンプを押す
func PushTRAQStamp(messageID, stampID string) error {
req, err := traQClient.New().
Post(fmt.Sprintf("api/v3/messages/%s/stamps/%s", messageID, stampID)).
BodyJSON(Map{"count": 1}).
Request()
if err != nil {
return err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return errors.New(res.Status)
}
return nil
func PushTRAQStamp(ctx context.Context, messageID, stampID string) error {
_, err := bot.API().
MessageApi.
AddMessageStamp(ctx, messageID, stampID).
PostMessageStampRequest(traq.PostMessageStampRequest{Count: 1}).
Execute()
return err
}

// cite traQのメッセージ引用形式を作る
Expand Down
47 changes: 27 additions & 20 deletions command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import (
"context"
"strings"

"github.com/traPtitech/traq-ws-bot/payload"
"go.uber.org/zap"
)

Expand All @@ -14,8 +18,9 @@ type Command interface {

// Context コマンド実行コンテキスト
type Context struct {
context.Context
// P BOTが受信したMESSAGE_CREATEDイベントの生のペイロード
P *MessageCreatedPayload
P *payload.MessageCreated
// Args 投稿メッセージを空白区切りで分けたもの
Args []string
}
Expand All @@ -25,56 +30,58 @@ func (ctx *Context) GetExecutor() string {
return ctx.P.Message.User.Name
}

// ReplyViaDM コマンドメッセージに返信します
func (ctx *Context) Reply(message, stamp string) (err error) {
if len(message) > 0 {
err = SendTRAQMessage(ctx.P.Message.ChannelID, message)
if err != nil {
return
}
// Reply コマンドメッセージに返信します
func (ctx *Context) Reply(message ...string) error {
return SendTRAQMessage(ctx, ctx.P.Message.ChannelID, strings.Join(message, "\n"))
}

func (ctx *Context) ReplyWithStamp(stamp string, message ...string) error {
err := PushTRAQStamp(ctx, ctx.P.Message.ID, stamp)
if err != nil {
return err
}
if len(stamp) > 0 {
err = PushTRAQStamp(ctx.P.Message.ID, stamp)
if len(message) > 0 {
err = ctx.Reply(message...)
if err != nil {
return
return err
}
}
return
return nil
}

// ReplyViaDM コマンド実行者にDMで返信します
func (ctx *Context) ReplyViaDM(message string) error {
return SendTRAQDirectMessage(ctx.P.Message.User.ID, message)
func (ctx *Context) ReplyViaDM(message ...string) error {
return SendTRAQDirectMessage(ctx, ctx.P.Message.User.ID, strings.Join(message, "\n"))
}

// ReplyBad コマンドメッセージにBadスタンプをつけて返信します
func (ctx *Context) ReplyBad(message ...string) (err error) {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.BadCommand)
return ctx.ReplyWithStamp(config.Stamps.BadCommand, message...)
}

// ReplyForbid コマンドメッセージにForbidスタンプをつけて返信します
func (ctx *Context) ReplyForbid(message ...string) error {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.Forbid)
return ctx.ReplyWithStamp(config.Stamps.Forbid, message...)
}

// ReplyAccept コマンドメッセージにAcceptスタンプをつけて返信します
func (ctx *Context) ReplyAccept(message ...string) error {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.Accept)
return ctx.ReplyWithStamp(config.Stamps.Accept, message...)
}

// ReplySuccess コマンドメッセージにSuccessスタンプをつけて返信します
func (ctx *Context) ReplySuccess(message ...string) error {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.Success)
return ctx.ReplyWithStamp(config.Stamps.Success, message...)
}

// ReplyFailure コマンドメッセージにFailureスタンプをつけて返信します
func (ctx *Context) ReplyFailure(message ...string) error {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.Failure)
return ctx.ReplyWithStamp(config.Stamps.Failure, message...)
}

// ReplyRunning コマンドメッセージにRunningスタンプをつけて返信します
func (ctx *Context) ReplyRunning(message ...string) error {
return ctx.Reply(stringOrEmpty(message...), config.Stamps.Running)
return ctx.ReplyWithStamp(config.Stamps.Running, message...)
}

func (ctx *Context) L() *zap.Logger {
Expand Down
Loading

0 comments on commit e0225bc

Please sign in to comment.