Skip to content

Commit

Permalink
Merge pull request #7 from traPtitech/epic/to-ws-bot
Browse files Browse the repository at this point in the history
feat!: Rewrite to WebSocket bot
  • Loading branch information
motoki317 authored Aug 23, 2024
2 parents 59f6504 + 03b3cdc commit 50ad252
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 712 deletions.
5 changes: 0 additions & 5 deletions .gcloudignore

This file was deleted.

27 changes: 0 additions & 27 deletions .github/workflows/master.yml

This file was deleted.

48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

on:
push:
tags:
- v*.*.*

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "APP_VERSION=$(echo ${GITHUB_REF:11})" >> $GITHUB_ENV

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
id: buildx
- name: Builder instance name
run: echo ${{ steps.buildx.outputs.name }}
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: traptitech
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/traptitech/traq-system-bot:latest
ghcr.io/traptitech/traq-system-bot:${{ env.APP_VERSION }}
cache-from: type=registry,ref=ghcr.io/traptitech/traq-system-bot:buildcache
cache-to: type=registry,ref=ghcr.io/traptitech/traq-system-bot:buildcache,mode=max

release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder

WORKDIR /work
ENV CGO_ENABLED=0

RUN apk add --update --no-cache git

COPY ./go.* ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download

COPY . .

ARG TARGETOS
ARG TARGETARCH
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go build -o /app/bot -ldflags "-s -w" .

FROM gcr.io/distroless/static-debian12:latest AS runtime
WORKDIR /app

COPY --from=builder /app/bot ./
ENTRYPOINT ["/app/bot"]
157 changes: 64 additions & 93 deletions bot.go
Original file line number Diff line number Diff line change
@@ -1,122 +1,93 @@
package traq_system_bot
package main

import (
"bytes"
"encoding/json"
"errors"
"context"
"fmt"
"net/http"
"github.com/traPtitech/go-traq"
traqwsbot "github.com/traPtitech/traq-ws-bot"
"github.com/traPtitech/traq-ws-bot/payload"
"log/slog"
"os"
)

func mustGetEnv(key string) string {
v, ok := os.LookupEnv(key)
if !ok {
panic(fmt.Sprintf("environment variable %s must be set", key))
}
return v
}

var (
verificationToken string
accessToken string
systemMessageChannelID string
traqOrigin string
systemMessageChannelID = mustGetEnv("BOT_SYSTEM_MESSAGE_CHANNEL_ID")
)

func init() {
verificationToken = os.Getenv("BOT_VERIFICATION_TOKEN")
accessToken = os.Getenv("BOT_ACCESS_TOKEN")
systemMessageChannelID = os.Getenv("BOT_SYSTEM_MESSAGE_CHANNEL_ID")
traqOrigin = os.Getenv("TRAQ_ORIGIN")
loggerInit()
}

