From 1f880e11d5ddce914c33c41b64042f18c5c90ae8 Mon Sep 17 00:00:00 2001 From: sharkbot-neko Date: Thu, 26 Mar 2026 11:14:12 +0900 Subject: [PATCH 01/14] =?UTF-8?q?fix:=20Readme=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c893c2b..37fdedc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # NewSharkBot SharkBotの書き直し + +# 招待する方法 + +まだ開発中です。公開までしばらくお待ちください。 \ No newline at end of file From 3a6eb988851aaabed3c7a0c2589ae3f6b338460e Mon Sep 17 00:00:00 2001 From: sharkbot-neko Date: Thu, 26 Mar 2026 15:13:19 +0900 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=E3=81=84=E3=81=A3=E3=81=9F?= =?UTF-8?q?=E3=82=93=E4=B8=80=E9=83=A8=E3=82=92goAPI=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- docker-compose.yaml | 10 +- src/api/scripts/run.ps1 | 22 ++ src/api/src/internal/dto/Guild.go | 5 + src/api/src/internal/router/guilds.go | 107 +++++++++ src/bot/cogs/welcome.py | 2 + .../app/api/guilds/[guildId]/modules/route.ts | 6 +- .../src/app/dashboard/[guildId]/client.tsx | 84 ++++++++ .../src/app/dashboard/[guildId]/page.tsx | 203 ++++-------------- src/dashboard/src/app/page.tsx | 2 +- .../src/components/LoadingSkeleton.tsx | 9 + src/dashboard/src/components/ModuleCard.tsx | 52 +++++ src/dashboard/src/constants/api/endpoints.ts | 2 +- src/dashboard/src/lib/api/requests.ts | 68 ++++++ src/dashboard/src/lib/modules.ts | 8 + 15 files changed, 412 insertions(+), 172 deletions(-) create mode 100644 src/api/scripts/run.ps1 create mode 100644 src/dashboard/src/app/dashboard/[guildId]/client.tsx create mode 100644 src/dashboard/src/components/LoadingSkeleton.tsx create mode 100644 src/dashboard/src/components/ModuleCard.tsx create mode 100644 src/dashboard/src/lib/api/requests.ts diff --git a/.gitignore b/.gitignore index ce7c3c0..363fcfc 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,6 @@ vite.config.ts.timestamp-* src/bot/.venv -__pycache__ \ No newline at end of file +__pycache__ + +src/api/build.sh \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index a748baa..d66dac1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,13 +8,16 @@ 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 postgres: @@ -29,6 +32,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: diff --git a/src/api/scripts/run.ps1 b/src/api/scripts/run.ps1 new file mode 100644 index 0000000..d3a14cf --- /dev/null +++ b/src/api/scripts/run.ps1 @@ -0,0 +1,22 @@ +if (-not (Test-Path ".\cmd\main.go")) { + Write-Host "Please run this script from the src/ directory." -ForegroundColor Red + exit 1 +} + +$COMMIT = git rev-parse --short HEAD +$BRANCH = git branch --show-current + +swag i -g cmd/main.go + +if ($args -contains "--dev") { + $env:DB_DSN = "host=localhost user=postgres password=postgres dbname=devdb port=5432 sslmode=disable" + $env:GIN_MODE = "debug" + + go run -ldflags "-X github.com/UniPro-tech/UniQUE-API/internal/config.GitCommit=$COMMIT -X github.com/UniPro-tech/UniQUE-API/internal/config.GitBranch=$BRANCH" cmd/main.go +} +else { + $VERSION = git describe --tags --abbrev=0 + $env:GIN_MODE = "release" + + go build -ldflags "-X github.com/SharkBot-Dev/NewSharkBot/api/internal.Version=$VERSION -X github.com/SharkBot-Dev/NewSharkBot/api/internal.GitCommit=$COMMIT -X github.com/SharkBot-Dev/NewSharkBot/api/internal.GitBranch=$BRANCH" cmd/main.go +} \ No newline at end of file diff --git a/src/api/src/internal/dto/Guild.go b/src/api/src/internal/dto/Guild.go index 19f6df9..91494b5 100644 --- a/src/api/src/internal/dto/Guild.go +++ b/src/api/src/internal/dto/Guild.go @@ -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"` +} diff --git a/src/api/src/internal/router/guilds.go b/src/api/src/internal/router/guilds.go index 066999b..ef5970a 100644 --- a/src/api/src/internal/router/guilds.go +++ b/src/api/src/internal/router/guilds.go @@ -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) } } @@ -124,3 +126,108 @@ 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) + + log.Printf("Name: %s", req.Module) + + 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) +} diff --git a/src/bot/cogs/welcome.py b/src/bot/cogs/welcome.py index e011d3e..246b369 100644 --- a/src/bot/cogs/welcome.py +++ b/src/bot/cogs/welcome.py @@ -17,6 +17,7 @@ def welcome_parse(self, template: str, member: discord.Member) -> str: @commands.Cog.listener() async def on_member_join(self, member: discord.Member): + return guild_id = str(member.guild.id) data = await self.bot.async_db["SharkBot"]["welcome_setting"].find_one({"guildId": guild_id}) @@ -58,6 +59,7 @@ async def on_member_join(self, member: discord.Member): @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): + return guild_id = str(member.guild.id) data = await self.bot.async_db["SharkBot"]["welcome_setting"].find_one({"guildId": guild_id}) diff --git a/src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts b/src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts index 1eb3f83..f75a681 100644 --- a/src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts +++ b/src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts @@ -73,14 +73,14 @@ export async function POST( try { const response = await fetch( - `${RESOURCE_API_BASE_URL}/guilds/${guildId}/modules`, + `${RESOURCE_API_BASE_URL}/guilds/${guildId}/module`, { - method: "POST", + method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify( - serializeRequest({ enabledModules: { [moduleId]: enabled } }), + serializeRequest({ enabled: enabled, module: moduleId }), ), }, ); diff --git a/src/dashboard/src/app/dashboard/[guildId]/client.tsx b/src/dashboard/src/app/dashboard/[guildId]/client.tsx new file mode 100644 index 0000000..6d98743 --- /dev/null +++ b/src/dashboard/src/app/dashboard/[guildId]/client.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { NoIconModuleSetting, type ModuleSetting } from "@/lib/modules"; +import ModuleCard from "@/components/ModuleCard"; +import { modules as modules_list } from "@/lib/modules"; + +export default function ModuleList({ + guildId, + initialModules +}: { + guildId: string, + initialModules: NoIconModuleSetting[] +}) { + const [modules, setModules] = useState(initialModules); + + const modulesWithIcons = useMemo(() => { + return modules.map(m => { + const original = modules_list.get(m.id); + return { + ...m, + icon: original?.icon + }; + }); + }, [modules]); + + const groupedModules = useMemo(() => { + return modulesWithIcons.reduce((acc, mod) => { + const group = mod.group || "その他"; + if (!acc[group]) acc[group] = []; + acc[group].push(mod); + return acc; + }, {} as Record); + }, [modulesWithIcons]); + + const toggleModule = async (moduleId: string) => { + const target = modules.find((m) => m.id === moduleId); + if (!target) return; + + const newState = !target.enabled; + + setModules((prev) => + prev.map((m) => (m.id === moduleId ? { ...m, enabled: newState } : m)) + ); + + try { + const res = await fetch(`/api/guilds/${guildId}/modules`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + moduleId: moduleId, + enabled: newState + }), + }); + + if (!res.ok) throw new Error("保存失敗"); + + } catch (error) { + console.error("Failed to update module:", error); + alert("設定の保存に失敗しました"); + + setModules((prev) => + prev.map((m) => (m.id === moduleId ? { ...m, enabled: !newState } : m)) + ); + } + }; + + return ( + <> + {Object.entries(groupedModules).map(([groupName, items]) => ( +
+

+ {groupName} +

