forked from mirrors/pronouns.cc
feat: start custom preferences on backend
This commit is contained in:
parent
86a1841f4f
commit
7ea5efae93
8 changed files with 2118 additions and 39 deletions
|
@ -4,9 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/u1f320/pronouns.cc/backend/common"
|
||||||
|
"codeberg.org/u1f320/pronouns.cc/backend/icons"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/georgysavva/scany/v2/pgxscan"
|
"github.com/georgysavva/scany/v2/pgxscan"
|
||||||
|
@ -49,8 +52,47 @@ type User struct {
|
||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
SelfDelete *bool
|
SelfDelete *bool
|
||||||
DeleteReason *string
|
DeleteReason *string
|
||||||
|
|
||||||
|
CustomPreferences CustomPreferences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomPreferences = map[string]CustomPreference
|
||||||
|
|
||||||
|
type CustomPreference struct {
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Tooltip string `json:"tooltip"`
|
||||||
|
Size PreferenceSize `json:"size"`
|
||||||
|
Muted bool `json:"muted"`
|
||||||
|
Favourite bool `json:"favourite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPreference) Validate() string {
|
||||||
|
if !icons.IsValid(c.Icon) {
|
||||||
|
return fmt.Sprintf("custom preference icon %q is invalid", c.Icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Tooltip == "" {
|
||||||
|
return "custom preference tooltip is empty"
|
||||||
|
}
|
||||||
|
if common.StringLength(&c.Tooltip) > FieldEntryMaxLength {
|
||||||
|
return fmt.Sprintf("custom preference tooltip is too long, max %d characters, is %d characters", FieldEntryMaxLength, common.StringLength(&c.Tooltip))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Size != PreferenceSizeLarge && c.Size != PreferenceSizeNormal && c.Size != PreferenceSizeSmall {
|
||||||
|
return fmt.Sprintf("custom preference size %q is invalid", string(c.Size))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreferenceSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PreferenceSizeLarge PreferenceSize = "large"
|
||||||
|
PreferenceSizeNormal PreferenceSize = "normal"
|
||||||
|
PreferenceSizeSmall PreferenceSize = "small"
|
||||||
|
)
|
||||||
|
|
||||||
func (u User) NumProviders() (numProviders int) {
|
func (u User) NumProviders() (numProviders int) {
|
||||||
if u.Discord != nil {
|
if u.Discord != nil {
|
||||||
numProviders++
|
numProviders++
|
||||||
|
|
1968
backend/icons/icons.go
Normal file
1968
backend/icons/icons.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -42,10 +42,11 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field, isOwnMember boo
|
||||||
Fields: db.NotNull(fields),
|
Fields: db.NotNull(fields),
|
||||||
|
|
||||||
User: PartialUser{
|
User: PartialUser{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Avatar: u.Avatar,
|
Avatar: u.Avatar,
|
||||||
|
CustomPreferences: u.CustomPreferences,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,10 +58,11 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field, isOwnMember boo
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartialUser struct {
|
type PartialUser struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getMember(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) getMember(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
|
@ -12,17 +12,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetUserResponse struct {
|
type GetUserResponse struct {
|
||||||
ID xid.ID `json:"id"`
|
ID xid.ID `json:"id"`
|
||||||
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"`
|
||||||
MemberTitle *string `json:"member_title"`
|
MemberTitle *string `json:"member_title"`
|
||||||
Avatar *string `json:"avatar"`
|
Avatar *string `json:"avatar"`
|
||||||
Links []string `json:"links"`
|
Links []string `json:"links"`
|
||||||
Names []db.FieldEntry `json:"names"`
|
Names []db.FieldEntry `json:"names"`
|
||||||
Pronouns []db.PronounEntry `json:"pronouns"`
|
Pronouns []db.PronounEntry `json:"pronouns"`
|
||||||
Members []PartialMember `json:"members"`
|
Members []PartialMember `json:"members"`
|
||||||
Fields []db.Field `json:"fields"`
|
Fields []db.Field `json:"fields"`
|
||||||
|
CustomPreferences db.CustomPreferences `json:"custom_preferences"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetMeResponse struct {
|
type GetMeResponse struct {
|
||||||
|
@ -59,16 +60,17 @@ type PartialMember struct {
|
||||||
|
|
||||||
func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUserResponse {
|
func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUserResponse {
|
||||||
resp := GetUserResponse{
|
resp := GetUserResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
DisplayName: u.DisplayName,
|
DisplayName: u.DisplayName,
|
||||||
Bio: u.Bio,
|
Bio: u.Bio,
|
||||||
MemberTitle: u.MemberTitle,
|
MemberTitle: u.MemberTitle,
|
||||||
Avatar: u.Avatar,
|
Avatar: u.Avatar,
|
||||||
Links: db.NotNull(u.Links),
|
Links: db.NotNull(u.Links),
|
||||||
Names: db.NotNull(u.Names),
|
Names: db.NotNull(u.Names),
|
||||||
Pronouns: db.NotNull(u.Pronouns),
|
Pronouns: db.NotNull(u.Pronouns),
|
||||||
Fields: db.NotNull(fields),
|
Fields: db.NotNull(fields),
|
||||||
|
CustomPreferences: u.CustomPreferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Members = make([]PartialMember, len(members))
|
resp.Members = make([]PartialMember, len(members))
|
||||||
|
|
|
@ -10,19 +10,21 @@ import (
|
||||||
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PatchUserRequest struct {
|
type PatchUserRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
MemberTitle *string `json:"member_title"`
|
MemberTitle *string `json:"member_title"`
|
||||||
Links *[]string `json:"links"`
|
Links *[]string `json:"links"`
|
||||||
Names *[]db.FieldEntry `json:"names"`
|
Names *[]db.FieldEntry `json:"names"`
|
||||||
Pronouns *[]db.PronounEntry `json:"pronouns"`
|
Pronouns *[]db.PronounEntry `json:"pronouns"`
|
||||||
Fields *[]db.Field `json:"fields"`
|
Fields *[]db.Field `json:"fields"`
|
||||||
Avatar *string `json:"avatar"`
|
Avatar *string `json:"avatar"`
|
||||||
ListPrivate *bool `json:"list_private"`
|
ListPrivate *bool `json:"list_private"`
|
||||||
|
CustomPreferences *db.CustomPreferences `json:"custom_preferences"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// patchUser parses a PatchUserRequest and updates the user with the given ID.
|
// patchUser parses a PatchUserRequest and updates the user with the given ID.
|
||||||
|
@ -115,6 +117,19 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
return *err
|
return *err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate custom preferences
|
||||||
|
if req.CustomPreferences != nil {
|
||||||
|
for k, v := range *req.CustomPreferences {
|
||||||
|
_, err := uuid.Parse(k)
|
||||||
|
if err != nil {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: "One or more custom preference IDs is not a UUID."}
|
||||||
|
}
|
||||||
|
if s := v.Validate(); s != "" {
|
||||||
|
return server.APIError{Code: server.ErrBadRequest, Details: s}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update avatar
|
// update avatar
|
||||||
var avatarHash *string = nil
|
var avatarHash *string = nil
|
||||||
if req.Avatar != nil {
|
if req.Avatar != nil {
|
||||||
|
|
44
frontend/icons.js
Normal file
44
frontend/icons.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// This script regenerates the list of icons for the frontend (frontend/src/icons.json)
|
||||||
|
// and the backend (backend/icons/icons.go) from the currently installed version of Bootstrap Icons.
|
||||||
|
// Run with `pnpm node icons.js` in the frontend directory.
|
||||||
|
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
import icons from "bootstrap-icons/font/bootstrap-icons.json" assert { type: "json" };
|
||||||
|
|
||||||
|
const keys = Object.keys(icons);
|
||||||
|
|
||||||
|
console.log(`Found ${keys.length} icons`);
|
||||||
|
const output = JSON.stringify(keys);
|
||||||
|
console.log(`Saving file as src/icons.json`);
|
||||||
|
|
||||||
|
writeFileSync("src/icons.json", output);
|
||||||
|
|
||||||
|
const goCode1 = `// Generated code. DO NOT EDIT
|
||||||
|
package icons
|
||||||
|
|
||||||
|
var icons = [...]string{
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let goOutput = goCode1;
|
||||||
|
|
||||||
|
keys.forEach((element) => {
|
||||||
|
goOutput += ` "${element}",\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
goOutput += goCode2;
|
||||||
|
|
||||||
|
console.log("Writing Go code");
|
||||||
|
writeFileSync("../backend/icons/icons.go", goOutput);
|
1
frontend/src/icons.json
Normal file
1
frontend/src/icons.json
Normal file
File diff suppressed because one or more lines are too long
5
scripts/migrate/015_custom_preferences.sql
Normal file
5
scripts/migrate/015_custom_preferences.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-- +migrate Up
|
||||||
|
|
||||||
|
-- 2023-04-19: Add custom preferences
|
||||||
|
|
||||||
|
alter table users add column custom_preferences jsonb not null default '{}';
|
Loading…
Reference in a new issue