feat(bots): co-owners handling
This commit is contained in:
parent
6c883f3867
commit
9df1d0de56
4 changed files with 177 additions and 14 deletions
|
|
@ -3,7 +3,9 @@ package bot
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
|
|
@ -23,7 +25,7 @@ func NewService(q *db.Queries) *Service {
|
|||
func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, error) {
|
||||
user := middleware.GetUser(ctx)
|
||||
|
||||
var count int32 = 0
|
||||
var count int32
|
||||
b, err := s.q.CreateBot(ctx, db.CreateBotParams{
|
||||
ID: data.ID,
|
||||
Overview: &data.Overview,
|
||||
|
|
@ -41,18 +43,115 @@ func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, e
|
|||
if err != nil {
|
||||
slog.Error("error submitting bot", "err", err, "bot_id", data.ID)
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
switch pgErr.Code {
|
||||
case "23505":
|
||||
return nil, errorutil.ErrBotAlreadyExists
|
||||
}
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return nil, errorutil.ErrBotAlreadyExists
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.addCoOwners(ctx, b.ID, user.ID, data.CoOwners); err != nil {
|
||||
// Bot was created but co-owners failed. Log and surface the error.
|
||||
// ideally this whole operation runs in a transaction but who cares lol.
|
||||
slog.Error("error adding co-owners after submit", "err", err, "bot_id", b.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// addCoOwners validates and inserts each co-owner for a bot.
|
||||
// mainOwnerID is provided to reject attempts to set the main owner as a co-owner.
|
||||
func (s *Service) addCoOwners(ctx context.Context, botID, mainOwnerID string, coOwnerIDs []string) error {
|
||||
for _, id := range coOwnerIDs {
|
||||
if err := validateCoOwner(id, mainOwnerID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
|
||||
BotID: botID,
|
||||
UserID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("adding co-owner %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCoOwner adds a single co-owner to a bot.
|
||||
// Only the main owner of the bot may call this.
|
||||
func (s *Service) AddCoOwner(ctx context.Context, botID, coOwnerID string) error {
|
||||
user := middleware.GetUser(ctx)
|
||||
|
||||
bot, err := s.q.GetBot(ctx, botID)
|
||||
if err != nil {
|
||||
return errorutil.ErrNotFound.Err
|
||||
}
|
||||
|
||||
if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
|
||||
return errorutil.ErrForbidden.Err
|
||||
}
|
||||
|
||||
if err := validateCoOwner(coOwnerID, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
|
||||
BotID: botID,
|
||||
UserID: coOwnerID,
|
||||
}); err != nil {
|
||||
slog.Error("error adding co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveCoOwner removes a co-owner from a bot.
|
||||
// Only the main owner of the bot may call this.
|
||||
func (s *Service) RemoveCoOwner(ctx context.Context, botID, coOwnerID string) error {
|
||||
user := middleware.GetUser(ctx)
|
||||
|
||||
bot, err := s.q.GetBot(ctx, botID)
|
||||
if err != nil {
|
||||
return errorutil.ErrNotFound.Err
|
||||
}
|
||||
|
||||
if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
|
||||
return errorutil.ErrForbidden.Err
|
||||
}
|
||||
|
||||
if err := s.q.RemoveBotCoOwner(ctx, db.RemoveBotCoOwnerParams{
|
||||
BotID: botID,
|
||||
UserID: coOwnerID,
|
||||
}); err != nil {
|
||||
slog.Error("error removing co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCoOwners returns the users who are co-owners of a bot.
|
||||
func (s *Service) ListCoOwners(ctx context.Context, botID string) ([]*db.User, error) {
|
||||
owners, err := s.q.ListCoOwnersByBot(ctx, botID)
|
||||
if err != nil {
|
||||
slog.Error("error listing co-owners", "err", err, "bot_id", botID)
|
||||
return nil, err
|
||||
}
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// validateCoOwner checks that the given ID is a valid Discord snowflake
|
||||
// and is not the same as the main owner.
|
||||
func validateCoOwner(id, mainOwnerID string) error {
|
||||
if _, err := strconv.ParseUint(id, 10, 64); err != nil {
|
||||
return errorutil.ErrInvalidID
|
||||
}
|
||||
if id == mainOwnerID {
|
||||
return errorutil.ErrMainOwnerAsCoOwner
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id string) (*db.Bot, error) {
|
||||
return s.q.GetBot(ctx, id)
|
||||
}
|
||||
|
|
@ -75,7 +174,6 @@ func (s *Service) List(
|
|||
Status: db.BotStatusApproved,
|
||||
Limit: p.Limit,
|
||||
Offset: p.Offset,
|
||||
// todo: query
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("error listing bots", "query", query, "err", err)
|
||||
|
|
|
|||
|
|
@ -26,5 +26,7 @@ func (c *CreateBotRequest) Bind(req *http.Request) error {
|
|||
return errorutil.ErrInvalidID
|
||||
}
|
||||
|
||||
// todo: proper checks idk
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,28 +31,32 @@ func NewRouter(q *db.Queries) *Router {
|
|||
}
|
||||
|
||||
func (r *Router) Routes() http.Handler {
|
||||
r.router.
|
||||
With(middleware.AuthGuardMiddleware).
|
||||
Post("/", r.submitBot)
|
||||
r.router.Get("/", r.listBots)
|
||||
r.router.Get("/", r.listBots) // todo: deprecate this
|
||||
r.router.With(middleware.AuthGuardMiddleware).Post("/", r.submitBot)
|
||||
r.router.Route("/{botID}", func(router chi.Router) {
|
||||
router.Use(r.BotContext)
|
||||
router.With(r.BotCache).Get("/", r.getBot)
|
||||
router.Route("/co-owners", func(c chi.Router) {
|
||||
c.Use(middleware.AuthGuardMiddleware)
|
||||
c.Get("/", r.listCoOwners)
|
||||
c.Post("/{userID}", r.addCoOwner)
|
||||
c.Delete("/{userID}", r.removeCoOwner)
|
||||
})
|
||||
})
|
||||
|
||||
return r.router
|
||||
}
|
||||
|
||||
func (r *Router) submitBot(w http.ResponseWriter, req *http.Request) {
|
||||
data := &CreateBotRequest{}
|
||||
var data CreateBotRequest
|
||||
|
||||
if err := render.Bind(req, data); err != nil {
|
||||
if err := render.Bind(req, &data); err != nil {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
bot, err := r.bots.Submit(ctx, *data)
|
||||
bot, err := r.bots.Submit(ctx, data)
|
||||
if errors.Is(err, errorutil.ErrBotAlreadyExists) {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
} else {
|
||||
|
|
@ -89,6 +93,59 @@ func (r *Router) listBots(w http.ResponseWriter, req *http.Request) {
|
|||
render.JSON(w, req, page)
|
||||
}
|
||||
|
||||
func (r *Router) listCoOwners(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := ctx.Value(botKey).(*db.Bot)
|
||||
|
||||
owners, err := r.bots.ListCoOwners(ctx, bot.ID)
|
||||
if err != nil {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, req, owners)
|
||||
}
|
||||
|
||||
func (r *Router) addCoOwner(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := ctx.Value(botKey).(*db.Bot)
|
||||
userID := chi.URLParam(req, "userID")
|
||||
|
||||
if err := r.bots.AddCoOwner(ctx, bot.ID, userID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errorutil.ErrNotFound.Err):
|
||||
render.Render(w, req, errorutil.ErrNotFound)
|
||||
case errors.Is(err, errorutil.ErrForbidden.Err):
|
||||
render.Render(w, req, errorutil.ErrForbidden)
|
||||
default:
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
render.NoContent(w, req)
|
||||
}
|
||||
|
||||
func (r *Router) removeCoOwner(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := ctx.Value(botKey).(*db.Bot)
|
||||
userID := chi.URLParam(req, "userID")
|
||||
|
||||
if err := r.bots.RemoveCoOwner(ctx, bot.ID, userID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errorutil.ErrNotFound.Err):
|
||||
render.Render(w, req, errorutil.ErrNotFound)
|
||||
case errors.Is(err, errorutil.ErrForbidden.Err):
|
||||
render.Render(w, req, errorutil.ErrForbidden)
|
||||
default:
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
render.NoContent(w, req)
|
||||
}
|
||||
|
||||
func (r *Router) BotContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrBotNotFound = errors.New("bot not found")
|
||||
ErrBotAlreadyExists = errors.New("this bot has already been submitted")
|
||||
ErrSearchFailed = errors.New("no bots found fitting this filter")
|
||||
ErrInvalidID = errors.New("invalid discord id")
|
||||
|
|
@ -54,6 +55,11 @@ var ErrUnauthorized = &ErrResponse{
|
|||
StatusText: "unauthorized",
|
||||
}
|
||||
|
||||
var ErrForbidden = &ErrResponse{
|
||||
HTTPStatusCode: 403,
|
||||
StatusText: "forbidden",
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue