Compare commits
No commits in common. "9ef5e33b82da60db9ba64ffee22a3cac674b8ff0" and "0326e90c1b6d1756e0f240c7668b9ba214c7f9f1" have entirely different histories.
9ef5e33b82
...
0326e90c1b
11 changed files with 50 additions and 306 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -26,11 +26,8 @@ go.work.sum
|
|||
|
||||
# env file
|
||||
.env
|
||||
.direnv/
|
||||
.direnv
|
||||
|
||||
# pg
|
||||
.pgdata/
|
||||
.pgsocket/
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ type Config struct {
|
|||
var config Config
|
||||
|
||||
func init() {
|
||||
gonsoleHandler := gonsole.New(os.Stdout, slog.LevelDebug)
|
||||
slogHandler := slog.New(gonsoleHandler)
|
||||
slog.SetDefault(slogHandler)
|
||||
|
||||
dotenv.MustLoad()
|
||||
if err := env.Load(&config); err != nil {
|
||||
panic(err)
|
||||
|
|
@ -32,6 +28,9 @@ func init() {
|
|||
}
|
||||
|
||||
func main() {
|
||||
gonsoleHandler := gonsole.New(os.Stdout, slog.LevelDebug)
|
||||
slogHandler := slog.New(gonsoleHandler)
|
||||
slog.SetDefault(slogHandler)
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := pgxpool.New(ctx, config.DatabaseURL)
|
||||
|
|
@ -41,7 +40,7 @@ func main() {
|
|||
}
|
||||
|
||||
queries := db.New(conn)
|
||||
server := server.NewServer(queries)
|
||||
server := server.NewServer(queries, slogHandler)
|
||||
|
||||
server.Start(config.Port)
|
||||
}
|
||||
27
flake.nix
27
flake.nix
|
|
@ -14,7 +14,6 @@
|
|||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
forEachSupportedSystem =
|
||||
f:
|
||||
inputs.nixpkgs.lib.genAttrs supportedSystems (
|
||||
|
|
@ -40,33 +39,7 @@
|
|||
go
|
||||
sqlc
|
||||
goose
|
||||
postgresql
|
||||
|
||||
(pkgs.writeShellScriptBin "pg-start" ''
|
||||
export PGDATA=${"$PWD"}/.pgdata
|
||||
export PGHOST=${"$PWD"}/.pgsocket
|
||||
|
||||
mkdir -p "$PGDATA"
|
||||
mkdir -p "$PGHOST"
|
||||
|
||||
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||
echo "Initializing database..."
|
||||
initdb -D "$PGDATA"
|
||||
fi
|
||||
|
||||
echo "Starting postgres..."
|
||||
pg_ctl -D "$PGDATA" -l logfile -o "-k $PGHOST" start
|
||||
'')
|
||||
|
||||
(pkgs.writeShellScriptBin "pg-stop" ''
|
||||
export PGDATA=${"$PWD"}/.pgdata
|
||||
pg_ctl -D "$PGDATA" stop
|
||||
'')
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export PGDATA=$PWD/.pgdata
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ package admin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/paginate"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -20,20 +18,6 @@ func NewService(q *db.Queries) *Service {
|
|||
return &Service{q: q}
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id string) (*db.Bot, error) {
|
||||
bot, err := s.q.GetBot(ctx, id)
|
||||
if err != nil {
|
||||
slog.Error("error getting bot", "err", err, "id", id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errorutil.ErrNotFound.Err
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListBots(ctx context.Context, status db.BotStatus, p paginate.Params) (paginate.Page[*db.Bot], error) {
|
||||
status = db.BotStatus(strings.ToLower(status.String()))
|
||||
total, err := s.q.CountBotsByUsername(ctx, db.CountBotsByUsernameParams{
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import (
|
|||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/middleware"
|
||||
"codeberg.org/nextgo/dbots/internal/paginate"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
|
@ -25,24 +23,14 @@ func NewRouter(q *db.Queries) *Router {
|
|||
}
|
||||
|
||||
func (r *Router) Routes() http.Handler {
|
||||
r.router.Get("/bots", r.listBots)
|
||||
r.router.Route("/bots", func(router chi.Router) {
|
||||
router.Get("/", r.listBots)
|
||||
router.Route("/{botID}", func(b chi.Router) {
|
||||
b.Use(middleware.BotContext(r.admin.q))
|
||||
b.Get("/", r.getBot)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
return r.router
|
||||
}
|
||||
|
||||
func (r *Router) getBot(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := middleware.GetBot(ctx)
|
||||
|
||||
render.JSON(w, req, bot)
|
||||
}
|
||||
|
||||
func (r *Router) listBots(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
status := db.BotStatus(req.URL.Query().Get("s"))
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@ package bot
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/middleware"
|
||||
"codeberg.org/nextgo/dbots/internal/paginate"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
|
|
@ -26,7 +23,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
|
||||
var count int32 = 0
|
||||
b, err := s.q.CreateBot(ctx, db.CreateBotParams{
|
||||
ID: data.ID,
|
||||
Overview: &data.Overview,
|
||||
|
|
@ -44,127 +41,20 @@ 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) && pgErr.Code == "23505" {
|
||||
return nil, errorutil.ErrBotAlreadyExists
|
||||
if errors.As(err, &pgErr) {
|
||||
switch pgErr.Code {
|
||||
case "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) {
|
||||
bot, err := s.q.GetBot(ctx, id)
|
||||
if err != nil {
|
||||
slog.Error("error getting bot", "err", err, "id", id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errorutil.ErrNotFound.Err
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
return s.q.GetBot(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) List(
|
||||
|
|
@ -185,6 +75,7 @@ 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,7 +26,5 @@ func (c *CreateBotRequest) Bind(req *http.Request) error {
|
|||
return errorutil.ErrInvalidID
|
||||
}
|
||||
|
||||
// todo: proper checks idk
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -13,6 +14,10 @@ import (
|
|||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const botKey contextKey = "bot"
|
||||
|
||||
type Router struct {
|
||||
bots *Service
|
||||
router chi.Router
|
||||
|
|
@ -26,39 +31,32 @@ func NewRouter(q *db.Queries) *Router {
|
|||
}
|
||||
|
||||
func (r *Router) Routes() http.Handler {
|
||||
r.router.Get("/", r.listBots) // todo: deprecate this
|
||||
r.router.With(middleware.AuthGuardMiddleware).Post("/", r.submitBot)
|
||||
r.router.
|
||||
With(middleware.AuthGuardMiddleware).
|
||||
Post("/", r.submitBot)
|
||||
r.router.Get("/", r.listBots)
|
||||
r.router.Route("/{botID}", func(router chi.Router) {
|
||||
router.Use(middleware.BotContext(r.bots.q))
|
||||
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) {
|
||||
var data CreateBotRequest
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, errorutil.ErrBotAlreadyExists) {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
} else {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
}
|
||||
return
|
||||
bot, err := r.bots.Submit(ctx, *data)
|
||||
if errors.Is(err, errorutil.ErrBotAlreadyExists) {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
} else {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
}
|
||||
|
||||
render.Status(req, http.StatusCreated)
|
||||
|
|
@ -67,7 +65,12 @@ func (r *Router) submitBot(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
func (r *Router) getBot(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := middleware.GetBot(ctx)
|
||||
|
||||
bot, ok := ctx.Value(botKey).(*db.Bot)
|
||||
if !ok {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(nil))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, req, bot)
|
||||
}
|
||||
|
|
@ -86,57 +89,19 @@ 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 := middleware.GetBot(ctx)
|
||||
|
||||
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 := middleware.GetBot(ctx)
|
||||
userID := chi.URLParam(req, "userID")
|
||||
|
||||
if err := r.bots.AddCoOwner(ctx, bot.ID, userID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errorutil.ErrNotFound.Err):
|
||||
func (r *Router) BotContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
botID := chi.URLParam(req, "botID")
|
||||
bot, err := r.bots.Get(ctx, botID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
render.NoContent(w, req)
|
||||
}
|
||||
|
||||
func (r *Router) removeCoOwner(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
bot := middleware.GetBot(ctx)
|
||||
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)
|
||||
ctx = context.WithValue(ctx, botKey, bot)
|
||||
next.ServeHTTP(w, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) BotCache(next http.Handler) http.Handler {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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")
|
||||
|
|
@ -55,11 +54,6 @@ var ErrUnauthorized = &ErrResponse{
|
|||
StatusText: "unauthorized",
|
||||
}
|
||||
|
||||
var ErrForbidden = &ErrResponse{
|
||||
HTTPStatusCode: 403,
|
||||
StatusText: "forbidden",
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const botKey contextKey = "bot"
|
||||
|
||||
func BotContext(q *db.Queries) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
botID := chi.URLParam(req, "botID")
|
||||
bot, err := q.GetBot(ctx, botID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
render.Render(w, req, errorutil.ErrNotFound)
|
||||
} else {
|
||||
render.Render(w, req, errorutil.ErrInternal(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, botKey, bot)
|
||||
next.ServeHTTP(w, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetBot(ctx context.Context) *db.Bot {
|
||||
bot, ok := ctx.Value(botKey).(*db.Bot)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bot
|
||||
}
|
||||
|
|
@ -21,10 +21,10 @@ type Server struct {
|
|||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewServer(queries *db.Queries) *Server {
|
||||
func NewServer(queries *db.Queries, logger *slog.Logger) *Server {
|
||||
router := chi.NewMux()
|
||||
|
||||
router.Use(httplog.RequestLogger(slog.Default(), &httplog.Options{}))
|
||||
router.Use(httplog.RequestLogger(logger, nil))
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue