Compare commits
3 Commits
8acfbf1215
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e76ee468b | |||
| bf78faba28 | |||
| 7ec38a443b |
21
app/app.go
21
app/app.go
@@ -42,6 +42,17 @@ func NewApp(cfg *config.Config) *App {
|
||||
// Channel pour les erreurs
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// database init
|
||||
dbConfig := database.DBConfig{
|
||||
Driver: cfg.Database.Type,
|
||||
DSN: cfg.GetDSN(),
|
||||
}
|
||||
|
||||
// 1) Initialiser la DB
|
||||
if err := database.Initialize(dbConfig); err != nil {
|
||||
//return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
// Servers
|
||||
udpServer := udp.NewServer(cfg.Server.BindAddr)
|
||||
httpServer := http.NewServer(cfg.Server.BindAddr)
|
||||
@@ -61,16 +72,6 @@ func (app *App) Run() error {
|
||||
// Context pour gérer l'arrêt gracieux
|
||||
defer app.cancel()
|
||||
|
||||
dbConfig := database.DBConfig{
|
||||
Driver: app.cfg.Database.Type,
|
||||
DSN: app.cfg.GetDSN(),
|
||||
}
|
||||
|
||||
// 1) Initialiser la DB
|
||||
if err := database.Initialize(dbConfig); err != nil {
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
// (optionnel) garder une référence locale si tu veux utiliser app.db ailleurs
|
||||
app.db = database.DB
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ func Initialize(config DBConfig) error {
|
||||
return fmt.Errorf("erreur lors de la connexion à la base de données: %w", err)
|
||||
}
|
||||
|
||||
// Activation du mode foreign keys pour SQLite
|
||||
if config.Driver == "sqlite" {
|
||||
if err := DB.Exec("PRAGMA foreign_keys = ON;").Error; err != nil {
|
||||
return fmt.Errorf("impossible d'activer les foreign keys SQLite: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Connexion à la base de données (%s) établie avec succès", config.Driver)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Attachment struct {
|
||||
@@ -17,3 +18,10 @@ type Attachment struct {
|
||||
// Relation optionnelle vers le message
|
||||
Message *Message `gorm:"foreignKey:MessageID" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Attachment) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == uuid.Nil {
|
||||
s.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Category struct {
|
||||
@@ -14,5 +15,12 @@ type Category struct {
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Relation optionnelle vers le serveur
|
||||
Server *Server `gorm:"foreignKey:ServerID" json:"server,omitempty"`
|
||||
Server *Server `gorm:"foreignKey:ServerID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"server,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Category) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == uuid.Nil {
|
||||
s.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// #[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)]
|
||||
@@ -48,3 +49,10 @@ type Channel struct {
|
||||
Server *Server `gorm:"foreignKey:ServerID" json:"server,omitempty"`
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Channel) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == uuid.Nil {
|
||||
s.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
)
|
||||
|
||||
type ChannelUser struct {
|
||||
ChannelID uuid.UUID `gorm:"primaryKey" json:"channel_id"`
|
||||
UserID uuid.UUID `gorm:"primaryKey" json:"user_id"`
|
||||
ID uuid.UUID `gorm:"primaryKey" json:"id"`
|
||||
ChannelID uuid.UUID `gorm:"index;not null" json:"channel_id"`
|
||||
UserID uuid.UUID `gorm:"index;not null" json:"user_id"`
|
||||
|
||||
Role string `gorm:"default:'member'" json:"role"`
|
||||
JoinedAt time.Time `gorm:"autoCreateTime" json:"joined_at"`
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// #[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
@@ -26,3 +27,10 @@ type Message struct {
|
||||
EditedAt *time.Time `gorm:"default:null" json:"edited_at,omitempty"`
|
||||
ReplyToID *uuid.UUID `gorm:"index" json:"reply_to_id,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Message) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == uuid.Nil {
|
||||
s.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
ID uuid.UUID `gorm:"primaryKey" json:"id"`
|
||||
Password string `gorm:"not null" json:"-"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Password *string `gorm:"" json:"-"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s *Server) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if s.ID == uuid.Nil {
|
||||
s.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go_oxspeak_server/models"
|
||||
"go_oxspeak_server/network/http/handler"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CategoryHandler struct {
|
||||
@@ -16,31 +19,105 @@ func NewCategoryHandler(h *handler.Handler) *CategoryHandler {
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
category := rg.Group("/category")
|
||||
category.GET("/", h.getCategories)
|
||||
category.GET("/:id/", h.getCategory)
|
||||
category.POST("/", h.addCategory)
|
||||
category.PUT("/:id/", h.updateCategory)
|
||||
category.DELETE("/:id/", h.deleteCategory)
|
||||
rg.GET("/", h.getCategories)
|
||||
rg.GET("/:id/", h.getCategory)
|
||||
rg.POST("/", h.addCategory)
|
||||
rg.PUT("/:id/", h.updateCategory)
|
||||
rg.DELETE("/:id/", h.deleteCategory)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) getCategories(c *gin.Context) {
|
||||
var categories []models.Category
|
||||
h.DB.Find(&categories)
|
||||
result := h.DB.Find(&categories)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, categories)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) getCategory(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var category models.Category
|
||||
h.DB.Find(&category)
|
||||
result := h.DB.First(&category, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, category)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) addCategory(c *gin.Context) {
|
||||
var req CreateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
category := models.Category{
|
||||
ServerID: req.ServerID,
|
||||
Name: req.Name,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, category)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) updateCategory(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var category models.Category
|
||||
result := h.DB.First(&category, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Model(&category).Updates(req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, category)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) deleteCategory(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var category models.Category
|
||||
result := h.DB.First(&category, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Delete(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
14
network/http/web/api/category_dto.go
Normal file
14
network/http/web/api/category_dto.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package api
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// DTOs pour Category
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
ServerID uuid.UUID `json:"server_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
type UpdateCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go_oxspeak_server/models"
|
||||
"go_oxspeak_server/network/http/handler"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChannelHandler struct {
|
||||
@@ -16,36 +19,111 @@ func NewChannelHandler(h *handler.Handler) *ChannelHandler {
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
channel := rg.Group("/channel")
|
||||
channel.GET("/", h.getChannels)
|
||||
channel.GET("/:id/", h.getChannel)
|
||||
channel.POST("/", h.addChannel)
|
||||
channel.PUT("/:id/", h.updateChannel)
|
||||
channel.DELETE("/:id/", h.deleteChannel)
|
||||
rg.GET("/", h.getChannels)
|
||||
rg.GET("/:id/", h.getChannel)
|
||||
rg.POST("/", h.addChannel)
|
||||
rg.PUT("/:id/", h.updateChannel)
|
||||
rg.DELETE("/:id/", h.deleteChannel)
|
||||
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) getChannels(c *gin.Context) {
|
||||
var users []models.User
|
||||
h.DB.Find(&users)
|
||||
var channels []models.Channel
|
||||
result := h.DB.Find(&channels)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, channels)
|
||||
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) getChannel(c *gin.Context) {
|
||||
var user models.User
|
||||
h.DB.Find(&user)
|
||||
id := c.Param("id")
|
||||
var channel models.Channel
|
||||
result := h.DB.First(&channel, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, channel)
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) addChannel(c *gin.Context) {
|
||||
var req CreateChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
ServerID: req.ServerID,
|
||||
CategoryID: req.CategoryID,
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
}
|
||||
if req.Position != nil {
|
||||
channel.Position = *req.Position
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, channel)
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) updateChannel(c *gin.Context) {
|
||||
var user models.User
|
||||
h.DB.Find(&user)
|
||||
id := c.Param("id")
|
||||
|
||||
var channel models.Channel
|
||||
result := h.DB.First(&channel, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Model(&channel).Updates(req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update channel"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, channel)
|
||||
}
|
||||
|
||||
func (h *ChannelHandler) deleteChannel(c *gin.Context) {
|
||||
var user models.User
|
||||
h.DB.Find(&user)
|
||||
id := c.Param("id")
|
||||
|
||||
var channel models.Channel
|
||||
result := h.DB.First(&channel, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Delete(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete channel"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
24
network/http/web/api/channel_dto.go
Normal file
24
network/http/web/api/channel_dto.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"go_oxspeak_server/models"
|
||||
)
|
||||
|
||||
// DTOs pour Channel
|
||||
|
||||
type CreateChannelRequest struct {
|
||||
ServerID *uuid.UUID `json:"server_id,omitempty"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
Position *int32 `json:"position,omitempty"`
|
||||
Type models.ChannelType `json:"type" binding:"required"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateChannelRequest struct {
|
||||
ServerID *uuid.UUID `json:"server_id,omitempty"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
Position *int32 `json:"position,omitempty"`
|
||||
Type *models.ChannelType `json:"type,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go_oxspeak_server/models"
|
||||
"go_oxspeak_server/network/http/handler"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
@@ -16,31 +19,105 @@ func NewMessageHandler(h *handler.Handler) *MessageHandler {
|
||||
}
|
||||
|
||||
func (h *MessageHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
message := rg.Group("/message")
|
||||
message.GET("/", h.getMessages)
|
||||
message.GET("/:id/", h.getMessage)
|
||||
message.POST("/", h.addMessage)
|
||||
message.PUT("/:id/", h.updateMessage)
|
||||
message.DELETE("/:id/", h.deleteMessage)
|
||||
rg.GET("/", h.getMessages)
|
||||
rg.GET("/:id/", h.getMessage)
|
||||
rg.POST("/", h.addMessage)
|
||||
rg.PUT("/:id/", h.updateMessage)
|
||||
rg.DELETE("/:id/", h.deleteMessage)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) getMessages(c *gin.Context) {
|
||||
var messages []models.Message
|
||||
h.DB.Find(&messages)
|
||||
result := h.DB.Find(&messages)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, messages)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) getMessage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var message models.Message
|
||||
h.DB.Find(&message)
|
||||
result := h.DB.First(&message, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, message)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) addMessage(c *gin.Context) {
|
||||
var req CreateMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
message := models.Message{
|
||||
ChannelID: req.ChannelID,
|
||||
UserID: req.UserID,
|
||||
Content: req.Content,
|
||||
ReplyToID: req.ReplyToID,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&message).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, message)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) updateMessage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var message models.Message
|
||||
result := h.DB.First(&message, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Model(&message).Updates(req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update message"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, message)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) deleteMessage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var message models.Message
|
||||
result := h.DB.First(&message, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Delete(&message).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete message"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
17
network/http/web/api/message_dto.go
Normal file
17
network/http/web/api/message_dto.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// DTOs pour Message
|
||||
|
||||
type CreateMessageRequest struct {
|
||||
ChannelID uuid.UUID `json:"channel_id" binding:"required"`
|
||||
UserID uuid.UUID `json:"user_id" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateMessageRequest struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"`
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go_oxspeak_server/models"
|
||||
"go_oxspeak_server/network/http/handler"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ServerHandler struct {
|
||||
@@ -16,31 +19,138 @@ func NewServerHandler(h *handler.Handler) *ServerHandler {
|
||||
}
|
||||
|
||||
func (h *ServerHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
server := rg.Group("/server")
|
||||
server.GET("/", h.getServers)
|
||||
server.GET("/:id/", h.getServer)
|
||||
server.POST("/", h.addServer)
|
||||
server.PUT("/:id/", h.updateServer)
|
||||
server.DELETE("/:id/", h.deleteServer)
|
||||
rg.GET("/", h.serverList)
|
||||
rg.GET("/:id/", h.serverDetail)
|
||||
rg.POST("/", h.serverAdd)
|
||||
rg.PUT("/:id/", h.serverUpdate)
|
||||
rg.DELETE("/:id/", h.serverDelete)
|
||||
rg.POST("/:id/join/", h.serverJoin)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) getServers(c *gin.Context) {
|
||||
func (h *ServerHandler) serverList(c *gin.Context) {
|
||||
var servers []models.Server
|
||||
h.DB.Find(&servers)
|
||||
result := h.DB.Find(&servers)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, servers)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) getServer(c *gin.Context) {
|
||||
func (h *ServerHandler) serverDetail(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var server models.Server
|
||||
h.DB.Find(&server)
|
||||
result := h.DB.First(&server, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) addServer(c *gin.Context) {
|
||||
func (h *ServerHandler) serverAdd(c *gin.Context) {
|
||||
|
||||
var req CreateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// construire le modèle
|
||||
server := models.Server{
|
||||
Name: req.Name,
|
||||
Password: req.Password,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&server).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) updateServer(c *gin.Context) {
|
||||
func (h *ServerHandler) serverUpdate(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var server models.Server
|
||||
result := h.DB.First(&server, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Model(&server).Updates(req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update server"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) deleteServer(c *gin.Context) {
|
||||
func (h *ServerHandler) serverDelete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var server models.Server
|
||||
result := h.DB.First(&server, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Delete(&server).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete server"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *ServerHandler) serverJoin(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var server models.Server
|
||||
result := h.DB.First(&server, "id = ?", id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var req JoinServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if server.Password != req.Password {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Wrong password"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
15
network/http/web/api/server_dto.go
Normal file
15
network/http/web/api/server_dto.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
type CreateServerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateServerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type JoinServerRequest struct {
|
||||
Password *string `json:"password,omitempty"`
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (h *AuthHandler) authenticate(c *gin.Context) {
|
||||
// Generate token
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"expiration_date": time.Now().Add(time.Hour * 72).Unix(),
|
||||
"expiration_date": time.Now().Add(time.Minute * 15).Unix(),
|
||||
"creation_date": time.Now().Unix(),
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,19 @@ func CreateRouter() *gin.Engine {
|
||||
|
||||
apiGroup := router.Group("/api")
|
||||
{
|
||||
serverHandler.RegisterRoutes(apiGroup)
|
||||
channelHandler.RegisterRoutes(apiGroup)
|
||||
categoryHandler.RegisterRoutes(apiGroup)
|
||||
messageHandler.RegisterRoutes(apiGroup)
|
||||
serverHandler.RegisterRoutes(apiGroup.Group("/server"))
|
||||
channelHandler.RegisterRoutes(apiGroup.Group("/channel"))
|
||||
categoryHandler.RegisterRoutes(apiGroup.Group("/category"))
|
||||
messageHandler.RegisterRoutes(apiGroup.Group("/message"))
|
||||
}
|
||||
|
||||
router.GET("/health", healthcheck)
|
||||
|
||||
// Expose OpenAPI specification (static file placed at repository root)
|
||||
router.GET("/openapi.yaml", func(c *gin.Context) {
|
||||
c.File("openapi.yaml")
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -4,64 +4,64 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// listenUDP crée workerCount sockets UDP distinctes avec SO_REUSEPORT activé.
|
||||
// Chaque socket écoute sur bindAddr et sera utilisée par un worker.
|
||||
func listenUDP(bindAddr string, workerCount int) ([]*net.UDPConn, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", bindAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot resolve address: %w", err)
|
||||
if workerCount <= 0 {
|
||||
workerCount = 1
|
||||
}
|
||||
|
||||
conns := make([]*net.UDPConn, 0, workerCount)
|
||||
var conns []*net.UDPConn
|
||||
|
||||
closeAll := func() {
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
lc := net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
var sockErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
|
||||
sockErr = fmt.Errorf("set SO_REUSEADDR: %w", err)
|
||||
return
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
sockErr = fmt.Errorf("set SO_REUSEPORT: %w", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sockErr
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < workerCount; i++ {
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
pc, err := lc.ListenPacket(context.Background(), "udp", bindAddr)
|
||||
if err != nil {
|
||||
// En cas d’erreur, on ferme ce qu’on a ouvert.
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("cannot listen on address (worker %d): %w", i, err)
|
||||
closeAll()
|
||||
return nil, fmt.Errorf("worker %d: listen failed on %q: %w", i, bindAddr, err)
|
||||
}
|
||||
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("cannot get raw connection (worker %d): %w", i, err)
|
||||
udpConn, ok := pc.(*net.UDPConn)
|
||||
if !ok {
|
||||
pc.Close()
|
||||
closeAll()
|
||||
return nil, fmt.Errorf("worker %d: PacketConn is not *net.UDPConn", i)
|
||||
}
|
||||
|
||||
var sockErr error
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
sockErr = unix.SetsockoptInt(
|
||||
int(fd),
|
||||
unix.SOL_SOCKET,
|
||||
unix.SO_REUSEPORT,
|
||||
1,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("control error (worker %d): %w", i, err)
|
||||
}
|
||||
if sockErr != nil {
|
||||
_ = conn.Close()
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("cannot set SO_REUSEPORT (worker %d): %w", i, sockErr)
|
||||
}
|
||||
|
||||
conns = append(conns, conn)
|
||||
conns = append(conns, udpConn)
|
||||
}
|
||||
|
||||
return conns, nil
|
||||
|
||||
@@ -8,17 +8,18 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// listenUDP sur Windows crée une seule socket UDP qui sera partagée par tous
|
||||
// les workers (tous les workerID utiliseront conns[0]).
|
||||
func listenUDP(bindAddr string, workerCount int) ([]*net.UDPConn, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", bindAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot resolve address: %w", err)
|
||||
return nil, fmt.Errorf("cannot resolve address %q: %w", bindAddr, err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot listen on address: %w", err)
|
||||
return nil, fmt.Errorf("cannot listen on address %q: %w", bindAddr, err)
|
||||
}
|
||||
|
||||
// Un seul conn partagé par tous les workers.
|
||||
return []*net.UDPConn{conn}, nil
|
||||
}
|
||||
|
||||
@@ -7,49 +7,61 @@ import (
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
)
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
type RoutingTable struct {
|
||||
mu sync.RWMutex
|
||||
routes map[string]mapset.Set[*net.UDPAddr]
|
||||
}
|
||||
|
||||
// NewRoutingTable crée une table vide.
|
||||
func NewRoutingTable() *RoutingTable {
|
||||
return &RoutingTable{
|
||||
routes: make(map[string]mapset.Set[*net.UDPAddr]),
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) AddClient(channelID string, addr *net.UDPAddr) {
|
||||
// Add enregistre un client dans un channel.
|
||||
func (rt *RoutingTable) Add(channel string, addr *net.UDPAddr) {
|
||||
rt.mu.Lock()
|
||||
defer rt.mu.Unlock()
|
||||
|
||||
if rt.routes[channelID] == nil {
|
||||
rt.routes[channelID] = mapset.NewSet[*net.UDPAddr]()
|
||||
set, ok := rt.routes[channel]
|
||||
if !ok {
|
||||
set = mapset.NewSet[*net.UDPAddr]()
|
||||
rt.routes[channel] = set
|
||||
}
|
||||
rt.routes[channelID].Add(addr)
|
||||
set.Add(addr)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) RemoveClient(channelID string, addr *net.UDPAddr) {
|
||||
// Remove supprime un client d'un channel.
|
||||
func (rt *RoutingTable) Remove(channel string, addr *net.UDPAddr) {
|
||||
rt.mu.Lock()
|
||||
defer rt.mu.Unlock()
|
||||
|
||||
if clients, exists := rt.routes[channelID]; exists {
|
||||
clients.Remove(addr)
|
||||
if clients.Cardinality() == 0 {
|
||||
delete(rt.routes, channelID)
|
||||
}
|
||||
set, ok := rt.routes[channel]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
set.Remove(addr)
|
||||
if set.Cardinality() == 0 {
|
||||
delete(rt.routes, channel)
|
||||
}
|
||||
}
|
||||
|
||||
// GetClients returns the clients connected to the given channelID
|
||||
// don't modify the returned set!
|
||||
func (rt *RoutingTable) GetClients(channelID string) mapset.Set[*net.UDPAddr] {
|
||||
// GetAddrs renvoie une copie de la liste des clients d'un channel.
|
||||
func (rt *RoutingTable) GetAddrs(channel string) []*net.UDPAddr {
|
||||
rt.mu.RLock()
|
||||
defer rt.mu.RUnlock()
|
||||
|
||||
clients, exists := rt.routes[channelID]
|
||||
if !exists {
|
||||
set, ok := rt.routes[channel]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return clients
|
||||
addrs := make([]*net.UDPAddr, 0, set.Cardinality())
|
||||
for addr := range set.Iter() {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
@@ -22,13 +22,22 @@ func NewServer(bindAddr string) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Server{
|
||||
bindAddr: bindAddr,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
bindAddr: bindAddr,
|
||||
routingTable: NewRoutingTable(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Router() *RoutingTable {
|
||||
return s.routingTable
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
if s.bindAddr == "" {
|
||||
return fmt.Errorf("bind address is empty")
|
||||
}
|
||||
|
||||
workerCount := runtime.NumCPU()
|
||||
|
||||
conns, err := listenUDP(s.bindAddr, workerCount)
|
||||
@@ -41,78 +50,112 @@ func (s *Server) Run() error {
|
||||
s.conns = conns
|
||||
|
||||
for _, conn := range s.conns {
|
||||
conn.SetReadBuffer(8 * 1024 * 1024)
|
||||
conn.SetWriteBuffer(8 * 1024 * 1024)
|
||||
_ = conn.SetReadBuffer(8 * 1024 * 1024)
|
||||
_ = conn.SetWriteBuffer(8 * 1024 * 1024)
|
||||
}
|
||||
|
||||
fmt.Println("Listening on", s.bindAddr)
|
||||
fmt.Println("[udp] listening on", s.bindAddr, "with", len(s.conns), "worker socket(s)")
|
||||
|
||||
for i, conn := range s.conns {
|
||||
// Cas Windows : listenUDP a créé une seule socket,
|
||||
// on lance workerCount goroutines sur la même conn.
|
||||
if len(s.conns) == 1 {
|
||||
conn := s.conns[0]
|
||||
for workerID := 0; workerID < workerCount; workerID++ {
|
||||
s.wg.Add(1)
|
||||
go s.workerLoop(workerID, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cas Unix : une conn par worker (SO_REUSEPORT).
|
||||
for workerID, conn := range s.conns {
|
||||
s.wg.Add(1)
|
||||
go s.workerLoop(i, conn)
|
||||
go s.workerLoop(workerID, conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) sendTo(data []byte, addr *net.UDPAddr) error {
|
||||
if len(s.conns) == 0 || s.conns[0] == nil {
|
||||
return fmt.Errorf("server not started")
|
||||
func (s *Server) Stop() {
|
||||
s.cancel()
|
||||
for _, c := range s.conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
// On utilise la première conn pour l’envoi (c’est suffisant pour UDP).
|
||||
_, err := s.conns[0].WriteToUDP(data, addr)
|
||||
return err
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *Server) workerLoop(id int, conn *net.UDPConn) {
|
||||
func (s *Server) workerLoop(workerID int, conn *net.UDPConn) {
|
||||
defer s.wg.Done()
|
||||
|
||||
buffer := make([]byte, 1500)
|
||||
fmt.Println("Worker", id, "started")
|
||||
buf := make([]byte, 1500)
|
||||
fmt.Println("[udp] worker", workerID, "started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
fmt.Println("Worker", id, "stopped")
|
||||
fmt.Println("[udp] worker", workerID, "stopped")
|
||||
return
|
||||
default:
|
||||
size, addr, err := conn.ReadFromUDP(buffer)
|
||||
n, addr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if s.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if opErr, ok := err.(*net.OpError); ok && opErr.Temporary() {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Error reading from UDP (worker %d): %v\n", id, err)
|
||||
fmt.Printf("[udp] worker %d: read error: %v\n", workerID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
s.handlePacket(buffer[:size], addr)
|
||||
// Le worker qui lit est aussi celui qui traite et qui écrit.
|
||||
s.handlePacket(conn, buf[:n], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePacket(data []byte, addr *net.UDPAddr) {
|
||||
// handlePacket reçoit la conn du worker.
|
||||
func (s *Server) handlePacket(conn *net.UDPConn, data []byte, addr *net.UDPAddr) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pt := PacketType(data[0])
|
||||
|
||||
switch pt {
|
||||
case PacketTypePing:
|
||||
_ = s.sendTo([]byte{byte(PacketTypePing)}, addr)
|
||||
return
|
||||
_, _ = conn.WriteToUDP([]byte{byte(PacketTypePing)}, addr)
|
||||
|
||||
case PacketTypeConnect:
|
||||
return
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
channelID := string(data[1:])
|
||||
s.routingTable.Add(channelID, addr)
|
||||
|
||||
case PacketTypeDisconnect:
|
||||
return
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
channelID := string(data[1:])
|
||||
s.routingTable.Remove(channelID, addr)
|
||||
|
||||
case PacketTypeVoiceData:
|
||||
// todo : déterminer le format du packet
|
||||
// channelID := string(data[1:5])
|
||||
return
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
channelID := string(data[1:]) // à adapter selon ton vrai format
|
||||
recipients := s.routingTable.GetAddrs(channelID)
|
||||
|
||||
for _, dst := range recipients {
|
||||
// optionnel: ne pas renvoyer à la source
|
||||
if dst.IP.Equal(addr.IP) && dst.Port == addr.Port {
|
||||
continue
|
||||
}
|
||||
_, _ = conn.WriteToUDP(data, dst)
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
// type inconnu -> ignore
|
||||
}
|
||||
}
|
||||
|
||||
492
openapi.yaml
Normal file
492
openapi.yaml
Normal file
@@ -0,0 +1,492 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OXSpeak Server API
|
||||
version: 1.0.0
|
||||
description: API HTTP exposée par le serveur OXSpeak.
|
||||
servers:
|
||||
- url: http://localhost:7000
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Vérifie l'état du service
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: ok
|
||||
|
||||
/auth/channel/login/:
|
||||
get:
|
||||
summary: Authentification par clé publique (renvoie un JWT)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Jeton JWT généré
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthResponse'
|
||||
'400':
|
||||
description: Requête invalide
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Erreur interne
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/api/server/:
|
||||
get:
|
||||
summary: Liste des serveurs
|
||||
responses:
|
||||
'200':
|
||||
description: Liste de serveurs
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Server'
|
||||
'500':
|
||||
description: Erreur serveur
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
post:
|
||||
summary: Crée un serveur
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateServerRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Serveur créé
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Server'
|
||||
'400': { description: Requête invalide }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/server/{id}/:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: Détail d'un serveur
|
||||
responses:
|
||||
'200':
|
||||
description: Serveur
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Server'
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
put:
|
||||
summary: Met à jour un serveur
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateServerRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Serveur mis à jour
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Server'
|
||||
'400': { description: Requête invalide }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
delete:
|
||||
summary: Supprime un serveur
|
||||
responses:
|
||||
'204': { description: Supprimé }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/category/:
|
||||
get:
|
||||
summary: Liste des catégories
|
||||
responses:
|
||||
'200':
|
||||
description: Liste des catégories
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Category'
|
||||
'500': { description: Erreur interne }
|
||||
post:
|
||||
summary: Crée une catégorie
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateCategoryRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Catégorie créée
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Category'
|
||||
'400': { description: Requête invalide }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/category/{id}/:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: Détail d'une catégorie
|
||||
responses:
|
||||
'200':
|
||||
description: Catégorie
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Category'
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
put:
|
||||
summary: Met à jour une catégorie
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateCategoryRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Catégorie mise à jour
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Category'
|
||||
'400': { description: Requête invalide }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
delete:
|
||||
summary: Supprime une catégorie
|
||||
responses:
|
||||
'204': { description: Supprimé }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/channel/:
|
||||
get:
|
||||
summary: Liste des canaux
|
||||
responses:
|
||||
'200':
|
||||
description: Liste des canaux
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Channel'
|
||||
'500': { description: Erreur interne }
|
||||
post:
|
||||
summary: Crée un canal
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateChannelRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Canal créé
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Channel'
|
||||
'400': { description: Requête invalide }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/channel/{id}/:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: Détail d'un canal
|
||||
responses:
|
||||
'200':
|
||||
description: Canal
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Channel'
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
put:
|
||||
summary: Met à jour un canal
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateChannelRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Canal mis à jour
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Channel'
|
||||
'400': { description: Requête invalide }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
delete:
|
||||
summary: Supprime un canal
|
||||
responses:
|
||||
'204': { description: Supprimé }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/message/:
|
||||
get:
|
||||
summary: Liste des messages
|
||||
responses:
|
||||
'200':
|
||||
description: Liste des messages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
'500': { description: Erreur interne }
|
||||
post:
|
||||
summary: Crée un message
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateMessageRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Message créé
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Message'
|
||||
'400': { description: Requête invalide }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
/api/message/{id}/:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
get:
|
||||
summary: Détail d'un message
|
||||
responses:
|
||||
'200':
|
||||
description: Message
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Message'
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
put:
|
||||
summary: Met à jour un message
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateMessageRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Message mis à jour
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Message'
|
||||
'400': { description: Requête invalide }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
delete:
|
||||
summary: Supprime un message
|
||||
responses:
|
||||
'204': { description: Supprimé }
|
||||
'404': { description: Introuvable }
|
||||
'500': { description: Erreur interne }
|
||||
|
||||
components:
|
||||
schemas:
|
||||
UUID:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
|
||||
AuthRequest:
|
||||
type: object
|
||||
required: [pub_key]
|
||||
properties:
|
||||
pub_key:
|
||||
type: string
|
||||
description: Clé publique du client
|
||||
|
||||
AuthResponse:
|
||||
type: object
|
||||
properties:
|
||||
JWT:
|
||||
type: string
|
||||
|
||||
Server:
|
||||
type: object
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/UUID' }
|
||||
name: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
required: [id, name, created_at, updated_at]
|
||||
|
||||
CreateServerRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
password: { type: string, nullable: true }
|
||||
|
||||
UpdateServerRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
password: { type: string, nullable: true }
|
||||
|
||||
Category:
|
||||
type: object
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/UUID' }
|
||||
server_id: { $ref: '#/components/schemas/UUID' }
|
||||
name: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
required: [id, server_id, name, created_at, updated_at]
|
||||
|
||||
CreateCategoryRequest:
|
||||
type: object
|
||||
required: [server_id, name]
|
||||
properties:
|
||||
server_id: { $ref: '#/components/schemas/UUID' }
|
||||
name: { type: string }
|
||||
|
||||
UpdateCategoryRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
|
||||
Channel:
|
||||
type: object
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/UUID' }
|
||||
server_id: { $ref: '#/components/schemas/UUID' }
|
||||
category_id: { $ref: '#/components/schemas/UUID' }
|
||||
position: { type: integer, format: int32 }
|
||||
type: { $ref: '#/components/schemas/ChannelType' }
|
||||
name: { type: string, nullable: true }
|
||||
created_at: { type: string, format: date-time }
|
||||
updated_at: { type: string, format: date-time }
|
||||
required: [id, position, type, created_at, updated_at]
|
||||
|
||||
ChannelType:
|
||||
type: string
|
||||
enum: [text, voice, dm]
|
||||
|
||||
CreateChannelRequest:
|
||||
type: object
|
||||
required: [type]
|
||||
properties:
|
||||
server_id: { $ref: '#/components/schemas/UUID' }
|
||||
category_id: { $ref: '#/components/schemas/UUID' }
|
||||
position: { type: integer, format: int32 }
|
||||
type: { $ref: '#/components/schemas/ChannelType' }
|
||||
name: { type: string, nullable: true }
|
||||
|
||||
UpdateChannelRequest:
|
||||
type: object
|
||||
properties:
|
||||
server_id: { $ref: '#/components/schemas/UUID' }
|
||||
category_id: { $ref: '#/components/schemas/UUID' }
|
||||
position: { type: integer, format: int32 }
|
||||
type: { $ref: '#/components/schemas/ChannelType' }
|
||||
name: { type: string, nullable: true }
|
||||
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/UUID' }
|
||||
channel_id: { $ref: '#/components/schemas/UUID' }
|
||||
user_id: { $ref: '#/components/schemas/UUID' }
|
||||
content: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
edited_at: { type: string, format: date-time, nullable: true }
|
||||
reply_to_id: { $ref: '#/components/schemas/UUID' }
|
||||
required: [id, channel_id, user_id, content, created_at]
|
||||
|
||||
CreateMessageRequest:
|
||||
type: object
|
||||
required: [channel_id, user_id, content]
|
||||
properties:
|
||||
channel_id: { $ref: '#/components/schemas/UUID' }
|
||||
user_id: { $ref: '#/components/schemas/UUID' }
|
||||
content: { type: string }
|
||||
reply_to_id: { $ref: '#/components/schemas/UUID' }
|
||||
|
||||
UpdateMessageRequest:
|
||||
type: object
|
||||
required: [content]
|
||||
properties:
|
||||
content: { type: string }
|
||||
reply_to_id: { $ref: '#/components/schemas/UUID' }
|
||||
Reference in New Issue
Block a user