+
+ {items.map((mod) => ( + + ))} +
+
+ ))} + + ); +} \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/page.tsx b/src/dashboard/src/app/dashboard/[guildId]/page.tsx index afd5d45..fd5e3d7 100644 --- a/src/dashboard/src/app/dashboard/[guildId]/page.tsx +++ b/src/dashboard/src/app/dashboard/[guildId]/page.tsx @@ -1,180 +1,53 @@ -"use client"; +import { Suspense } from "react"; +import { redirect } from "next/navigation"; +import { modules as modules_list } from "@/lib/modules"; +import ModuleCard from "@/components/ModuleCard"; +import { fetchGuildSettings } from "@/lib/api/requests"; +import ModuleList from "./client"; +import LoadingSkeleton from "@/components/LoadingSkeleton"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState, useMemo } from "react"; -import { type ModuleSetting, modules as modules_list } from "@/lib/modules"; +export default async function DashboardPage({ params }: { params: { guildId: string } }) { + const { guildId } = await params; -const ADMIN_PERMISSION = 0x8; - -export default function DashboardPage() { - const params = useParams(); - const router = useRouter(); - const guildId = params.guildId as string; - - const [loading, setLoading] = useState(true); - const [modules, setModules] = useState( - Array.from(modules_list.values()) - ); - - const groupedModules = useMemo(() => { - return modules.reduce((acc, mod) => { - const group = mod.group || "その他"; - if (!acc[group]) acc[group] = []; - acc[group].push(mod); - return acc; - }, {} as Record); - }, [modules]); - - useEffect(() => { - async function init() { - setLoading(true); - try { - const guildRes = await fetch("/api/discord/guilds"); - if (!guildRes.ok) throw new Error("認証に失敗しました"); - - const userGuilds = await guildRes.json(); - const targetGuild = userGuilds.find((g: any) => g.id === guildId); - - const hasPermission = - targetGuild && - (BigInt(targetGuild.permissions) & BigInt(ADMIN_PERMISSION)) === - BigInt(ADMIN_PERMISSION); - - if (!hasPermission) { - alert("このサーバーを管理する権限がありません。"); - router.push("/dashboard"); - return; - } - - const res = await fetch(`/api/guilds/${guildId}/modules`); - if (!res.ok) throw new Error("データの取得に失敗しました"); - - const data = await res.json(); - - if (data.modules) { - setModules((prev) => - prev.map((m) => ({ - ...m, - enabled: !!data.modules[m.id], - })) - ); - } - } catch (error) { - console.error("Initialization error:", error); - router.push("/dashboard"); - } finally { - setLoading(false); - } - } - - init(); - }, [guildId, router]); - - const toggleModule = async (moduleId: string) => { - const target = modules.find((m) => m.id === moduleId); - if (!target) return; - - const newState = !target.enabled; - - setModules((prev) => - prev.map((m) => (m.id === moduleId ? { ...m, enabled: newState } : m)) - ); - - try { - const res = await fetch(`/api/guilds/${guildId}/modules`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ moduleId, enabled: newState }), - }); - - if (!res.ok) throw new Error("保存失敗"); - } catch { - alert("設定の保存に失敗しました"); - setModules((prev) => - prev.map((m) => (m.id === moduleId ? { ...m, enabled: !newState } : m)) - ); - } - }; - - if (loading) { - return ( -
-
-
- ); - } + const dataPromise = fetchGuildSettings(guildId); return (
-
+

サーバー管理

ID: {guildId}

- -
- - {Object.entries(groupedModules).map(([groupName, items]) => ( -
-

- - {items.length} - - {groupName} -

- -
- {items.map((mod) => ( -
- -
-
- ))} -
- - ))} + }> + +
); +} + +async function ModuleListWrapper({ guildId, dataPromise }: { guildId: string, dataPromise: Promise }) { + const data = await dataPromise; + + if (!data) { + redirect("/dashboard"); + } + + const initialModules = Array.from(modules_list.values()).map(m => ({ + id: m.id, + name: m.name, + description: m.description, + group: m.group, + enabled: !!(data.EnabledModules && data.EnabledModules[m.id]) + })); + + return ( + + ); } \ No newline at end of file diff --git a/src/dashboard/src/app/page.tsx b/src/dashboard/src/app/page.tsx index 956708f..9ee2557 100644 --- a/src/dashboard/src/app/page.tsx +++ b/src/dashboard/src/app/page.tsx @@ -4,7 +4,7 @@ export default async function Page() { return (
-

SharkBot

+

SudoBot

あなたのサーバーを便利にするBot

diff --git a/src/dashboard/src/components/LoadingSkeleton.tsx b/src/dashboard/src/components/LoadingSkeleton.tsx new file mode 100644 index 0000000..7344d5f --- /dev/null +++ b/src/dashboard/src/components/LoadingSkeleton.tsx @@ -0,0 +1,9 @@ +"use client"; + +export default function LoadingSkeleton() { + return ( +
+
+
+ ); +} diff --git a/src/dashboard/src/components/ModuleCard.tsx b/src/dashboard/src/components/ModuleCard.tsx new file mode 100644 index 0000000..d2e8c00 --- /dev/null +++ b/src/dashboard/src/components/ModuleCard.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { type ModuleSetting } from "@/lib/modules"; + +interface ModuleCardProps { + mod: ModuleSetting; + guildId: string; + onToggle?: (moduleId: string) => Promise; +} + +export default function ModuleCard({ mod, guildId, onToggle }: ModuleCardProps) { + const router = useRouter(); + + return ( +
router.push(`/dashboard/${guildId}/${mod.id}`)} + > +
+

+ {mod.name} +

+

+ {mod.description} +

+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/dashboard/src/constants/api/endpoints.ts b/src/dashboard/src/constants/api/endpoints.ts index 5acb784..ed4ce7b 100644 --- a/src/dashboard/src/constants/api/endpoints.ts +++ b/src/dashboard/src/constants/api/endpoints.ts @@ -1,2 +1,2 @@ export const RESOURCE_API_BASE_URL = - process.env.RESOURCE_API_BASE_URL || "http://localhost:8080/api"; + process.env.RESOURCE_API_BASE_URL || "http://localhost:8080"; diff --git a/src/dashboard/src/lib/api/requests.ts b/src/dashboard/src/lib/api/requests.ts new file mode 100644 index 0000000..3fb010f --- /dev/null +++ b/src/dashboard/src/lib/api/requests.ts @@ -0,0 +1,68 @@ +import { RESOURCE_API_BASE_URL } from "@/constants/api/endpoints"; + +const isValidDiscordId = (id: string) => /^\d{17,20}$/.test(id); + +export async function createGuildEntry(guildId: string) { + if (!isValidDiscordId(guildId)) { + throw new Error("Invalid Guild ID"); + } + const data = await fetch(`${RESOURCE_API_BASE_URL}/guilds/${guildId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ id: guildId, EnabledModules: { + "help": true, + } }) + }); + if (!data.ok) { + throw new Error(`Failed to create guild entry: ${data.statusText}`); + } + return data.json(); + +} + +export async function fetchGuildSettings(guildId: string) { + if (!isValidDiscordId(guildId)) { + throw new Error("Invalid Guild ID"); + } + if (!guildId) { + throw new Error("Guild ID is required"); + } + const data = await fetch(`${RESOURCE_API_BASE_URL}/guilds/${guildId}`); + if (!data.ok) { + await createGuildEntry(guildId); + return; + } + return data.json(); +} + +export async function isModuleEnabled(guildId: string, moduleName: string) { + if (!isValidDiscordId(guildId)) { + throw new Error("Invalid Guild ID"); + } + if (!guildId) { + throw new Error("Guild ID is required"); + } + const data = await fetch(`${RESOURCE_API_BASE_URL}/guilds/${guildId}/module?module=${encodeURIComponent(moduleName)}`); + if (!data.ok) { + throw new Error(`Failed to fetch module status: ${data.statusText}`); + } + return data.json(); +} + +export async function setModuleStatus(guildId: string, moduleName: string) { + if (!isValidDiscordId(guildId)) { + throw new Error("Invalid Guild ID"); + } + if (!guildId) { + throw new Error("Guild ID is required"); + } + const data = await fetch(`${RESOURCE_API_BASE_URL}/guilds/${guildId}/module?module=${encodeURIComponent(moduleName)}`, { + method: "PATCH", + }); + if (!data.ok) { + throw new Error(`Failed to update module status: ${data.statusText}`); + } + return data.json(); +} \ No newline at end of file diff --git a/src/dashboard/src/lib/modules.ts b/src/dashboard/src/lib/modules.ts index 05e190b..e391cc0 100644 --- a/src/dashboard/src/lib/modules.ts +++ b/src/dashboard/src/lib/modules.ts @@ -9,6 +9,14 @@ export interface ModuleSetting { group?: string; } +export interface NoIconModuleSetting { + id: string; + name: string; + description: string; + enabled: boolean; + group?: string; +} + export const modules = new Map([ [ "test", From e29e8bb2102d13104ffa454f1deb76d1cfd1aa42 Mon Sep 17 00:00:00 2001 From: sharkbot-neko Date: Thu, 26 Mar 2026 16:01:50 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix:=20=E5=9F=8B=E3=82=81=E8=BE=BC?= =?UTF-8?q?=E3=81=BF=E8=A8=AD=E5=AE=9A=E3=82=92go=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/src/cmd/main.go | 4 +- src/api/src/internal/model/EmbedSetting.go | 34 +++ src/api/src/internal/model/MessageSetting.go | 17 ++ src/api/src/internal/router/embed.go | 110 ++++++++ src/api/src/internal/router/message.go | 106 ++++++++ .../guilds/[guildId]/modules/embed/route.ts | 247 ++++++------------ .../[guildId]/embed/EmbedEditorClient.tsx | 136 ++++++++++ .../app/dashboard/[guildId]/embed/page.tsx | 193 ++------------ .../src/app/dashboard/[guildId]/help/page.tsx | 74 ++---- .../src/app/dashboard/[guildId]/test/page.tsx | 88 +++---- src/dashboard/src/components/Alert.tsx | 25 ++ src/dashboard/src/lib/api/requests.ts | 92 +++++++ 12 files changed, 689 insertions(+), 437 deletions(-) create mode 100644 src/api/src/internal/model/EmbedSetting.go create mode 100644 src/api/src/internal/model/MessageSetting.go create mode 100644 src/api/src/internal/router/embed.go create mode 100644 src/api/src/internal/router/message.go create mode 100644 src/dashboard/src/app/dashboard/[guildId]/embed/EmbedEditorClient.tsx create mode 100644 src/dashboard/src/components/Alert.tsx diff --git a/src/api/src/cmd/main.go b/src/api/src/cmd/main.go index 6d74515..b8906fb 100644 --- a/src/api/src/cmd/main.go +++ b/src/api/src/cmd/main.go @@ -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") } @@ -73,6 +73,8 @@ func main() { // ルートグループを登録 router.RegisterGuildsRoutes(r.Group("/")) + router.RegisterEmbed(r.Group("/")) + router.RegisterMessageSettings(r.Group("/")) // シンプルなGETエンドポイントを定義 r.GET("/health", healthCheck) diff --git a/src/api/src/internal/model/EmbedSetting.go b/src/api/src/internal/model/EmbedSetting.go new file mode 100644 index 0000000..a103285 --- /dev/null +++ b/src/api/src/internal/model/EmbedSetting.go @@ -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 +} diff --git a/src/api/src/internal/model/MessageSetting.go b/src/api/src/internal/model/MessageSetting.go new file mode 100644 index 0000000..40df98b --- /dev/null +++ b/src/api/src/internal/model/MessageSetting.go @@ -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"` + + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/src/api/src/internal/router/embed.go b/src/api/src/internal/router/embed.go new file mode 100644 index 0000000..9a3cd21 --- /dev/null +++ b/src/api/src/internal/router/embed.go @@ -0,0 +1,110 @@ +package router + +import ( + "net/http" + + "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/:name", 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) { + id := c.Param("id") + name := c.Param("name") + db := c.MustGet("db").(*gorm.DB) + + result := db.Where("guild_id = ? AND name = ?", id, name).Delete(&model.EmbedSetting{}) + if result.RowsAffected == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "Setting not found"}) + return + } + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Deleted successfully"}) +} diff --git a/src/api/src/internal/router/message.go b/src/api/src/internal/router/message.go new file mode 100644 index 0000000..ce5a965 --- /dev/null +++ b/src/api/src/internal/router/message.go @@ -0,0 +1,106 @@ +package router + +import ( + "net/http" + + "github.com/SharkBot-Dev/NewSharkBot/api/internal/model" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterMessageSettings(router *gin.RouterGroup) { + // /guilds/message/welcome や /guilds/message/goodbye でアクセス + guilds := router.Group("/guilds/message") + { + guilds.GET("/:id/:type", getMessageSetting) // 取得 + guilds.POST("/:id/:type", saveMessageSetting) // 作成・更新 (Upsert) + guilds.DELETE("/:id/:type", deleteMessageSetting) // 削除 + } +} + +// 取得 (Welcome または Goodbye) +func getMessageSetting(c *gin.Context) { + id := c.Param("id") + msgType := c.Param("type") // "welcome" or "goodbye" + + db := c.MustGet("db").(*gorm.DB) + var setting model.MessageSetting + + // Embedデータも一緒に取得するために Preload を使用 + result := db.Preload("Embed").Where("guild_id = ? AND type = ?", id, msgType).First(&setting) + + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Setting not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + } + return + } + c.JSON(http.StatusOK, setting) +} + +// 保存 (作成・更新) +func saveMessageSetting(c *gin.Context) { + id := c.Param("id") + msgType := c.Param("type") + db := c.MustGet("db").(*gorm.DB) + + var input struct { + ChannelID string `json:"channel_id"` + Content string `json:"content"` + EmbedID *uint `json:"embed_id"` // nullを許容 + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var setting model.MessageSetting + // 既存レコードを探す + result := db.Where("guild_id = ? AND type = ?", id, msgType).First(&setting) + + // フィールド更新 + setting.GuildID = id + setting.Type = msgType + setting.ChannelID = input.ChannelID + setting.Content = input.Content + setting.EmbedID = input.EmbedID + + if result.Error == gorm.ErrRecordNotFound { + // 新規作成 + if err := db.Create(&setting).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create setting"}) + return + } + } else { + // 更新 + if err := db.Save(&setting).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update setting"}) + return + } + } + + c.JSON(http.StatusOK, setting) +} + +// 削除 +func deleteMessageSetting(c *gin.Context) { + id := c.Param("id") + msgType := c.Param("type") + db := c.MustGet("db").(*gorm.DB) + + result := db.Where("guild_id = ? AND type = ?", id, msgType).Delete(&model.MessageSetting{}) + + if result.RowsAffected == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "Setting not found"}) + return + } + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": msgType + " setting deleted successfully"}) +} diff --git a/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts b/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts index 0db5d72..2d9b287 100644 --- a/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts +++ b/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts @@ -1,204 +1,113 @@ import { auth } from "@/lib/auth"; -import { checkAdminPermission } from "@/lib/discord"; -import clientPromise from "@/lib/mongodb"; +import { checkAdminPermission } from "@/lib/Discord/User"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; -export async function GET( - _request: Request, - { params }: { params: Promise<{ guildId: string }> }, -) { - try { - const { guildId } = await params; - let discordToken: { - accessToken: string; - accessTokenExpiresAt: Date | undefined; - scopes: string[]; - idToken: string | undefined; - }; - try { - const allLinkedAccounts = await auth.api.listUserAccounts({ +const BACKEND_URL = process.env.BACKEND_API_URL || "http://localhost:8080"; + +/** + * 共通の認証・権限チェック関数 + */ +async function validateAdmin(guildId: string) { + const allLinkedAccounts = await auth.api.listUserAccounts({ headers: await headers(), - }); - const discordAccountData = allLinkedAccounts.find( - (account) => account.providerId === "discord", - ); - if (!discordAccountData) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - discordToken = await auth.api.getAccessToken({ + }); + const discordAccountData = allLinkedAccounts.find( + (account) => account.providerId === "discord" + ); + + if (!discordAccountData) throw new Error("Unauthorized"); + + const discordToken = await auth.api.getAccessToken({ headers: await headers(), body: { providerId: "discord", accountId: discordAccountData.accountId, userId: discordAccountData.userId, }, - }); - } catch (e) { - console.log("Error fetching access token:", e); - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + }); - if ( - !discordToken.accessTokenExpiresAt || - Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime() - ) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + if (!discordToken.accessToken || !discordToken.accessTokenExpiresAt || + Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime()) { + throw new Error("Unauthorized"); } - const hasPermission = await checkAdminPermission( - guildId, - discordToken.accessToken, - ); - if (!hasPermission) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } + const hasPermission = await checkAdminPermission(guildId, discordToken.accessToken); + if (!hasPermission) throw new Error("Forbidden"); - const client = await clientPromise; - const db = client.db("SharkBot"); - const settings = await db.collection("embed_setting").findOne({ guildId }); - - return NextResponse.json({ enabled: !!settings, settings: settings?.embeds || {} }); - } catch (error) { - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } + return discordToken; } +export async function GET( + _request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + // Goサーバーから一覧を取得 + const res = await fetch(`${BACKEND_URL}/guilds/embeds/${guildId}`, { + cache: 'no-store' + }); + + if (!res.ok) return NextResponse.json({ error: "Backend error" }, { status: res.status }); + + const data = await res.json(); + return NextResponse.json({ enabled: true, settings: data }); + } catch (error: any) { + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status }); + } +} export async function POST( - request: Request, - { params }: { params: Promise<{ guildId: string }> } + request: Request, + { params }: { params: Promise<{ guildId: string }> } ) { - try { - const { guildId } = await params; - let discordToken: { - accessToken: string; - accessTokenExpiresAt: Date | undefined; - scopes: string[]; - idToken: string | undefined; - }; try { - const allLinkedAccounts = await auth.api.listUserAccounts({ - headers: await headers(), - }); - const discordAccountData = allLinkedAccounts.find( - (account) => account.providerId === "discord", - ); - if (!discordAccountData) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - discordToken = await auth.api.getAccessToken({ - headers: await headers(), - body: { - providerId: "discord", - accountId: discordAccountData.accountId, - userId: discordAccountData.userId, - }, + const { guildId } = await params; + await validateAdmin(guildId); + + const body = await request.json(); + + // Goサーバーへ保存リクエスト + const res = await fetch(`${BACKEND_URL}/guilds/embeds/${guildId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: body.title, // Go側は Name フィールド + data: body // Dictデータとして全体を渡す + }), }); - } catch (e) { - console.log("Error fetching access token:", e); - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - if ( - !discordToken.accessTokenExpiresAt || - Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime() - ) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } + if (!res.ok) return NextResponse.json({ error: "Failed to save to backend" }, { status: res.status }); - const hasPermission = await checkAdminPermission( - guildId, - discordToken.accessToken, - ); - if (!hasPermission) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + return NextResponse.json({ success: true, message: "Settings saved!" }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); } - - const body = await request.json(); - - const client = await clientPromise; - const db = client.db("SharkBot"); - - await db - .collection("embed_setting") - .updateOne( - { guildId }, - { $set: { [`embeds.${body.title}`]: body } }, - { upsert: true }, - ); - - return NextResponse.json({ success: true, message: "Settings saved!" }); - } catch (error) { - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } } export async function DELETE( - request: Request, - { params }: { params: Promise<{ guildId: string }> } + request: Request, + { params }: { params: Promise<{ guildId: string }> } ) { - try { - const { guildId } = await params; - let discordToken: { - accessToken: string; - accessTokenExpiresAt: Date | undefined; - scopes: string[]; - idToken: string | undefined; - }; try { - const allLinkedAccounts = await auth.api.listUserAccounts({ - headers: await headers(), - }); - const discordAccountData = allLinkedAccounts.find( - (account) => account.providerId === "discord", - ); - if (!discordAccountData) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - discordToken = await auth.api.getAccessToken({ - headers: await headers(), - body: { - providerId: "discord", - accountId: discordAccountData.accountId, - userId: discordAccountData.userId, - }, - }); - } catch (e) { - console.log("Error fetching access token:", e); - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const { guildId } = await params; + await validateAdmin(guildId); - if ( - !discordToken.accessTokenExpiresAt || - Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime() - ) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - - const hasPermission = await checkAdminPermission( - guildId, - discordToken.accessToken, - ); + const body = await request.json(); // { title: "..." } - if (!hasPermission) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - - const body = await request.json(); - - const client = await clientPromise; - const db = client.db("SharkBot"); + // Goサーバーへ削除リクエスト + const res = await fetch(`${BACKEND_URL}/guilds/embeds/${guildId}/${body.title}`, { + method: "DELETE", + }); - await db - .collection("embed_setting") - .updateOne( - { guildId }, - { $unset: { [`embeds.${body.title}`]: "" } }, - ); + if (!res.ok) return NextResponse.json({ error: "Failed to delete from backend" }, { status: res.status }); - return NextResponse.json({ success: true, message: "Embed deleted!" }); - } catch (error) { - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } + return NextResponse.json({ success: true, message: "Embed deleted!" }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } } \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/embed/EmbedEditorClient.tsx b/src/dashboard/src/app/dashboard/[guildId]/embed/EmbedEditorClient.tsx new file mode 100644 index 0000000..c61429a --- /dev/null +++ b/src/dashboard/src/app/dashboard/[guildId]/embed/EmbedEditorClient.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { Terminal, Save } from "lucide-react"; +import { useState } from "react"; +import DiscordEmbedBuilder from "@/components/EmbedBuilder"; +import CollapsibleSection from "@/components/CollapsibleSection"; +import { EmbedSetting } from "@/lib/api/requests"; // 型定義 + +interface Props { + guildId: string; + initialEmbeds: EmbedSetting[]; // すでにGoバックエンドから取得済みのリスト +} + +export default function EmbedEditorClient({ guildId, initialEmbeds }: Props) { + const [savedEmbeds, setSavedEmbeds] = useState(initialEmbeds); + const [currentEmbedData, setCurrentEmbedData] = useState(null); + const [saving, setSaving] = useState(false); + + // 保存処理 (Next.jsのAPI Route /api/guilds/[id]/modules/embed を叩く) + const handleSave = async () => { + // Builder側で入力されたタイトルをNameとして扱う + const name = currentEmbedData?.title; + if (!name) { + alert("埋め込みのタイトルを入力してください。"); + return; + } + + setSaving(true); + try { + // 修正したNext.jsのAPI RouteへPOST + const response = await fetch(`/api/guilds/${guildId}/modules/embed`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(currentEmbedData), // Titleを含むEmbed Dict全体を送信 + }); + + if (!response.ok) throw new Error("Failed to save"); + + // Go側から返ってくる構造に合わせてリストを更新 + const newSetting: EmbedSetting = { + guild_id: guildId, + name: name, + data: currentEmbedData, + }; + + setSavedEmbeds((prev) => { + const filtered = prev.filter((e) => e.name !== name); + return [...filtered, newSetting]; + }); + + alert("埋め込みを保存しました!"); + } catch (error) { + console.error(error); + alert("保存中にエラーが発生しました。"); + } finally { + setSaving(false); + } + }; + + // 削除処理 + const handleDelete = async (name: string) => { + if (!confirm(`埋め込み「${name}」を削除してもよろしいですか?`)) return; + + try { + const response = await fetch(`/api/guilds/${guildId}/modules/embed`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: name }), + }); + + if (response.ok) { + setSavedEmbeds((prev) => prev.filter((e) => e.name !== name)); + } else { + alert("削除に失敗しました。"); + } + } catch (error) { + console.error("Delete error:", error); + } + }; + + return ( +
+
+ +
+ +
+
+ +

エディター

+
+ + +
+ + +
+ {savedEmbeds.length === 0 ? ( +
+

保存されたテンプレートはありません。

+
+ ) : ( + savedEmbeds.map((embed) => ( +
+
+

{embed.name}

+

+ {typeof embed.data.description === 'string' ? embed.data.description : "説明なし"} +

+
+
+ +
+
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx b/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx index 5487123..6530e4a 100644 --- a/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx +++ b/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx @@ -1,180 +1,43 @@ -"use client"; +import { Suspense } from "react"; +import { fetchEmbedSettings } from "@/lib/api/requests"; // ラッパー関数の場所 +import EmbedEditorClient from "./EmbedEditorClient"; +import LoadingSkeleton from "@/components/LoadingSkeleton"; -import { Terminal, Save } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import DiscordEmbedBuilder from "@/components/EmbedBuilder"; -import CollapsibleSection from "@/components/CollapsibleSection"; +interface Props { + params: { guildId: string }; +} -export default function WelcomeGoodbyeModulePage() { - const params = useParams(); - const router = useRouter(); - const guildId = params.guildId as string; - - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - - // 現在ビルダーで編集中のデータ - const [currentEmbed, setCurrentEmbed] = useState(null); - // 保存済みの埋め込みリスト - const [savedEmbeds, setSavedEmbeds] = useState([]); - - useEffect(() => { - async function init() { - try { - // モジュール有効化チェック - const statusRes = await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=embed`); - const statusData = await statusRes.json(); - - if (!statusData.enabled) { - alert("このサーバーではモジュールが有効になっていません。"); - router.push(`/dashboard/${guildId}`); - return; - } - - // 埋め込みデータの取得 - const res = await fetch(`/api/guilds/${guildId}/modules/embed`); - const data = await res.json(); - - if (res.ok) { - setSavedEmbeds(Object.values(data.settings || {})); - } - } catch (e) { - console.error("Failed to load settings", e); - } finally { - setLoading(false); - } - } - init(); - }, [guildId, router]); - - const handleSave = async () => { - if (!currentEmbed || !currentEmbed.title) { - alert("埋め込みのタイトルを入力してください。"); - return; - } - - setSaving(true); - try { - const response = await fetch(`/api/guilds/${guildId}/modules/embed`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(currentEmbed), - }); - - if (response.ok) { - setSavedEmbeds((prev) => { - const filtered = prev.filter((e) => e.title !== currentEmbed.title); - return [...filtered, currentEmbed]; - }); - alert("埋め込みを保存しました!"); - } else { - throw new Error("Failed to save"); - } - } catch (error) { - alert("保存中にエラーが発生しました。"); - } finally { - setSaving(false); - } - }; - - const handleEmbedDelete = async (title: string) => { - if (!confirm(`埋め込み「${title}」を削除してもよろしいですか?`)) return; - - try { - const response = await fetch(`/api/guilds/${guildId}/modules/embed`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title }), - }); - - if (response.ok) { - setSavedEmbeds((prev) => prev.filter((e) => e.title !== title)); - } else { - alert("削除に失敗しました。"); - } - } catch (error) { - console.error("Delete error:", error); - } - }; - - const handleEditClick = (embed: any) => { - setCurrentEmbed(embed); - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - - if (loading) { - return ( -
-
-
- ); - } +export default async function WelcomeGoodbyeModulePage({ params }: Props) { + const { guildId } = await params; return (
-
-
-

埋め込み作成モジュール

-

特定のタイトルで埋め込みを保存・管理できます。

-
- - +
+

埋め込み作成モジュール

+

特定のタイトルで埋め込みを保存・管理できます。

-
-
-
- -

エディター

-
- - -
- - -
- {savedEmbeds.length === 0 ? ( -
-

保存された埋め込みはまだありません。

-
- ) : ( - savedEmbeds.map((embed: any) => ( -
-
-

{embed.title}

-

{embed.description || "説明なし"}

-
-
- -
-
- )) - )} -
-
-
+ {/* データの取得とエディター部分を分離 */} + }> + +

- ※ 同じタイトルの埋め込みを保存すると、上書きされます。 + ※ 同じ名前の埋め込みを保存すると、上書きされます。

); +} + +async function EmbedContent({ guildId }: { guildId: string }) { + const initialEmbeds = await fetchEmbedSettings(guildId); + + return ( + + ); } \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/help/page.tsx b/src/dashboard/src/app/dashboard/[guildId]/help/page.tsx index a48d523..63fb385 100644 --- a/src/dashboard/src/app/dashboard/[guildId]/help/page.tsx +++ b/src/dashboard/src/app/dashboard/[guildId]/help/page.tsx @@ -1,9 +1,11 @@ -"use client"; - +import { Suspense } from "react"; +import { redirect } from "next/navigation"; import { Terminal } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; import CommandsControl from "@/components/commands"; +import LoadingSkeleton from "@/components/LoadingSkeleton"; + +import { isModuleEnabled } from "@/lib/api/requests"; +import Alert from "@/components/Alert"; const commands = [ { @@ -24,50 +26,27 @@ const commands = [ }, ]; -export default function HelpModuleSetting() { - const params = useParams(); - const router = useRouter(); - const guildId = params.guildId as string; +export default async function HelpModuleSetting({ params }: { params: { guildId: string } }) { + const { guildId } = await params; + try { + const data = await isModuleEnabled(guildId, "help"); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function init() { - (await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=help`)) - .json() - .then((data) => { - if (data.enabled) { - setLoading(false); - } else { - alert("このサーバーではモジュールが有効になっていません。"); - router.push(`/dashboard/${guildId}`); - } - }); + if (!data.enabled) { + return ; } - init(); - }, [guildId, router]); - - if (loading) { - return ( -
-
-
- ); + } catch (error) { + redirect(`/dashboard/${guildId}`); } return (
-
-
-

- ヘルプモジュール -

-

- サーバーで使用するスラッシュコマンドの有効・無効を切り替えます。 -

-
-
+
+

ヘルプモジュール

+

+ サーバーで使用するスラッシュコマンドの有効・無効を切り替えます。 +

+

@@ -80,17 +59,18 @@ export default function HelpModuleSetting() {
-
- -
+ }> +
+ +
+

- ※ - 反映まで数分かかる場合があります。同期が完了しない場合はページを更新してください。 + ※ 反映まで数分かかる場合があります。同期が完了しない場合はページを更新してください。

); -} +} \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/test/page.tsx b/src/dashboard/src/app/dashboard/[guildId]/test/page.tsx index 140e439..c1f91c7 100644 --- a/src/dashboard/src/app/dashboard/[guildId]/test/page.tsx +++ b/src/dashboard/src/app/dashboard/[guildId]/test/page.tsx @@ -1,54 +1,40 @@ -"use client"; - +import { Suspense } from "react"; +import { redirect } from "next/navigation"; import { Terminal } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; import CommandsControl from "@/components/commands"; +import LoadingSkeleton from "@/components/LoadingSkeleton"; -export default function TestModuleSetting() { - const params = useParams(); - const router = useRouter(); - const guildId = params.guildId as string; +import { isModuleEnabled } from "@/lib/api/requests"; +import Alert from "@/components/Alert"; - const [loading, setLoading] = useState(true); +const commands = [ + { + name: "test", + description: "テストと返します。動作確認用に使用してください。", + }, +]; - useEffect(() => { - async function init() { - (await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=test`)) - .json() - .then((data) => { - if (data.enabled) { - setLoading(false); - } else { - alert("このサーバーではモジュールが有効になっていません。"); - router.push(`/dashboard/${guildId}`); - } - }); - } - init(); - }, [guildId, router]); +export default async function TestModuleSetting({ params }: { params: { guildId: string } }) { + const { guildId } = await params; + try { + const data = await isModuleEnabled(guildId, "test"); - if (loading) { - return ( -
-
-
- ); + if (!data.enabled) { + return ; + } + } catch (error) { + redirect(`/dashboard/${guildId}`); } return (
-
-
-

- テストモジュール -

-

- サーバーで使用するスラッシュコマンドの有効・無効を切り替えます。 -

-
-
+
+

テストモジュール

+

+ サーバーで使用するスラッシュコマンドの有効・無効を切り替えます。 +

+

@@ -61,26 +47,18 @@ export default function TestModuleSetting() {
-
- -
+ }> +
+ +
+

- ※ - 反映まで数分かかる場合があります。同期が完了しない場合はページを更新してください。 + ※ 反映まで数分かかる場合があります。同期が完了しない場合はページを更新してください。

); -} +} \ No newline at end of file diff --git a/src/dashboard/src/components/Alert.tsx b/src/dashboard/src/components/Alert.tsx new file mode 100644 index 0000000..7cf86d9 --- /dev/null +++ b/src/dashboard/src/components/Alert.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +interface Props { + text: string; + redirectUrl?: string; +} + +export default function Alert({ text, redirectUrl }: Props) { + const router = useRouter(); + useEffect(() => { + alert(text); + if (redirectUrl) { + router.push(redirectUrl); + } else { + router.push("/dashboard"); + } + }, [router]); + + return ( +

...

+ ); +} \ No newline at end of file diff --git a/src/dashboard/src/lib/api/requests.ts b/src/dashboard/src/lib/api/requests.ts index 3fb010f..f6345a2 100644 --- a/src/dashboard/src/lib/api/requests.ts +++ b/src/dashboard/src/lib/api/requests.ts @@ -1,5 +1,23 @@ import { RESOURCE_API_BASE_URL } from "@/constants/api/endpoints"; +export interface EmbedSetting { + id?: number; + guild_id: string; + name: string; + data: Record; // DiscordのEmbed Dict + updated_at?: string; +} + +export interface MessageSetting { + guild_id: string; + type: 'welcome' | 'goodbye'; + channel_id: string; + content: string; + embed_id: number | null; + embed?: EmbedSetting; // Preloadされたデータ用 + updated_at?: string; +} + const isValidDiscordId = (id: string) => /^\d{17,20}$/.test(id); export async function createGuildEntry(guildId: string) { @@ -65,4 +83,78 @@ export async function setModuleStatus(guildId: string, moduleName: string) { throw new Error(`Failed to update module status: ${data.statusText}`); } return data.json(); +} + +export async function fetchMessageSetting(guildId: string, type: 'welcome' | 'goodbye'): Promise { + if (!isValidDiscordId(guildId)) throw new Error("Invalid Guild ID"); + + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/message/${guildId}/${type}`); + + if (response.status === 404) return null; + if (!response.ok) throw new Error(`Failed to fetch ${type} settings`); + + return response.json(); +} + +/** + * 通知設定を保存・更新 + */ +export async function saveMessageSetting(guildId: string, type: 'welcome' | 'goodbye', data: Partial) { + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/message/${guildId}/${type}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error(`Failed to save ${type} settings`); + return response.json(); +} + +/** + * 通知設定を削除 + */ +export async function deleteMessageSetting(guildId: string, type: 'welcome' | 'goodbye') { + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/message/${guildId}/${type}`, { + method: "DELETE", + }); + + if (!response.ok) throw new Error(`Failed to delete ${type} settings`); + return response.json(); +} + +// --- 埋め込み設定 (Embeds) --- + +/** + * サーバー内のEmbed設定一覧を取得 + */ +export async function fetchEmbedSettings(guildId: string): Promise { + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/embeds/${guildId}`); + if (!response.ok) return []; + return response.json(); +} + +/** + * Embed設定を保存・更新 + */ +export async function saveEmbedSetting(guildId: string, name: string, embedData: Record) { + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/embeds/${guildId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, data: embedData }), + }); + + if (!response.ok) throw new Error("Failed to save embed setting"); + return response.json(); +} + +/** + * 特定のEmbed設定を削除 + */ +export async function deleteEmbedSetting(guildId: string, name: string) { + const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/embeds/${guildId}/${name}`, { + method: "DELETE", + }); + + if (!response.ok) throw new Error("Failed to delete embed setting"); + return response.json(); } \ No newline at end of file From e0f3abdbaafb33fef7e2282a301136286fee3c5e Mon Sep 17 00:00:00 2001 From: sharkbot-neko Date: Thu, 26 Mar 2026 18:14:39 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20=E5=8F=82=E5=8A=A0=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=AA=E3=81=A9=E3=82=92?= =?UTF-8?q?Go=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/src/internal/router/message.go | 2 + src/bot/cogs/welcome.py | 107 +++----- src/bot/lib/api.py | 151 +++++++++++ src/bot/main.py | 11 + .../api/guilds/[guildId]/channels/route.tsx | 3 +- .../guilds/[guildId]/modules/goodbye/route.ts | 107 ++++++++ .../guilds/[guildId]/modules/welcome/route.ts | 231 +++++++--------- .../app/dashboard/[guildId]/embed/page.tsx | 13 +- .../welcome/WelcomeGoodbyeEditor.tsx | 227 ++++++++++++++++ .../app/dashboard/[guildId]/welcome/page.tsx | 251 ++++-------------- .../src/components/EmbedSelecter.tsx | 86 ++++-- src/dashboard/src/lib/Discord/Bot.ts | 16 ++ src/dashboard/src/lib/api/requests.ts | 7 +- src/dashboard/src/lib/modules.ts | 8 +- 14 files changed, 781 insertions(+), 439 deletions(-) create mode 100644 src/bot/lib/api.py create mode 100644 src/dashboard/src/app/api/guilds/[guildId]/modules/goodbye/route.ts create mode 100644 src/dashboard/src/app/dashboard/[guildId]/welcome/WelcomeGoodbyeEditor.tsx diff --git a/src/api/src/internal/router/message.go b/src/api/src/internal/router/message.go index ce5a965..859be3d 100644 --- a/src/api/src/internal/router/message.go +++ b/src/api/src/internal/router/message.go @@ -1,6 +1,7 @@ package router import ( + "log" "net/http" "github.com/SharkBot-Dev/NewSharkBot/api/internal/model" @@ -53,6 +54,7 @@ func saveMessageSetting(c *gin.Context) { } if err := c.ShouldBindJSON(&input); err != nil { + log.Printf("BindError: %x", err.Error()) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } diff --git a/src/bot/cogs/welcome.py b/src/bot/cogs/welcome.py index 246b369..b784dc4 100644 --- a/src/bot/cogs/welcome.py +++ b/src/bot/cogs/welcome.py @@ -15,88 +15,55 @@ def __init__(self, bot: NewSharkBot): def welcome_parse(self, template: str, member: discord.Member) -> str: return template.replace("{ユーザー名}", member.display_name).replace("{ユーザーID}", str(member.id)).replace("{メンション}", member.mention).replace("{サーバー名}", member.guild.name) - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - return - guild_id = str(member.guild.id) - - data = await self.bot.async_db["SharkBot"]["welcome_setting"].find_one({"guildId": guild_id}) - if not data or "welcome" not in data: + async def _handle_member_event(self, member: discord.Member, event_type: str): + if not self.bot.api: return - - if data["welcome"].get("enabled", False) == False: - return - - message = self.welcome_parse(data["welcome"].get("message", "ようこそ、{ユーザー名}!"), member) - channel = member.guild.get_channel(int(data["welcome"]["channelId"])) - if not channel: - return + guild_id = str(member.guild.id) - embed_name = data["welcome"].get('embed', None) - if not embed_name: - if message == "": + try: + setting = await self.bot.api.fetch_message_setting(guild_id, event_type) + if not setting: return - await channel.send(content=message) - return - embed_data = await self.bot.embed.getEmbed(guild_id, embed_name) - if not embed_data: - if message == "": + content = self.welcome_parse(setting.get("content", ""), member) + channel_id = setting.get("channel_id") + + if not channel_id: + return + + channel = member.guild.get_channel(int(channel_id)) + if not channel: return - await channel.send(content=message) - return - - embed_data = copy.deepcopy(embed_data) - embed_data["description"] = self.welcome_parse(embed_data.get("description", ""), member) - embed_data["title"] = self.welcome_parse(embed_data.get("title", ""), member) - - embed = discord.Embed.from_dict(embed_data) - if message == "": - await channel.send(embed=embed) - return - await channel.send(content=message, embed=embed) - - @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): - return - guild_id = str(member.guild.id) - - data = await self.bot.async_db["SharkBot"]["welcome_setting"].find_one({"guildId": guild_id}) - if not data or "goodbye" not in data: - return - - if data["goodbye"].get("enabled", False) == False: - return - - message = self.welcome_parse(data["goodbye"].get("message", "{ユーザー名}が退出しました。"), member) - - channel = member.guild.get_channel(int(data["goodbye"]["channelId"])) - if not channel: - return - - embed_name = data["goodbye"].get('embed', None) - if not embed_name: - if message == "": + embed = None + embed_setting = setting.get("embed") + + if embed_setting: + embed_data = copy.deepcopy(embed_setting.get("data", {})) + + if "description" in embed_data: + embed_data["description"] = self.welcome_parse(embed_data["description"], member) + if "title" in embed_data: + embed_data["title"] = self.welcome_parse(embed_data["title"], member) + + embed = discord.Embed.from_dict(embed_data) + + if not content and not embed: return - await channel.send(content=message) - return + await channel.send(content=content or None, embed=embed) - embed_data = await self.bot.embed.getEmbed(guild_id, embed_name) - if not embed_data: - if message == "": - return - await channel.send(content=message) - return + except Exception as e: + print(f"Error in {event_type} event: {e}") - embed_data = copy.deepcopy(embed_data) - embed_data["description"] = self.welcome_parse(embed_data.get("description", ""), member) - embed_data["title"] = self.welcome_parse(embed_data.get("title", ""), member) + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + await self._handle_member_event(member, "welcome") - embed = discord.Embed.from_dict(embed_data) - await channel.send(content=self.welcome_parse(data["goodbye"].get("message", "{ユーザー名}が退出しました。"), member), embed=embed) + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member): + await self._handle_member_event(member, "goodbye") async def setup(bot): await bot.add_cog(WelcomeCog(bot)) diff --git a/src/bot/lib/api.py b/src/bot/lib/api.py new file mode 100644 index 0000000..f9dbb59 --- /dev/null +++ b/src/bot/lib/api.py @@ -0,0 +1,151 @@ +import aiohttp +import re +from typing import Optional, List, Dict, Any, Union, Literal +from dataclasses import dataclass, asdict + +# --- 型定義 --- + +@dataclass +class EmbedSetting: + guild_id: str + name: str + data: Dict[str, Any] + id: Optional[int] = None + updated_at: Optional[str] = None + +@dataclass +class MessageSetting: + guild_id: str + type: Literal['welcome', 'goodbye'] + channel_id: str + content: str + embed_id: Optional[int] + embed: Optional[EmbedSetting] = None + updated_at: Optional[str] = None + +# --- クラス実装 --- + +class ResourceAPIClient: + def __init__(self, session: aiohttp.ClientSession, base_url: str): + self.session = session + self.base_url = base_url.rstrip('/') + + def _is_valid_discord_id(self, discord_id: str) -> bool: + return bool(re.match(r'^\d{17,20}$', discord_id)) + + def _validate_guild_id(self, guild_id: str): + if not guild_id: + raise ValueError("Guild ID is required") + if not self._is_valid_discord_id(guild_id): + raise ValueError("Invalid Guild ID") + + async def create_guild_entry(self, guild_id: str) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + payload = { + "id": guild_id, + "EnabledModules": {"help": True} + } + + async with self.session.put( + f"{self.base_url}/guilds/{guild_id}", + json=payload + ) as resp: + if not resp.ok: + raise Exception(f"Failed to create guild entry: {resp.status} {await resp.text()}") + return await resp.json() + + async def fetch_guild_settings(self, guild_id: str) -> Optional[Dict[str, Any]]: + self._validate_guild_id(guild_id) + + async with self.session.get(f"{self.base_url}/guilds/{guild_id}") as resp: + if resp.status == 404: + return await self.create_guild_entry(guild_id) + if not resp.ok: + raise Exception(f"Failed to fetch guild settings: {resp.status}") + return await resp.json() + + async def is_module_enabled(self, guild_id: str, module_name: str) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + params = {"module": module_name} + async with self.session.get( + f"{self.base_url}/guilds/{guild_id}/module", + params=params + ) as resp: + if not resp.ok: + raise Exception(f"Failed to fetch module status: {resp.status}") + return await resp.json() + + async def set_module_status(self, guild_id: str, module_name: str) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + params = {"module": module_name} + async with self.session.patch( + f"{self.base_url}/guilds/{guild_id}/module", + params=params + ) as resp: + if not resp.ok: + raise Exception(f"Failed to update module status: {resp.status}") + return await resp.json() + + # --- Message Settings --- + + async def fetch_message_setting(self, guild_id: str, setting_type: Literal['welcome', 'goodbye']) -> Optional[Dict[str, Any]]: + self._validate_guild_id(guild_id) + + async with self.session.get(f"{self.base_url}/guilds/message/{guild_id}/{setting_type}") as resp: + if resp.status == 404: + return None + if not resp.ok: + raise Exception(f"Failed to fetch {setting_type} settings") + return await resp.json() + + async def save_message_setting(self, guild_id: str, setting_type: Literal['welcome', 'goodbye'], data: Dict[str, Any]) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + async with self.session.post( + f"{self.base_url}/guilds/message/{guild_id}/{setting_type}", + json=data + ) as resp: + if not resp.ok: + raise Exception(f"Failed to save {setting_type} settings") + return await resp.json() + + async def delete_message_setting(self, guild_id: str, setting_type: Literal['welcome', 'goodbye']) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + async with self.session.delete(f"{self.base_url}/guilds/message/{guild_id}/{setting_type}") as resp: + if not resp.ok: + raise Exception(f"Failed to delete {setting_type} settings") + return await resp.json() + + # --- Embed Settings --- + + async def fetch_embed_settings(self, guild_id: str) -> List[Dict[str, Any]]: + self._validate_guild_id(guild_id) + + async with self.session.get(f"{self.base_url}/guilds/embeds/{guild_id}") as resp: + if not resp.ok: + return [] + return await resp.json() + + async def save_embed_setting(self, guild_id: str, name: str, embed_data: Dict[str, Any]) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + payload = {"name": name, "data": embed_data} + async with self.session.post( + f"{self.base_url}/guilds/embeds/{guild_id}", + json=payload + ) as resp: + if not resp.ok: + raise Exception("Failed to save embed setting") + return await resp.json() + + async def delete_embed_setting(self, guild_id: str, name: str) -> Dict[str, Any]: + self._validate_guild_id(guild_id) + + async with self.session.delete(f"{self.base_url}/guilds/embeds/{guild_id}/{name}") as resp: + if not resp.ok: + raise Exception("Failed to delete embed setting") + return await resp.json() \ No newline at end of file diff --git a/src/bot/main.py b/src/bot/main.py index 89301db..e5dd67e 100644 --- a/src/bot/main.py +++ b/src/bot/main.py @@ -1,5 +1,6 @@ import os from typing import Dict +import aiohttp import dotenv from discord.ext import commands import discord @@ -7,6 +8,7 @@ from lib import tree from lib.command import Command from lib.embed import Embed as customEmbed +from lib.api import ResourceAPIClient dotenv.load_dotenv() @@ -21,6 +23,8 @@ def __init__(self): ) print("InitDone") + self.api = None + self.slashcommands: Dict[str, Command] = {} self.embed = customEmbed(self) @@ -48,6 +52,13 @@ async def load_cogs(bot: commands.Bot, base_folder="cogs"): async def setup_hook() -> None: await load_cogs(bot) +@bot.event +async def on_ready(): + print(f"Logged in as {bot.user} (ID: {bot.user.id})") + print("------") + + async with aiohttp.ClientSession() as session: + bot.api = ResourceAPIClient(session, os.environ.get("RESOURCE_API_BASE_URL", "http://localhost:8000")) if __name__ == "__main__": bot.run(os.environ.get("DISCORD_TOKEN")) diff --git a/src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx b/src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx index f020bb6..2de6b84 100644 --- a/src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx +++ b/src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx @@ -1,7 +1,8 @@ import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { auth } from "@/lib/auth"; -import { checkAdminPermission, getGuildChannels } from "@/lib/discord"; +import { checkAdminPermission } from "@/lib/Discord/User"; +import { getGuildChannels } from "@/lib/Discord/Bot"; export async function GET( _request: Request, diff --git a/src/dashboard/src/app/api/guilds/[guildId]/modules/goodbye/route.ts b/src/dashboard/src/app/api/guilds/[guildId]/modules/goodbye/route.ts new file mode 100644 index 0000000..e922d2f --- /dev/null +++ b/src/dashboard/src/app/api/guilds/[guildId]/modules/goodbye/route.ts @@ -0,0 +1,107 @@ +import { deleteMessageSetting, fetchMessageSetting, saveMessageSetting } from "@/lib/api/requests"; +import { auth } from "@/lib/auth"; +import { checkAdminPermission } from "@/lib/Discord/User"; +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; + +const BACKEND_URL = process.env.BACKEND_API_URL || "http://localhost:8080"; + +async function validateAdmin(guildId: string) { + const allLinkedAccounts = await auth.api.listUserAccounts({ + headers: await headers(), + }); + const discordAccountData = allLinkedAccounts.find( + (account) => account.providerId === "discord" + ); + + if (!discordAccountData) throw new Error("Unauthorized"); + + const discordToken = await auth.api.getAccessToken({ + headers: await headers(), + body: { + providerId: "discord", + accountId: discordAccountData.accountId, + userId: discordAccountData.userId, + }, + }); + + if (!discordToken.accessToken || !discordToken.accessTokenExpiresAt || + Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime()) { + throw new Error("Unauthorized"); + } + + const hasPermission = await checkAdminPermission(guildId, discordToken.accessToken); + if (!hasPermission) throw new Error("Forbidden"); + + return discordToken; +} + +/** + * GET: GoサーバーからWelcomeとGoodbyeの設定を取得して整形 + */ +export async function GET( + _request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + const goodbye = await fetchMessageSetting(guildId, "goodbye"); + + // フロントエンドの既存インターフェースに合わせる + const fixedSettings = { + channel_id: goodbye?.channel_id || "", + content: goodbye?.content || "", + embed_id: goodbye?.embed_id || null, + enabled: !!goodbye + }; + + return NextResponse.json({ success: true, settings: fixedSettings }); + } catch (error: any) { + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status }); + } +} + +/** + * POST: フロントからのリクエストをGoサーバーへ転送 + */ + +export async function POST( + request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + const body = await request.json(); + + await saveMessageSetting(guildId, "goodbye", body.goodbye); + + return NextResponse.json({ success: true, message: "Settings synced successfully" }); + } catch (error: any) { + console.error("Settings POST Error:", error); + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status: status || 500 }); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + await deleteMessageSetting(guildId, "goodbye"); + + return NextResponse.json({ success: true, message: "Settings synced successfully" }); + } catch (error: any) { + console.error("Settings POST Error:", error); + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status: status || 500 }); + } +} \ No newline at end of file diff --git a/src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts b/src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts index 62e064e..170ed03 100644 --- a/src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts +++ b/src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts @@ -1,166 +1,113 @@ +import { deleteMessageSetting, fetchMessageSetting, saveMessageSetting } from "@/lib/api/requests"; +import { auth } from "@/lib/auth"; +import { checkAdminPermission } from "@/lib/Discord/User"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; -import { auth } from "@/lib/auth"; -import { checkAdminPermission } from "@/lib/discord"; -import clientPromise from "@/lib/mongodb"; - -interface WelcomeSetting { - guildId: string; - welcome: { - channelId: string; - message: string; - embed: any; - enabled: boolean; - }, - goodbye: { - channelId: string; - message: string; - embed: any; - enabled: boolean; - }; -} -export async function GET( - request: Request, - { params }: { params: Promise<{ guildId: string }> }, -) { - try { - const { guildId } = await params; - let discordToken: { - accessToken: string; - accessTokenExpiresAt: Date | undefined; - scopes: string[]; - idToken: string | undefined; - }; - try { - const allLinkedAccounts = await auth.api.listUserAccounts({ +const BACKEND_URL = process.env.BACKEND_API_URL || "http://localhost:8080"; + +async function validateAdmin(guildId: string) { + const allLinkedAccounts = await auth.api.listUserAccounts({ headers: await headers(), - }); - const discordAccountData = allLinkedAccounts.find( - (account) => account.providerId === "discord", - ); - if (!discordAccountData) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - discordToken = await auth.api.getAccessToken({ + }); + const discordAccountData = allLinkedAccounts.find( + (account) => account.providerId === "discord" + ); + + if (!discordAccountData) throw new Error("Unauthorized"); + + const discordToken = await auth.api.getAccessToken({ headers: await headers(), body: { providerId: "discord", accountId: discordAccountData.accountId, userId: discordAccountData.userId, }, - }); - } catch (e) { - console.log("Error fetching access token:", e); - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + }); - if ( - !discordToken.accessTokenExpiresAt || - Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime() - ) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + if (!discordToken.accessToken || !discordToken.accessTokenExpiresAt || + Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime()) { + throw new Error("Unauthorized"); } - const hasPermission = await checkAdminPermission( - guildId, - discordToken.accessToken, - ); - if (!hasPermission) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } + const hasPermission = await checkAdminPermission(guildId, discordToken.accessToken); + if (!hasPermission) throw new Error("Forbidden"); - const client = await clientPromise; - const db = client.db("SharkBot"); - const settings = await db.collection("welcome_setting").findOne({ guildId }); - - const fixedSettings = { - welcome: { - channelId: settings?.welcome?.channelId || "", - message: settings?.welcome?.message || "", - embed: settings?.welcome?.embed || null, - enabled: settings?.welcome?.enabled || false, - }, - goodbye: { - channelId: settings?.goodbye?.channelId || "", - message: settings?.goodbye?.message || "", - embed: settings?.goodbye?.embed || null, - enabled: settings?.goodbye?.enabled || false, - }, - }; - return NextResponse.json({ success: true, settings: fixedSettings }); - } catch (error) { - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } + return discordToken; } -export async function POST( - request: Request, - { params }: { params: { guildId: string } }, +/** + * GET: GoサーバーからWelcomeとGoodbyeの設定を取得して整形 + */ +export async function GET( + _request: Request, + { params }: { params: Promise<{ guildId: string }> } ) { - try { - const { guildId } = await params; - let discordToken: { - accessToken: string; - accessTokenExpiresAt: Date | undefined; - scopes: string[]; - idToken: string | undefined; - }; try { - const allLinkedAccounts = await auth.api.listUserAccounts({ - headers: await headers(), - }); - const discordAccountData = allLinkedAccounts.find( - (account) => account.providerId === "discord", - ); - if (!discordAccountData) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - discordToken = await auth.api.getAccessToken({ - headers: await headers(), - body: { - providerId: "discord", - accountId: discordAccountData.accountId, - userId: discordAccountData.userId, - }, - }); - } catch (e) { - console.log("Error fetching access token:", e); - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const { guildId } = await params; + await validateAdmin(guildId); + + const welcome = await fetchMessageSetting(guildId, "welcome"); + + console.log("Fetched welcome setting:", welcome); + + // フロントエンドの既存インターフェースに合わせる + const fixedSettings = { + channel_id: welcome?.channel_id || "", + content: welcome?.content || "", + embed_id: welcome?.embed_id || null, + enabled: !!welcome + }; + + console.log("Fixed welcome settings:", fixedSettings); - if ( - !discordToken.accessTokenExpiresAt || - Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime() - ) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json({ success: true, settings: fixedSettings }); + } catch (error: any) { + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status }); } +} - const hasPermission = await checkAdminPermission( - guildId, - discordToken.accessToken, - ); - if (!hasPermission) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); +/** + * POST: フロントからのリクエストをGoサーバーへ転送 + */ + +export async function POST( + request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + const body = await request.json(); + + console.log("Received POST body:", body); + + await saveMessageSetting(guildId, "welcome", body.welcome); + + return NextResponse.json({ success: true, message: "Settings synced successfully" }); + } catch (error: any) { + console.error("Settings POST Error:", error); + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status: status || 500 }); } +} - const body = await request.json(); - const settung: WelcomeSetting = { - guildId, - ...body - }; - - const client = await clientPromise; - const db = client.db("SharkBot"); - await db.collection("welcome_setting").updateOne( - { guildId }, - { $set: { ...settung } }, - { upsert: true }, - ) - - return NextResponse.json({ success: true, message: "Settings saved!" }); - - } catch (error) { - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } +export async function DELETE( + request: Request, + { params }: { params: Promise<{ guildId: string }> } +) { + try { + const { guildId } = await params; + await validateAdmin(guildId); + + await deleteMessageSetting(guildId, "welcome"); + + return NextResponse.json({ success: true, message: "Settings synced successfully" }); + } catch (error: any) { + console.error("Settings POST Error:", error); + const status = error.message === "Forbidden" ? 403 : 401; + return NextResponse.json({ error: error.message }, { status: status || 500 }); + } } \ No newline at end of file diff --git a/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx b/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx index 6530e4a..ec480e0 100644 --- a/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx +++ b/src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx @@ -1,7 +1,9 @@ import { Suspense } from "react"; -import { fetchEmbedSettings } from "@/lib/api/requests"; // ラッパー関数の場所 +import { fetchEmbedSettings, isModuleEnabled } from "@/lib/api/requests"; // ラッパー関数の場所 import EmbedEditorClient from "./EmbedEditorClient"; import LoadingSkeleton from "@/components/LoadingSkeleton"; +import Alert from "@/components/Alert"; +import { redirect } from "next/navigation"; interface Props { params: { guildId: string }; @@ -9,6 +11,15 @@ interface Props { export default async function WelcomeGoodbyeModulePage({ params }: Props) { const { guildId } = await params; + try { + const data = await isModuleEnabled(guildId, "embed"); + + if (!data.enabled) { + return ; + } + } catch (error) { + redirect(`/dashboard/${guildId}`); + } return (
diff --git a/src/dashboard/src/app/dashboard/[guildId]/welcome/WelcomeGoodbyeEditor.tsx b/src/dashboard/src/app/dashboard/[guildId]/welcome/WelcomeGoodbyeEditor.tsx new file mode 100644 index 0000000..60dee53 --- /dev/null +++ b/src/dashboard/src/app/dashboard/[guildId]/welcome/WelcomeGoodbyeEditor.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { Save } from "lucide-react"; +import { useState } from "react"; +import ChannelSelecter from "@/components/channel-selecter"; +import EmbedSelecter from "@/components/EmbedSelecter"; +import ToggleSwitch from "@/components/toggleSwitch"; +import CollapsibleSection from "@/components/CollapsibleSection"; + +interface Setting { + enabled: boolean; + channelId: string; + content: string; + embed_id: any; +} + +interface Props { + guildId: string; + initialData: { welcome: Setting; goodbye: Setting }; + embeds: any[]; +} + +export default function WelcomeGoodbyeEditor({ guildId, initialData, embeds }: Props) { + const [saving, setSaving] = useState(false); + const [welcome, setWelcome] = useState({ + ...initialData.welcome, + channelId: initialData.welcome.channelId || "", + content: initialData.welcome.content || "", + embed_id: initialData.welcome.embed_id || null, + }); + const [goodbye, setGoodbye] = useState({ + ...initialData.goodbye, + channelId: initialData.goodbye.channelId || "", + content: initialData.goodbye.content || "", + embed_id: initialData.goodbye.embed_id || null, + }); + + const handleWelcomeSave = async () => { + setSaving(true); + try { + if (!welcome.enabled) { + const response = await fetch(`/api/guilds/${guildId}/modules/welcome`, { + headers: { "Content-Type": "application/json" }, + method: "DELETE", + }); + + if (!response.ok) throw new Error(); + + alert("保存しました!"); + return + } + + const body = { + welcome: { + channel_id: welcome.channelId, + content: welcome.content, + embed_id: welcome.embed_id ? Number(welcome.embed_id) : null, + } + }; + + console.log("Saving welcome settings with body:", body); + + const response = await fetch(`/api/guilds/${guildId}/modules/welcome`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error(); + alert("保存しました!"); + } catch (e) { + alert("保存に失敗しました。"); + } finally { + setSaving(false); + } + }; + + const handleGoodbyeSave = async () => { + setSaving(true); + try { + if (!goodbye.enabled) { + const response = await fetch(`/api/guilds/${guildId}/modules/goodbye`, { + headers: { "Content-Type": "application/json" }, + method: "DELETE", + }); + + if (!response.ok) throw new Error(); + + alert("保存しました!"); + return + } + + const body = { + goodbye: { + channel_id: welcome.channelId, + content: goodbye.content, + embed_id: welcome.embed_id ? Number(welcome.embed_id) : null, + } + }; + + const response = await fetch(`/api/guilds/${guildId}/modules/goodbye`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error(); + alert("保存しました!"); + } catch (e) { + alert("保存に失敗しました。"); + } finally { + setSaving(false); + } + }; + + return ( +
+ {/* 参加メッセージ設定 */} + +
+
+ 機能を有効にする + setWelcome({ ...welcome, enabled: !welcome.enabled })} + /> +
+ +
+
+
+ + setWelcome({ ...welcome, channelId: id })} + /> +
+
+ + setWelcome({ ...welcome, embed_id: val })} + /> +
+
+
+ +