Merge remote-tracking branch 'origin/stable'
This commit is contained in:
commit
7a4518184b
|
@ -0,0 +1,13 @@
|
|||
when:
|
||||
branch:
|
||||
exclude: stable
|
||||
|
||||
steps:
|
||||
check:
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- apk update && apk add curl vips-dev build-base
|
||||
- make backend
|
||||
# Install golangci-lint
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
|
||||
- golangci-lint run
|
|
@ -0,0 +1,20 @@
|
|||
when:
|
||||
branch:
|
||||
exclude: stable
|
||||
|
||||
steps:
|
||||
check:
|
||||
image: node
|
||||
directory: frontend
|
||||
environment: # SvelteKit expects these in the environment during build time.
|
||||
- PRIVATE_SENTRY_DSN=
|
||||
- PUBLIC_BASE_URL=http://pronouns.localhost
|
||||
- PUBLIC_MEDIA_URL=http://pronouns.localhost/media
|
||||
- PUBLIC_SHORT_BASE=http://prns.localhost
|
||||
- PUBLIC_HCAPTCHA_SITEKEY=non_existent_sitekey
|
||||
commands:
|
||||
- corepack enable
|
||||
- pnpm install
|
||||
- pnpm check
|
||||
- pnpm lint
|
||||
- pnpm build
|
2
Makefile
2
Makefile
|
@ -2,7 +2,7 @@ all: generate backend frontend
|
|||
|
||||
.PHONY: backend
|
||||
backend:
|
||||
go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long`" .
|
||||
go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long --always`" .
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
|
|
|
@ -79,7 +79,7 @@ func (db *DB) CreateExport(ctx context.Context, userID xid.ID, filename string,
|
|||
return de, errors.Wrap(err, "building query")
|
||||
}
|
||||
|
||||
pgxscan.Get(ctx, db, &de, sql, args...)
|
||||
err = pgxscan.Get(ctx, db, &de, sql, args...)
|
||||
if err != nil {
|
||||
return de, errors.Wrap(err, "executing sql")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/georgysavva/scany/v2/pgxscan"
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
@ -43,7 +44,12 @@ func (db *DB) CreateInvite(ctx context.Context, userID xid.ID) (i Invite, err er
|
|||
if err != nil {
|
||||
return i, errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var maxInvites, inviteCount int
|
||||
err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/georgysavva/scany/v2/pgxscan"
|
||||
|
@ -41,12 +42,14 @@ const (
|
|||
)
|
||||
|
||||
// member names must match this regex
|
||||
var memberNameRegex = regexp.MustCompile("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,]{1,100}$")
|
||||
var memberNameRegex = regexp.MustCompile("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$")
|
||||
|
||||
// List of member names that cannot be used because they would break routing or be inaccessible due to page conflicts.
|
||||
var invalidMemberNames = []string{
|
||||
// these break routing outright
|
||||
".",
|
||||
"..",
|
||||
// the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
|
||||
"edit",
|
||||
}
|
||||
|
||||
|
@ -285,7 +288,12 @@ func (db *DB) RerollMemberSID(ctx context.Context, userID, memberID xid.ID) (new
|
|||
if err != nil {
|
||||
return "", errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sql, args, err := sq.Update("members").
|
||||
Set("sid", squirrel.Expr("find_free_member_sid()")).
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
|
||||
"github.com/davidbyttow/govips/v2/vips"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/render"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
@ -23,6 +24,19 @@ var Command = &cli.Command{
|
|||
}
|
||||
|
||||
func run(c *cli.Context) error {
|
||||
// initialize sentry
|
||||
if dsn := os.Getenv("SENTRY_DSN"); dsn != "" {
|
||||
// We don't need to check the error here--it's fine if no DSN is set.
|
||||
_ = sentry.Init(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
Debug: os.Getenv("DEBUG") == "true",
|
||||
Release: server.Tag,
|
||||
EnableTracing: os.Getenv("SENTRY_TRACING") == "true",
|
||||
TracesSampleRate: 0.05,
|
||||
ProfilesSampleRate: 0.05,
|
||||
})
|
||||
}
|
||||
|
||||
// set vips log level to WARN, else it will spam logs on info level
|
||||
vips.LoggingSettings(nil, vips.LogLevelWarning)
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package backend
|
|||
|
||||
import (
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/auth"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/bot"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/member"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod"
|
||||
|
@ -21,7 +20,6 @@ func mountRoutes(s *server.Server) {
|
|||
auth.Mount(s, r)
|
||||
user.Mount(s, r)
|
||||
member.Mount(s, r)
|
||||
bot.Mount(s, r)
|
||||
meta.Mount(s, r)
|
||||
mod.Mount(s, r)
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/rs/xid"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -61,7 +62,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
// if the state can't be validated, return
|
||||
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "validating state")
|
||||
}
|
||||
|
||||
return server.APIError{Code: server.ErrInvalidState}
|
||||
|
@ -79,7 +80,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
dg, _ := discordgo.New(token.Type() + " " + token.AccessToken)
|
||||
du, err := dg.User("@me")
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting discord user")
|
||||
}
|
||||
|
||||
u, err := s.DB.DiscordUser(ctx, du.ID)
|
||||
|
@ -90,7 +91,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.saveUndeleteToken(ctx, u.ID, token)
|
||||
if err != nil {
|
||||
log.Errorf("saving undelete token: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "saving undelete token")
|
||||
}
|
||||
|
||||
render.JSON(w, r, discordCallbackResponse{
|
||||
|
@ -114,7 +115,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
tokenID := xid.New()
|
||||
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "creating token")
|
||||
}
|
||||
|
||||
// save token to database
|
||||
|
@ -137,7 +138,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
|
||||
} else if err != db.ErrUserNotFound { // internal error
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
// no user found, so save a ticket + save their Discord info in Redis
|
||||
|
@ -145,7 +146,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600")
|
||||
if err != nil {
|
||||
log.Errorf("setting Discord user for ticket %q: %v", ticket, err)
|
||||
return err
|
||||
return errors.Wrap(err, "caching discord user for ticket")
|
||||
}
|
||||
|
||||
render.JSON(w, r, discordCallbackResponse{
|
||||
|
@ -278,7 +279,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "checking if username is taken")
|
||||
}
|
||||
if !valid {
|
||||
return server.APIError{Code: server.ErrInvalidUsername}
|
||||
|
@ -291,7 +292,12 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
du := new(discordgo.User)
|
||||
err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
@ -54,7 +55,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
// if the state can't be validated, return
|
||||
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "validating state")
|
||||
}
|
||||
|
||||
return server.APIError{Code: server.ErrInvalidState}
|
||||
|
@ -111,7 +112,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
err = s.saveUndeleteToken(ctx, u.ID, token)
|
||||
if err != nil {
|
||||
log.Errorf("saving undelete token: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "saving undelete token")
|
||||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
|
@ -135,7 +136,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
tokenID := xid.New()
|
||||
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "creating token")
|
||||
}
|
||||
|
||||
// save token to database
|
||||
|
@ -158,7 +159,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
return nil
|
||||
|
||||
} else if err != db.ErrUserNotFound { // internal error
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
// no user found, so save a ticket + save their Mastodon info in Redis
|
||||
|
@ -166,7 +167,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600")
|
||||
if err != nil {
|
||||
log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting user for ticket")
|
||||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
|
@ -306,7 +307,7 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "checking if username is taken")
|
||||
}
|
||||
if !valid {
|
||||
return server.APIError{Code: server.ErrInvalidUsername}
|
||||
|
@ -319,7 +320,12 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mu := new(partialMastodonAccount)
|
||||
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
@ -90,7 +91,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.saveUndeleteToken(ctx, u.ID, token)
|
||||
if err != nil {
|
||||
log.Errorf("saving undelete token: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "saving undelete token")
|
||||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
|
@ -114,7 +115,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
tokenID := xid.New()
|
||||
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "creating token")
|
||||
}
|
||||
|
||||
// save token to database
|
||||
|
@ -137,7 +138,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
|
||||
} else if err != db.ErrUserNotFound { // internal error
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
// no user found, so save a ticket + save their Misskey info in Redis
|
||||
|
@ -145,7 +146,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
|
||||
if err != nil {
|
||||
log.Errorf("setting misskey user for ticket %q: %v", ticket, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting user for ticket")
|
||||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
|
@ -234,7 +235,7 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "checking if username is taken")
|
||||
}
|
||||
if !valid {
|
||||
return server.APIError{Code: server.ErrInvalidUsername}
|
||||
|
@ -247,7 +248,12 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mu := new(partialMisskeyAccount)
|
||||
err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/rs/xid"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -60,7 +61,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
// if the state can't be validated, return
|
||||
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "validating state")
|
||||
}
|
||||
|
||||
return server.APIError{Code: server.ErrInvalidState}
|
||||
|
@ -109,7 +110,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.saveUndeleteToken(ctx, u.ID, token)
|
||||
if err != nil {
|
||||
log.Errorf("saving undelete token: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "saving undelete token")
|
||||
}
|
||||
|
||||
render.JSON(w, r, googleCallbackResponse{
|
||||
|
@ -133,7 +134,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
tokenID := xid.New()
|
||||
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "creating token")
|
||||
}
|
||||
|
||||
// save token to database
|
||||
|
@ -156,7 +157,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
|
||||
} else if err != db.ErrUserNotFound { // internal error
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
// no user found, so save a ticket + save their Google info in Redis
|
||||
|
@ -164,7 +165,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600")
|
||||
if err != nil {
|
||||
log.Errorf("setting Google user for ticket %q: %v", ticket, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting user for ticket")
|
||||
}
|
||||
|
||||
render.JSON(w, r, googleCallbackResponse{
|
||||
|
@ -281,7 +282,7 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "checking if username is taken")
|
||||
}
|
||||
if !valid {
|
||||
return server.APIError{Code: server.ErrInvalidUsername}
|
||||
|
@ -294,7 +295,12 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
gu := new(partialGoogleUser)
|
||||
err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu)
|
||||
|
|
|
@ -185,7 +185,7 @@ func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
|
|||
if googleOAuthConfig.ClientID != "" {
|
||||
googleCfg := googleOAuthConfig
|
||||
googleCfg.RedirectURL = req.CallbackDomain + "/auth/login/google"
|
||||
resp.Google = googleCfg.AuthCodeURL(state)
|
||||
resp.Google = googleCfg.AuthCodeURL(state) + "&prompt=select_account"
|
||||
}
|
||||
|
||||
render.JSON(w, r, resp)
|
||||
|
|
|
@ -5,9 +5,11 @@ import (
|
|||
"time"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -63,7 +65,12 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/rs/xid"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -77,7 +78,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
// if the state can't be validated, return
|
||||
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "validating state")
|
||||
}
|
||||
|
||||
return server.APIError{Code: server.ErrInvalidState}
|
||||
|
@ -142,7 +143,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.saveUndeleteToken(ctx, u.ID, token)
|
||||
if err != nil {
|
||||
log.Errorf("saving undelete token: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "saving undelete token")
|
||||
}
|
||||
|
||||
render.JSON(w, r, tumblrCallbackResponse{
|
||||
|
@ -166,7 +167,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
tokenID := xid.New()
|
||||
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "creating token")
|
||||
}
|
||||
|
||||
// save token to database
|
||||
|
@ -189,7 +190,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
|
||||
} else if err != db.ErrUserNotFound { // internal error
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
// no user found, so save a ticket + save their Tumblr info in Redis
|
||||
|
@ -197,7 +198,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600")
|
||||
if err != nil {
|
||||
log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting user for ticket")
|
||||
}
|
||||
|
||||
render.JSON(w, r, tumblrCallbackResponse{
|
||||
|
@ -314,7 +315,7 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "checking if username is taken")
|
||||
}
|
||||
if !valid {
|
||||
return server.APIError{Code: server.ErrInvalidUsername}
|
||||
|
@ -327,7 +328,12 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
tui := new(tumblrUserInfo)
|
||||
err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui)
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
*server.Server
|
||||
|
||||
publicKey ed25519.PublicKey
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func (bot *Bot) UserAvatarURL(u db.User) string {
|
||||
if u.Avatar == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return bot.baseURL + "/media/users/" + u.ID.String() + "/" + *u.Avatar + ".webp"
|
||||
}
|
||||
|
||||
func Mount(srv *server.Server, r chi.Router) {
|
||||
publicKey, err := hex.DecodeString(os.Getenv("DISCORD_PUBLIC_KEY"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b := &Bot{
|
||||
Server: srv,
|
||||
publicKey: publicKey,
|
||||
baseURL: os.Getenv("BASE_URL"),
|
||||
}
|
||||
|
||||
r.HandleFunc("/interactions", b.handle)
|
||||
}
|
||||
|
||||
func (bot *Bot) handle(w http.ResponseWriter, r *http.Request) {
|
||||
if !discordgo.VerifyInteraction(r, bot.publicKey) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var ev *discordgo.InteractionCreate
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&ev); err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// we can always respond to ping with pong
|
||||
if ev.Type == discordgo.InteractionPing {
|
||||
log.Debug("received ping interaction")
|
||||
render.JSON(w, r, discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponsePong,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if ev.Type != discordgo.InteractionApplicationCommand {
|
||||
return
|
||||
}
|
||||
data := ev.ApplicationCommandData()
|
||||
|
||||
switch data.Name {
|
||||
case "Show user's pronouns":
|
||||
bot.userPronouns(w, r, ev)
|
||||
case "Show author's pronouns":
|
||||
}
|
||||
}
|
||||
|
||||
func (bot *Bot) userPronouns(w http.ResponseWriter, r *http.Request, ev *discordgo.InteractionCreate) {
|
||||
ctx := r.Context()
|
||||
|
||||
var du *discordgo.User
|
||||
for _, user := range ev.ApplicationCommandData().Resolved.Users {
|
||||
du = user
|
||||
break
|
||||
}
|
||||
if du == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := bot.DB.DiscordUser(ctx, du.ID)
|
||||
if err != nil {
|
||||
if err == db.ErrUserNotFound {
|
||||
respond(w, r, &discordgo.MessageEmbed{
|
||||
Description: du.String() + " does not have any pronouns set.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("getting discord user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
avatarURL := du.AvatarURL("")
|
||||
if url := bot.UserAvatarURL(u); url != "" {
|
||||
avatarURL = url
|
||||
}
|
||||
name := u.Username
|
||||
if u.DisplayName != nil {
|
||||
name = fmt.Sprintf("%s (%s)", *u.DisplayName, u.Username)
|
||||
}
|
||||
url := bot.baseURL
|
||||
if url != "" {
|
||||
url += "/@" + u.Username
|
||||
}
|
||||
|
||||
e := &discordgo.MessageEmbed{
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: name,
|
||||
IconURL: avatarURL,
|
||||
URL: url,
|
||||
},
|
||||
}
|
||||
|
||||
if u.Bio != nil {
|
||||
e.Fields = append(e.Fields, &discordgo.MessageEmbedField{
|
||||
Name: "Bio",
|
||||
Value: *u.Bio,
|
||||
})
|
||||
}
|
||||
|
||||
fields, err := bot.DB.UserFields(ctx, u.ID)
|
||||
if err != nil {
|
||||
respond(w, r, e)
|
||||
|
||||
log.Errorf("getting user fields: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
var favs []db.FieldEntry
|
||||
|
||||
for _, e := range field.Entries {
|
||||
if e.Status == "favourite" {
|
||||
favs = append(favs, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(favs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var value string
|
||||
for _, fav := range favs {
|
||||
if len(fav.Value) > 500 {
|
||||
break
|
||||
}
|
||||
|
||||
value += fav.Value + "\n"
|
||||
}
|
||||
|
||||
e.Fields = append(e.Fields, &discordgo.MessageEmbedField{
|
||||
Name: field.Name,
|
||||
Value: value,
|
||||
Inline: true,
|
||||
})
|
||||
}
|
||||
|
||||
respond(w, r, e)
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request, embeds ...*discordgo.MessageEmbed) {
|
||||
render.JSON(w, r, discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: embeds,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type CreateMemberRequest struct {
|
||||
|
@ -119,7 +120,12 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "starting transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links)
|
||||
if err != nil {
|
||||
|
@ -127,14 +133,14 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
return server.APIError{Code: server.ErrMemberNameInUse}
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.Wrap(err, "creating member")
|
||||
}
|
||||
|
||||
// set names, pronouns, fields
|
||||
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(cmr.Names), db.NotNull(cmr.Pronouns))
|
||||
if err != nil {
|
||||
log.Errorf("setting names and pronouns for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting names/pronouns")
|
||||
}
|
||||
m.Names = cmr.Names
|
||||
m.Pronouns = cmr.Pronouns
|
||||
|
@ -142,7 +148,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields)
|
||||
if err != nil {
|
||||
log.Errorf("setting fields for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting fields")
|
||||
}
|
||||
|
||||
if cmr.Avatar != "" {
|
||||
|
@ -161,13 +167,13 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
}
|
||||
|
||||
log.Errorf("converting member avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "converting avatar")
|
||||
}
|
||||
|
||||
hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
|
||||
if err != nil {
|
||||
log.Errorf("uploading member avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "uploading avatar")
|
||||
}
|
||||
|
||||
err = tx.QueryRow(ctx, "UPDATE members SET avatar = $1 WHERE id = $2", hash, m.ID).Scan(&m.Avatar)
|
||||
|
@ -180,7 +186,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating last active time")
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
|
|
|
@ -66,7 +66,7 @@ func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
err = s.DB.UpdateActiveTime(ctx, s.DB, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating last active time")
|
||||
}
|
||||
|
||||
render.NoContent(w, r)
|
||||
|
|
|
@ -105,7 +105,7 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
|
|||
|
||||
u, err := s.DB.User(ctx, m.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
if u.DeletedAt != nil {
|
||||
|
@ -119,12 +119,12 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
|
|||
|
||||
fields, err := s.DB.MemberFields(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member fields")
|
||||
}
|
||||
|
||||
flags, err := s.DB.MemberFlags(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member flags")
|
||||
}
|
||||
|
||||
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember))
|
||||
|
@ -159,12 +159,12 @@ func (s *Server) getUserMember(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
fields, err := s.DB.MemberFields(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member fields")
|
||||
}
|
||||
|
||||
flags, err := s.DB.MemberFlags(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member flags")
|
||||
}
|
||||
|
||||
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember))
|
||||
|
@ -189,12 +189,12 @@ func (s *Server) getMeMember(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
fields, err := s.DB.MemberFields(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member fields")
|
||||
}
|
||||
|
||||
flags, err := s.DB.MemberFlags(ctx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting member flags")
|
||||
}
|
||||
|
||||
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, true))
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
|
@ -74,7 +75,7 @@ func (s *Server) getUserMembers(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
ms, err := s.DB.UserMembers(ctx, u.ID, isSelf)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting members")
|
||||
}
|
||||
|
||||
render.JSON(w, r, membersToMemberList(ms, isSelf))
|
||||
|
@ -87,7 +88,7 @@ func (s *Server) getMeMembers(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
ms, err := s.DB.UserMembers(ctx, claims.UserID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "getting members")
|
||||
}
|
||||
|
||||
render.JSON(w, r, membersToMemberList(ms, true))
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -220,13 +221,13 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
log.Errorf("converting member avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "converting member avatar")
|
||||
}
|
||||
|
||||
hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
|
||||
if err != nil {
|
||||
log.Errorf("uploading member avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "writing member avatar")
|
||||
}
|
||||
avatarHash = &hash
|
||||
|
||||
|
@ -244,9 +245,14 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
tx, err := s.DB.Begin(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("creating transaction: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
|
||||
if err != nil {
|
||||
|
@ -275,7 +281,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, names, pronouns)
|
||||
if err != nil {
|
||||
log.Errorf("setting names for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting names/pronouns")
|
||||
}
|
||||
m.Names = names
|
||||
m.Pronouns = pronouns
|
||||
|
@ -286,14 +292,14 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetMemberFields(ctx, tx, m.ID, *req.Fields)
|
||||
if err != nil {
|
||||
log.Errorf("setting fields for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting fields")
|
||||
}
|
||||
fields = *req.Fields
|
||||
} else {
|
||||
fields, err = s.DB.MemberFields(ctx, m.ID)
|
||||
if err != nil {
|
||||
log.Errorf("getting fields for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting fields")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,7 +312,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
log.Errorf("updating flags for member %v: %v", m.ID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating flags")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,20 +320,20 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating last active time")
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("committing transaction: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "committing transaction")
|
||||
}
|
||||
|
||||
// get flags to return (we need to return full flag objects, not the array of IDs in the request body)
|
||||
flags, err := s.DB.MemberFlags(ctx, m.ID)
|
||||
if err != nil {
|
||||
log.Errorf("getting user flags: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting flags")
|
||||
}
|
||||
|
||||
// echo the updated member back on success
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type resolveReportRequest struct {
|
||||
|
@ -43,7 +44,12 @@ func (s *Server) resolveReport(w http.ResponseWriter, r *http.Request) error {
|
|||
log.Errorf("creating transaction: %v", err)
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
report, err := s.DB.Report(ctx, tx, id)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,9 +3,11 @@ package user
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
|
||||
|
@ -20,7 +22,12 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
|
||||
if err != nil {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
|
@ -71,7 +72,7 @@ func (s *Server) getExport(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
log.Errorf("getting export for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting export")
|
||||
}
|
||||
|
||||
render.JSON(w, r, dataExportResponse{
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -80,7 +81,12 @@ func (s *Server) postUserFlag(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "starting transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description)
|
||||
if err != nil {
|
||||
|
@ -192,7 +198,12 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
|
@ -146,7 +147,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
|
|||
}
|
||||
} else if err != nil {
|
||||
log.Errorf("Error getting user by username: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,13 +163,13 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
|
|||
fields, err := s.DB.UserFields(ctx, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user fields: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting fields")
|
||||
}
|
||||
|
||||
flags, err := s.DB.UserFlags(ctx, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("getting user flags: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting flags")
|
||||
}
|
||||
|
||||
var members []db.Member
|
||||
|
@ -176,7 +177,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
|
|||
members, err = s.DB.UserMembers(ctx, u.ID, isSelf)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user members: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting user members")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,25 +192,25 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error {
|
|||
u, err := s.DB.User(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting users")
|
||||
}
|
||||
|
||||
fields, err := s.DB.UserFields(ctx, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user fields: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting fields")
|
||||
}
|
||||
|
||||
members, err := s.DB.UserMembers(ctx, u.ID, true)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user members: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting members")
|
||||
}
|
||||
|
||||
flags, err := s.DB.UserFlags(ctx, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("getting user flags: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting flags")
|
||||
}
|
||||
|
||||
render.JSON(w, r, GetMeResponse{
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -195,13 +196,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
log.Errorf("converting user avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "converting avatar")
|
||||
}
|
||||
|
||||
hash, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg)
|
||||
if err != nil {
|
||||
log.Errorf("uploading user avatar: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "uploading avatar")
|
||||
}
|
||||
avatarHash = &hash
|
||||
|
||||
|
@ -219,9 +220,14 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
tx, err := s.DB.Begin(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("creating transaction: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// update username
|
||||
if req.Username != nil && *req.Username != u.Username {
|
||||
|
@ -243,7 +249,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.MemberTitle, req.ListPrivate, req.Links, avatarHash, req.Timezone, req.CustomPreferences)
|
||||
if err != nil && errors.Cause(err) != db.ErrNothingToUpdate {
|
||||
log.Errorf("updating user: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating user")
|
||||
}
|
||||
|
||||
if req.Names != nil || req.Pronouns != nil {
|
||||
|
@ -260,7 +266,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetUserNamesPronouns(ctx, tx, claims.UserID, names, pronouns)
|
||||
if err != nil {
|
||||
log.Errorf("setting names for member %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting names/pronouns")
|
||||
}
|
||||
u.Names = names
|
||||
u.Pronouns = pronouns
|
||||
|
@ -271,14 +277,14 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields)
|
||||
if err != nil {
|
||||
log.Errorf("setting fields for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "setting fields")
|
||||
}
|
||||
fields = *req.Fields
|
||||
} else {
|
||||
fields, err = s.DB.UserFields(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("getting fields for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting fields")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,7 +297,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
log.Errorf("updating flags for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating flags")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,13 +305,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("updating last active time for user %v: %v", claims.UserID, err)
|
||||
return err
|
||||
return errors.Wrap(err, "updating last active time")
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("committing transaction: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "committing transaction")
|
||||
}
|
||||
|
||||
// get fedi instance name if the user has a linked fedi account
|
||||
|
@ -321,7 +327,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
flags, err := s.DB.UserFlags(ctx, u.ID)
|
||||
if err != nil {
|
||||
log.Errorf("getting user flags: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting flags")
|
||||
}
|
||||
|
||||
// echo the updated user back on success
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
|
@ -13,7 +14,7 @@ func (s *Server) GetSettings(w http.ResponseWriter, r *http.Request) (err error)
|
|||
u, err := s.DB.User(r.Context(), claims.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("getting user: %v", err)
|
||||
return err
|
||||
return errors.Wrap(err, "getting user")
|
||||
}
|
||||
|
||||
render.JSON(w, r, u.Settings)
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
|
@ -12,6 +16,11 @@ import (
|
|||
// The inner HandlerFunc additionally returns an error.
|
||||
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
hub := sentry.GetHubFromContext(r.Context())
|
||||
if hub == nil {
|
||||
hub = sentry.CurrentHub().Clone()
|
||||
}
|
||||
|
||||
err := hn(w, r)
|
||||
if err != nil {
|
||||
// if the function returned an API error, just render that verbatim
|
||||
|
@ -24,10 +33,20 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
|
|||
return
|
||||
}
|
||||
|
||||
// otherwise, we log the error and return an internal server error message
|
||||
log.Errorf("error in http handler: %v", err)
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
hub.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetTag("method", rctx.RouteMethod)
|
||||
scope.SetTag("path", rctx.RoutePattern())
|
||||
})
|
||||
|
||||
apiErr := APIError{Code: ErrInternalServerError}
|
||||
var eventID *sentry.EventID = nil
|
||||
if isExpectedError(err) {
|
||||
log.Infof("expected error in handler for %v %v, ignoring", rctx.RouteMethod, rctx.RoutePattern())
|
||||
} else {
|
||||
log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err)
|
||||
eventID = hub.CaptureException(err)
|
||||
}
|
||||
apiErr := APIError{ID: eventID, Code: ErrInternalServerError}
|
||||
apiErr.prepare()
|
||||
|
||||
render.Status(r, apiErr.Status)
|
||||
|
@ -36,10 +55,15 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
|
|||
}
|
||||
}
|
||||
|
||||
func isExpectedError(err error) bool {
|
||||
return errors.Is(err, context.Canceled)
|
||||
}
|
||||
|
||||
// APIError is an object returned by the API when an error occurs.
|
||||
// It implements the error interface and can be returned by handlers.
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
ID *sentry.EventID `json:"id,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
func (s *Server) sentry(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
|
||||
ctx := r.Context()
|
||||
hub := sentry.GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = sentry.CurrentHub().Clone()
|
||||
ctx = sentry.SetHubOnContext(ctx, hub)
|
||||
}
|
||||
|
||||
options := []sentry.SpanOption{
|
||||
sentry.WithOpName("http.server"),
|
||||
sentry.ContinueFromRequest(r),
|
||||
sentry.WithTransactionSource(sentry.SourceURL),
|
||||
}
|
||||
// We don't mind getting an existing transaction back so we don't need to
|
||||
// check if it is.
|
||||
transaction := sentry.StartTransaction(ctx,
|
||||
fmt.Sprintf("%s %s", r.Method, r.URL.Path),
|
||||
options...,
|
||||
)
|
||||
defer transaction.Finish()
|
||||
r = r.WithContext(transaction.Context())
|
||||
hub.Scope().SetRequest(r)
|
||||
defer recoverWithSentry(hub, r)
|
||||
handler.ServeHTTP(ww, r)
|
||||
|
||||
transaction.Status = httpStatusToSentryStatus(ww.Status())
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
transaction.Name = rctx.RouteMethod + " " + rctx.RoutePattern()
|
||||
})
|
||||
}
|
||||
|
||||
func recoverWithSentry(hub *sentry.Hub, r *http.Request) {
|
||||
if err := recover(); err != nil {
|
||||
hub.RecoverWithContext(
|
||||
context.WithValue(r.Context(), sentry.RequestContextKey, r),
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func httpStatusToSentryStatus(status int) sentry.SpanStatus {
|
||||
// c.f. https://develop.sentry.dev/sdk/event-payloads/span/
|
||||
|
||||
if status >= 200 && status < 400 {
|
||||
return sentry.SpanStatusOK
|
||||
}
|
||||
|
||||
switch status {
|
||||
case 499:
|
||||
return sentry.SpanStatusCanceled
|
||||
case 500:
|
||||
return sentry.SpanStatusInternalError
|
||||
case 400:
|
||||
return sentry.SpanStatusInvalidArgument
|
||||
case 504:
|
||||
return sentry.SpanStatusDeadlineExceeded
|
||||
case 404:
|
||||
return sentry.SpanStatusNotFound
|
||||
case 409:
|
||||
return sentry.SpanStatusAlreadyExists
|
||||
case 403:
|
||||
return sentry.SpanStatusPermissionDenied
|
||||
case 429:
|
||||
return sentry.SpanStatusResourceExhausted
|
||||
case 501:
|
||||
return sentry.SpanStatusUnimplemented
|
||||
case 503:
|
||||
return sentry.SpanStatusUnavailable
|
||||
case 401:
|
||||
return sentry.SpanStatusUnauthenticated
|
||||
default:
|
||||
return sentry.SpanStatusUnknown
|
||||
}
|
||||
}
|
|
@ -50,6 +50,9 @@ func New() (*Server, error) {
|
|||
s.Router.Use(middleware.Logger)
|
||||
}
|
||||
s.Router.Use(middleware.Recoverer)
|
||||
// add Sentry tracing handler
|
||||
s.Router.Use(s.sentry)
|
||||
|
||||
// add CORS
|
||||
s.Router.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
|
@ -97,23 +100,23 @@ func New() (*Server, error) {
|
|||
|
||||
// set scopes
|
||||
// users
|
||||
rateLimiter.Scope("GET", "/users/*", 60)
|
||||
rateLimiter.Scope("PATCH", "/users/@me", 10)
|
||||
_ = rateLimiter.Scope("GET", "/users/*", 60)
|
||||
_ = rateLimiter.Scope("PATCH", "/users/@me", 10)
|
||||
|
||||
// members
|
||||
rateLimiter.Scope("GET", "/users/*/members", 60)
|
||||
rateLimiter.Scope("GET", "/users/*/members/*", 60)
|
||||
_ = rateLimiter.Scope("GET", "/users/*/members", 60)
|
||||
_ = rateLimiter.Scope("GET", "/users/*/members/*", 60)
|
||||
|
||||
rateLimiter.Scope("POST", "/members", 10)
|
||||
rateLimiter.Scope("GET", "/members/*", 60)
|
||||
rateLimiter.Scope("PATCH", "/members/*", 20)
|
||||
rateLimiter.Scope("DELETE", "/members/*", 5)
|
||||
_ = rateLimiter.Scope("POST", "/members", 10)
|
||||
_ = rateLimiter.Scope("GET", "/members/*", 60)
|
||||
_ = rateLimiter.Scope("PATCH", "/members/*", 20)
|
||||
_ = rateLimiter.Scope("DELETE", "/members/*", 5)
|
||||
|
||||
// auth
|
||||
rateLimiter.Scope("*", "/auth/*", 20)
|
||||
rateLimiter.Scope("*", "/auth/tokens", 10)
|
||||
rateLimiter.Scope("*", "/auth/invites", 10)
|
||||
rateLimiter.Scope("POST", "/auth/discord/*", 10)
|
||||
_ = rateLimiter.Scope("*", "/auth/*", 20)
|
||||
_ = rateLimiter.Scope("*", "/auth/tokens", 10)
|
||||
_ = rateLimiter.Scope("*", "/auth/invites", 10)
|
||||
_ = rateLimiter.Scope("POST", "/auth/discord/*", 10)
|
||||
|
||||
s.Router.Use(rateLimiter.Handler())
|
||||
|
||||
|
|
|
@ -46,8 +46,9 @@ A user can set custom word preferences, which can have custom icons and tooltips
|
|||
## Pride flag
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----------- | ------- | ------------------------------------- |
|
||||
| ----------- | --------- | ------------------------------------- |
|
||||
| id | string | the flag's unique ID |
|
||||
| id_new | snowflake | the flag's unique snowflake ID |
|
||||
| hash | string | the flag's [image hash](/api/#images) |
|
||||
| name | string | the flag's name |
|
||||
| description | string? | the flag's description or alt text |
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
| Field | Type | Description |
|
||||
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| id | string | the member's unique ID |
|
||||
| id_new | snowflake | the member's unique snowflake ID |
|
||||
| sid | string | the member's 6-letter short ID |
|
||||
| name | string | the member's name |
|
||||
| display_name | string? | the member's display name or nickname |
|
||||
|
@ -23,6 +24,7 @@
|
|||
| Field | Type | Description |
|
||||
| ------------------ | ---------------------------------------------------- | -------------------------------------- |
|
||||
| id | string | the user's unique ID |
|
||||
| id_new | snowflake | the user's unique snowflake ID |
|
||||
| name | string | the user's username |
|
||||
| display_name | string? | the user's display name or nickname |
|
||||
| avatar | string? | the user's [avatar hash](/api/#images) |
|
||||
|
@ -96,7 +98,7 @@ Returns the updated [member](./members#member-object) on success.
|
|||
#### Request body parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------------ | -------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| ------------ | --------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| name | string | the member's new name. Must be unique per user, and be between 1 and 100 characters. |
|
||||
| display_name | string | the member's new display name. Must be between 1 and 100 characters |
|
||||
| bio | string | the member's new bio. Must be between 1 and 1000 characters |
|
||||
|
@ -104,7 +106,7 @@ Returns the updated [member](./members#member-object) on success.
|
|||
| names | field_entry[] | the member's new preferred names |
|
||||
| pronouns | pronoun_entry[] | the member's new preferred pronouns |
|
||||
| fields | field[] | the member's new profile fields |
|
||||
| flags | string[] | the member's new flags. This must be an array of [pride flag](./#pride-flag) IDs. |
|
||||
| flags | string[] | the member's new flags. This must be an array of [pride flag](./#pride-flag) IDs, _not_ snowflake IDs. |
|
||||
| avatar | string | the member's new avatar. This must be a PNG, JPEG, or WebP image, encoded in base64 data URI format |
|
||||
| unlisted | bool | whether or not the member should be hidden from the member list |
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
| Field | Type | Description |
|
||||
| ------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| id | string | the user's unique ID |
|
||||
| id_new | snowflake | the user's unique snowflake ID |
|
||||
| sid | string | the user's 5 letter short ID |
|
||||
| name | string | the user's username |
|
||||
| display_name | string? | the user's display name or nickname |
|
||||
|
@ -45,6 +46,7 @@
|
|||
| Field | Type | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------------------- |
|
||||
| id | string | the member's unique ID |
|
||||
| id_new | snowflake | the member's unique snowflake ID |
|
||||
| sid | string | the member's 6-letter short ID |
|
||||
| name | string | the member's name |
|
||||
| display_name | string? | the member's display name or nickname |
|
||||
|
@ -89,7 +91,7 @@ Returns the updated [user](./users#user-object) object on success.
|
|||
| names | field_entry[] | the user's new preferred names |
|
||||
| pronouns | pronoun_entry[] | the user's new preferred pronouns |
|
||||
| fields | field[] | the user's new profile fields |
|
||||
| flags | string[] | the user's new flags. This must be an array of [pride flag](./#pride-flag) IDs. |
|
||||
| flags | string[] | the user's new flags. This must be an array of [pride flag](./#pride-flag) IDs, _not_ snowflake IDs. |
|
||||
| avatar | string | the user's new avatar. This must be a PNG, JPEG, or WebP image, encoded in base64 data URI format |
|
||||
| timezone | string | the user's new timezone. Must be in IANA timezone database format |
|
||||
| list_private | bool | whether or not the user's member list should be hidden |
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
If there is an error in your request, or the server encounters an error while processing it, an error object will be returned.
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------------- | ------- | --------------------------------------------------------------- |
|
||||
| --------------- | ------- | ------------------------------------------------------------------- |
|
||||
| code | int | an [error code](./errors#error-codes) |
|
||||
| id | ?string | an opaque Sentry event ID, only returned for internal server errors |
|
||||
| message | ?string | a human-readable description of the error |
|
||||
| details | ?string | more details about the error, most often for bad request errors |
|
||||
| ratelimit_reset | ?int | the unix time when an expired rate limit will reset |
|
||||
|
|
|
@ -62,19 +62,23 @@ The "type" column in tables is formatted as follows:
|
|||
|
||||
## IDs
|
||||
|
||||
::: info
|
||||
pronouns.cc is [planning a transition](https://codeberg.org/pronounscc/pronouns.cc/issues/89)
|
||||
to [Snowflake IDs](https://en.wikipedia.org/wiki/Snowflake_ID).
|
||||
The information below pertains to the current ID format.
|
||||
:::
|
||||
### Snowflake IDs
|
||||
|
||||
The API uses [xid](https://github.com/rs/xid) for unique IDs. These are always serialized as strings.
|
||||
For [multiple reasons](https://codeberg.org/pronounscc/pronouns.cc/issues/89),
|
||||
pronouns.cc is transitioning to using snowflakes for unique IDs. These will become the default in the next API version,
|
||||
but are already returned as `id_new` in the relevant objects (users, members, and flags).
|
||||
|
||||
### xids
|
||||
|
||||
[xid](https://github.com/rs/xid) is the previous unique ID format. These are always serialized as strings.
|
||||
Although xids have timestamp information embedded in them, this is non-trivial to extract.
|
||||
xids are unique across _all_ resources, they are never shared (for example, a user and a member cannot share the same ID).
|
||||
|
||||
### prns.cc IDs
|
||||
|
||||
Users and members also have an additional ID type, `sid`.
|
||||
These are randomly generated 5 or 6 letter strings, and are used for the prns.cc URL shortener.
|
||||
They can be rerolled once per hour.
|
||||
**These can change at any time**, as short IDs can be rerolled once per hour.
|
||||
|
||||
## Images
|
||||
|
||||
|
|
|
@ -17,4 +17,15 @@ module.exports = {
|
|||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,26 +16,22 @@ writeFileSync("src/icons.ts", `const icons = ${output};\nexport default icons;`)
|
|||
const goCode1 = `// Generated code. DO NOT EDIT
|
||||
package icons
|
||||
|
||||
var icons = [...]string{
|
||||
var icons = map[string]struct{}{
|
||||
`;
|
||||
|
||||
const goCode2 = `}
|
||||
|
||||
// IsValid returns true if the input is the name of a Bootstrap icon.
|
||||
func IsValid(name string) bool {
|
||||
for i := range icons {
|
||||
if icons[i] == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
_, ok := icons[name]
|
||||
return ok
|
||||
}
|
||||
`;
|
||||
|
||||
let goOutput = goCode1;
|
||||
|
||||
keys.forEach((element) => {
|
||||
goOutput += ` "${element}",\n`;
|
||||
goOutput += ` "${element}": {},\n`;
|
||||
});
|
||||
|
||||
goOutput += goCode2;
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.2.3",
|
||||
"@sveltejs/kit": "^1.15.0",
|
||||
"@types/luxon": "^3.2.2",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@sveltestrap/sveltestrap": "^6.0.5",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/markdown-it": "^13.0.7",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
|
@ -25,14 +27,13 @@
|
|||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-svelte": "^2.10.0",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-check": "^3.1.4",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.0",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-hcaptcha": "^0.1.1",
|
||||
"sveltestrap": "^5.10.0",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.2.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-markdown": "^2.1.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
@ -41,8 +42,8 @@
|
|||
"@popperjs/core": "^2.11.7",
|
||||
"@sentry/node": "^7.46.0",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"bootstrap": "5.3.0-alpha1",
|
||||
"bootstrap-icons": "^1.10.4",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.2",
|
||||
"jose": "^4.13.1",
|
||||
"luxon": "^3.3.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
|
|
@ -16,23 +16,4 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module "svelte-hcaptcha" {
|
||||
import type { SvelteComponent } from "svelte";
|
||||
|
||||
export interface HCaptchaProps {
|
||||
sitekey?: string;
|
||||
apihost?: string;
|
||||
hl?: string;
|
||||
reCaptchaCompat?: boolean;
|
||||
theme?: CaptchaTheme;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
declare class HCaptcha extends SvelteComponent {
|
||||
$$prop_def: HCaptchaProps;
|
||||
}
|
||||
|
||||
export default HCaptcha;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
@ -2,7 +2,9 @@ import { PRIVATE_SENTRY_DSN } from "$env/static/private";
|
|||
import * as Sentry from "@sentry/node";
|
||||
import type { HandleServerError } from "@sveltejs/kit";
|
||||
|
||||
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
|
||||
if (PRIVATE_SENTRY_DSN) {
|
||||
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
|
||||
}
|
||||
|
||||
export const handleError = (({ error, event }) => {
|
||||
console.log(error);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public";
|
||||
|
||||
export const MAX_MEMBERS = 500;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { NavLink } from "sveltestrap";
|
||||
import { NavLink } from "@sveltestrap/sveltestrap";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let href: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { APIError } from "$lib/api/entities";
|
||||
import { Alert } from "sveltestrap";
|
||||
import { Alert } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let error: APIError;
|
||||
</script>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
export let urls: string[];
|
||||
export let alt: string;
|
||||
export let width = 300;
|
||||
export let lazyLoad = false;
|
||||
|
||||
const contentTypeFor = (url: string) => {
|
||||
if (url.endsWith(".webp")) {
|
||||
|
@ -31,6 +32,7 @@
|
|||
src={urls[0] || defaultAvatars[0]}
|
||||
{alt}
|
||||
class="rounded-circle img-fluid"
|
||||
loading={lazyLoad ? "lazy" : "eager"}
|
||||
/>
|
||||
</picture>
|
||||
{:else}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Button, Icon, Tooltip } from "sveltestrap";
|
||||
import { Button, Icon, Tooltip } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let icon: string;
|
||||
export let color: "primary" | "secondary" | "success" | "danger";
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
type User,
|
||||
type CustomPreferences,
|
||||
} from "$lib/api/entities";
|
||||
import { Icon, Tooltip } from "sveltestrap";
|
||||
import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
|
||||
import FallbackImage from "./FallbackImage.svelte";
|
||||
|
||||
export let user: User;
|
||||
export let member: PartialMember & {
|
||||
unlisted?: boolean
|
||||
unlisted?: boolean;
|
||||
};
|
||||
|
||||
let pronouns: string | undefined;
|
||||
|
@ -46,13 +46,18 @@
|
|||
|
||||
<div>
|
||||
<a href="/@{user.name}/{member.name}">
|
||||
<FallbackImage urls={memberAvatars(member)} width={200} alt="Avatar for {member.name}" />
|
||||
<FallbackImage
|
||||
urls={memberAvatars(member)}
|
||||
width={200}
|
||||
lazyLoad
|
||||
alt="Avatar for {member.name}"
|
||||
/>
|
||||
</a>
|
||||
<p class="m-2">
|
||||
<a class="text-reset fs-5 text-break" href="/@{user.name}/{member.name}">
|
||||
{member.display_name ?? member.name}
|
||||
{#if member.unlisted === true}
|
||||
<span bind:this={iconElement} tabindex={0}><Icon name="lock"/></span>
|
||||
<span bind:this={iconElement} tabindex={0}><Icon name="lock" /></span>
|
||||
<Tooltip target={iconElement} placement="top">This member is hidden</Tooltip>
|
||||
{/if}
|
||||
</a>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Icon, Tooltip } from "sveltestrap";
|
||||
import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import type { CustomPreference, CustomPreferences } from "$lib/api/entities";
|
||||
import defaultPreferences from "$lib/api/default_preferences";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Toast } from "sveltestrap";
|
||||
import { Toast } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let header: string | undefined = undefined;
|
||||
export let body: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Field, CustomPreferences } from "$lib/api/entities";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import { Button, Input, InputGroup } from "sveltestrap";
|
||||
import { Button, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
import FieldEntry from "./FieldEntry.svelte";
|
||||
|
||||
export let field: Field;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
DropdownToggle,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let value: string;
|
||||
export let status: string;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
InputGroupText,
|
||||
Popover,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let pronoun: Pronoun;
|
||||
export let preferences: CustomPreferences;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
DropdownToggle,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let value: string;
|
||||
export let status: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { flagURL, type PrideFlag } from "$lib/api/entities";
|
||||
import { Button, Tooltip } from "sveltestrap";
|
||||
import { Button, Tooltip } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let flag: PrideFlag;
|
||||
export let tooltip: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Icon, Modal } from "sveltestrap";
|
||||
import { Icon, Modal } from "@sveltestrap/sveltestrap";
|
||||
|
||||
let isOpen = false;
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
import { settingsStore } from "$lib/store";
|
||||
import { toastStore } from "$lib/toast";
|
||||
import Toast from "$lib/components/Toast.svelte";
|
||||
import { Alert, Icon } from "sveltestrap";
|
||||
import { Alert, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { apiFetchClient } from "$lib/api/fetch";
|
||||
import type { Settings } from "$lib/api/entities";
|
||||
import { renderUnsafeMarkdown } from "$lib/utils";
|
||||
|
@ -31,6 +31,8 @@
|
|||
const resp = await apiFetchClient<Settings>(
|
||||
"/users/@me/settings",
|
||||
"PATCH",
|
||||
// If this function is run, notice will always be non-null
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
{ read_global_notice: data.notice!.id },
|
||||
2,
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||
import { Button } from "sveltestrap";
|
||||
import { Button } from "@sveltestrap/sveltestrap";
|
||||
import { userStore } from "$lib/store";
|
||||
</script>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const load = async ({ params }) => {
|
|||
return resp;
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.UserNotFound) {
|
||||
throw error(404, e as APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
|
@ -14,8 +13,8 @@
|
|||
ModalBody,
|
||||
ModalFooter,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { DateTime, FixedOffsetZone } from "luxon";
|
||||
import FieldCard from "$lib/components/FieldCard.svelte";
|
||||
import PronounLink from "$lib/components/PronounLink.svelte";
|
||||
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
|
||||
|
@ -46,6 +45,7 @@
|
|||
import ProfileFlag from "./ProfileFlag.svelte";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import Badges from "./badges/Badges.svelte";
|
||||
import PreferencesCheatsheet from "./PreferencesCheatsheet.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -190,14 +190,15 @@
|
|||
{/if}
|
||||
{#if data.utc_offset}
|
||||
<Tooltip target="user-clock" placement="top">Current time</Tooltip>
|
||||
<Icon id="user-clock" name="clock" aria-label="This user's current time" /> {currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
|
||||
<Icon id="user-clock" name="clock" aria-label="This user's current time" />
|
||||
{currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
|
||||
{/if}
|
||||
{#if profileEmpty && $userStore?.id === data.id}
|
||||
<hr />
|
||||
<p>
|
||||
<em>
|
||||
Your profile is empty! You can customize it by going to the <a href="/@{data.name}/edit"
|
||||
>edit profile</a
|
||||
Your profile is empty! You can customize it by going to the <a
|
||||
href="/@{data.name}/edit">edit profile</a
|
||||
> page.</em
|
||||
> <span class="text-muted">(only you can see this)</span>
|
||||
</p>
|
||||
|
@ -258,6 +259,12 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<PreferencesCheatsheet
|
||||
preferences={data.custom_preferences}
|
||||
names={data.names}
|
||||
pronouns={data.pronouns}
|
||||
fields={data.fields}
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<InputGroup>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import type {
|
||||
CustomPreferences,
|
||||
CustomPreference,
|
||||
Field,
|
||||
FieldEntry,
|
||||
Pronoun,
|
||||
} from "$lib/api/entities";
|
||||
import defaultPreferences from "$lib/api/default_preferences";
|
||||
import StatusIcon from "$lib/components/StatusIcon.svelte";
|
||||
|
||||
export let preferences: CustomPreferences;
|
||||
export let names: FieldEntry[];
|
||||
export let pronouns: Pronoun[];
|
||||
export let fields: Field[];
|
||||
|
||||
let mergedPreferences: CustomPreferences;
|
||||
$: mergedPreferences = Object.assign({}, defaultPreferences, preferences);
|
||||
|
||||
// Filter default preferences to the ones the user/member has used
|
||||
// This is done separately from custom preferences to make the shown list cleaner
|
||||
let usedDefaultPreferences: Array<{ id: string; preference: CustomPreference }>;
|
||||
$: usedDefaultPreferences = Object.keys(defaultPreferences)
|
||||
.filter(
|
||||
(pref) =>
|
||||
names.some((entry) => entry.status === pref) ||
|
||||
pronouns.some((entry) => entry.status === pref) ||
|
||||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
|
||||
)
|
||||
.map((key) => ({
|
||||
id: key,
|
||||
preference: defaultPreferences[key],
|
||||
}));
|
||||
// Do the same for custom preferences
|
||||
let usedCustomPreferences: Array<{ id: string; preference: CustomPreference }>;
|
||||
$: usedCustomPreferences = Object.keys(preferences)
|
||||
.filter(
|
||||
(pref) =>
|
||||
names.some((entry) => entry.status === pref) ||
|
||||
pronouns.some((entry) => entry.status === pref) ||
|
||||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
|
||||
)
|
||||
.map((pref) => ({ id: pref, preference: mergedPreferences[pref] }));
|
||||
</script>
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="list-inline text-body-secondary">
|
||||
{#each usedDefaultPreferences as pref (pref.id)}
|
||||
<li class="list-inline-item mx-2">
|
||||
<StatusIcon {preferences} status={pref.id} />
|
||||
{pref.preference.tooltip}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if usedCustomPreferences}
|
||||
<ul class="list-inline text-body-secondary">
|
||||
{#each usedCustomPreferences as pref (pref.id)}
|
||||
<li class="list-inline-item mx-2">
|
||||
<StatusIcon {preferences} status={pref.id} />
|
||||
{pref.preference.tooltip}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { flagURL, type PrideFlag } from "$lib/api/entities";
|
||||
import { Tooltip } from "sveltestrap";
|
||||
import { Tooltip } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let flag: PrideFlag;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Icon } from "sveltestrap";
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let link: string;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { fastFetchClient } from "$lib/api/fetch";
|
||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||
import { addToast } from "$lib/toast";
|
||||
import { Button, FormGroup, Icon, Modal, ModalBody, ModalFooter } from "sveltestrap";
|
||||
import { Button, FormGroup, Icon, Modal, ModalBody, ModalFooter } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let subject: string;
|
||||
export let reportUrl: string;
|
||||
|
|
|
@ -14,9 +14,9 @@ export const load = async ({ params }) => {
|
|||
(e as APIError).code === ErrorCode.UserNotFound ||
|
||||
(e as APIError).code === ErrorCode.MemberNotFound
|
||||
) {
|
||||
throw error(404, e as APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
throw error(500, e as APIError);
|
||||
error(500, e as App.Error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import type { PageData } from "./$types";
|
||||
import PronounLink from "$lib/components/PronounLink.svelte";
|
||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||
import { Alert, Button, Icon, InputGroup } from "sveltestrap";
|
||||
import { Alert, Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
memberAvatars,
|
||||
pronounDisplay,
|
||||
|
@ -22,6 +22,7 @@
|
|||
import { addToast } from "$lib/toast";
|
||||
import ProfileFlag from "../ProfileFlag.svelte";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import PreferencesCheatsheet from "../PreferencesCheatsheet.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -154,6 +155,12 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<PreferencesCheatsheet
|
||||
preferences={data.user.custom_preferences}
|
||||
names={data.names}
|
||||
pronouns={data.pronouns}
|
||||
fields={data.fields}
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<InputGroup>
|
||||
|
|
|
@ -5,7 +5,15 @@
|
|||
import type { LayoutData } from "./$types";
|
||||
import { addToast, delToast } from "$lib/toast";
|
||||
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
|
||||
import { Button, ButtonGroup, Modal, ModalBody, ModalFooter, Nav, NavItem } from "sveltestrap";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Nav,
|
||||
NavItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { goto } from "$app/navigation";
|
||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
|
|
|
@ -32,7 +32,7 @@ export const load = (async ({ params }) => {
|
|||
member.user.name !== params.username ||
|
||||
member.name !== params.memberName
|
||||
) {
|
||||
throw redirect(303, `/@${user.name}/${member.name}`);
|
||||
redirect(303, `/@${user.name}/${member.name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -41,8 +41,9 @@ export const load = (async ({ params }) => {
|
|||
pronouns: pronouns.autocomplete,
|
||||
flags,
|
||||
};
|
||||
} catch (e) {
|
||||
if ("code" in e) throw error(500, e as APIError);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if ("code" in e) error(500, e as App.Error);
|
||||
throw e;
|
||||
}
|
||||
}) satisfies LayoutLoad;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { encode } from "base64-arraybuffer";
|
||||
import { FormGroup, Icon, Input } from "sveltestrap";
|
||||
import { FormGroup, Icon, Input } from "@sveltestrap/sveltestrap";
|
||||
import { memberAvatars, type Member } from "$lib/api/entities";
|
||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||
import EditableName from "$lib/components/edit/EditableName.svelte";
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { MAX_DESCRIPTION_LENGTH, type Member } from "$lib/api/entities";
|
||||
import { charCount, renderMarkdown } from "$lib/utils";
|
||||
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
|
||||
import { Card, CardBody, CardHeader } from "sveltestrap";
|
||||
import { Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
|
||||
|
||||
const member = getContext<Writable<Member>>("member");
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { Button, Icon } from "sveltestrap";
|
||||
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import type { Member } from "$lib/api/entities";
|
||||
import EditableField from "$lib/components/edit/EditableField.svelte";
|
||||
|
@ -45,9 +45,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}
|
||||
>
|
||||
<Button on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}>
|
||||
<Icon name="plus" aria-hidden /> Add new field
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { Alert, ButtonGroup, Input } from "sveltestrap";
|
||||
import { Alert, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import type { Member, PrideFlag } from "$lib/api/entities";
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { DateTime } from "luxon";
|
||||
import { Button, ButtonGroup, Icon } from "sveltestrap";
|
||||
import { Button, ButtonGroup, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import type { APIError, Member } from "$lib/api/entities";
|
||||
import { PUBLIC_SHORT_BASE } from "$env/static/public";
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { Member } from "$lib/api/entities";
|
||||
import { Button, Icon, Popover } from "sveltestrap";
|
||||
import { Button, Icon, Popover } from "@sveltestrap/sveltestrap";
|
||||
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Tooltip } from "sveltestrap";
|
||||
import { Tooltip } from "@sveltestrap/sveltestrap";
|
||||
|
||||
let icon: HTMLElement;
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { Button, ButtonGroup, Icon, Nav, NavItem } from "sveltestrap";
|
||||
import { Button, ButtonGroup, Icon, Nav, NavItem } from "@sveltestrap/sveltestrap";
|
||||
import type { MeUser, APIError } from "$lib/api/entities";
|
||||
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
|
||||
import { addToast, delToast } from "$lib/toast";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { PrideFlag, APIError, MeUser, PronounsJson } from "$lib/api/entities";
|
||||
import type { PrideFlag, MeUser, PronounsJson } from "$lib/api/entities";
|
||||
import { apiFetchClient } from "$lib/api/fetch";
|
||||
import { error, redirect, type Redirect } from "@sveltejs/kit";
|
||||
import { error, redirect } from "@sveltejs/kit";
|
||||
|
||||
import pronounsRaw from "$lib/pronouns.json";
|
||||
const pronouns = pronounsRaw as PronounsJson;
|
||||
|
@ -13,7 +13,7 @@ export const load = async ({ params }) => {
|
|||
const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags");
|
||||
|
||||
if (params.username !== user.name) {
|
||||
throw redirect(303, `/@${user.name}/edit`);
|
||||
redirect(303, `/@${user.name}/edit`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -21,8 +21,9 @@ export const load = async ({ params }) => {
|
|||
pronouns: pronouns.autocomplete,
|
||||
flags,
|
||||
};
|
||||
} catch (e) {
|
||||
if ("code" in e) throw error(500, e as APIError);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if ("code" in e) error(500, e as App.Error);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { encode } from "base64-arraybuffer";
|
||||
import { FormGroup, Icon, Input } from "sveltestrap";
|
||||
import { FormGroup, Icon, Input } from "@sveltestrap/sveltestrap";
|
||||
import { userAvatars, type MeUser } from "$lib/api/entities";
|
||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||
import EditableName from "$lib/components/edit/EditableName.svelte";
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { MAX_DESCRIPTION_LENGTH, type MeUser } from "$lib/api/entities";
|
||||
import { charCount, renderMarkdown } from "$lib/utils";
|
||||
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
|
||||
import { Card, CardBody, CardHeader } from "sveltestrap";
|
||||
import { Card, CardBody, CardHeader } from "@sveltestrap/sveltestrap";
|
||||
|
||||
const user = getContext<Writable<MeUser>>("user");
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { Button, Icon } from "sveltestrap";
|
||||
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import type { MeUser } from "$lib/api/entities";
|
||||
import EditableField from "$lib/components/edit/EditableField.svelte";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { Alert, ButtonGroup, Input } from "sveltestrap";
|
||||
import { Alert, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import type { MeUser, PrideFlag } from "$lib/api/entities";
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
|
||||
import { PreferenceSize, type APIError, type MeUser } from "$lib/api/entities";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import { Button, ButtonGroup, FormGroup, Icon, Input, InputGroup } from "sveltestrap";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { PUBLIC_SHORT_BASE } from "$env/static/public";
|
||||
import CustomPreference from "./CustomPreference.svelte";
|
||||
import { DateTime, FixedOffsetZone } from "luxon";
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
Icon,
|
||||
InputGroup,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import IconPicker from "./IconPicker.svelte";
|
||||
|
||||
export let preference: CustomPreference;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
Icon,
|
||||
Input,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import icons from "../../../../icons";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { MeUser } from "$lib/api/entities";
|
||||
import { Button, Icon, Popover } from "sveltestrap";
|
||||
import { Button, Icon, Popover } from "@sveltestrap/sveltestrap";
|
||||
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
|
|
@ -8,10 +8,10 @@ export const load = async ({ params }) => {
|
|||
method: "GET",
|
||||
});
|
||||
|
||||
throw redirect(303, `/@${resp.name}`);
|
||||
redirect(303, `/@${resp.name}`);
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.UserNotFound) {
|
||||
throw error(404, e as APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let authType: string;
|
||||
export let remoteName: string | undefined;
|
||||
|
@ -64,8 +64,10 @@
|
|||
) => Promise<void>;
|
||||
|
||||
let captchaToken = "";
|
||||
// svelte-hcaptcha doesn't have types, so we can't use anything except `any` here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let captcha: any;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const captchaSuccess = (token: any) => {
|
||||
captchaToken = token.detail.token;
|
||||
};
|
||||
|
@ -88,6 +90,8 @@
|
|||
await fastFetch("/auth/force-delete", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// We know for sure this value is non-null if this function is run at all
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
"X-Delete-Token": token!,
|
||||
},
|
||||
});
|
||||
|
@ -105,6 +109,8 @@
|
|||
await fastFetch("/auth/cancel-delete", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// We know for sure this value is non-null if this function is run at all
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
"X-Delete-Token": token!,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -16,14 +16,14 @@ export const load = async ({ params }) => {
|
|||
} as APIError;
|
||||
}
|
||||
|
||||
throw redirect(303, `/@${user.name}/${member.name}/edit`);
|
||||
redirect(303, `/@${user.name}/${member.name}/edit`);
|
||||
} catch (e) {
|
||||
if (
|
||||
(e as APIError).code === ErrorCode.Forbidden ||
|
||||
(e as APIError).code === ErrorCode.InvalidToken ||
|
||||
(e as APIError).code === ErrorCode.NotOwnMember
|
||||
) {
|
||||
throw error(403, e as APIError);
|
||||
error(403, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -8,13 +8,13 @@ export const load = async () => {
|
|||
try {
|
||||
const resp = await apiFetchClient<MeUser>(`/users/@me`);
|
||||
|
||||
throw redirect(303, `/@${resp.name}/edit`);
|
||||
redirect(303, `/@${resp.name}/edit`);
|
||||
} catch (e) {
|
||||
if (
|
||||
(e as APIError).code === ErrorCode.Forbidden ||
|
||||
(e as APIError).code === ErrorCode.InvalidToken
|
||||
) {
|
||||
throw error(403, e as APIError);
|
||||
error(403, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
NavbarToggler,
|
||||
NavItem,
|
||||
NavLink,
|
||||
} from "sveltestrap";
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import Logo from "./Logo.svelte";
|
||||
import { userStore, themeStore, CURRENT_CHANGELOG, settingsStore } from "$lib/store";
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
|
||||
// eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
import { html } from "./about.md";
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
|
||||
// eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
import { html } from "./changelog.md";
|
||||
import { CURRENT_CHANGELOG } from "$lib/store";
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
|
||||
// eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
import { html } from "./privacy.md";
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
|
||||
// eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
import { html } from "./terms.md";
|
||||
</script>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue