1package tinyclient23import (4 "crypto/sha256"5 "fmt"6 "net/http"7 "regexp"8 "strings"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/seedmark"12)1314var avatarPath = regexp.MustCompile(`^/avatars/v1/([0-9a-f]{64})\.svg$`)1516func avatarURL(pubkey string) string {17 key := strings.ToLower(pubkey)18 if !eventIDPattern.MatchString(key) {19 key = strings.Repeat("0", 64)20 }21 return "/avatars/v1/" + key + ".svg"22}2324func roomAvatarURL(relay, id string) string {25 seed := sha256.Sum256([]byte("tinyrelay:room:v1\x00" + strings.TrimRight(relay, "/") + "\x00" + id))26 return avatarURL(fmt.Sprintf("%x", seed))27}2829// Avatars depend only on the supplied seed and never look up tenant, room or30// profile data. Versioned URLs keep each identity's appearance stable.31func serveAvatar(w http.ResponseWriter, r *http.Request) {32 if r.Method != http.MethodGet && r.Method != http.MethodHead {33 w.Header().Set("Allow", "GET, HEAD")34 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)35 return36 }37 match := avatarPath.FindStringSubmatch(r.URL.Path)38 if match == nil {39 http.NotFound(w, r)40 return41 }42 w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")43 w.Header().Set("X-Content-Type-Options", "nosniff")44 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")45 w.Header().Set("ETag", `"seedmark-v1-`+match[1]+`"`)46 http.ServeContent(w, r, "avatar.svg", time.Time{}, strings.NewReader(seedmark.Avatar(match[1])))47}