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