func BotEndpoint(w http.ResponseWriter, r *http.Request) {
defer logger.Flush()
if r.Header.Get("X-TRAQ-BOT-TOKEN") != verificationToken {
infoL(r, "Wrong X-TRAQ-BOT-TOKEN request was received")
w.WriteHeader(http.StatusUnauthorized)
return
func main() {
bot, err := traqwsbot.NewBot(&traqwsbot.Options{
AccessToken: mustGetEnv("BOT_ACCESS_TOKEN"),
Origin: os.Getenv("TRAQ_ORIGIN"),
})
if err != nil {
panic(err)
}

event := r.Header.Get("X-TRAQ-BOT-EVENT")
switch event {
case "PING":
infoL(r, "PING was received")
w.WriteHeader(http.StatusNoContent)
case "USER_CREATED":
var req userCreatedPayload
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
infoL(r, fmt.Sprintf("USER_CREATED(UID:%s) was received", req.User.ID))
registerHandlers(bot)

if !req.User.Bot {
if err := sendMessage(systemMessageChannelID, fmt.Sprintf(`%s がtraQに参加しました`, createUserMention(req.User))); err != nil {
errorL(r, fmt.Sprintf("sendMessage failed: %v", err))
}
}
w.WriteHeader(http.StatusNoContent)
case "USER_ACTIVATED":
var req userActivatedPayload
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
infoL(r, fmt.Sprintf("USER_ACTIVATED(UID:%s) was received", req.User.ID))
err = bot.Start()
if err != nil {
panic(err)
}
}

if !req.User.Bot {
if err := sendMessage(systemMessageChannelID, fmt.Sprintf(`%s がtraQに帰ってきました`, createUserMention(req.User))); err != nil {
errorL(r, fmt.Sprintf("sendMessage failed: %v", err))
func registerHandlers(bot *traqwsbot.Bot) {
bot.OnUserCreated(func(p *payload.UserCreated) {
slog.Info("USER_CREATED event received", "uid", p.User.ID)
if !p.User.Bot {
if err := sendMessage(bot, fmt.Sprintf(`%s がtraQに参加しました`, createUserMention(p.User))); err != nil {
slog.Error("sendMessage failed", "err", err)
}
}
w.WriteHeader(http.StatusNoContent)
case "CHANNEL_CREATED":
var req channelCreatedPayload
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
})
bot.OnUserActivated(func(p *payload.UserActivated) {
slog.Info("USER_ACTIVATED event received", "uid", p.User.ID)
if !p.User.Bot {
if err := sendMessage(bot, fmt.Sprintf(`%s がtraQに帰ってきました`, createUserMention(p.User))); err != nil {
slog.Error("sendMessage failed", "err", err)
}
}
infoL(r, fmt.Sprintf("CHANNEL_CREATED(UID:%s) was received", req.Channel.ID))
})

if err := sendMessage(systemMessageChannelID, fmt.Sprintf(`%s がチャンネル %s を作成しました`, createUserMention(req.Channel.Creator), createChannelMention(req.Channel))); err != nil {
errorL(r, fmt.Sprintf("sendMessage failed: %v", err))
}
w.WriteHeader(http.StatusNoContent)
case "STAMP_CREATED":
var req stampCreatedPayload
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
bot.OnChannelCreated(func(p *payload.ChannelCreated) {
slog.Info("CHANNEL_CREATED event received", "cid", p.Channel.ID)
if err := sendMessage(bot, fmt.Sprintf(`%s がチャンネル %s を作成しました`, createUserMention(p.Channel.Creator), createChannelMention(p.Channel))); err != nil {
slog.Error("sendMessage failed", "err", err)
}
infoL(r, fmt.Sprintf("STAMP_CREATED(SID:%s) was received", req.ID))
})

if err := sendMessage(systemMessageChannelID, fmt.Sprintf("%s がスタンプ `:%s:` を作成しました\n:%s.ex-large:", createUserMention(req.Creator), req.Name, req.Name)); err != nil {
errorL(r, fmt.Sprintf("sendMessage failed: %v", err))
bot.OnStampCreated(func(p *payload.StampCreated) {
slog.Info("STAMP_CREATED event received", "sid", p.ID)
if err := sendMessage(bot, fmt.Sprintf("%s がスタンプ `:%s:` を作成しました\n:%s.ex-large:", createUserMention(p.Creator), p.Name, p.Name)); err != nil {
slog.Error("sendMessage failed", "err", err)
}
w.WriteHeader(http.StatusNoContent)
default:
infoL(r, fmt.Sprintf("Unknown X-TRAQ-BOT-EVENT was received: %s", event))
w.WriteHeader(http.StatusBadRequest)
}
return
})
}

func createUserMention(user userPayload) string {
func createUserMention(user payload.User) string {
return fmt.Sprintf(`!{"type":"user","raw":"@%s","id":"%s"}`, user.Name, user.ID)
}

func createChannelMention(channel channelPayload) string {
func createChannelMention(channel payload.Channel) string {
return fmt.Sprintf(`!{"type":"channel","raw":"%s","id":"%s"}`, channel.Path, channel.ID)
}

func sendMessage(channelID string, text string) error {
b, _ := json.Marshal(map[string]string{"content": text})
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v3/channels/%s/messages", traqOrigin, channelID), bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.Header.Set("Authorization", "Bearer "+accessToken)

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 sendMessage(bot *traqwsbot.Bot, text string) error {
_, _, err := bot.API().
ChannelApi.
PostMessage(context.Background(), systemMessageChannelID).
PostMessageRequest(traq.PostMessageRequest{
Content: text,
Embed: nil,
}).
Execute()
return err
}
13 changes: 10 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
module github.com/traPtitech/traq-system-bot

go 1.22.6

require (
github.com/traPtitech/go-traq v0.0.0-20240725071454-97c7b85dc879
github.com/traPtitech/traq-ws-bot v1.2.0
)

require (
cloud.google.com/go v0.81.0
cloud.google.com/go/logging v1.4.1
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
)
Loading

0 comments on commit 50ad252

Please sign in to comment.