Skip to content

Commit

Permalink
avatar-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
wxiaoguang committed Sep 22, 2021
1 parent f2e7d54 commit cc8a453
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 175 deletions.
4 changes: 2 additions & 2 deletions integrations/api_user_orgs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestUserOrgs(t *testing.T) {
ID: 3,
UserName: user3.Name,
FullName: user3.FullName,
AvatarURL: user3.AvatarLink(),
AvatarURL: user3.AvatarLinkDefaultSize(),
Description: "",
Website: "",
Location: "",
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestMyOrgs(t *testing.T) {
ID: 3,
UserName: user3.Name,
FullName: user3.FullName,
AvatarURL: user3.AvatarLink(),
AvatarURL: user3.AvatarLinkDefaultSize(),
Description: "",
Website: "",
Location: "",
Expand Down
2 changes: 1 addition & 1 deletion integrations/user_avatar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestUserAvatar(t *testing.T) {

user2 = db.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo3, is an org

req = NewRequest(t, "GET", user2.AvatarLink())
req = NewRequest(t, "GET", user2.AvatarLinkDefaultSize())
resp := session.MakeRequest(t, req, http.StatusFound)
location := resp.Header().Get("Location")
if !strings.HasPrefix(location, "/avatars") {
Expand Down
116 changes: 71 additions & 45 deletions models/avatar.go → models/avatars/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package models
package avatars

import (
"crypto/md5"
"fmt"
"net/url"
"path"
"strconv"
Expand All @@ -19,7 +17,16 @@ import (
"code.gitea.io/gitea/modules/setting"
)

// EmailHash represents a pre-generated hash map
// DefaultAvatarSize is a sentinel value for the default avatar size, as determined by the avatar-hosting service.
const DefaultAvatarSize = -1

// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
const DefaultAvatarPixelSize = 28

// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
const AvatarRenderedSizeFactor = 4

// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
type EmailHash struct {
Hash string `xorm:"pk varchar(32)"`
Email string `xorm:"UNIQUE NOT NULL"`
Expand All @@ -41,18 +48,7 @@ func DefaultAvatarLink() string {
return u.String()
}

// DefaultAvatarSize is a sentinel value for the default avatar size, as
// determined by the avatar-hosting service.
const DefaultAvatarSize = -1

// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
const DefaultAvatarPixelSize = 28

// AvatarRenderedSizeFactor is the factor by which the default size is increased for finer rendering
const AvatarRenderedSizeFactor = 4

// HashEmail hashes email address to MD5 string.
// https://en.gravatar.com/site/implement/hash/
// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {
return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
}
Expand All @@ -69,8 +65,8 @@ func GetEmailForHash(md5Sum string) (string, error) {
})
}

// LibravatarURL returns the URL for the given email. This function should only
// be called if a federated avatar service is enabled.
// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
// This function should only be called if a federated avatar service is enabled.
func LibravatarURL(email string) (*url.URL, error) {
urlStr, err := setting.LibravatarService.FromEmail(email)
if err != nil {
Expand All @@ -85,14 +81,15 @@ func LibravatarURL(email string) (*url.URL, error) {
return u, nil
}

// HashedAvatarLink returns an avatar link for a provided email
func HashedAvatarLink(email string, size int) string {
// saveEmailHash returns an avatar link for a provided email,
// the email and hash are saved into database, which will be used by GetEmailForHash later
func saveEmailHash(email string) string {
lowerEmail := strings.ToLower(strings.TrimSpace(email))
sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
emailHash := HashEmail(lowerEmail)
_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) {
emailHash := &EmailHash{
Email: lowerEmail,
Hash: sum,
Hash: emailHash,
}
// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
if err := db.WithTx(func(ctx *db.Context) error {
Expand All @@ -109,39 +106,68 @@ func HashedAvatarLink(email string, size int) string {
}
return lowerEmail, nil
})
if size > 0 {
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) + "?size=" + strconv.Itoa(size)
}
return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
return emailHash
}

// MakeFinalAvatarURL constructs the final avatar URL string
func MakeFinalAvatarURL(u *url.URL, size int) string {
vals := u.Query()
vals.Set("d", "identicon")
if size != DefaultAvatarSize {
vals.Set("s", strconv.Itoa(size))
// GenerateUserAvatarFastLink returns a fast link to the user's avatar via the local explore page.
func GenerateUserAvatarFastLink(userName string, size int) string {
if size < 0 {
size = 0
}
u.RawQuery = vals.Encode()
return u.String()
link := setting.AppSubURL + "/user/avatar/" + userName + "/" + strconv.Itoa(size)
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
}

// SizedAvatarLink returns a sized link to the avatar for the given email address.
func SizedAvatarLink(email string, size int) string {
// generateEmailAvatarLink returns a email avatar link.
// if final is true, it may use a slow path (eg: query DNS).
// if final is false, it always uses a fast path.
func generateEmailAvatarLink(email string, size int, final bool) string {
if size <= 0 {
size = DefaultAvatarSize
}

email = strings.TrimSpace(email)
if email == "" {
return DefaultAvatarLink()
}

var avatarURL *url.URL
var err error

if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
// This is the slow path that would need to call LibravatarURL() which
// does DNS lookups. Avoid it by issuing a redirect so we don't block
// the template render with network requests.
return HashedAvatarLink(email, size)
emailHash := saveEmailHash(email)
if final {
if avatarURL, err = LibravatarURL(email); err != nil {
return DefaultAvatarLink()
}
} else {
if size > 0 {
return setting.AppSubURL + "/avatar/" + emailHash + "?size=" + strconv.Itoa(size)
}
return setting.AppSubURL + "/avatar/" + emailHash
}
} else if !setting.DisableGravatar {
// copy GravatarSourceURL, because we will modify its Path.
copyOfGravatarSourceURL := *setting.GravatarSourceURL
avatarURL = &copyOfGravatarSourceURL
avatarURLDummy := *setting.GravatarSourceURL // copy GravatarSourceURL, because we will modify its Path.
avatarURL = &avatarURLDummy
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
} else {
return DefaultAvatarLink()
}

return MakeFinalAvatarURL(avatarURL, size)
avatarURL.Query().Set("d", "identicon")
if size > 0 {
avatarURL.Query().Set("s", strconv.Itoa(size))
}
avatarURL.RawQuery = avatarURL.Query().Encode()
return avatarURL.String()
}

//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one)
func GenerateEmailAvatarFastLink(email string, size int) string {
return generateEmailAvatarLink(email, size, false)
}

//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
func GenerateEmailAvatarFinalLink(email string, size int) string {
return generateEmailAvatarLink(email, size, true)
}
6 changes: 3 additions & 3 deletions models/avatar_test.go → models/avatars/avatar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package models
package avatars

import (
"net/url"
Expand Down Expand Up @@ -44,11 +44,11 @@ func TestSizedAvatarLink(t *testing.T) {

disableGravatar()
assert.Equal(t, "/testsuburl/assets/img/avatar_default.png",
SizedAvatarLink("gitea@example.com", 100))
GenerateEmailAvatarFastLink("gitea@example.com", 100))

enableGravatar(t)
assert.Equal(t,
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
SizedAvatarLink("gitea@example.com", 100),
GenerateEmailAvatarFastLink("gitea@example.com", 100),
)
}
4 changes: 2 additions & 2 deletions models/repo_activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int)
}
users := make(map[int64]*ActivityAuthorData)
var unknownUserID int64
unknownUserAvatarLink := NewGhostUser().AvatarLink()
unknownUserAvatarLink := NewGhostUser().AvatarLinkDefaultSize()
for _, v := range code.Authors {
if len(v.Email) == 0 {
continue
Expand All @@ -116,7 +116,7 @@ func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int)
users[u.ID] = &ActivityAuthorData{
Name: u.DisplayName(),
Login: u.LowerName,
AvatarLink: u.AvatarLink(),
AvatarLink: u.AvatarLinkDefaultSize(),
HomeLink: u.HomeLink(),
Commits: v.Commits,
}
Expand Down
60 changes: 22 additions & 38 deletions models/user_avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"image/png"
"io"
"strconv"
"strings"

"code.gitea.io/gitea/models/avatars"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/avatar"
"code.gitea.io/gitea/modules/log"
Expand Down Expand Up @@ -40,7 +40,7 @@ func (u *User) generateRandomAvatar(e db.Engine) error {
return fmt.Errorf("RandomImage: %v", err)
}

u.Avatar = HashEmail(seed)
u.Avatar = avatars.HashEmail(seed)

// Don't share the images so that we can delete them easily
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
Expand All @@ -60,61 +60,45 @@ func (u *User) generateRandomAvatar(e db.Engine) error {
return nil
}

// SizedRelAvatarLink returns a link to the user's avatar via
// the local explore page. Function returns immediately.
// When applicable, the link is for an avatar of the indicated size (in pixels).
func (u *User) SizedRelAvatarLink(size int) string {
return setting.AppSubURL + "/user/avatar/" + u.Name + "/" + strconv.Itoa(size)
}

// RealSizedAvatarLink returns a link to the user's avatar. When
// applicable, the link is for an avatar of the indicated size (in pixels).
//
// This function make take time to return when federated avatars
// are in use, due to a DNS lookup need
//
func (u *User) RealSizedAvatarLink(size int) string {
// AvatarLinkWithSize returns a link to the user's avatar with size
func (u *User) AvatarLinkWithSize(size int) string {
if u.ID == -1 {
return DefaultAvatarLink()
// ghost user
return avatars.DefaultAvatarLink()
}

var useLocalAvatar bool

switch {
case u.UseCustomAvatar:
if u.Avatar == "" {
return DefaultAvatarLink()
}
if size > 0 {
return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
}
return setting.AppSubURL + "/avatars/" + u.Avatar
useLocalAvatar = true
case setting.DisableGravatar, setting.OfflineMode:
useLocalAvatar = true
if u.Avatar == "" {
if err := u.GenerateRandomAvatar(); err != nil {
log.Error("GenerateRandomAvatar: %v", err)
}
}
default:
useLocalAvatar = false
}

if useLocalAvatar {
if u.Avatar == "" {
return avatars.DefaultAvatarLink()
}
if size > 0 {
return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
}
return setting.AppSubURL + "/avatars/" + u.Avatar
}
return SizedAvatarLink(u.AvatarEmail, size)
}

// RelAvatarLink returns a relative link to the user's avatar. The link
// may either be a sub-URL to this site, or a full URL to an external avatar
// service.
func (u *User) RelAvatarLink() string {
return u.SizedRelAvatarLink(DefaultAvatarSize)
return avatars.GenerateEmailAvatarFastLink(u.AvatarEmail, size)
}

// AvatarLink returns user avatar absolute link.
func (u *User) AvatarLink() string {
link := u.RelAvatarLink()
if link[0] == '/' && link[1] != '/' {
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
}
return link
// AvatarLinkDefaultSize returns a avatar link with default size
func (u *User) AvatarLinkDefaultSize() string {
return u.AvatarLinkWithSize(avatars.DefaultAvatarSize)
}

// UploadAvatar saves custom avatar for user.
Expand Down
2 changes: 1 addition & 1 deletion modules/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey {
func ToOrganization(org *models.User) *api.Organization {
return &api.Organization{
ID: org.ID,
AvatarURL: org.AvatarLink(),
AvatarURL: org.AvatarLinkDefaultSize(),
UserName: org.Name,
FullName: org.FullName,
Description: org.Description,
Expand Down
2 changes: 1 addition & 1 deletion modules/convert/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func toUser(user *models.User, signed, authed bool) *api.User {
UserName: user.Name,
FullName: markup.Sanitize(user.FullName),
Email: user.GetEmail(),
AvatarURL: user.AvatarLink(),
AvatarURL: user.AvatarLinkDefaultSize(),
Created: user.CreatedUnix.AsTime(),
Restricted: user.IsRestricted,
Location: user.Location,
Expand Down
7 changes: 4 additions & 3 deletions modules/repository/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/avatars"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
Expand Down Expand Up @@ -139,14 +140,14 @@ func (pc *PushCommits) AvatarLink(email string) string {
return avatar
}

size := models.DefaultAvatarPixelSize * models.AvatarRenderedSizeFactor
size := avatars.DefaultAvatarPixelSize * avatars.AvatarRenderedSizeFactor

u, ok := pc.emailUsers[email]
if !ok {
var err error
u, err = models.GetUserByEmail(email)
if err != nil {
pc.avatars[email] = models.SizedAvatarLink(email, size)
pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(email, size)
if !models.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return ""
Expand All @@ -156,7 +157,7 @@ func (pc *PushCommits) AvatarLink(email string) string {
}
}
if u != nil {
pc.avatars[email] = u.RealSizedAvatarLink(size)
pc.avatars[email] = u.AvatarLinkWithSize(size)
}

return pc.avatars[email]
Expand Down
Loading

0 comments on commit cc8a453

Please sign in to comment.