init DiscordGo

This commit is contained in:
Penguin-71630
2025-12-07 17:17:02 +08:00
parent ec986f2dbf
commit 5c32bb11c9
7 changed files with 176 additions and 391 deletions

View File

@@ -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

View File

@@ -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: 用於 <img src="...">
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"

View File

@@ -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);

79
bot/bot.go Normal file
View File

@@ -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()
}
}

35
config/config.go Normal file
View File

@@ -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
}

14
go.mod Normal file
View File

@@ -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
)

48
main.go Normal file
View File

@@ -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.")
}