225 lines
6.3 KiB
Go
225 lines
6.3 KiB
Go
package bot
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
|
|
"codeberg.org/nextgo/dbots/internal/db"
|
|
"codeberg.org/nextgo/dbots/internal/discord"
|
|
"codeberg.org/nextgo/dbots/internal/errorutil"
|
|
"codeberg.org/nextgo/dbots/internal/middleware"
|
|
"codeberg.org/nextgo/dbots/internal/paginate"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
type DiscordApiBot struct {
|
|
Application DiscordApplication `json:"application"`
|
|
Bot DiscordBot `json:"bot"`
|
|
}
|
|
|
|
type DiscordApplication struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Icon string `json:"icon"`
|
|
Description string `json:"description"`
|
|
IsVerified bool `json:"is_verified"`
|
|
BotPublic bool `json:"bot_public"`
|
|
}
|
|
|
|
type DiscordBot struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Avatar string `json:"avatar"`
|
|
Bot bool `json:"bot"`
|
|
ApproximateGuildCount int32 `json:"approximate_guild_count"`
|
|
}
|
|
|
|
type Service struct {
|
|
q *db.Queries
|
|
client *discord.Client
|
|
}
|
|
|
|
func NewService(q *db.Queries, client *discord.Client) *Service {
|
|
return &Service{q: q, client: client}
|
|
}
|
|
|
|
func (s *Service) Submit(ctx context.Context, data CreateBotRequest) (*db.Bot, error) {
|
|
user := middleware.GetUser(ctx)
|
|
application, err := s.client.GetApplication(ctx, data.ID)
|
|
if err != nil {
|
|
return nil, errorutil.ErrBotNotExists // todo: some old bots have different client id (not the same as the user id)
|
|
}
|
|
|
|
var count int32
|
|
b, err := s.q.CreateBot(ctx, db.CreateBotParams{
|
|
ID: data.ID,
|
|
Overview: &data.Overview,
|
|
IsSlash: data.IsSlash,
|
|
InstallContext: data.InstallContext,
|
|
ImportedFrom: nil, // reserved for custom endpoint, should always be nil here ok??
|
|
Username: application.Name,
|
|
Avatar: &application.Icon,
|
|
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) && pgErr.Code == "23505" {
|
|
return nil, errorutil.ErrBotAlreadyExists
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.addCoOwners(ctx, b.ID, user.ID, data.CoOwners); err != nil {
|
|
// Bot was created but co-owners failed. Log and surface the error.
|
|
// ideally this whole operation runs in a transaction but who cares lol.
|
|
slog.Error("error adding co-owners after submit", "err", err, "bot_id", b.ID)
|
|
// so if co-owners fail it's because the co-owner isn't registered in the botlist,
|
|
// however, i should handle this but i'm lazy as fuck. for now i will only output
|
|
// a silent error log instead of aborting the whole request.
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// AddCoOwner adds a single co-owner to a bot.
|
|
// Only the main owner of the bot may call this.
|
|
func (s *Service) AddCoOwner(ctx context.Context, botID, coOwnerID string) error {
|
|
user := middleware.GetUser(ctx)
|
|
|
|
bot, err := s.q.GetBot(ctx, botID)
|
|
if err != nil {
|
|
return errorutil.ErrNotFound.Err
|
|
}
|
|
|
|
if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
|
|
return errorutil.ErrForbidden.Err
|
|
}
|
|
|
|
if err := validateCoOwner(coOwnerID, user.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
|
|
BotID: botID,
|
|
UserID: coOwnerID,
|
|
}); err != nil {
|
|
slog.Error("error adding co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveCoOwner removes a co-owner from a bot.
|
|
// Only the main owner of the bot may call this.
|
|
func (s *Service) RemoveCoOwner(ctx context.Context, botID, coOwnerID string) error {
|
|
user := middleware.GetUser(ctx)
|
|
|
|
bot, err := s.q.GetBot(ctx, botID)
|
|
if err != nil {
|
|
return errorutil.ErrNotFound.Err
|
|
}
|
|
|
|
if bot.MainOwnerID == nil || *bot.MainOwnerID != user.ID {
|
|
return errorutil.ErrForbidden.Err
|
|
}
|
|
|
|
if err := s.q.RemoveBotCoOwner(ctx, db.RemoveBotCoOwnerParams{
|
|
BotID: botID,
|
|
UserID: coOwnerID,
|
|
}); err != nil {
|
|
slog.Error("error removing co-owner", "err", err, "bot_id", botID, "user_id", coOwnerID)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListCoOwners returns the users who are co-owners of a bot.
|
|
func (s *Service) ListCoOwners(ctx context.Context, botID string) ([]*db.User, error) {
|
|
owners, err := s.q.ListCoOwnersByBot(ctx, botID)
|
|
if err != nil {
|
|
slog.Error("error listing co-owners", "err", err, "bot_id", botID)
|
|
return nil, err
|
|
}
|
|
return owners, nil
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, id string) (*db.Bot, error) {
|
|
bot, err := s.q.GetBot(ctx, id)
|
|
if err != nil {
|
|
slog.Error("error getting bot", "err", err, "id", id)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errorutil.ErrNotFound.Err
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return bot, nil
|
|
}
|
|
|
|
func (s *Service) 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
|
|
}
|
|
|
|
// validateCoOwner checks that the given ID is a valid Discord snowflake
|
|
// and is not the same as the main owner.
|
|
func validateCoOwner(id, mainOwnerID string) error {
|
|
if _, err := strconv.ParseUint(id, 10, 64); err != nil {
|
|
return errorutil.ErrInvalidID
|
|
}
|
|
if id == mainOwnerID {
|
|
return errorutil.ErrMainOwnerAsCoOwner
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addCoOwners validates and inserts each co-owner for a bot.
|
|
// mainOwnerID is provided to reject attempts to set the main owner as a co-owner.
|
|
func (s *Service) addCoOwners(ctx context.Context, botID, mainOwnerID string, coOwnerIDs []string) error {
|
|
for _, id := range coOwnerIDs {
|
|
if err := validateCoOwner(id, mainOwnerID); err != nil {
|
|
return err
|
|
}
|
|
if err := s.q.AddBotCoOwner(ctx, db.AddBotCoOwnerParams{
|
|
BotID: botID,
|
|
UserID: id,
|
|
}); err != nil {
|
|
return fmt.Errorf("adding co-owner %s: %w", id, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|