diff --git a/Makefile b/Makefile deleted file mode 100644 index 841c8a5..0000000 --- a/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -.PHONY: viewapi - -# 定義要檢查的模組目錄 -NODE_MODULES_PATH := node_modules - -viewapi: - @echo "Bootstrapping API document viewer..." - if [ ! -d "$(NODE_MODULES_PATH)" ]; then \ - echo "Dependencies not installed, installing swagger-ui-express, yamljs..."; \ - [ ! -f "package.json" ] && npm init -y > /dev/null; \ - npm install express swagger-ui-express yamljs; \ - else \ - echo "Dependencies already installed."; \ - fi && \ - echo "Booting up API document viewer..." && \ - node api/view-API-doc.js \ No newline at end of file diff --git a/api/openapi.yml b/api/openapi.yml deleted file mode 100644 index cf0da2d..0000000 --- a/api/openapi.yml +++ /dev/null @@ -1,347 +0,0 @@ -openapi: 3.0.3 -info: - title: Discord MemeBot API - description: > - Go Final Project 後端 API 文件。 - 包含圖片上傳、檢索、Alias 管理以及 Discord OAuth 認證流程。 - version: 1.0.0 - -servers: - - url: http://localhost:8080 - description: 本地開發伺服器 - -# 定義 Tag 的順序與描述 (讓文件更漂亮) -tags: - - name: Image - description: 圖片相關操作 (查詢、上傳、刪除、取得檔案) - - name: Alias - description: 全域 Alias 列表 - - name: Linking/Unlinking - description: 圖片與 Alias 之間的關聯管理 - - name: Authentication - description: 使用者身分驗證與 Token 交換 - -# 全域安全驗證 -security: - - bearerAuth: [] - -paths: - # ---------------------------------------------------------------- - # Image (圖片資源) - # ---------------------------------------------------------------- - /api/images: - get: - tags: - - Image - summary: 取得圖片列表 - description: 列出圖片,支援關鍵字搜尋、分頁與無 Alias 過濾。 - parameters: - - name: search - in: query - description: 搜尋 Alias 關鍵字 (模糊搜尋) - schema: - type: string - - name: null_alias - in: query - description: 若為 true,只回傳沒有任何 alias 的圖片 - schema: - type: boolean - - name: limit - in: query - description: 分頁:每頁幾筆 - schema: - type: integer - default: 20 - - name: page - in: query - description: 分頁:第幾頁 - schema: - type: integer - default: 1 - responses: - '200': - description: 成功取得列表 - content: - application/json: - schema: - type: object - properties: - images: - type: array - items: - $ref: '#/components/schemas/Image' - - post: - tags: - - Image - summary: 上傳圖片 - description: 上傳圖片並設定初始 aliases。 - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - imgfile: - type: string - format: binary - description: 圖片檔案 - aliases: - type: array - items: - type: string - description: 初始 Alias 列表 (支援重複 key 傳送) - required: - - imgfile - responses: - '201': - description: 圖片建立成功 - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - - /api/images/{id}: - get: - tags: - - Image - summary: 取得單張圖片資訊 (Metadata) - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - '200': - description: 成功 - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - - delete: - tags: - - Image - summary: 刪除圖片 - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - '204': - description: 刪除成功 (No Content) - - /api/images/{id}/file: - get: - tags: - - Image - summary: 取得圖片原始檔案 (Binary) - description: 用於 - security: [] # 若圖片公開可留空 - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - '200': - description: 圖片串流 - content: - image/png: - schema: - type: string - format: binary - image/jpeg: - schema: - type: string - format: binary - image/gif: - schema: - type: string - format: binary - - # ---------------------------------------------------------------- - # Alias (全域列表) - # ---------------------------------------------------------------- - /api/aliases: - get: - tags: - - Alias - summary: 取得所有 Alias - responses: - '200': - description: 成功 - content: - application/json: - schema: - type: object - properties: - aliases: - type: array - items: - type: string - example: ["114514", "哼哼啊啊啊啊", "poop"] - - # ---------------------------------------------------------------- - # Linking/Unlinking (關聯管理) - # ---------------------------------------------------------------- - /api/images/{id}/aliases: - post: - tags: - - Linking/Unlinking - summary: 新增單一 Alias (關聯) - parameters: - - name: id - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - alias: - type: string - example: "先輩" - responses: - '200': - description: 新增成功,回傳更新後的圖片資訊 - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - - delete: - tags: - - Linking/Unlinking - summary: 刪除單一 Alias (關聯) - parameters: - - name: id - in: path - required: true - schema: - type: string - - name: alias - in: query - description: 要刪除的 alias 文字 - required: true - schema: - type: string - example: "先輩" - responses: - '204': - description: 刪除成功 (No Content) - - put: - tags: - - Linking/Unlinking - summary: 批次取代 Alias 列表 - description: 用於前端編輯視窗的 Save 功能,完全取代舊有列表。 - parameters: - - name: id - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - aliases: - type: array - items: - type: string - example: ["先輩", "好時代", "114514"] - responses: - '200': - description: 更新成功,回傳更新後的圖片資訊 - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - - # ---------------------------------------------------------------- - # Authentication (驗證) - # ---------------------------------------------------------------- - /auth/gen-access-url: - post: - tags: - - Authentication - summary: JWT 交換 (Login) - description: 使用 Discord OAuth Code 交換 Access Token - security: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - code: - type: string - description: Discord OAuth Code - redirect_uri: - type: string - grant_type: - type: string - default: authorization_code - required: - - code - responses: - '200': - description: 登入成功 - content: - application/json: - schema: - type: object - properties: - access_token: - type: string - token_type: - type: string - example: "Bearer" - expires_in: - type: integer - example: 3600 - -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - - schemas: - Image: - type: object - properties: - id: - type: string - example: "101" - uploaded_user_id: - type: string - example: "konchin.shih" - uploaded_at: - type: string - format: date-time - example: "2023-10-20T12:00:00Z" - aliases: - type: array - items: - type: string - example: ["野獸", "先輩", "114514"] - url: - type: string - description: 圖片二進制檔案的 API 路徑 - example: "/api/images/101/file" \ No newline at end of file diff --git a/api/view-API-doc.js b/api/view-API-doc.js deleted file mode 100644 index 94a114e..0000000 --- a/api/view-API-doc.js +++ /dev/null @@ -1,28 +0,0 @@ -const express = require('express'); -const app = express(); -const swaggerUi = require('swagger-ui-express'); -const YAML = require('yamljs'); -const path = require('path'); - -// 設定你的 yaml 檔案路徑 -// 假設這個 js 檔在根目錄,而 yaml 在 api 資料夾內 -const filePath = path.join(__dirname, 'openapi.yml'); - -try { - // 嘗試讀取檔案 - const swaggerDocument = YAML.load(filePath); - - // 設定路由 - app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - - console.log('=================================================='); - console.log(`文件伺服器已啟動!`); - console.log(`請用瀏覽器打開: http://localhost:3000/docs`); - console.log('=================================================='); - -} catch (e) { - console.error("讀取 yaml 檔案失敗,請檢查路徑是否正確:", filePath); - console.error(e); -} - -app.listen(3000); \ No newline at end of file diff --git a/bot/bot.go b/bot/bot.go new file mode 100644 index 0000000..9a387a0 --- /dev/null +++ b/bot/bot.go @@ -0,0 +1,79 @@ +package bot + +import ( + "fmt" + "log" + + "github.com/Penguin-71630/meme-bot-frontend-dc/api" + "github.com/Penguin-71630/meme-bot-frontend-dc/config" + "github.com/bwmarrin/discordgo" +) + +type Bot struct { + session *discordgo.Session + config *Config + apiClient *api.Client +} + +type Config struct { + Token string + APIClient *api.Client + Prefix string +} + +func New(cfg *config.Config) (*Bot, error) { + // Create Discord session + session, err := discordgo.New("Bot " + cfg.DiscordToken) + if err != nil { + return nil, fmt.Errorf("error creating Discord session: %w", err) + } + + // Create API client + apiClient := api.NewClient(cfg.APIBaseURL) + + bot := &Bot{ + session: session, + apiClient: apiClient, + config: &Config{ + Token: cfg.DiscordToken, + APIClient: apiClient, + Prefix: cfg.BotPrefix, + }, + } + + // Register handlers + bot.registerHandlers() + + // Set intents + session.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsDirectMessages | discordgo.IntentsMessageContent + + return bot, nil +} + +func (b *Bot) registerHandlers() { + b.session.AddHandler(b.onReady) + b.session.AddHandler(b.onMessageCreate) +} + +func (b *Bot) onReady(s *discordgo.Session, event *discordgo.Ready) { + log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator) + + // Set bot status + err := s.UpdateGameStatus(0, fmt.Sprintf("%shelp for commands", b.config.Prefix)) + if err != nil { + log.Printf("Error setting status: %v", err) + } +} + +func (b *Bot) Start() error { + if err := b.session.Open(); err != nil { + return fmt.Errorf("error opening connection: %w", err) + } + return nil +} + +func (b *Bot) Stop() { + if b.session != nil { + b.session.Close() + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a0fb92f --- /dev/null +++ b/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "errors" + "os" +) + +type Config struct { + DiscordToken string + APIBaseURL string + BotPrefix string +} + +func Load() (*Config, error) { + token := os.Getenv("DISCORD_BOT_TOKEN") + if token == "" { + return nil, errors.New("DISCORD_BOT_TOKEN is required") + } + + apiURL := os.Getenv("API_BASE_URL") + if apiURL == "" { + apiURL = "http://localhost:8080" // Default + } + + prefix := os.Getenv("BOT_PREFIX") + if prefix == "" { + prefix = "!" // Default prefix + } + + return &Config{ + DiscordToken: token, + APIBaseURL: apiURL, + BotPrefix: prefix, + }, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..547d194 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/Penguin-71630/meme-bot-frontend-dc + +go 1.21 + +require ( + github.com/bwmarrin/discordgo v0.27.1 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/gorilla/websocket v1.5.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..2380b34 --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/Penguin-71630/meme-bot-frontend-dc/bot" + "github.com/Penguin-71630/meme-bot-frontend-dc/config" + "github.com/joho/godotenv" +) + +func main() { + // Load environment variables + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using system environment variables") + } + + // Load configuration + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Initialize bot + discordBot, err := bot.New(cfg) + if err != nil { + log.Fatalf("Failed to create bot: %v", err) + } + + // Start bot + if err := discordBot.Start(); err != nil { + log.Fatalf("Failed to start bot: %v", err) + } + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + + // Wait for interrupt signal + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-sc + + // Cleanup + discordBot.Stop() + fmt.Println("Bot stopped gracefully.") +}