chore: base
This commit is contained in:
parent
3ad879c867
commit
1062fc7c68
30 changed files with 2059 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.direnv
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
46
cmd/server/main.go
Normal file
46
cmd/server/main.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/server"
|
||||
"codeberg.org/ungo/env"
|
||||
"codeberg.org/ungo/env/dotenv"
|
||||
"codeberg.org/ungo/gonsole"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DatabaseURL string `env:"DATABASE_URL,required"`
|
||||
Port int `env:"PORT,default=8080"`
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
||||
func init() {
|
||||
dotenv.MustLoad()
|
||||
if err := env.Load(&config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Error("error connecting to postgres", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
queries := db.New(conn)
|
||||
server := server.NewServer(queries, slogHandler)
|
||||
|
||||
server.Start(config.Port)
|
||||
}
|
||||
25
flake.lock
generated
Normal file
25
flake.lock
generated
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1774106199,
|
||||
"narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
|
||||
"rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
|
||||
"revCount": 967235,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.967235%2Brev-6c9a78c09ff4d6c21d0319114873508a6ec01655/019d198c-70dc-7753-b1d1-721451f578ae/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
47
flake.nix
Normal file
47
flake.nix
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
description = "minimal flake for go dev";
|
||||
|
||||
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1";
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
let
|
||||
goVersion = 26;
|
||||
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forEachSupportedSystem =
|
||||
f:
|
||||
inputs.nixpkgs.lib.genAttrs supportedSystems (
|
||||
system:
|
||||
f {
|
||||
pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ inputs.self.overlays.default ];
|
||||
};
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
go = final."go_1_${toString goVersion}";
|
||||
};
|
||||
|
||||
devShells = forEachSupportedSystem (
|
||||
{ pkgs }:
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
sqlc
|
||||
goose
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
21
go.mod
Normal file
21
go.mod
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
module codeberg.org/nextgo/dbots
|
||||
|
||||
go 1.25.8
|
||||
|
||||
require (
|
||||
codeberg.org/ungo/env v0.0.0-20260315114019-c4fbd9390cb3
|
||||
codeberg.org/ungo/gonsole v0.1.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
38
go.sum
Normal file
38
go.sum
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
codeberg.org/ungo/env v0.0.0-20260315114019-c4fbd9390cb3 h1:Xn8IiW5uYGajGqYPXU0kS8zXxqRs5E/MTfYjm0O1KrI=
|
||||
codeberg.org/ungo/env v0.0.0-20260315114019-c4fbd9390cb3/go.mod h1:pXfrNASG7JyxL30Zof3b1vbpd1dsHePTh3zGfPFgJKs=
|
||||
codeberg.org/ungo/gonsole v0.1.0 h1:QE/qpSyovejIXzIh29tzmrwgDWfaKUqNTCMZPJEDfvY=
|
||||
codeberg.org/ungo/gonsole v0.1.0/go.mod h1:xPHtFAI4CeujxpwhkiVrRa6pMvWYtaNsC+jJIib5EgA=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
|
||||
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
42
internal/admin/admin.go
Normal file
42
internal/admin/admin.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/paginate"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
q *db.Queries
|
||||
}
|
||||
|
||||
func NewService(q *db.Queries) *Service {
|
||||
return &Service{q: q}
|
||||
}
|
||||
|
||||
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{
|
||||
Status: status,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("error counting bots", "err", err)
|
||||
return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
|
||||
}
|
||||
|
||||
bots, err := s.q.ListBotsByStatus(ctx, db.ListBotsByStatusParams{
|
||||
Status: status,
|
||||
Limit: p.Limit,
|
||||
Offset: p.Offset,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("error listing bots", "err", err)
|
||||
return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
|
||||
}
|
||||
|
||||
return paginate.NewPage(bots, int(total), p), nil
|
||||
}
|
||||
46
internal/admin/router.go
Normal file
46
internal/admin/router.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"codeberg.org/nextgo/dbots/internal/paginate"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
admin *Service
|
||||
router chi.Router
|
||||
}
|
||||
|
||||
func NewRouter(q *db.Queries) *Router {
|
||||
return &Router{
|
||||
admin: NewService(q),
|
||||
router: chi.NewRouter(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Routes() http.Handler {
|
||||
r.router.Get("/bots", r.listBots)
|
||||
r.router.Route("/bots", func(router chi.Router) {
|
||||
|
||||
})
|
||||
|
||||
return r.router
|
||||
}
|
||||
|
||||
func (r *Router) listBots(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
status := db.BotStatus(req.URL.Query().Get("s"))
|
||||
p := paginate.ParseParams(req)
|
||||
|
||||
page, err := r.admin.ListBots(ctx, status, p)
|
||||
if err != nil {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, req, page)
|
||||
}
|
||||
85
internal/bot/bot.go
Normal file
85
internal/bot/bot.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"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/pgconn"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
q *db.Queries
|
||||
}
|
||||
|
||||
func NewService(q *db.Queries) *Service {
|
||||
return &Service{q: q}
|
||||
}
|
||||
|
||||
func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, error) {
|
||||
user := middleware.GetUser(ctx)
|
||||
|
||||
var count int32 = 0
|
||||
b, err := s.q.CreateBot(ctx, db.CreateBotParams{
|
||||
ID: data.ID,
|
||||
Overview: &data.Overview,
|
||||
IsSlash: data.IsSlash,
|
||||
InstallContext: data.InstallContext,
|
||||
ImportedFrom: nil,
|
||||
Username: data.Username,
|
||||
Avatar: &data.Avatar,
|
||||
Description: &data.Description,
|
||||
GuildCount: &count,
|
||||
InstallCount: &count,
|
||||
Prefix: data.Prefix,
|
||||
MainOwnerID: &user.ID,
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id string) (*db.Bot, error) {
|
||||
return s.q.GetBot(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) List(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
p paginate.Params,
|
||||
) (paginate.Page[*db.Bot], error) {
|
||||
total, err := s.q.CountBotsByUsername(ctx, db.CountBotsByUsernameParams{
|
||||
Status: db.BotStatusApproved,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("error counting bots", "query", query, "err", err)
|
||||
return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
|
||||
}
|
||||
|
||||
bots, err := s.q.ListBotsByStatus(ctx, db.ListBotsByStatusParams{
|
||||
Status: db.BotStatusApproved,
|
||||
Limit: p.Limit,
|
||||
Offset: p.Offset,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("error listing bots", "query", query, "err", err)
|
||||
return paginate.Page[*db.Bot]{}, errorutil.ErrSearchFailed
|
||||
}
|
||||
|
||||
return paginate.NewPage(bots, int(total), p), nil
|
||||
}
|
||||
30
internal/bot/input.go
Normal file
30
internal/bot/input.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
)
|
||||
|
||||
type CreateBotRequest struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
Overview string `json:"overview"`
|
||||
Description string `json:"description"`
|
||||
IsSlash bool `json:"is_slash"`
|
||||
InstallContext db.InstallContext `json:"install_context"`
|
||||
Prefix *string `json:"prefix"`
|
||||
ImportedFrom *string `json:"imported_from"`
|
||||
CoOwners []string `json:"co_owners"`
|
||||
}
|
||||
|
||||
func (c *CreateBotRequest) Bind(req *http.Request) error {
|
||||
if _, err := strconv.ParseUint(c.ID, 10, 64); err != nil {
|
||||
return errorutil.ErrInvalidID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
110
internal/bot/router.go
Normal file
110
internal/bot/router.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const botKey contextKey = "bot"
|
||||
|
||||
type Router struct {
|
||||
bots *Service
|
||||
router chi.Router
|
||||
}
|
||||
|
||||
func NewRouter(q *db.Queries) *Router {
|
||||
return &Router{
|
||||
bots: NewService(q),
|
||||
router: chi.NewRouter(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Routes() http.Handler {
|
||||
r.router.
|
||||
With(middleware.AuthGuardMiddleware).
|
||||
Post("/", r.submitBot)
|
||||
r.router.Get("/", r.listBots)
|
||||
r.router.Route("/{botID}", func(router chi.Router) {
|
||||
router.Use(r.BotContext)
|
||||
router.With(r.BotCache).Get("/", r.getBot)
|
||||
})
|
||||
|
||||
return r.router
|
||||
}
|
||||
|
||||
func (r *Router) submitBot(w http.ResponseWriter, req *http.Request) {
|
||||
data := &CreateBotRequest{}
|
||||
|
||||
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 {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(req, http.StatusCreated)
|
||||
render.JSON(w, req, bot)
|
||||
}
|
||||
|
||||
func (r *Router) getBot(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
bot, ok := ctx.Value(botKey).(*db.Bot)
|
||||
if !ok {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(nil))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, req, bot)
|
||||
}
|
||||
|
||||
func (r *Router) listBots(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
query := req.URL.Query().Get("q")
|
||||
p := paginate.ParseParams(req)
|
||||
|
||||
page, err := r.bots.List(ctx, query, p)
|
||||
if err != nil {
|
||||
render.Render(w, req, errorutil.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, req, page)
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, botKey, bot)
|
||||
next.ServeHTTP(w, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) BotCache(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Add("Cache-Control", "max-age=3600")
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
138
internal/db/bot_co_owners.sql.go
Normal file
138
internal/db/bot_co_owners.sql.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: bot_co_owners.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const addBotCoOwner = `-- name: AddBotCoOwner :exec
|
||||
INSERT INTO bot_co_owners (bot_id, user_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
type AddBotCoOwnerParams struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddBotCoOwner(ctx context.Context, arg AddBotCoOwnerParams) error {
|
||||
_, err := q.db.Exec(ctx, addBotCoOwner, arg.BotID, arg.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBotCoOwner = `-- name: GetBotCoOwner :one
|
||||
SELECT bot_id, user_id FROM bot_co_owners
|
||||
WHERE bot_id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type GetBotCoOwnerParams struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetBotCoOwner(ctx context.Context, arg GetBotCoOwnerParams) (*BotCoOwner, error) {
|
||||
row := q.db.QueryRow(ctx, getBotCoOwner, arg.BotID, arg.UserID)
|
||||
var i BotCoOwner
|
||||
err := row.Scan(&i.BotID, &i.UserID)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const listBotsByCoOwner = `-- name: ListBotsByCoOwner :many
|
||||
SELECT b.id, b.username, b.avatar, b.overview, b.description, b.is_slash, b.install_context, b.guild_count, b.install_count, b.imported_from, b.prefix, b.created_at, b.updated_at, b.status, b.main_owner_id
|
||||
FROM bot_co_owners bco
|
||||
JOIN bots b ON b.id = bco.bot_id
|
||||
WHERE bco.user_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListBotsByCoOwner(ctx context.Context, userID string) ([]*Bot, error) {
|
||||
rows, err := q.db.Query(ctx, listBotsByCoOwner, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Bot{}
|
||||
for rows.Next() {
|
||||
var i Bot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listCoOwnersByBot = `-- name: ListCoOwnersByBot :many
|
||||
SELECT u.id, u.username, u.biography
|
||||
FROM bot_co_owners bco
|
||||
JOIN users u ON u.id = bco.user_id
|
||||
WHERE bco.bot_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListCoOwnersByBot(ctx context.Context, botID string) ([]*User, error) {
|
||||
rows, err := q.db.Query(ctx, listCoOwnersByBot, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*User{}
|
||||
for rows.Next() {
|
||||
var i User
|
||||
if err := rows.Scan(&i.ID, &i.Username, &i.Biography); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeAllCoOwnersByBot = `-- name: RemoveAllCoOwnersByBot :exec
|
||||
DELETE FROM bot_co_owners
|
||||
WHERE bot_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveAllCoOwnersByBot(ctx context.Context, botID string) error {
|
||||
_, err := q.db.Exec(ctx, removeAllCoOwnersByBot, botID)
|
||||
return err
|
||||
}
|
||||
|
||||
const removeBotCoOwner = `-- name: RemoveBotCoOwner :exec
|
||||
DELETE FROM bot_co_owners
|
||||
WHERE bot_id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type RemoveBotCoOwnerParams struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveBotCoOwner(ctx context.Context, arg RemoveBotCoOwnerParams) error {
|
||||
_, err := q.db.Exec(ctx, removeBotCoOwner, arg.BotID, arg.UserID)
|
||||
return err
|
||||
}
|
||||
468
internal/db/bots.sql.go
Normal file
468
internal/db/bots.sql.go
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: bots.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const countBotsByUsername = `-- name: CountBotsByUsername :one
|
||||
SELECT COUNT(*) FROM bots
|
||||
WHERE ($2::text IS NULL OR username ILIKE '%' || $2 || '%') AND status = $1
|
||||
`
|
||||
|
||||
type CountBotsByUsernameParams struct {
|
||||
Status BotStatus `json:"status"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountBotsByUsername(ctx context.Context, arg CountBotsByUsernameParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countBotsByUsername, arg.Status, arg.Query)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createBot = `-- name: CreateBot :one
|
||||
INSERT INTO bots (
|
||||
id,
|
||||
username,
|
||||
avatar,
|
||||
overview,
|
||||
description,
|
||||
is_slash,
|
||||
install_context,
|
||||
guild_count,
|
||||
install_count,
|
||||
imported_from,
|
||||
prefix,
|
||||
main_owner_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id
|
||||
`
|
||||
|
||||
type CreateBotParams struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Overview *string `json:"overview"`
|
||||
Description *string `json:"description"`
|
||||
IsSlash bool `json:"is_slash"`
|
||||
InstallContext InstallContext `json:"install_context"`
|
||||
GuildCount *int32 `json:"guild_count"`
|
||||
InstallCount *int32 `json:"install_count"`
|
||||
ImportedFrom *string `json:"imported_from"`
|
||||
Prefix *string `json:"prefix"`
|
||||
MainOwnerID *string `json:"main_owner_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateBot(ctx context.Context, arg CreateBotParams) (*Bot, error) {
|
||||
row := q.db.QueryRow(ctx, createBot,
|
||||
arg.ID,
|
||||
arg.Username,
|
||||
arg.Avatar,
|
||||
arg.Overview,
|
||||
arg.Description,
|
||||
arg.IsSlash,
|
||||
arg.InstallContext,
|
||||
arg.GuildCount,
|
||||
arg.InstallCount,
|
||||
arg.ImportedFrom,
|
||||
arg.Prefix,
|
||||
arg.MainOwnerID,
|
||||
)
|
||||
var i Bot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const deleteBot = `-- name: DeleteBot :exec
|
||||
DELETE FROM bots
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteBot(ctx context.Context, id string) error {
|
||||
_, err := q.db.Exec(ctx, deleteBot, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBot = `-- name: GetBot :one
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetBot(ctx context.Context, id string) (*Bot, error) {
|
||||
row := q.db.QueryRow(ctx, getBot, id)
|
||||
var i Bot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getBotByUsername = `-- name: GetBotByUsername :one
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetBotByUsername(ctx context.Context, username string) (*Bot, error) {
|
||||
row := q.db.QueryRow(ctx, getBotByUsername, username)
|
||||
var i Bot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const listBots = `-- name: ListBots :many
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
ORDER BY id
|
||||
LIMIT $1
|
||||
OFFSET $2
|
||||
`
|
||||
|
||||
type ListBotsParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListBots(ctx context.Context, arg ListBotsParams) ([]*Bot, error) {
|
||||
rows, err := q.db.Query(ctx, listBots, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Bot{}
|
||||
for rows.Next() {
|
||||
var i Bot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listBotsByOwner = `-- name: ListBotsByOwner :many
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
WHERE main_owner_id = $1
|
||||
ORDER BY id
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
`
|
||||
|
||||
type ListBotsByOwnerParams struct {
|
||||
MainOwnerID *string `json:"main_owner_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListBotsByOwner(ctx context.Context, arg ListBotsByOwnerParams) ([]*Bot, error) {
|
||||
rows, err := q.db.Query(ctx, listBotsByOwner, arg.MainOwnerID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Bot{}
|
||||
for rows.Next() {
|
||||
var i Bot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listBotsByStatus = `-- name: ListBotsByStatus :many
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
WHERE status = $1
|
||||
ORDER BY id
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
`
|
||||
|
||||
type ListBotsByStatusParams struct {
|
||||
Status BotStatus `json:"status"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListBotsByStatus(ctx context.Context, arg ListBotsByStatusParams) ([]*Bot, error) {
|
||||
rows, err := q.db.Query(ctx, listBotsByStatus, arg.Status, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Bot{}
|
||||
for rows.Next() {
|
||||
var i Bot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const searchBotsByUsername = `-- name: SearchBotsByUsername :many
|
||||
SELECT id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id FROM bots
|
||||
WHERE ($3::text IS NULL OR username ILIKE '%' || $3 || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
OFFSET $2
|
||||
`
|
||||
|
||||
type SearchBotsByUsernameParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchBotsByUsername(ctx context.Context, arg SearchBotsByUsernameParams) ([]*Bot, error) {
|
||||
rows, err := q.db.Query(ctx, searchBotsByUsername, arg.Limit, arg.Offset, arg.Query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Bot{}
|
||||
for rows.Next() {
|
||||
var i Bot
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateBot = `-- name: UpdateBot :one
|
||||
UPDATE bots
|
||||
SET
|
||||
username = coalesce($1, username),
|
||||
avatar = coalesce($2, avatar),
|
||||
overview = coalesce($3, overview),
|
||||
description = coalesce($4, description),
|
||||
is_slash = coalesce($5, is_slash),
|
||||
install_context = coalesce($6, install_context),
|
||||
guild_count = coalesce($7, guild_count),
|
||||
install_count = coalesce($8, install_count),
|
||||
imported_from = coalesce($9, imported_from),
|
||||
prefix = coalesce($10, prefix),
|
||||
main_owner_id = coalesce($11, main_owner_id),
|
||||
updated_at = now()
|
||||
WHERE id = $12
|
||||
RETURNING id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id
|
||||
`
|
||||
|
||||
type UpdateBotParams struct {
|
||||
Username *string `json:"username"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Overview *string `json:"overview"`
|
||||
Description *string `json:"description"`
|
||||
IsSlash *bool `json:"is_slash"`
|
||||
InstallContext InstallContext `json:"install_context"`
|
||||
GuildCount *int32 `json:"guild_count"`
|
||||
InstallCount *int32 `json:"install_count"`
|
||||
ImportedFrom *string `json:"imported_from"`
|
||||
Prefix *string `json:"prefix"`
|
||||
MainOwnerID *string `json:"main_owner_id"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateBot(ctx context.Context, arg UpdateBotParams) (*Bot, error) {
|
||||
row := q.db.QueryRow(ctx, updateBot,
|
||||
arg.Username,
|
||||
arg.Avatar,
|
||||
arg.Overview,
|
||||
arg.Description,
|
||||
arg.IsSlash,
|
||||
arg.InstallContext,
|
||||
arg.GuildCount,
|
||||
arg.InstallCount,
|
||||
arg.ImportedFrom,
|
||||
arg.Prefix,
|
||||
arg.MainOwnerID,
|
||||
arg.ID,
|
||||
)
|
||||
var i Bot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const updateBotStatus = `-- name: UpdateBotStatus :one
|
||||
UPDATE bots
|
||||
SET
|
||||
status = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, username, avatar, overview, description, is_slash, install_context, guild_count, install_count, imported_from, prefix, created_at, updated_at, status, main_owner_id
|
||||
`
|
||||
|
||||
type UpdateBotStatusParams struct {
|
||||
ID string `json:"id"`
|
||||
Status BotStatus `json:"status"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateBotStatus(ctx context.Context, arg UpdateBotStatusParams) (*Bot, error) {
|
||||
row := q.db.QueryRow(ctx, updateBotStatus, arg.ID, arg.Status)
|
||||
var i Bot
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Avatar,
|
||||
&i.Overview,
|
||||
&i.Description,
|
||||
&i.IsSlash,
|
||||
&i.InstallContext,
|
||||
&i.GuildCount,
|
||||
&i.InstallCount,
|
||||
&i.ImportedFrom,
|
||||
&i.Prefix,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.MainOwnerID,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
43
internal/db/custom.go
Normal file
43
internal/db/custom.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package db
|
||||
|
||||
type InstallContext string
|
||||
|
||||
const (
|
||||
InstallContextUser InstallContext = "user"
|
||||
InstallContextGuild InstallContext = "guild"
|
||||
)
|
||||
|
||||
func (c InstallContext) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func (c InstallContext) IsValid() bool {
|
||||
switch c {
|
||||
case InstallContextUser, InstallContextGuild:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type BotStatus string
|
||||
|
||||
const (
|
||||
BotStatusDeleted BotStatus = "deleted"
|
||||
BotStatusPending BotStatus = "pending"
|
||||
BotStatusRejected BotStatus = "rejected"
|
||||
BotStatusApproved BotStatus = "approved"
|
||||
)
|
||||
|
||||
func (s BotStatus) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s BotStatus) IsValid() bool {
|
||||
switch s {
|
||||
case BotStatusDeleted, BotStatusPending, BotStatusRejected, BotStatusApproved:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
32
internal/db/db.go
Normal file
32
internal/db/db.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
44
internal/db/models.go
Normal file
44
internal/db/models.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Overview *string `json:"overview"`
|
||||
Description *string `json:"description"`
|
||||
IsSlash bool `json:"is_slash"`
|
||||
InstallContext InstallContext `json:"install_context"`
|
||||
GuildCount *int32 `json:"guild_count"`
|
||||
InstallCount *int32 `json:"install_count"`
|
||||
ImportedFrom *string `json:"imported_from"`
|
||||
Prefix *string `json:"prefix"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
Status BotStatus `json:"status"`
|
||||
MainOwnerID *string `json:"main_owner_id"`
|
||||
}
|
||||
|
||||
type BotCoOwner struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Biography *string `json:"biography"`
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
UserID string `json:"user_id"`
|
||||
BotID string `json:"bot_id"`
|
||||
VotedAt time.Time `json:"voted_at"`
|
||||
}
|
||||
43
internal/db/querier.go
Normal file
43
internal/db/querier.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
AddBotCoOwner(ctx context.Context, arg AddBotCoOwnerParams) error
|
||||
CanVoteAgain(ctx context.Context, arg CanVoteAgainParams) (bool, error)
|
||||
CountBotsByUsername(ctx context.Context, arg CountBotsByUsernameParams) (int64, error)
|
||||
CountVotesByBot(ctx context.Context, botID string) (int64, error)
|
||||
CreateBot(ctx context.Context, arg CreateBotParams) (*Bot, error)
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (*User, error)
|
||||
CreateVote(ctx context.Context, arg CreateVoteParams) (*Vote, error)
|
||||
DeleteBot(ctx context.Context, id string) error
|
||||
DeleteUser(ctx context.Context, id string) error
|
||||
GetBot(ctx context.Context, id string) (*Bot, error)
|
||||
GetBotByUsername(ctx context.Context, username string) (*Bot, error)
|
||||
GetBotCoOwner(ctx context.Context, arg GetBotCoOwnerParams) (*BotCoOwner, error)
|
||||
GetUser(ctx context.Context, id string) (*User, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (*User, error)
|
||||
GetVote(ctx context.Context, arg GetVoteParams) (*Vote, error)
|
||||
ListBots(ctx context.Context, arg ListBotsParams) ([]*Bot, error)
|
||||
ListBotsByCoOwner(ctx context.Context, userID string) ([]*Bot, error)
|
||||
ListBotsByOwner(ctx context.Context, arg ListBotsByOwnerParams) ([]*Bot, error)
|
||||
ListBotsByStatus(ctx context.Context, arg ListBotsByStatusParams) ([]*Bot, error)
|
||||
ListCoOwnersByBot(ctx context.Context, botID string) ([]*User, error)
|
||||
ListUsers(ctx context.Context) ([]*User, error)
|
||||
ListVotesByBot(ctx context.Context, botID string) ([]*Vote, error)
|
||||
ListVotesByUser(ctx context.Context, userID string) ([]*Vote, error)
|
||||
RemoveAllCoOwnersByBot(ctx context.Context, botID string) error
|
||||
RemoveBotCoOwner(ctx context.Context, arg RemoveBotCoOwnerParams) error
|
||||
SearchBotsByUsername(ctx context.Context, arg SearchBotsByUsernameParams) ([]*Bot, error)
|
||||
UpdateBot(ctx context.Context, arg UpdateBotParams) (*Bot, error)
|
||||
UpdateBotStatus(ctx context.Context, arg UpdateBotStatusParams) (*Bot, error)
|
||||
UpdateUser(ctx context.Context, arg UpdateUserParams) (*User, error)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
62
internal/db/sql/migrations/20260323184653_init.sql
Normal file
62
internal/db/sql/migrations/20260323184653_init.sql
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
create table users (
|
||||
id text primary key,
|
||||
username text not null,
|
||||
biography text
|
||||
);
|
||||
|
||||
create table bots (
|
||||
id text primary key,
|
||||
username text not null,
|
||||
avatar text,
|
||||
overview text,
|
||||
description text,
|
||||
is_slash boolean not null,
|
||||
install_context text check (install_context in ('guild', 'user')) not null,
|
||||
guild_count int,
|
||||
install_count int,
|
||||
imported_from text,
|
||||
prefix text,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now(),
|
||||
status text check (
|
||||
status in ('deleted', 'pending', 'rejected', 'approved')
|
||||
) default 'pending',
|
||||
main_owner_id text references users (id)
|
||||
);
|
||||
|
||||
create table bot_co_owners (
|
||||
bot_id text references bots (id),
|
||||
user_id text references users (id),
|
||||
primary key (bot_id, user_id)
|
||||
);
|
||||
|
||||
create table votes (
|
||||
user_id text references users (id),
|
||||
bot_id text references bots (id),
|
||||
voted_at timestamp with time zone default now(),
|
||||
primary key (user_id, bot_id, voted_at)
|
||||
);
|
||||
|
||||
create
|
||||
or replace function can_vote_again (user_id text, bot_id text) returns boolean as $$
|
||||
BEGIN
|
||||
RETURN NOT EXISTS (
|
||||
SELECT 1 FROM votes
|
||||
WHERE user_id = $1 AND bot_id = $2 AND voted_at > now() - interval '12 hours'
|
||||
);
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
drop function if exists can_vote_again;
|
||||
|
||||
drop table if exists votes;
|
||||
drop table if exists bot_co_owners;
|
||||
drop table if exists bots;
|
||||
drop table if exists users;
|
||||
-- +goose StatementEnd
|
||||
28
internal/db/sql/queries/bot_co_owners.sql
Normal file
28
internal/db/sql/queries/bot_co_owners.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- name: GetBotCoOwner :one
|
||||
SELECT * FROM bot_co_owners
|
||||
WHERE bot_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: ListCoOwnersByBot :many
|
||||
SELECT u.*
|
||||
FROM bot_co_owners bco
|
||||
JOIN users u ON u.id = bco.user_id
|
||||
WHERE bco.bot_id = $1;
|
||||
|
||||
-- name: ListBotsByCoOwner :many
|
||||
SELECT b.*
|
||||
FROM bot_co_owners bco
|
||||
JOIN bots b ON b.id = bco.bot_id
|
||||
WHERE bco.user_id = $1;
|
||||
|
||||
-- name: AddBotCoOwner :exec
|
||||
INSERT INTO bot_co_owners (bot_id, user_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: RemoveBotCoOwner :exec
|
||||
DELETE FROM bot_co_owners
|
||||
WHERE bot_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: RemoveAllCoOwnersByBot :exec
|
||||
DELETE FROM bot_co_owners
|
||||
WHERE bot_id = $1;
|
||||
86
internal/db/sql/queries/bots.sql
Normal file
86
internal/db/sql/queries/bots.sql
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
-- name: GetBot :one
|
||||
SELECT * FROM bots
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetBotByUsername :one
|
||||
SELECT * FROM bots
|
||||
WHERE username = $1;
|
||||
|
||||
-- name: CreateBot :one
|
||||
INSERT INTO bots (
|
||||
id,
|
||||
username,
|
||||
avatar,
|
||||
overview,
|
||||
description,
|
||||
is_slash,
|
||||
install_context,
|
||||
guild_count,
|
||||
install_count,
|
||||
imported_from,
|
||||
prefix,
|
||||
main_owner_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateBot :one
|
||||
UPDATE bots
|
||||
SET
|
||||
username = coalesce(sqlc.narg('username'), username),
|
||||
avatar = coalesce(sqlc.narg('avatar'), avatar),
|
||||
overview = coalesce(sqlc.narg('overview'), overview),
|
||||
description = coalesce(sqlc.narg('description'), description),
|
||||
is_slash = coalesce(sqlc.narg('is_slash'), is_slash),
|
||||
install_context = coalesce(sqlc.narg('install_context'), install_context),
|
||||
guild_count = coalesce(sqlc.narg('guild_count'), guild_count),
|
||||
install_count = coalesce(sqlc.narg('install_count'), install_count),
|
||||
imported_from = coalesce(sqlc.narg('imported_from'), imported_from),
|
||||
prefix = coalesce(sqlc.narg('prefix'), prefix),
|
||||
main_owner_id = coalesce(sqlc.narg('main_owner_id'), main_owner_id),
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateBotStatus :one
|
||||
UPDATE bots
|
||||
SET
|
||||
status = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: CountBotsByUsername :one
|
||||
SELECT COUNT(*) FROM bots
|
||||
WHERE (@query::text IS NULL OR username ILIKE '%' || @query || '%') AND status = $1;
|
||||
|
||||
-- name: SearchBotsByUsername :many
|
||||
SELECT * FROM bots
|
||||
WHERE (@query::text IS NULL OR username ILIKE '%' || @query || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
OFFSET $2;
|
||||
|
||||
-- name: ListBots :many
|
||||
SELECT * FROM bots
|
||||
ORDER BY id
|
||||
LIMIT $1
|
||||
OFFSET $2;
|
||||
|
||||
-- name: ListBotsByStatus :many
|
||||
SELECT * FROM bots
|
||||
WHERE status = $1
|
||||
ORDER BY id
|
||||
LIMIT $2
|
||||
OFFSET $3;
|
||||
|
||||
-- name: ListBotsByOwner :many
|
||||
SELECT * FROM bots
|
||||
WHERE main_owner_id = $1
|
||||
ORDER BY id
|
||||
LIMIT $2
|
||||
OFFSET $3;
|
||||
|
||||
-- name: DeleteBot :exec
|
||||
DELETE FROM bots
|
||||
WHERE id = $1;
|
||||
28
internal/db/sql/queries/users.sql
Normal file
28
internal/db/sql/queries/users.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- name: GetUser :one
|
||||
SELECT * FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT * FROM users
|
||||
WHERE username = $1;
|
||||
|
||||
-- name: ListUsers :many
|
||||
SELECT * FROM users
|
||||
ORDER BY id;
|
||||
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (id, username, biography)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUser :one
|
||||
UPDATE users
|
||||
SET
|
||||
username = coalesce(sqlc.narg('username'), username),
|
||||
biography = coalesce(sqlc.narg('biography'), biography)
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteUser :exec
|
||||
DELETE FROM users
|
||||
WHERE id = $1;
|
||||
27
internal/db/sql/queries/votes.sql
Normal file
27
internal/db/sql/queries/votes.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- name: GetVote :one
|
||||
SELECT * FROM votes
|
||||
WHERE user_id = $1 AND bot_id = $2
|
||||
ORDER BY voted_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: ListVotesByBot :many
|
||||
SELECT * FROM votes
|
||||
WHERE bot_id = $1
|
||||
ORDER BY voted_at DESC;
|
||||
|
||||
-- name: ListVotesByUser :many
|
||||
SELECT * FROM votes
|
||||
WHERE user_id = $1
|
||||
ORDER BY voted_at DESC;
|
||||
|
||||
-- name: CountVotesByBot :one
|
||||
SELECT count(*) FROM votes
|
||||
WHERE bot_id = $1;
|
||||
|
||||
-- name: CreateVote :one
|
||||
INSERT INTO votes (user_id, bot_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING *;
|
||||
|
||||
-- name: CanVoteAgain :one
|
||||
SELECT can_vote_again($1, $2);
|
||||
110
internal/db/users.sql.go
Normal file
110
internal/db/users.sql.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: users.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (id, username, biography)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, username, biography
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Biography *string `json:"biography"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (*User, error) {
|
||||
row := q.db.QueryRow(ctx, createUser, arg.ID, arg.Username, arg.Biography)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Biography)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const deleteUser = `-- name: DeleteUser :exec
|
||||
DELETE FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteUser(ctx context.Context, id string) error {
|
||||
_, err := q.db.Exec(ctx, deleteUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getUser = `-- name: GetUser :one
|
||||
SELECT id, username, biography FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUser(ctx context.Context, id string) (*User, error) {
|
||||
row := q.db.QueryRow(ctx, getUser, id)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Biography)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, username, biography FROM users
|
||||
WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (*User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByUsername, username)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Biography)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const listUsers = `-- name: ListUsers :many
|
||||
SELECT id, username, biography FROM users
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListUsers(ctx context.Context) ([]*User, error) {
|
||||
rows, err := q.db.Query(ctx, listUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*User{}
|
||||
for rows.Next() {
|
||||
var i User
|
||||
if err := rows.Scan(&i.ID, &i.Username, &i.Biography); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :one
|
||||
UPDATE users
|
||||
SET
|
||||
username = coalesce($1, username),
|
||||
biography = coalesce($2, biography)
|
||||
WHERE id = $3
|
||||
RETURNING id, username, biography
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
Username *string `json:"username"`
|
||||
Biography *string `json:"biography"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (*User, error) {
|
||||
row := q.db.QueryRow(ctx, updateUser, arg.Username, arg.Biography, arg.ID)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Username, &i.Biography)
|
||||
return &i, err
|
||||
}
|
||||
127
internal/db/votes.sql.go
Normal file
127
internal/db/votes.sql.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: votes.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const canVoteAgain = `-- name: CanVoteAgain :one
|
||||
SELECT can_vote_again($1, $2)
|
||||
`
|
||||
|
||||
type CanVoteAgainParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
BotID string `json:"bot_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CanVoteAgain(ctx context.Context, arg CanVoteAgainParams) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, canVoteAgain, arg.UserID, arg.BotID)
|
||||
var can_vote_again bool
|
||||
err := row.Scan(&can_vote_again)
|
||||
return can_vote_again, err
|
||||
}
|
||||
|
||||
const countVotesByBot = `-- name: CountVotesByBot :one
|
||||
SELECT count(*) FROM votes
|
||||
WHERE bot_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) CountVotesByBot(ctx context.Context, botID string) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countVotesByBot, botID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createVote = `-- name: CreateVote :one
|
||||
INSERT INTO votes (user_id, bot_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING user_id, bot_id, voted_at
|
||||
`
|
||||
|
||||
type CreateVoteParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
BotID string `json:"bot_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateVote(ctx context.Context, arg CreateVoteParams) (*Vote, error) {
|
||||
row := q.db.QueryRow(ctx, createVote, arg.UserID, arg.BotID)
|
||||
var i Vote
|
||||
err := row.Scan(&i.UserID, &i.BotID, &i.VotedAt)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getVote = `-- name: GetVote :one
|
||||
SELECT user_id, bot_id, voted_at FROM votes
|
||||
WHERE user_id = $1 AND bot_id = $2
|
||||
ORDER BY voted_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetVoteParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
BotID string `json:"bot_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetVote(ctx context.Context, arg GetVoteParams) (*Vote, error) {
|
||||
row := q.db.QueryRow(ctx, getVote, arg.UserID, arg.BotID)
|
||||
var i Vote
|
||||
err := row.Scan(&i.UserID, &i.BotID, &i.VotedAt)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const listVotesByBot = `-- name: ListVotesByBot :many
|
||||
SELECT user_id, bot_id, voted_at FROM votes
|
||||
WHERE bot_id = $1
|
||||
ORDER BY voted_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListVotesByBot(ctx context.Context, botID string) ([]*Vote, error) {
|
||||
rows, err := q.db.Query(ctx, listVotesByBot, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Vote{}
|
||||
for rows.Next() {
|
||||
var i Vote
|
||||
if err := rows.Scan(&i.UserID, &i.BotID, &i.VotedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listVotesByUser = `-- name: ListVotesByUser :many
|
||||
SELECT user_id, bot_id, voted_at FROM votes
|
||||
WHERE user_id = $1
|
||||
ORDER BY voted_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListVotesByUser(ctx context.Context, userID string) ([]*Vote, error) {
|
||||
rows, err := q.db.Query(ctx, listVotesByUser, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Vote{}
|
||||
for rows.Next() {
|
||||
var i Vote
|
||||
if err := rows.Scan(&i.UserID, &i.BotID, &i.VotedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
62
internal/errorutil/errorutil.go
Normal file
62
internal/errorutil/errorutil.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package errorutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBotAlreadyExists = errors.New("this bot has already been submitted")
|
||||
ErrSearchFailed = errors.New("no bots found fitting this filter")
|
||||
ErrInvalidID = errors.New("invalid discord id")
|
||||
ErrMainOwnerAsCoOwner = errors.New("you cannot set yourself as a co-owner")
|
||||
)
|
||||
|
||||
type ErrResponse struct {
|
||||
Err error `json:"-"`
|
||||
HTTPStatusCode int `json:"-"`
|
||||
StatusText string `json:"status"`
|
||||
ErrorText string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
render.Status(r, e.HTTPStatusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ErrInvalidRequest(err error) render.Renderer {
|
||||
return &ErrResponse{
|
||||
Err: err,
|
||||
HTTPStatusCode: 400,
|
||||
StatusText: "invalid request",
|
||||
ErrorText: errString(err),
|
||||
}
|
||||
}
|
||||
|
||||
func ErrInternal(err error) render.Renderer {
|
||||
return &ErrResponse{
|
||||
Err: err,
|
||||
HTTPStatusCode: 500,
|
||||
StatusText: "internal server error",
|
||||
ErrorText: errString(err),
|
||||
}
|
||||
}
|
||||
|
||||
var ErrNotFound = &ErrResponse{
|
||||
HTTPStatusCode: 404,
|
||||
StatusText: "resource not found",
|
||||
}
|
||||
|
||||
var ErrUnauthorized = &ErrResponse{
|
||||
HTTPStatusCode: 401,
|
||||
StatusText: "unauthorized",
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
57
internal/middleware/auth.go
Normal file
57
internal/middleware/auth.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
"codeberg.org/nextgo/dbots/internal/errorutil"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
var userKey contextKey = "user"
|
||||
var mainUserID = "774287684944134155"
|
||||
|
||||
// AuthMiddleware is a middleware to set the user as context value.
|
||||
// this middleware does not prevents the user from accessing the route
|
||||
// if not authorized.
|
||||
func AuthMiddleware(q *db.Queries) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := q.GetUser(r.Context(), mainUserID)
|
||||
if err != nil {
|
||||
user, err = q.CreateUser(r.Context(), db.CreateUserParams{
|
||||
ID: mainUserID,
|
||||
Username: "elisiei",
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("eeeehhh", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userKey, user) // mocked
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AuthGuardMiddleware is a middleware that prevents the user
|
||||
// from accessing the route if they are NOT authorized.
|
||||
func AuthGuardMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := r.Context().Value(userKey).(*db.User)
|
||||
if !ok {
|
||||
render.Render(w, r, errorutil.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func GetUser(ctx context.Context) *db.User {
|
||||
user := ctx.Value(userKey).(*db.User)
|
||||
return user
|
||||
}
|
||||
87
internal/paginate/paginate.go
Normal file
87
internal/paginate/paginate.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package paginate
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLimit = 20
|
||||
MaxLimit = 100
|
||||
)
|
||||
|
||||
// PageInfo holds pagination metadata returned alongside the data.
|
||||
type PageInfo struct {
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
// Page is the generic paginated response envelope.
|
||||
type Page[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Pagination PageInfo `json:"pagination"`
|
||||
}
|
||||
|
||||
// NewPage constructs a Page from a slice already fetched from the DB
|
||||
// (which owns the limit/offset), the total record count, and the Params
|
||||
// that were passed to the query.
|
||||
func NewPage[T any](data []T, total int, p Params) Page[T] {
|
||||
if data == nil {
|
||||
data = []T{}
|
||||
}
|
||||
return Page[T]{
|
||||
Data: data,
|
||||
Pagination: PageInfo{
|
||||
Total: total,
|
||||
Limit: int(p.Limit),
|
||||
Offset: int(p.Offset),
|
||||
HasNext: int(p.Offset)+int(p.Limit) < total,
|
||||
HasPrev: p.Offset > 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Params holds parsed limit/offset values
|
||||
type Params struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
}
|
||||
|
||||
// ParseParams reads "limit" and "offset" from the request query string and
|
||||
// returns safe, clamped values. Invalid or missing values fall back to defaults.
|
||||
//
|
||||
// GET /bots?limit=10&offset=30
|
||||
func ParseParams(r *http.Request) Params {
|
||||
limit := parseIntParam(r, "limit", DefaultLimit)
|
||||
offset := parseIntParam(r, "offset", 0)
|
||||
|
||||
if limit <= 0 {
|
||||
limit = DefaultLimit
|
||||
}
|
||||
if limit > MaxLimit {
|
||||
limit = MaxLimit
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
return Params{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
}
|
||||
}
|
||||
|
||||
func parseIntParam(r *http.Request, key string, fallback int) int {
|
||||
raw := r.URL.Query().Get(key)
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
55
internal/server/server.go
Normal file
55
internal/server/server.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"codeberg.org/nextgo/dbots/internal/admin"
|
||||
"codeberg.org/nextgo/dbots/internal/bot"
|
||||
"codeberg.org/nextgo/dbots/internal/db"
|
||||
customMiddlewares "codeberg.org/nextgo/dbots/internal/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httplog/v3"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewServer(queries *db.Queries, logger *slog.Logger) *Server {
|
||||
router := chi.NewMux()
|
||||
|
||||
router.Use(httplog.RequestLogger(logger, nil))
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(customMiddlewares.AuthMiddleware(queries))
|
||||
|
||||
return &Server{
|
||||
router: router,
|
||||
queries: queries,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Register() {
|
||||
botRouter := bot.NewRouter(s.queries)
|
||||
adminRouter := admin.NewRouter(s.queries)
|
||||
|
||||
s.router.Mount("/bots", botRouter.Routes())
|
||||
s.router.Mount("/admin", adminRouter.Routes())
|
||||
}
|
||||
|
||||
func (s *Server) Start(port int) {
|
||||
s.Register()
|
||||
slog.Info("server started", "port", port)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), s.router); err != nil {
|
||||
slog.Error("error starting server", "port", port, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
38
sqlc.yaml
Normal file
38
sqlc.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "internal/db/sql/queries/*.sql"
|
||||
schema: "internal/db/sql/migrations"
|
||||
gen:
|
||||
go:
|
||||
package: "db"
|
||||
out: "internal/db/"
|
||||
sql_package: "pgx/v5"
|
||||
emit_interface: true
|
||||
emit_json_tags: true
|
||||
emit_empty_slices: true
|
||||
emit_enum_valid_method: true
|
||||
emit_all_enum_values: true
|
||||
emit_prepared_queries: true
|
||||
emit_pointers_for_null_types: true
|
||||
omit_unused_structs: true
|
||||
emit_result_struct_pointers: true
|
||||
overrides:
|
||||
- db_type: "pg_catalog.timestamptz"
|
||||
nullable: false
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
|
||||
- db_type: "pg_catalog.timestamptz"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
- column: "bots.install_context"
|
||||
go_type:
|
||||
type: "InstallContext"
|
||||
- column: "bots.status"
|
||||
go_type:
|
||||
type: "BotStatus"
|
||||
Loading…
Add table
Reference in a new issue