Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,6 @@ vite.config.ts.timestamp-*

src/bot/.venv

__pycache__
__pycache__

src/api/build.sh
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# NewSharkBot

SharkBotの書き直し

# 招待する方法

まだ開発中です。公開までしばらくお待ちください。
12 changes: 11 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ services:
DB_DSN: "host=postgres port=5432 user=sharkbot password=password dbname=sharkbot sslmode=disable"
GIN_MODE: "debug"
depends_on:
- postgres
postgres:
condition: service_healthy
networks:
- sharkbot_network
bot:
build: ./src/bot
depends_on:
- api
env_file:
- ./src/bot/.env
networks:
- sharkbot_network
environment:
RESOURCE_API_BASE_URL: "http://api:8080"
postgres:
image: postgres:18
environment:
Expand All @@ -29,6 +34,11 @@ services:
- postgres_data:/var/lib/postgresql
networks:
- sharkbot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sharkbot"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
networks:
Expand Down
4 changes: 3 additions & 1 deletion src/api/src/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func main() {
}

// マイグレート
err = db.AutoMigrate(&model.GuildSetting{})
err = db.AutoMigrate(&model.GuildSetting{}, &model.EmbedSetting{}, &model.MessageSetting{})
if err != nil {
panic("failed to migrate database")
}
Expand All @@ -73,6 +73,8 @@ func main() {

// ルートグループを登録
router.RegisterGuildsRoutes(r.Group("/"))
router.RegisterEmbed(r.Group("/"))
router.RegisterMessageSettings(r.Group("/"))

// シンプルなGETエンドポイントを定義
r.GET("/health", healthCheck)
Expand Down
5 changes: 5 additions & 0 deletions src/api/src/internal/dto/Guild.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ type ListGuildsResponse struct {
type CreateOrUpdateGuildSettingRequest struct {
EnabledModules map[string]bool `json:"enabledModules"`
}

type UpdateModuleRequest struct {
Module string `json:"module"`
Enabled bool `json:"enabled"`
}
34 changes: 34 additions & 0 deletions src/api/src/internal/model/EmbedSetting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package model

import (
"database/sql/driver"
"encoding/json"
"errors"
"time"
)

type EmbedData map[string]interface{}

func (ed *EmbedData) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, ed)
}

func (ed EmbedData) Value() (driver.Value, error) {
if len(ed) == 0 {
return nil, nil
}
return json.Marshal(ed)
}

type EmbedSetting struct {
ID uint `gorm:"primaryKey"`
GuildID string `gorm:"index;not null" json:"guild_id"` // どのサーバーの設定か
Name string `json:"name"` // "welcome_custom", "goodbye_v1" など
Data EmbedData `gorm:"type:jsonb" json:"data"` // Discord EmbedのDict
CreatedAt time.Time
UpdatedAt time.Time
}
17 changes: 17 additions & 0 deletions src/api/src/internal/model/MessageSetting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package model

import "time"

type MessageSetting struct {
GuildID string `gorm:"primaryKey" json:"guild_id"`
Type string `gorm:"primaryKey" json:"type"` // "welcome" or "goodbye"

ChannelID string `json:"channel_id"`
Content string `json:"content"`

EmbedID *uint `json:"embed_id"`
Embed EmbedSetting `gorm:"foreignKey:EmbedID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`

CreatedAt time.Time
UpdatedAt time.Time
}
119 changes: 119 additions & 0 deletions src/api/src/internal/router/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package router

import (
"net/http"
"strconv"

"github.com/SharkBot-Dev/NewSharkBot/api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

func RegisterEmbed(router *gin.RouterGroup) {
guilds := router.Group("/guilds/embeds")
{
guilds.GET("/:id", getEmbedSettingList) // 一覧取得
guilds.GET("/:id/:name", getEmbedSetting) // 個別取得
guilds.POST("/:id", createOrUpdateEmbedSetting) // 作成・更新
guilds.DELETE("/:id/:embed_id", deleteEmbedSetting) // 削除
}
}

// 全件取得
func getEmbedSettingList(c *gin.Context) {
id := c.Param("id")
var settings []model.EmbedSetting
db := c.MustGet("db").(*gorm.DB)

if err := db.Where("guild_id = ?", id).Find(&settings).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
}
c.JSON(http.StatusOK, settings)
}

// 個別取得
func getEmbedSetting(c *gin.Context) {
id := c.Param("id")
name := c.Param("name")
var setting model.EmbedSetting
db := c.MustGet("db").(*gorm.DB)

if err := db.Where("guild_id = ? AND name = ?", id, name).First(&setting).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Embed setting not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
}
return
}
c.JSON(http.StatusOK, setting)
}

// 作成・更新
func createOrUpdateEmbedSetting(c *gin.Context) {
id := c.Param("id")
db := c.MustGet("db").(*gorm.DB)

// リクエストボディをパースするための構造体
var input struct {
Name string `json:"name" binding:"required"`
Data model.EmbedData `json:"data" binding:"required"`
}

if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

var setting model.EmbedSetting
// 既存の設定があるか確認
result := db.Where("guild_id = ? AND name = ?", id, input.Name).First(&setting)

setting.GuildID = id
setting.Name = input.Name
setting.Data = input.Data

if result.Error == gorm.ErrRecordNotFound {
// 新規作成
if err := db.Create(&setting).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create"})
return
}
} else {
// 更新
if err := db.Save(&setting).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update"})
return
}
}

c.JSON(http.StatusOK, setting)
}

// 削除
func deleteEmbedSetting(c *gin.Context) {
guildID := c.Param("id")
embedID := c.Param("embed_id")
db := c.MustGet("db").(*gorm.DB)

embedIdInt, err := strconv.Atoi(embedID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid embed_id format"})
return
}

result := db.Where("guild_id = ? AND id = ?", guildID, embedIdInt).Delete(&model.EmbedSetting{})

if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete"})
return
}

if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Setting not found"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Deleted successfully"})
}
105 changes: 105 additions & 0 deletions src/api/src/internal/router/guilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ func RegisterGuildsRoutes(router *gin.RouterGroup) {
guilds.GET("/", listGuilds)
guilds.GET("/:id", getGuildSettingByID)
guilds.PUT("/:id", createOrUpdateGuildSetting)
guilds.GET("/:id/module", isGuildModuleEnabled)
guilds.PATCH("/:id/module", updateGuildModuleSetting)
}
}

Expand Down Expand Up @@ -124,3 +126,106 @@ func createOrUpdateGuildSetting(c *gin.Context) {
}
c.JSON(200, guildSetting)
}

// isGuildModuleEnabled godoc
// @Summary Check if a specific module is enabled
// @Tags Guilds
// @Param id path string true "Guild ID"
// @Param module query string true "Module Name"
// @Success 200 {object} map[string]bool
// @Router /guilds/{id}/module/check [get]
func isGuildModuleEnabled(c *gin.Context) {
id := c.Param("id")
moduleName := c.Query("module")

if moduleName == "" {
c.JSON(400, gin.H{"error": "Module parameter is required"})
return
}

dbRaw, exists := c.Get("db")
if !exists {
c.JSON(500, gin.H{"error": "Database connection not found"})
return
}
db := dbRaw.(*gorm.DB)

var guildSetting model.GuildSetting
result := db.First(&guildSetting, "guild_id = ?", id)

if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
c.JSON(200, gin.H{"module": moduleName, "enabled": false})
} else {
log.Printf("Error: %s", result.Error.Error())
c.JSON(500, gin.H{"error": "Internal server error"})
}
return
}

enabled := false
if guildSetting.EnabledModules != nil {
enabled = guildSetting.EnabledModules[moduleName]
}

c.JSON(200, gin.H{
"guild_id": id,
"module": moduleName,
"enabled": enabled,
})
}

func updateGuildModuleSetting(c *gin.Context) {
id := c.Param("id")

var req dto.UpdateModuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

dbRaw, exists := c.Get("db")
if !exists {
c.JSON(500, gin.H{"error": "Database connection not found"})
return
}
db := dbRaw.(*gorm.DB)

var guildSetting model.GuildSetting
result := db.First(&guildSetting, "guild_id = ?", id)

if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
guildSetting = model.GuildSetting{
GuildID: id,
EnabledModules: map[string]bool{
req.Module: req.Enabled,
},
}

if err := db.Create(&guildSetting).Error; err != nil {
log.Printf("Error: %s", err.Error())
c.JSON(500, gin.H{"error": "Internal server error"})
return
}
} else if result.Error != nil {
log.Printf("Error: %s", result.Error.Error())
c.JSON(500, gin.H{"error": "Internal server error"})
return
}
} else {
if guildSetting.EnabledModules == nil {
guildSetting.EnabledModules = make(map[string]bool)
}

guildSetting.EnabledModules[req.Module] = req.Enabled

if err := db.Save(&guildSetting).Error; err != nil {
log.Printf("Error: %s", err.Error())
c.JSON(500, gin.H{"error": "Internal server error"})
return
}
}

c.JSON(200, guildSetting)
}
Loading
Loading