forked from mirrors/pronouns.cc
merge branch 'feature/snowflakes' into main
NOTES: - After running the migration, you MUST manually run `database create-snowflakes`. The entire backend assumes snowflakes are never null, so if this isn't done, all requests will fail. - Avatar and flag files are still saved with xids, this will change later.
This commit is contained in:
commit
6c8f2b648e
32 changed files with 3127 additions and 131 deletions
43
.air.toml
Normal file
43
.air.toml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = ["web"]
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["docs", "frontend", "prns", "pronounslib", "tmp", "target"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ package
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
target
|
target
|
||||||
|
tmp
|
||||||
|
|
65
backend/common/generator.go
Normal file
65
backend/common/generator.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generator is a snowflake generator.
|
||||||
|
// For compatibility with other snowflake implementations, both worker and PID are set,
|
||||||
|
// but they are randomized for every generator.
|
||||||
|
type IDGenerator struct {
|
||||||
|
inc *uint64
|
||||||
|
worker, pid uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultGenerator = NewIDGenerator(0, 0)
|
||||||
|
|
||||||
|
// NewIDGenerator creates a new ID generator with the given worker and pid.
|
||||||
|
// If worker or pid is empty, it will be set to a random number.
|
||||||
|
func NewIDGenerator(worker, pid uint64) *IDGenerator {
|
||||||
|
if worker == 0 {
|
||||||
|
worker = rand.Uint64()
|
||||||
|
}
|
||||||
|
if pid == 0 {
|
||||||
|
pid = rand.Uint64()
|
||||||
|
}
|
||||||
|
|
||||||
|
g := &IDGenerator{
|
||||||
|
inc: new(uint64),
|
||||||
|
worker: worker % 32,
|
||||||
|
pid: pid % 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateID generates a new snowflake with the default generator.
|
||||||
|
// If you need to customize the worker and PID, manually call (*Generator).Generate.
|
||||||
|
func GenerateID() Snowflake {
|
||||||
|
return defaultGenerator.Generate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateID generates a new snowflake with the given time with the default generator.
|
||||||
|
// If you need to customize the worker and PID, manually call (*Generator).GenerateWithTime.
|
||||||
|
func GenerateIDWithTime(t time.Time) Snowflake {
|
||||||
|
return defaultGenerator.GenerateWithTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates a snowflake with the current time.
|
||||||
|
func (g *IDGenerator) Generate() Snowflake {
|
||||||
|
return g.GenerateWithTime(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateWithTime generates a snowflake with the given time.
|
||||||
|
// To generate a snowflake for comparison, use the top-level New function instead.
|
||||||
|
func (g *IDGenerator) GenerateWithTime(t time.Time) Snowflake {
|
||||||
|
increment := atomic.AddUint64(g.inc, 1)
|
||||||
|
ts := uint64(t.UnixMilli() - Epoch)
|
||||||
|
|
||||||
|
worker := g.worker << 17
|
||||||
|
pid := g.pid << 12
|
||||||
|
|
||||||
|
return Snowflake(ts<<22 | worker | pid | (increment % 4096))
|
||||||
|
}
|
83
backend/common/snowflake.go
Normal file
83
backend/common/snowflake.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Epoch is the pronouns.cc epoch (January 1st 2022 at 00:00:00 UTC) in milliseconds.
|
||||||
|
const Epoch = 1_640_995_200_000
|
||||||
|
const epochDuration = Epoch * time.Millisecond
|
||||||
|
|
||||||
|
const NullSnowflake = ^Snowflake(0)
|
||||||
|
|
||||||
|
// Snowflake is a 64-bit integer used as a unique ID, with an embedded timestamp.
|
||||||
|
type Snowflake uint64
|
||||||
|
|
||||||
|
// ID is an alias to Snowflake.
|
||||||
|
type ID = Snowflake
|
||||||
|
|
||||||
|
// ParseSnowflake parses a snowflake from a string.
|
||||||
|
func ParseSnowflake(sf string) (Snowflake, error) {
|
||||||
|
if sf == "null" {
|
||||||
|
return NullSnowflake, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := strconv.ParseUint(sf, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Snowflake(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSnowflake creates a new snowflake from the given time.
|
||||||
|
func NewSnowflake(t time.Time) Snowflake {
|
||||||
|
ts := time.Duration(t.UnixNano()) - epochDuration
|
||||||
|
|
||||||
|
return Snowflake((ts / time.Millisecond) << 22)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the snowflake as a string.
|
||||||
|
func (s Snowflake) String() string { return strconv.FormatUint(uint64(s), 10) }
|
||||||
|
|
||||||
|
// Time returns the creation time of the snowflake.
|
||||||
|
func (s Snowflake) Time() time.Time {
|
||||||
|
ts := time.Duration(s>>22)*time.Millisecond + epochDuration
|
||||||
|
return time.Unix(0, int64(ts))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Snowflake) IsValid() bool {
|
||||||
|
return s != 0 && s != NullSnowflake
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Snowflake) MarshalJSON() ([]byte, error) {
|
||||||
|
if !s.IsValid() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(`"` + strconv.FormatUint(uint64(s), 10) + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snowflake) UnmarshalJSON(src []byte) error {
|
||||||
|
sf, err := ParseSnowflake(strings.Trim(string(src), `"`))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = sf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Snowflake) Worker() uint8 {
|
||||||
|
return uint8(s & 0x3E0000 >> 17)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Snowflake) PID() uint8 {
|
||||||
|
return uint8(s & 0x1F000 >> 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Snowflake) Increment() uint16 {
|
||||||
|
return uint16(s & 0xFFF)
|
||||||
|
}
|
39
backend/common/snowflake_types.go
Normal file
39
backend/common/snowflake_types.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type UserID Snowflake
|
||||||
|
|
||||||
|
func (id UserID) String() string { return Snowflake(id).String() }
|
||||||
|
func (id UserID) Time() time.Time { return Snowflake(id).Time() }
|
||||||
|
func (id UserID) IsValid() bool { return Snowflake(id).IsValid() }
|
||||||
|
func (id UserID) Worker() uint8 { return Snowflake(id).Worker() }
|
||||||
|
func (id UserID) PID() uint8 { return Snowflake(id).PID() }
|
||||||
|
func (id UserID) Increment() uint16 { return Snowflake(id).Increment() }
|
||||||
|
|
||||||
|
func (id UserID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
||||||
|
func (id *UserID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
||||||
|
|
||||||
|
type MemberID Snowflake
|
||||||
|
|
||||||
|
func (id MemberID) String() string { return Snowflake(id).String() }
|
||||||
|
func (id MemberID) Time() time.Time { return Snowflake(id).Time() }
|
||||||
|
func (id MemberID) IsValid() bool { return Snowflake(id).IsValid() }
|
||||||
|
func (id MemberID) Worker() uint8 { return Snowflake(id).Worker() }
|
||||||
|
func (id MemberID) PID() uint8 { return Snowflake(id).PID() }
|
||||||
|
func (id MemberID) Increment() uint16 { return Snowflake(id).Increment() }
|
||||||
|
|
||||||
|
func (id MemberID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
||||||
|
func (id *MemberID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
||||||
|
|
||||||
|
type FlagID Snowflake
|
||||||
|
|
||||||
|
func (id FlagID) String() string { return Snowflake(id).String() }
|
||||||
|
func (id FlagID) Time() time.Time { return Snowflake(id).Time() }
|
||||||
|
func (id FlagID) IsValid() bool { return Snowflake(id).IsValid() }
|
||||||
|
func (id FlagID) Worker() uint8 { return Snowflake(id).Worker() }
|
||||||
|
func (id FlagID) PID() uint8 { return Snowflake(id).PID() }
|
||||||
|
func (id FlagID) Increment() uint16 { return Snowflake(id).Increment() }
|
||||||
|
|
||||||
|
func (id FlagID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
|
||||||
|
func (id *FlagID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/davidbyttow/govips/v2/vips"
|
"github.com/davidbyttow/govips/v2/vips"
|
||||||
|
@ -20,11 +21,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PrideFlag struct {
|
type PrideFlag struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
UserID xid.ID `json:"-"`
|
SnowflakeID common.FlagID `json:"id_new"`
|
||||||
Hash string `json:"hash"`
|
UserID xid.ID `json:"-"`
|
||||||
Name string `json:"name"`
|
Hash string `json:"hash"`
|
||||||
Description *string `json:"description"`
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserFlag struct {
|
type UserFlag struct {
|
||||||
|
@ -194,11 +196,12 @@ func (db *DB) CreateFlag(ctx context.Context, tx pgx.Tx, userID xid.ID, name, de
|
||||||
|
|
||||||
sql, args, err := sq.Insert("pride_flags").
|
sql, args, err := sq.Insert("pride_flags").
|
||||||
SetMap(map[string]any{
|
SetMap(map[string]any{
|
||||||
"id": xid.New(),
|
"id": xid.New(),
|
||||||
"hash": "",
|
"snowflake_id": common.GenerateID(),
|
||||||
"user_id": userID.String(),
|
"hash": "",
|
||||||
"name": name,
|
"user_id": userID.String(),
|
||||||
"description": description,
|
"name": name,
|
||||||
|
"description": description,
|
||||||
}).Suffix("RETURNING *").ToSql()
|
}).Suffix("RETURNING *").ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, errors.Wrap(err, "building query")
|
return f, errors.Wrap(err, "building query")
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/georgysavva/scany/v2/pgxscan"
|
"github.com/georgysavva/scany/v2/pgxscan"
|
||||||
|
@ -22,6 +23,7 @@ const (
|
||||||
type Member struct {
|
type Member struct {
|
||||||
ID xid.ID
|
ID xid.ID
|
||||||
UserID xid.ID
|
UserID xid.ID
|
||||||
|
SnowflakeID common.MemberID
|
||||||
SID string `db:"sid"`
|
SID string `db:"sid"`
|
||||||
Name string
|
Name string
|
||||||
DisplayName *string
|
DisplayName *string
|
||||||
|
@ -71,9 +73,23 @@ func (db *DB) Member(ctx context.Context, id xid.ID) (m Member, err error) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) MemberBySnowflake(ctx context.Context, id common.MemberID) (m Member, err error) {
|
||||||
|
sql, args, err := sq.Select("*").From("members").Where("snowflake_id = ?", id).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return m, errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgxscan.Get(ctx, db, &m, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return m, errors.Wrap(err, "executing query")
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UserMember returns a member scoped by user.
|
// UserMember returns a member scoped by user.
|
||||||
func (db *DB) UserMember(ctx context.Context, userID xid.ID, memberRef string) (m Member, err error) {
|
func (db *DB) UserMember(ctx context.Context, userID xid.ID, memberRef string) (m Member, err error) {
|
||||||
sql, args, err := sq.Select("*").From("members").Where("user_id = ?", userID).Where("(id = ? or name = ?)", memberRef, memberRef).ToSql()
|
sf, _ := common.ParseSnowflake(memberRef) // error can be ignored as the zero value will never be used as an ID
|
||||||
|
sql, args, err := sq.Select("*").From("members").Where("user_id = ?", userID).Where("(id = ? or snowflake_id = ? or name = ?)", memberRef, sf, memberRef).ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return m, errors.Wrap(err, "building sql")
|
return m, errors.Wrap(err, "building sql")
|
||||||
}
|
}
|
||||||
|
@ -135,8 +151,8 @@ func (db *DB) CreateMember(
|
||||||
name string, displayName *string, bio string, links []string,
|
name string, displayName *string, bio string, links []string,
|
||||||
) (m Member, err error) {
|
) (m Member, err error) {
|
||||||
sql, args, err := sq.Insert("members").
|
sql, args, err := sq.Insert("members").
|
||||||
Columns("user_id", "id", "sid", "name", "display_name", "bio", "links").
|
Columns("user_id", "snowflake_id", "id", "sid", "name", "display_name", "bio", "links").
|
||||||
Values(userID, xid.New(), squirrel.Expr("find_free_member_sid()"), name, displayName, bio, links).
|
Values(userID, common.GenerateID(), xid.New(), squirrel.Expr("find_free_member_sid()"), name, displayName, bio, links).
|
||||||
Suffix("RETURNING *").ToSql()
|
Suffix("RETURNING *").ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return m, errors.Wrap(err, "building sql")
|
return m, errors.Wrap(err, "building sql")
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID xid.ID
|
ID xid.ID
|
||||||
|
SnowflakeID common.UserID
|
||||||
SID string `db:"sid"`
|
SID string `db:"sid"`
|
||||||
Username string
|
Username string
|
||||||
DisplayName *string
|
DisplayName *string
|
||||||
|
@ -206,7 +207,7 @@ func (db *DB) CreateUser(ctx context.Context, tx pgx.Tx, username string) (u Use
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sql, args, err := sq.Insert("users").Columns("id", "username", "sid").Values(xid.New(), username, squirrel.Expr("find_free_user_sid()")).Suffix("RETURNING *").ToSql()
|
sql, args, err := sq.Insert("users").Columns("id", "snowflake_id", "username", "sid").Values(xid.New(), common.GenerateID(), username, squirrel.Expr("find_free_user_sid()")).Suffix("RETURNING *").ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return u, errors.Wrap(err, "building sql")
|
return u, errors.Wrap(err, "building sql")
|
||||||
}
|
}
|
||||||
|
@ -494,6 +495,26 @@ func (db *DB) User(ctx context.Context, id xid.ID) (u User, err error) {
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserBySnowflake gets a user by their snowflake ID.
|
||||||
|
func (db *DB) UserBySnowflake(ctx context.Context, id common.UserID) (u User, err error) {
|
||||||
|
sql, args, err := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance").
|
||||||
|
From("users").Where("snowflake_id = ?", id).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return u, errors.Wrap(err, "building sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pgxscan.Get(ctx, db, &u, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == pgx.ErrNoRows {
|
||||||
|
return u, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, errors.Wrap(err, "getting user from db")
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Username gets a user by username.
|
// Username gets a user by username.
|
||||||
func (db *DB) Username(ctx context.Context, name string) (u User, err error) {
|
func (db *DB) Username(ctx context.Context, name string) (u User, err error) {
|
||||||
sql, args, err := sq.Select("*").From("users").Where("username = ?", name).ToSql()
|
sql, args, err := sq.Select("*").From("users").Where("username = ?", name).ToSql()
|
||||||
|
|
|
@ -62,9 +62,8 @@ func run(c *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
case err := <-e:
|
case err := <-e:
|
||||||
log.Fatalf("Error running server: %v", err)
|
log.Fatalf("Error running server: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MaxContentLength = 2 * 1024 * 1024
|
const MaxContentLength = 2 * 1024 * 1024
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -25,6 +26,7 @@ type Server struct {
|
||||||
|
|
||||||
type userResponse struct {
|
type userResponse struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
|
SnowflakeID common.UserID `json:"id_new"`
|
||||||
Username string `json:"name"`
|
Username string `json:"name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
|
@ -51,6 +53,7 @@ type userResponse struct {
|
||||||
func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
||||||
return &userResponse{
|
return &userResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
SnowflakeID: u.SnowflakeID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Bio: u.Bio,
|
Bio: u.Bio,
|
||||||
|
|
|
@ -8,12 +8,13 @@ import (
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
@ -22,18 +23,27 @@ func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "this token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "this token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := xid.FromString(chi.URLParam(r, "memberRef"))
|
var m db.Member
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "memberRef")); err == nil {
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
m, err = s.DB.Member(ctx, id)
|
||||||
}
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
m, err := s.DB.Member(ctx, id)
|
return errors.Wrap(err, "getting member")
|
||||||
if err != nil {
|
|
||||||
if err == db.ErrMemberNotFound {
|
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
|
||||||
}
|
}
|
||||||
|
} else if id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef")); err == nil {
|
||||||
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "getting member")
|
return errors.Wrap(err, "getting member")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.UserID != claims.UserID {
|
if m.UserID != claims.UserID {
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -13,13 +15,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetMemberResponse struct {
|
type GetMemberResponse struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
SID string `json:"sid"`
|
SnowflakeID common.MemberID `json:"id_new"`
|
||||||
Name string `json:"name"`
|
SID string `json:"sid"`
|
||||||
DisplayName *string `json:"display_name"`
|
Name string `json:"name"`
|
||||||
Bio *string `json:"bio"`
|
DisplayName *string `json:"display_name"`
|
||||||
Avatar *string `json:"avatar"`
|
Bio *string `json:"bio"`
|
||||||
Links []string `json:"links"`
|
Avatar *string `json:"avatar"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
|
||||||
Names []db.FieldEntry `json:"names"`
|
Names []db.FieldEntry `json:"names"`
|
||||||
Pronouns []db.PronounEntry `json:"pronouns"`
|
Pronouns []db.PronounEntry `json:"pronouns"`
|
||||||
|
@ -34,6 +37,7 @@ type GetMemberResponse struct {
|
||||||
func dbMemberToMember(u db.User, m db.Member, fields []db.Field, flags []db.MemberFlag, isOwnMember bool) GetMemberResponse {
|
func dbMemberToMember(u db.User, m db.Member, fields []db.Field, flags []db.MemberFlag, isOwnMember bool) GetMemberResponse {
|
||||||
r := GetMemberResponse{
|
r := GetMemberResponse{
|
||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
|
SnowflakeID: m.SnowflakeID,
|
||||||
SID: m.SID,
|
SID: m.SID,
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
DisplayName: m.DisplayName,
|
DisplayName: m.DisplayName,
|
||||||
|
@ -48,6 +52,7 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field, flags []db.Memb
|
||||||
|
|
||||||
User: PartialUser{
|
User: PartialUser{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
SnowflakeID: u.SnowflakeID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Avatar: u.Avatar,
|
Avatar: u.Avatar,
|
||||||
|
@ -64,26 +69,37 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field, flags []db.Memb
|
||||||
|
|
||||||
type PartialUser struct {
|
type PartialUser struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
|
SnowflakeID common.UserID `json:"id_new"`
|
||||||
Username string `json:"name"`
|
Username string `json:"name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Avatar *string `json:"avatar"`
|
Avatar *string `json:"avatar"`
|
||||||
CustomPreferences db.CustomPreferences `json:"custom_preferences"`
|
CustomPreferences db.CustomPreferences `json:"custom_preferences"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getMember(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
id, err := xid.FromString(chi.URLParam(r, "memberRef"))
|
var m db.Member
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "memberRef")); err == nil {
|
||||||
return server.APIError{
|
m, err = s.DB.Member(ctx, id)
|
||||||
Code: server.ErrMemberNotFound,
|
if err != nil {
|
||||||
|
log.Errorf("getting member by xid: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// xid was not valid
|
||||||
|
if !m.SnowflakeID.IsValid() {
|
||||||
|
id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef"))
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{
|
||||||
|
Code: server.ErrMemberNotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m, err := s.DB.Member(ctx, id)
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.APIError{
|
return server.APIError{
|
||||||
Code: server.ErrMemberNotFound,
|
Code: server.ErrMemberNotFound,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,12 +202,22 @@ func (s *Server) getMeMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) parseUser(ctx context.Context, userRef string) (u db.User, err error) {
|
func (s *Server) parseUser(ctx context.Context, userRef string) (u db.User, err error) {
|
||||||
if id, err := xid.FromString(userRef); err != nil {
|
// check xid first
|
||||||
|
if id, err := xid.FromString(userRef); err == nil {
|
||||||
u, err := s.DB.User(ctx, id)
|
u, err := s.DB.User(ctx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not an xid, check by snowflake
|
||||||
|
if id, err := common.ParseSnowflake(userRef); err == nil {
|
||||||
|
u, err := s.DB.UserBySnowflake(ctx, common.UserID(id))
|
||||||
|
if err == nil {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else, use username
|
||||||
return s.DB.Username(ctx, userRef)
|
return s.DB.Username(ctx, userRef)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package member
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
|
|
||||||
type memberListResponse struct {
|
type memberListResponse struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
|
SnowflakeID common.MemberID `json:"id_new"`
|
||||||
SID string `json:"sid"`
|
SID string `json:"sid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
|
@ -28,6 +30,7 @@ func membersToMemberList(ms []db.Member, isSelf bool) []memberListResponse {
|
||||||
for i := range ms {
|
for i := range ms {
|
||||||
resps[i] = memberListResponse{
|
resps[i] = memberListResponse{
|
||||||
ID: ms[i].ID,
|
ID: ms[i].ID,
|
||||||
|
SnowflakeID: ms[i].SnowflakeID,
|
||||||
SID: ms[i].SID,
|
SID: ms[i].SID,
|
||||||
Name: ms[i].Name,
|
Name: ms[i].Name,
|
||||||
DisplayName: ms[i].DisplayName,
|
DisplayName: ms[i].DisplayName,
|
||||||
|
|
|
@ -38,23 +38,41 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := xid.FromString(chi.URLParam(r, "memberRef"))
|
|
||||||
if err != nil {
|
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, claims.UserID)
|
u, err := s.DB.User(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "getting user")
|
return errors.Wrap(err, "getting user")
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := s.DB.Member(ctx, id)
|
var m db.Member
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "memberRef")); err == nil {
|
||||||
if err == db.ErrMemberNotFound {
|
log.Debugf("%v/%v is xid", chi.URLParam(r, "memberRef"), id)
|
||||||
|
|
||||||
|
m, err = s.DB.Member(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(err, "getting member")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef"))
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("%v/%v is not valid snowflake", chi.URLParam(r, "memberRef"), id)
|
||||||
|
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "getting member")
|
log.Debugf("%v/%v is valid snowflake", chi.URLParam(r, "memberRef"), id)
|
||||||
|
|
||||||
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(err, "getting member")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.UserID != claims.UserID {
|
if m.UserID != claims.UserID {
|
||||||
|
@ -234,7 +252,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
m, err = s.DB.UpdateMember(ctx, tx, id, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
|
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch errors.Cause(err) {
|
switch errors.Cause(err) {
|
||||||
case db.ErrNothingToUpdate:
|
case db.ErrNothingToUpdate:
|
||||||
|
@ -258,9 +276,9 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
pronouns = *req.Pronouns
|
pronouns = *req.Pronouns
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.DB.SetMemberNamesPronouns(ctx, tx, id, names, pronouns)
|
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, names, pronouns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting names for member %v: %v", id, err)
|
log.Errorf("setting names for member %v: %v", m.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.Names = names
|
m.Names = names
|
||||||
|
@ -269,16 +287,16 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
var fields []db.Field
|
var fields []db.Field
|
||||||
if req.Fields != nil {
|
if req.Fields != nil {
|
||||||
err = s.DB.SetMemberFields(ctx, tx, id, *req.Fields)
|
err = s.DB.SetMemberFields(ctx, tx, m.ID, *req.Fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("setting fields for member %v: %v", id, err)
|
log.Errorf("setting fields for member %v: %v", m.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fields = *req.Fields
|
fields = *req.Fields
|
||||||
} else {
|
} else {
|
||||||
fields, err = s.DB.MemberFields(ctx, id)
|
fields, err = s.DB.MemberFields(ctx, m.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("getting fields for member %v: %v", id, err)
|
log.Errorf("getting fields for member %v: %v", m.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -321,7 +339,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) rerollMemberSID(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) rerollMemberSID(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
@ -330,9 +348,32 @@ func (s *Server) rerollMemberSID(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := xid.FromString(chi.URLParam(r, "memberRef"))
|
var m db.Member
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "memberRef")); err == nil {
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
m, err = s.DB.Member(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
|
return errors.Wrap(err, "getting user")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id, err := common.ParseSnowflake(chi.URLParam(r, "memberRef"))
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
|
return errors.Wrap(err, "getting user")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, claims.UserID)
|
u, err := s.DB.User(ctx, claims.UserID)
|
||||||
|
@ -340,15 +381,6 @@ func (s *Server) rerollMemberSID(w http.ResponseWriter, r *http.Request) error {
|
||||||
return errors.Wrap(err, "getting user")
|
return errors.Wrap(err, "getting user")
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := s.DB.Member(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if err == db.ErrMemberNotFound {
|
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrap(err, "getting member")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.UserID != claims.UserID {
|
if m.UserID != claims.UserID {
|
||||||
return server.APIError{Code: server.ErrNotOwnMember}
|
return server.APIError{Code: server.ErrNotOwnMember}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package mod
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -18,7 +19,7 @@ type CreateReportRequest struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) createUserReport(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) createUserReport(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
|
||||||
|
@ -26,19 +27,32 @@ func (s *Server) createUserReport(w http.ResponseWriter, r *http.Request) error
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := xid.FromString(chi.URLParam(r, "id"))
|
var u db.User
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "id")); err == nil {
|
||||||
return server.APIError{Code: server.ErrBadRequest, Details: "Invalid user ID"}
|
u, err = s.DB.User(ctx, id)
|
||||||
}
|
if err != nil {
|
||||||
|
if err == db.ErrUserNotFound {
|
||||||
|
return server.APIError{Code: server.ErrUserNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, userID)
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
if err != nil {
|
return errors.Wrap(err, "getting user")
|
||||||
if err == db.ErrUserNotFound {
|
}
|
||||||
|
} else {
|
||||||
|
id, err := common.ParseSnowflake(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
return server.APIError{Code: server.ErrUserNotFound}
|
return server.APIError{Code: server.ErrUserNotFound}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Errorf("getting user %v: %v", userID, err)
|
u, err = s.DB.UserBySnowflake(ctx, common.UserID(id))
|
||||||
return errors.Wrap(err, "getting user")
|
if err != nil {
|
||||||
|
if err == db.ErrUserNotFound {
|
||||||
|
return server.APIError{Code: server.ErrUserNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
|
return errors.Wrap(err, "getting user")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.DeletedAt != nil {
|
if u.DeletedAt != nil {
|
||||||
|
@ -73,19 +87,32 @@ func (s *Server) createMemberReport(w http.ResponseWriter, r *http.Request) erro
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := xid.FromString(chi.URLParam(r, "id"))
|
var m db.Member
|
||||||
if err != nil {
|
if id, err := xid.FromString(chi.URLParam(r, "id")); err == nil {
|
||||||
return server.APIError{Code: server.ErrBadRequest, Details: "Invalid member ID"}
|
m, err = s.DB.Member(ctx, id)
|
||||||
}
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
m, err := s.DB.Member(ctx, memberID)
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
if err != nil {
|
return errors.Wrap(err, "getting user")
|
||||||
if err == db.ErrMemberNotFound {
|
}
|
||||||
return server.APIError{Code: server.ErrMemberNotFound}
|
} else {
|
||||||
|
id, err := common.ParseSnowflake(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrUserNotFound}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Errorf("getting member %v: %v", memberID, err)
|
m, err = s.DB.MemberBySnowflake(ctx, common.MemberID(id))
|
||||||
return errors.Wrap(err, "getting member")
|
if err != nil {
|
||||||
|
if err == db.ErrMemberNotFound {
|
||||||
|
return server.APIError{Code: server.ErrMemberNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("getting user %v: %v", id, err)
|
||||||
|
return errors.Wrap(err, "getting user")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := s.DB.User(ctx, m.UserID)
|
u, err := s.DB.User(ctx, m.UserID)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -121,6 +122,24 @@ type patchUserFlagRequest struct {
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) parseFlag(ctx context.Context, flags []db.PrideFlag, flagRef string) (db.PrideFlag, bool) {
|
||||||
|
if id, err := xid.FromString(flagRef); err == nil {
|
||||||
|
for _, f := range flags {
|
||||||
|
if f.ID == id {
|
||||||
|
return f, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id, err := common.ParseSnowflake(flagRef); err == nil {
|
||||||
|
for _, f := range flags {
|
||||||
|
if f.SnowflakeID == common.FlagID(id) {
|
||||||
|
return f, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db.PrideFlag{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, _ := server.ClaimsFromContext(ctx)
|
claims, _ := server.ClaimsFromContext(ctx)
|
||||||
|
@ -129,28 +148,13 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
flagID, err := xid.FromString(chi.URLParam(r, "flagID"))
|
|
||||||
if err != nil {
|
|
||||||
return server.APIError{Code: server.ErrNotFound, Details: "Invalid flag ID"}
|
|
||||||
}
|
|
||||||
|
|
||||||
flags, err := s.DB.AccountFlags(ctx, claims.UserID)
|
flags, err := s.DB.AccountFlags(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "getting current user flags")
|
return errors.Wrap(err, "getting current user flags")
|
||||||
}
|
}
|
||||||
if len(flags) >= db.MaxPrideFlags {
|
|
||||||
return server.APIError{
|
flag, ok := s.parseFlag(ctx, flags, chi.URLParam(r, "flagID"))
|
||||||
Code: server.ErrFlagLimitReached,
|
if !ok {
|
||||||
}
|
|
||||||
}
|
|
||||||
var found bool
|
|
||||||
for _, flag := range flags {
|
|
||||||
if flag.ID == flagID {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return server.APIError{Code: server.ErrNotFound, Details: "No flag with that ID found"}
|
return server.APIError{Code: server.ErrNotFound, Details: "No flag with that ID found"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +194,7 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
flag, err := s.DB.EditFlag(ctx, tx, flagID, req.Name, req.Description, nil)
|
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "updating flag")
|
return errors.Wrap(err, "updating flag")
|
||||||
}
|
}
|
||||||
|
@ -212,19 +216,16 @@ func (s *Server) deleteUserFlag(w http.ResponseWriter, r *http.Request) error {
|
||||||
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
return server.APIError{Code: server.ErrMissingPermissions, Details: "This token is read-only"}
|
||||||
}
|
}
|
||||||
|
|
||||||
flagID, err := xid.FromString(chi.URLParam(r, "flagID"))
|
flags, err := s.DB.AccountFlags(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.APIError{Code: server.ErrNotFound, Details: "Invalid flag ID"}
|
return errors.Wrap(err, "getting current user flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
flag, err := s.DB.UserFlag(ctx, flagID)
|
flag, ok := s.parseFlag(ctx, flags, chi.URLParam(r, "flagID"))
|
||||||
if err != nil {
|
if !ok {
|
||||||
if err == db.ErrFlagNotFound {
|
return server.APIError{Code: server.ErrNotFound, Details: "No flag with that ID found"}
|
||||||
return server.APIError{Code: server.ErrNotFound, Details: "Flag not found"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrap(err, "getting flag object")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flag.UserID != claims.UserID {
|
if flag.UserID != claims.UserID {
|
||||||
return server.APIError{Code: server.ErrNotFound, Details: "Flag not found"}
|
return server.APIError{Code: server.ErrNotFound, Details: "Flag not found"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
"codeberg.org/pronounscc/pronouns.cc/backend/db"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||||
|
@ -14,6 +15,7 @@ import (
|
||||||
|
|
||||||
type GetUserResponse struct {
|
type GetUserResponse struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
|
SnowflakeID common.UserID `json:"id_new"`
|
||||||
SID string `json:"sid"`
|
SID string `json:"sid"`
|
||||||
Username string `json:"name"`
|
Username string `json:"name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
|
@ -58,6 +60,7 @@ type GetMeResponse struct {
|
||||||
|
|
||||||
type PartialMember struct {
|
type PartialMember struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
|
SnowflakeID common.MemberID `json:"id_new"`
|
||||||
SID string `json:"sid"`
|
SID string `json:"sid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
|
@ -71,6 +74,7 @@ type PartialMember struct {
|
||||||
func dbUserToResponse(u db.User, fields []db.Field, members []db.Member, flags []db.UserFlag) GetUserResponse {
|
func dbUserToResponse(u db.User, fields []db.Field, members []db.Member, flags []db.UserFlag) GetUserResponse {
|
||||||
resp := GetUserResponse{
|
resp := GetUserResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
SnowflakeID: u.SnowflakeID,
|
||||||
SID: u.SID,
|
SID: u.SID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
|
@ -97,6 +101,7 @@ func dbUserToResponse(u db.User, fields []db.Field, members []db.Member, flags [
|
||||||
for i := range members {
|
for i := range members {
|
||||||
resp.Members[i] = PartialMember{
|
resp.Members[i] = PartialMember{
|
||||||
ID: members[i].ID,
|
ID: members[i].ID,
|
||||||
|
SnowflakeID: members[i].SnowflakeID,
|
||||||
SID: members[i].SID,
|
SID: members[i].SID,
|
||||||
Name: members[i].Name,
|
Name: members[i].Name,
|
||||||
DisplayName: members[i].DisplayName,
|
DisplayName: members[i].DisplayName,
|
||||||
|
@ -124,6 +129,15 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.ID.IsNil() {
|
||||||
|
if id, err := common.ParseSnowflake(userRef); err == nil {
|
||||||
|
u, err = s.DB.UserBySnowflake(ctx, common.UserID(id))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getting user by snowflake: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if u.ID.IsNil() {
|
if u.ID.IsNil() {
|
||||||
u, err = s.DB.Username(ctx, userRef)
|
u, err = s.DB.Username(ctx, userRef)
|
||||||
if err == db.ErrUserNotFound {
|
if err == db.ErrUserNotFound {
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const MAX_FLAGS = 500;
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
id_new: string;
|
||||||
sid: string;
|
sid: string;
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
|
@ -79,6 +80,7 @@ export interface Pronoun {
|
||||||
|
|
||||||
export interface PartialMember {
|
export interface PartialMember {
|
||||||
id: string;
|
id: string;
|
||||||
|
id_new: string;
|
||||||
sid: string;
|
sid: string;
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
|
@ -99,6 +101,7 @@ export interface Member extends PartialMember {
|
||||||
|
|
||||||
export interface MemberPartialUser {
|
export interface MemberPartialUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
id_new: string;
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string | null;
|
display_name: string | null;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
@ -107,6 +110,7 @@ export interface MemberPartialUser {
|
||||||
|
|
||||||
export interface PrideFlag {
|
export interface PrideFlag {
|
||||||
id: string;
|
id: string;
|
||||||
|
id_new: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|
|
@ -274,7 +274,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $userStore && $userStore.id !== data.id}
|
{#if $userStore && $userStore.id !== data.id}
|
||||||
<ReportButton subject="user" reportUrl="/users/{data.id}/reports" />
|
<ReportButton subject="user" reportUrl="/users/{data.id_new}/reports" />
|
||||||
{/if}
|
{/if}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $userStore && $userStore.id !== data.user.id}
|
{#if $userStore && $userStore.id !== data.user.id}
|
||||||
<ReportButton subject="member" reportUrl="/members/{data.id}/reports" />
|
<ReportButton subject="member" reportUrl="/members/{data.id_new}/reports" />
|
||||||
{/if}
|
{/if}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
const deleteMember = async () => {
|
const deleteMember = async () => {
|
||||||
try {
|
try {
|
||||||
await fastFetchClient(`/members/${data.member.id}`, "DELETE");
|
await fastFetchClient(`/members/${data.member.id_new}`, "DELETE");
|
||||||
|
|
||||||
toggleDeleteOpen();
|
toggleDeleteOpen();
|
||||||
addToast({
|
addToast({
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetchClient<Member>(`/members/${data.member.id}`, "PATCH", {
|
const resp = await apiFetchClient<Member>(`/members/${data.member.id_new}`, "PATCH", {
|
||||||
name: $member.name,
|
name: $member.name,
|
||||||
display_name: $member.display_name,
|
display_name: $member.display_name,
|
||||||
avatar: $member.avatar,
|
avatar: $member.avatar,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
const rerollSid = async () => {
|
const rerollSid = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetchClient<Member>(`/members/${data.member.id}/reroll`);
|
const resp = await apiFetchClient<Member>(`/members/${data.member.id_new}/reroll`);
|
||||||
addToast({ header: "Success", body: "Rerolled short ID!" });
|
addToast({ header: "Success", body: "Rerolled short ID!" });
|
||||||
error = null;
|
error = null;
|
||||||
$member.sid = resp.sid;
|
$member.sid = resp.sid;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { ErrorCode, type APIError, type Report } from "$lib/api/entities";
|
import type { Report } from "$lib/api/entities";
|
||||||
import { apiFetchClient } from "$lib/api/fetch";
|
import { apiFetchClient } from "$lib/api/fetch";
|
||||||
import { error } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load = async ({ url }) => {
|
export const load = async ({ url }) => {
|
||||||
const { searchParams } = url;
|
const { searchParams } = url;
|
||||||
|
|
|
@ -194,7 +194,11 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">ID</th>
|
<th scope="row">ID</th>
|
||||||
<td><code>{data.user.id}</code></td>
|
<td><code>{data.user.id_new}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">xid <em>(deprecated)</em></th>
|
||||||
|
<td><del><code>{data.user.id}</code></del></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Account created at</th>
|
<th scope="row">Account created at</th>
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
error = null;
|
error = null;
|
||||||
|
|
||||||
addToast({ header: "Deleted flag", body: "Successfully deleted flag!" });
|
addToast({ header: "Deleted flag", body: "Successfully deleted flag!" });
|
||||||
data.flags = data.flags.filter((entry) => entry.id !== id);
|
data.flags = data.flags.filter((entry) => entry.id_new !== id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e as APIError;
|
error = e as APIError;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
const updateFlag = async () => {
|
const updateFlag = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetchClient<PrideFlag>(`/users/@me/flags/${flag.id}`, "PATCH", {
|
const resp = await apiFetchClient<PrideFlag>(`/users/@me/flags/${flag.id_new}`, "PATCH", {
|
||||||
name,
|
name,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
Are you sure you want to delete the {flag.name} flag? <strong>This cannot be undone!</strong>
|
Are you sure you want to delete the {flag.name} flag? <strong>This cannot be undone!</strong>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="danger" on:click={() => deleteFlag(flag.id)}>Delete flag</Button>
|
<Button color="danger" on:click={() => deleteFlag(flag.id_new)}>Delete flag</Button>
|
||||||
<Button color="secondary" on:click={toggleDeleteModal}>Cancel</Button>
|
<Button color="secondary" on:click={toggleDeleteModal}>Cancel</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
2
main.go
2
main.go
|
@ -13,6 +13,7 @@ import (
|
||||||
"codeberg.org/pronounscc/pronouns.cc/scripts/genkey"
|
"codeberg.org/pronounscc/pronouns.cc/scripts/genkey"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/scripts/migrate"
|
"codeberg.org/pronounscc/pronouns.cc/scripts/migrate"
|
||||||
"codeberg.org/pronounscc/pronouns.cc/scripts/seeddb"
|
"codeberg.org/pronounscc/pronouns.cc/scripts/seeddb"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/scripts/snowflakes"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ var app = &cli.App{
|
||||||
migrate.Command,
|
migrate.Command,
|
||||||
seeddb.Command,
|
seeddb.Command,
|
||||||
cleandb.Command,
|
cleandb.Command,
|
||||||
|
snowflakes.Command,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "pronouns",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"air\" \"make dev\""
|
||||||
|
},
|
||||||
|
"author": "sam <sam@sleepycat.moe>",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^8.2.1"
|
||||||
|
}
|
||||||
|
}
|
2462
pnpm-lock.yaml
generated
Normal file
2462
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- docs
|
||||||
|
- frontend
|
13
scripts/migrate/021_snowflakes.sql
Normal file
13
scripts/migrate/021_snowflakes.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-- 2023-08-17: Add snowflake ID columns
|
||||||
|
|
||||||
|
-- +migrate Up
|
||||||
|
|
||||||
|
alter table users add column snowflake_id bigint unique;
|
||||||
|
alter table members add column snowflake_id bigint unique;
|
||||||
|
alter table pride_flags add column snowflake_id bigint unique;
|
||||||
|
|
||||||
|
-- +migrate Down
|
||||||
|
|
||||||
|
alter table users drop column snowflake_id;
|
||||||
|
alter table members drop column snowflake_id;
|
||||||
|
alter table pride_flags drop column snowflake_id;
|
111
scripts/snowflakes/main.go
Normal file
111
scripts/snowflakes/main.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package snowflakes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/common"
|
||||||
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
||||||
|
"github.com/georgysavva/scany/v2/pgxscan"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Command = &cli.Command{
|
||||||
|
Name: "create-snowflakes",
|
||||||
|
Usage: "Give all users, members, and flags snowflake IDs.",
|
||||||
|
Action: run,
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(c *cli.Context) error {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("loading .env file:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := pgx.Connect(c.Context, os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("opening database:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close(c.Context)
|
||||||
|
log.Info("opened database")
|
||||||
|
|
||||||
|
tx, err := conn.Begin(c.Context)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating transaction:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(c.Context)
|
||||||
|
|
||||||
|
var userIDs []xid.ID
|
||||||
|
err = pgxscan.Select(c.Context, conn, &userIDs, "SELECT id FROM users WHERE snowflake_id IS NULL")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("selecting users without snowflake:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Now()
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
t := userID.Time()
|
||||||
|
snowflake := common.UserID(common.GenerateIDWithTime(t))
|
||||||
|
|
||||||
|
_, err = tx.Exec(c.Context, "UPDATE users SET snowflake_id = $1 WHERE id = $2", snowflake, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("updating user with ID %v: %v", userID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("updated %v users in %v", len(userIDs), time.Since(t))
|
||||||
|
|
||||||
|
var memberIDs []xid.ID
|
||||||
|
err = pgxscan.Select(c.Context, conn, &memberIDs, "SELECT id FROM members WHERE snowflake_id IS NULL")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("selecting users without snowflake:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t = time.Now()
|
||||||
|
for _, memberID := range memberIDs {
|
||||||
|
t := memberID.Time()
|
||||||
|
snowflake := common.MemberID(common.GenerateIDWithTime(t))
|
||||||
|
|
||||||
|
_, err = tx.Exec(c.Context, "UPDATE members SET snowflake_id = $1 WHERE id = $2", snowflake, memberID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("updating user with ID %v: %v", memberID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("updated %v members in %v", len(memberIDs), time.Since(t))
|
||||||
|
|
||||||
|
var flagIDs []xid.ID
|
||||||
|
err = pgxscan.Select(c.Context, conn, &flagIDs, "SELECT id FROM pride_flags WHERE snowflake_id IS NULL")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("selecting users without snowflake:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t = time.Now()
|
||||||
|
for _, flagID := range flagIDs {
|
||||||
|
t := flagID.Time()
|
||||||
|
snowflake := common.FlagID(common.GenerateIDWithTime(t))
|
||||||
|
|
||||||
|
_, err = tx.Exec(c.Context, "UPDATE pride_flags SET snowflake_id = $1 WHERE id = $2", snowflake, flagID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("updating user with ID %v: %v", flagID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("updated %v flags in %v", len(flagIDs), time.Since(t))
|
||||||
|
|
||||||
|
err = tx.Commit(c.Context)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("committing transaction:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue