feat: hashes in avatar file names (closes #19)
This commit is contained in:
		
							parent
							
								
									e36bd247f5
								
							
						
					
					
						commit
						163e7c3fd6
					
				
					 17 changed files with 133 additions and 77 deletions
				
			
		|  | @ -3,7 +3,9 @@ package db | |||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"io" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
|  | @ -23,8 +25,8 @@ const ErrInvalidContentType = errors.Sentinel("invalid avatar content type") | |||
| 
 | ||||
| // ConvertAvatar parses an avatar from a data URI, converts it to WebP and JPEG, and returns the results. | ||||
| func (db *DB) ConvertAvatar(data string) ( | ||||
| 	webp io.Reader, | ||||
| 	jpg io.Reader, | ||||
| 	webp *bytes.Buffer, | ||||
| 	jpg *bytes.Buffer, | ||||
| 	err error, | ||||
| ) { | ||||
| 	data = strings.TrimSpace(data) | ||||
|  | @ -142,53 +144,59 @@ func (db *DB) ConvertAvatar(data string) ( | |||
| } | ||||
| 
 | ||||
| func (db *DB) WriteUserAvatar(ctx context.Context, | ||||
| 	userID xid.ID, webp io.Reader, jpeg io.Reader, | ||||
| 	userID xid.ID, webp *bytes.Buffer, jpeg *bytes.Buffer, | ||||
| ) ( | ||||
| 	webpLocation string, | ||||
| 	jpegLocation string, | ||||
| 	err error, | ||||
| 	hash string, err error, | ||||
| ) { | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".webp", webp, -1, minio.PutObjectOptions{ | ||||
| 	hasher := sha256.New() | ||||
| 	_, err = hasher.Write(webp.Bytes()) | ||||
| 	if err != nil { | ||||
| 		return "", errors.Wrap(err, "hashing webp avatar") | ||||
| 	} | ||||
| 	hash = hex.EncodeToString(hasher.Sum(nil)) | ||||
| 
 | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".webp", webp, -1, minio.PutObjectOptions{ | ||||
| 		ContentType: "image/webp", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", "", errors.Wrap(err, "uploading webp avatar") | ||||
| 		return "", errors.Wrap(err, "uploading webp avatar") | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{ | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".jpg", jpeg, -1, minio.PutObjectOptions{ | ||||
| 		ContentType: "image/jpeg", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", "", errors.Wrap(err, "uploading jpeg avatar") | ||||
| 		return "", errors.Wrap(err, "uploading jpeg avatar") | ||||
| 	} | ||||
| 
 | ||||
| 	return db.baseURL.JoinPath("/media/users/" + userID.String() + ".webp").String(), | ||||
| 		db.baseURL.JoinPath("/media/users/" + userID.String() + ".jpg").String(), | ||||
| 		nil | ||||
| 	return hash, nil | ||||
| } | ||||
| 
 | ||||
| func (db *DB) WriteMemberAvatar(ctx context.Context, | ||||
| 	memberID xid.ID, webp io.Reader, jpeg io.Reader, | ||||
| 	memberID xid.ID, webp *bytes.Buffer, jpeg *bytes.Buffer, | ||||
| ) ( | ||||
| 	webpLocation string, | ||||
| 	jpegLocation string, | ||||
| 	err error, | ||||
| 	hash string, err error, | ||||
| ) { | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".webp", webp, -1, minio.PutObjectOptions{ | ||||
| 	hasher := sha256.New() | ||||
| 	_, err = hasher.Write(webp.Bytes()) | ||||
| 	if err != nil { | ||||
| 		return "", errors.Wrap(err, "hashing webp avatar") | ||||
| 	} | ||||
| 	hash = hex.EncodeToString(hasher.Sum(nil)) | ||||
| 
 | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".webp", webp, -1, minio.PutObjectOptions{ | ||||
| 		ContentType: "image/webp", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", "", errors.Wrap(err, "uploading webp avatar") | ||||
| 		return "", errors.Wrap(err, "uploading webp avatar") | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{ | ||||
| 	_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".jpg", jpeg, -1, minio.PutObjectOptions{ | ||||
| 		ContentType: "image/jpeg", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", "", errors.Wrap(err, "uploading jpeg avatar") | ||||
| 		return "", errors.Wrap(err, "uploading jpeg avatar") | ||||
| 	} | ||||
| 
 | ||||
| 	return db.baseURL.JoinPath("/media/members/" + memberID.String() + ".webp").String(), | ||||
| 		db.baseURL.JoinPath("/media/members/" + memberID.String() + ".jpg").String(), | ||||
| 		nil | ||||
| 	return hash, nil | ||||
| } | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ type Member struct { | |||
| 	Name        string | ||||
| 	DisplayName *string | ||||
| 	Bio         *string | ||||
| 	AvatarURLs  []string `db:"avatar_urls"` | ||||
| 	Avatar      *string | ||||
| 	Links       []string | ||||
| 	Names       []FieldEntry | ||||
| 	Pronouns    []PronounEntry | ||||
|  | @ -61,7 +61,7 @@ func (db *DB) UserMember(ctx context.Context, userID xid.ID, memberRef string) ( | |||
| 
 | ||||
| // UserMembers returns all of a user's members, sorted by name. | ||||
| func (db *DB) UserMembers(ctx context.Context, userID xid.ID) (ms []Member, err error) { | ||||
| 	sql, args, err := sq.Select("id", "user_id", "name", "display_name", "bio", "avatar_urls", "names", "pronouns"). | ||||
| 	sql, args, err := sq.Select("id", "user_id", "name", "display_name", "bio", "avatar", "names", "pronouns"). | ||||
| 		From("members").Where("user_id = ?", userID). | ||||
| 		OrderBy("name", "id").ToSql() | ||||
| 	if err != nil { | ||||
|  | @ -141,9 +141,9 @@ func (db *DB) UpdateMember( | |||
| 	tx pgx.Tx, id xid.ID, | ||||
| 	name, displayName, bio *string, | ||||
| 	links *[]string, | ||||
| 	avatarURLs []string, | ||||
| 	avatar *string, | ||||
| ) (m Member, err error) { | ||||
| 	if name == nil && displayName == nil && bio == nil && links == nil && avatarURLs == nil { | ||||
| 	if name == nil && displayName == nil && bio == nil && links == nil && avatar == nil { | ||||
| 		// get member | ||||
| 		sql, args, err := sq.Select("*").From("members").Where("id = ?", id).ToSql() | ||||
| 		if err != nil { | ||||
|  | @ -183,8 +183,12 @@ func (db *DB) UpdateMember( | |||
| 		builder = builder.Set("links", *links) | ||||
| 	} | ||||
| 
 | ||||
| 	if avatarURLs != nil { | ||||
| 		builder = builder.Set("avatar_urls", avatarURLs) | ||||
| 	if avatar != nil { | ||||
| 		if *avatar == "" { | ||||
| 			builder = builder.Set("avatar", nil) | ||||
| 		} else { | ||||
| 			builder = builder.Set("avatar", avatar) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	sql, args, err := builder.ToSql() | ||||
|  |  | |||
|  | @ -19,8 +19,8 @@ type User struct { | |||
| 	DisplayName *string | ||||
| 	Bio         *string | ||||
| 
 | ||||
| 	AvatarURLs []string `db:"avatar_urls"` | ||||
| 	Links      []string | ||||
| 	Avatar *string | ||||
| 	Links  []string | ||||
| 
 | ||||
| 	Names    []FieldEntry | ||||
| 	Pronouns []PronounEntry | ||||
|  | @ -208,9 +208,9 @@ func (db *DB) UpdateUser( | |||
| 	tx pgx.Tx, id xid.ID, | ||||
| 	displayName, bio *string, | ||||
| 	links *[]string, | ||||
| 	avatarURLs []string, | ||||
| 	avatar *string, | ||||
| ) (u User, err error) { | ||||
| 	if displayName == nil && bio == nil && links == nil && avatarURLs == nil { | ||||
| 	if displayName == nil && bio == nil && links == nil && avatar == nil { | ||||
| 		sql, args, err := sq.Select("*").From("users").Where("id = ?", id).ToSql() | ||||
| 		if err != nil { | ||||
| 			return u, errors.Wrap(err, "building sql") | ||||
|  | @ -243,8 +243,12 @@ func (db *DB) UpdateUser( | |||
| 		builder = builder.Set("links", *links) | ||||
| 	} | ||||
| 
 | ||||
| 	if avatarURLs != nil { | ||||
| 		builder = builder.Set("avatar_urls", avatarURLs) | ||||
| 	if avatar != nil { | ||||
| 		if *avatar == "" { | ||||
| 			builder = builder.Set("avatar", nil) | ||||
| 		} else { | ||||
| 			builder = builder.Set("avatar", avatar) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	sql, args, err := builder.ToSql() | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ type userResponse struct { | |||
| 	Username    string            `json:"name"` | ||||
| 	DisplayName *string           `json:"display_name"` | ||||
| 	Bio         *string           `json:"bio"` | ||||
| 	AvatarURLs  []string          `json:"avatar_urls"` | ||||
| 	Avatar      *string           `json:"avatar"` | ||||
| 	Links       []string          `json:"links"` | ||||
| 	Names       []db.FieldEntry   `json:"names"` | ||||
| 	Pronouns    []db.PronounEntry `json:"pronouns"` | ||||
|  | @ -40,7 +40,7 @@ func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse { | |||
| 		Username:        u.Username, | ||||
| 		DisplayName:     u.DisplayName, | ||||
| 		Bio:             u.Bio, | ||||
| 		AvatarURLs:      db.NotNull(u.AvatarURLs), | ||||
| 		Avatar:          u.Avatar, | ||||
| 		Links:           db.NotNull(u.Links), | ||||
| 		Names:           db.NotNull(u.Names), | ||||
| 		Pronouns:        db.NotNull(u.Pronouns), | ||||
|  |  | |||
|  | @ -23,6 +23,14 @@ type Bot struct { | |||
| 	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 { | ||||
|  | @ -97,8 +105,8 @@ func (bot *Bot) userPronouns(w http.ResponseWriter, r *http.Request, ev *discord | |||
| 	} | ||||
| 
 | ||||
| 	avatarURL := du.AvatarURL("") | ||||
| 	if len(u.AvatarURLs) > 0 { | ||||
| 		avatarURL = u.AvatarURLs[0] | ||||
| 	if url := bot.UserAvatarURL(u); url != "" { | ||||
| 		avatarURL = url | ||||
| 	} | ||||
| 	name := u.Username | ||||
| 	if u.DisplayName != nil { | ||||
|  |  | |||
|  | @ -125,13 +125,13 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error | |||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		webpURL, jpgURL, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg) | ||||
| 		hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("uploading member avatar: %v", err) | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = tx.QueryRow(ctx, "UPDATE members SET avatar_urls = $1 WHERE id = $2", []string{webpURL, jpgURL}, m.ID).Scan(&m.AvatarURLs) | ||||
| 		err = tx.QueryRow(ctx, "UPDATE members SET avatar = $1 WHERE id = $2", hash, m.ID).Scan(&m.Avatar) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "setting avatar urls in db") | ||||
| 		} | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ type GetMemberResponse struct { | |||
| 	Name        string   `json:"name"` | ||||
| 	DisplayName *string  `json:"display_name"` | ||||
| 	Bio         *string  `json:"bio"` | ||||
| 	AvatarURLs  []string `json:"avatar_urls"` | ||||
| 	Avatar      *string  `json:"avatar"` | ||||
| 	Links       []string `json:"links"` | ||||
| 
 | ||||
| 	Names    []db.FieldEntry   `json:"names"` | ||||
|  | @ -32,7 +32,7 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field) GetMemberRespon | |||
| 		Name:        m.Name, | ||||
| 		DisplayName: m.DisplayName, | ||||
| 		Bio:         m.Bio, | ||||
| 		AvatarURLs:  db.NotNull(m.AvatarURLs), | ||||
| 		Avatar:      m.Avatar, | ||||
| 		Links:       db.NotNull(m.Links), | ||||
| 
 | ||||
| 		Names:    db.NotNull(m.Names), | ||||
|  | @ -43,16 +43,16 @@ func dbMemberToMember(u db.User, m db.Member, fields []db.Field) GetMemberRespon | |||
| 			ID:          u.ID, | ||||
| 			Username:    u.Username, | ||||
| 			DisplayName: u.DisplayName, | ||||
| 			AvatarURLs:  db.NotNull(u.AvatarURLs), | ||||
| 			Avatar:      u.Avatar, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type PartialUser struct { | ||||
| 	ID          xid.ID   `json:"id"` | ||||
| 	Username    string   `json:"name"` | ||||
| 	DisplayName *string  `json:"display_name"` | ||||
| 	AvatarURLs  []string `json:"avatar_urls"` | ||||
| 	ID          xid.ID  `json:"id"` | ||||
| 	Username    string  `json:"name"` | ||||
| 	DisplayName *string `json:"display_name"` | ||||
| 	Avatar      *string `json:"avatar"` | ||||
| } | ||||
| 
 | ||||
| func (s *Server) getMember(w http.ResponseWriter, r *http.Request) error { | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ type memberListResponse struct { | |||
| 	Name        string            `json:"name"` | ||||
| 	DisplayName *string           `json:"display_name"` | ||||
| 	Bio         *string           `json:"bio"` | ||||
| 	AvatarURLs  []string          `json:"avatar_urls"` | ||||
| 	Avatar      *string           `json:"avatar"` | ||||
| 	Links       []string          `json:"links"` | ||||
| 	Names       []db.FieldEntry   `json:"names"` | ||||
| 	Pronouns    []db.PronounEntry `json:"pronouns"` | ||||
|  | @ -25,13 +25,13 @@ func membersToMemberList(ms []db.Member) []memberListResponse { | |||
| 	resps := make([]memberListResponse, len(ms)) | ||||
| 	for i := range ms { | ||||
| 		resps[i] = memberListResponse{ | ||||
| 			ID:         ms[i].ID, | ||||
| 			Name:       ms[i].Name, | ||||
| 			Bio:        ms[i].Bio, | ||||
| 			AvatarURLs: db.NotNull(ms[i].AvatarURLs), | ||||
| 			Links:      db.NotNull(ms[i].Links), | ||||
| 			Names:      db.NotNull(ms[i].Names), | ||||
| 			Pronouns:   db.NotNull(ms[i].Pronouns), | ||||
| 			ID:       ms[i].ID, | ||||
| 			Name:     ms[i].Name, | ||||
| 			Bio:      ms[i].Bio, | ||||
| 			Avatar:   ms[i].Avatar, | ||||
| 			Links:    db.NotNull(ms[i].Links), | ||||
| 			Names:    db.NotNull(ms[i].Names), | ||||
| 			Pronouns: db.NotNull(ms[i].Pronouns), | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -127,7 +127,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { | |||
| 	} | ||||
| 
 | ||||
| 	// update avatar | ||||
| 	var avatarURLs []string = nil | ||||
| 	var avatarHash *string = nil | ||||
| 	if req.Avatar != nil { | ||||
| 		webp, jpg, err := s.DB.ConvertAvatar(*req.Avatar) | ||||
| 		if err != nil { | ||||
|  | @ -147,12 +147,12 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { | |||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		webpURL, jpgURL, err := s.DB.WriteMemberAvatar(ctx, id, webp, jpg) | ||||
| 		hash, err := s.DB.WriteMemberAvatar(ctx, id, webp, jpg) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("uploading member avatar: %v", err) | ||||
| 			return err | ||||
| 		} | ||||
| 		avatarURLs = []string{webpURL, jpgURL} | ||||
| 		avatarHash = &hash | ||||
| 	} | ||||
| 
 | ||||
| 	// start transaction | ||||
|  | @ -163,7 +163,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { | |||
| 	} | ||||
| 	defer tx.Rollback(ctx) | ||||
| 
 | ||||
| 	m, err = s.DB.UpdateMember(ctx, tx, id, req.Name, req.DisplayName, req.Bio, req.Links, avatarURLs) | ||||
| 	m, err = s.DB.UpdateMember(ctx, tx, id, req.Name, req.DisplayName, req.Bio, req.Links, avatarHash) | ||||
| 	if err != nil { | ||||
| 		switch errors.Cause(err) { | ||||
| 		case db.ErrNothingToUpdate: | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ type GetUserResponse struct { | |||
| 	Username    string            `json:"name"` | ||||
| 	DisplayName *string           `json:"display_name"` | ||||
| 	Bio         *string           `json:"bio"` | ||||
| 	AvatarURLs  []string          `json:"avatar_urls"` | ||||
| 	Avatar      *string           `json:"avatar"` | ||||
| 	Links       []string          `json:"links"` | ||||
| 	Names       []db.FieldEntry   `json:"names"` | ||||
| 	Pronouns    []db.PronounEntry `json:"pronouns"` | ||||
|  | @ -36,7 +36,7 @@ type PartialMember struct { | |||
| 	Name        string            `json:"name"` | ||||
| 	DisplayName *string           `json:"display_name"` | ||||
| 	Bio         *string           `json:"bio"` | ||||
| 	AvatarURLs  []string          `json:"avatar_urls"` | ||||
| 	Avatar      *string           `json:"avatar"` | ||||
| 	Links       []string          `json:"links"` | ||||
| 	Names       []db.FieldEntry   `json:"names"` | ||||
| 	Pronouns    []db.PronounEntry `json:"pronouns"` | ||||
|  | @ -48,7 +48,7 @@ func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUser | |||
| 		Username:    u.Username, | ||||
| 		DisplayName: u.DisplayName, | ||||
| 		Bio:         u.Bio, | ||||
| 		AvatarURLs:  db.NotNull(u.AvatarURLs), | ||||
| 		Avatar:      u.Avatar, | ||||
| 		Links:       db.NotNull(u.Links), | ||||
| 		Names:       db.NotNull(u.Names), | ||||
| 		Pronouns:    db.NotNull(u.Pronouns), | ||||
|  | @ -62,7 +62,7 @@ func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUser | |||
| 			Name:        members[i].Name, | ||||
| 			DisplayName: members[i].DisplayName, | ||||
| 			Bio:         members[i].Bio, | ||||
| 			AvatarURLs:  db.NotNull(members[i].AvatarURLs), | ||||
| 			Avatar:      members[i].Avatar, | ||||
| 			Links:       db.NotNull(members[i].Links), | ||||
| 			Names:       db.NotNull(members[i].Names), | ||||
| 			Pronouns:    db.NotNull(members[i].Pronouns), | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { | |||
| 	} | ||||
| 
 | ||||
| 	// update avatar | ||||
| 	var avatarURLs []string = nil | ||||
| 	var avatarHash *string = nil | ||||
| 	if req.Avatar != nil { | ||||
| 		webp, jpg, err := s.DB.ConvertAvatar(*req.Avatar) | ||||
| 		if err != nil { | ||||
|  | @ -121,12 +121,12 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { | |||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		webpURL, jpgURL, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg) | ||||
| 		hash, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("uploading user avatar: %v", err) | ||||
| 			return err | ||||
| 		} | ||||
| 		avatarURLs = []string{webpURL, jpgURL} | ||||
| 		avatarHash = &hash | ||||
| 	} | ||||
| 
 | ||||
| 	// start transaction | ||||
|  | @ -152,7 +152,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.Links, avatarURLs) | ||||
| 	u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.Links, avatarHash) | ||||
| 	if err != nil && errors.Cause(err) != db.ErrNothingToUpdate { | ||||
| 		log.Errorf("updating user: %v", err) | ||||
| 		return err | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| import { PUBLIC_BASE_URL } from "$env/static/public"; | ||||
| 
 | ||||
| export interface User { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   display_name: string | null; | ||||
|   bio: string | null; | ||||
|   avatar_urls: string[]; | ||||
|   avatar: string | null; | ||||
|   links: string[]; | ||||
| 
 | ||||
|   names: FieldEntry[]; | ||||
|  | @ -47,7 +49,7 @@ export interface PartialMember { | |||
|   name: string; | ||||
|   display_name: string | null; | ||||
|   bio: string | null; | ||||
|   avatar_urls: string[]; | ||||
|   avatar: string | null; | ||||
|   links: string[]; | ||||
|   names: FieldEntry[]; | ||||
|   pronouns: Pronoun[]; | ||||
|  | @ -63,7 +65,7 @@ export interface MemberPartialUser { | |||
|   id: string; | ||||
|   name: string; | ||||
|   display_name: string | null; | ||||
|   avatar_urls: string[]; | ||||
|   avatar: string | null; | ||||
| } | ||||
| 
 | ||||
| export interface APIError { | ||||
|  | @ -98,3 +100,21 @@ export enum ErrorCode { | |||
| 
 | ||||
|   RequestTooBig = 4001, | ||||
| } | ||||
| 
 | ||||
| export const userAvatars = (user: User | MeUser | MemberPartialUser) => { | ||||
|   if (!user.avatar) return []; | ||||
| 
 | ||||
|   return [ | ||||
|     `${PUBLIC_BASE_URL}/media/users/${user.id}/${user.avatar}.webp`, | ||||
|     `${PUBLIC_BASE_URL}/media/users/${user.id}/${user.avatar}.webp`, | ||||
|   ]; | ||||
| }; | ||||
| 
 | ||||
| export const memberAvatars = (member: Member | PartialMember) => { | ||||
|   if (!member.avatar) return []; | ||||
| 
 | ||||
|   return [ | ||||
|     `${PUBLIC_BASE_URL}/media/members/${member.id}/${member.avatar}.webp`, | ||||
|     `${PUBLIC_BASE_URL}/media/members/${member.id}/${member.avatar}.webp`, | ||||
|   ]; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts"> | ||||
|   import { WordStatus, type PartialMember, type User } from "$lib/api/entities"; | ||||
|   import { memberAvatars, WordStatus, type PartialMember, type User } from "$lib/api/entities"; | ||||
|   import FallbackImage from "./FallbackImage.svelte"; | ||||
| 
 | ||||
|   export let user: User; | ||||
|  | @ -31,7 +31,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   <FallbackImage urls={member.avatar_urls} width={200} alt="Avatar for {member.name}" /> | ||||
|   <FallbackImage urls={memberAvatars(member)} width={200} alt="Avatar for {member.name}" /> | ||||
|   <p class="m-2"> | ||||
|     <a class="text-reset fs-5" href="/@{user.name}/{member.name}"> | ||||
|       {member.display_name ?? member.name} | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
|   import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { userStore } from "$lib/store"; | ||||
|   import { userAvatars } from "$lib/api/entities"; | ||||
| 
 | ||||
|   export let data: PageData; | ||||
| 
 | ||||
|  | @ -32,7 +33,7 @@ | |||
|   <div class="grid row-gap-3"> | ||||
|     <div class="row"> | ||||
|       <div class="col-md text-center"> | ||||
|         <FallbackImage width={200} urls={data.avatar_urls} alt="Avatar for @{data.name}" /> | ||||
|         <FallbackImage width={200} urls={userAvatars(data)} alt="Avatar for @{data.name}" /> | ||||
|       </div> | ||||
|       <div class="col-md"> | ||||
|         {#if data.display_name} | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ | |||
|   import PronounLink from "$lib/components/PronounLink.svelte"; | ||||
|   import FallbackImage from "$lib/components/FallbackImage.svelte"; | ||||
|   import { Button, Icon } from "sveltestrap"; | ||||
|   import { memberAvatars } from "$lib/api/entities"; | ||||
| 
 | ||||
|   export let data: PageData; | ||||
| 
 | ||||
|  | @ -29,7 +30,7 @@ | |||
|   <div class="grid"> | ||||
|     <div class="row"> | ||||
|       <div class="col-md text-center"> | ||||
|         <FallbackImage width={200} urls={data.avatar_urls} alt="Avatar for @{data.name}" /> | ||||
|         <FallbackImage width={200} urls={memberAvatars(data)} alt="Avatar for @{data.name}" /> | ||||
|       </div> | ||||
|       <div class="col-md"> | ||||
|         <h2>{data.display_name ?? data.name}</h2> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <script lang="ts"> | ||||
|   import { goto } from "$app/navigation"; | ||||
|   import { | ||||
|     userAvatars, | ||||
|     WordStatus, | ||||
|     type APIError, | ||||
|     type Field, | ||||
|  | @ -195,7 +196,7 @@ | |||
|             {#if avatar} | ||||
|               <img width={200} src={avatar} alt="New avatar" class="rounded-circle img-fluid" /> | ||||
|             {:else} | ||||
|               <FallbackImage alt="Current avatar" urls={$userStore.avatar_urls} width={200} /> | ||||
|               <FallbackImage alt="Current avatar" urls={userAvatars($userStore)} width={200} /> | ||||
|             {/if} | ||||
|           </div> | ||||
|           <div class="col-md"> | ||||
|  |  | |||
							
								
								
									
										9
									
								
								scripts/migrate/007_hashed_avatars.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								scripts/migrate/007_hashed_avatars.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| -- +migrate Up | ||||
| 
 | ||||
| -- 2023-03-13: Change avatar URLs to hashes | ||||
| 
 | ||||
| alter table users drop column avatar_urls; | ||||
| alter table members drop column avatar_urls; | ||||
| 
 | ||||
| alter table users add column avatar text; | ||||
| alter table members add column avatar text; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue