diff --git a/pkg/modules/avatar/avatar.go b/pkg/modules/avatar/avatar.go
new file mode 100644
index 000000000..e8e10c6f5
--- /dev/null
+++ b/pkg/modules/avatar/avatar.go
@@ -0,0 +1,25 @@
+// Vikunja is a to-do-list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package avatar
+
+import "code.vikunja.io/api/pkg/user"
+
+// Provider defines the avatar provider interface
+type Provider interface {
+ // GetAvatar is the method used to get an actual avatar for a user
+ GetAvatar(user *user.User, size int64) (avatar []byte, mimeType string, err error)
+}
diff --git a/pkg/modules/avatar/gravatar/gravatar.go b/pkg/modules/avatar/gravatar/gravatar.go
new file mode 100644
index 000000000..6aba62662
--- /dev/null
+++ b/pkg/modules/avatar/gravatar/gravatar.go
@@ -0,0 +1,77 @@
+// Vikunja is a to-do-list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package gravatar
+
+import (
+ "code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/user"
+ "io/ioutil"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+type avatar struct {
+ content []byte
+ loadedAt time.Time
+}
+
+// Provider is the gravatar provider
+type Provider struct {
+}
+
+// avatars is a global map which contains cached avatars of the users
+var avatars map[string]*avatar
+
+func init() {
+ avatars = make(map[string]*avatar)
+}
+
+// GetAvatar implements getting the avatar for the user
+func (g *Provider) GetAvatar(user *user.User, size int64) ([]byte, string, error) {
+ sizeString := strconv.FormatInt(size, 10)
+ cacheKey := user.Username + "_" + sizeString
+ a, exists := avatars[cacheKey]
+ var needsRefetch bool
+ if exists {
+ // elaped is alway < 0 so the next check would always succeed.
+ // To have it make sense, we flip that.
+ elapsed := a.loadedAt.Sub(time.Now()) * -1
+ needsRefetch = elapsed > 10*time.Second
+ if needsRefetch {
+ log.Debugf("Refetching avatar for user %d after %v", user.ID, elapsed)
+ } else {
+ log.Debugf("Serving avatar for user %d from cache", user.ID)
+ }
+ }
+ if !exists || needsRefetch {
+ log.Debugf("Gravatar for user %d with size %d not cached, requesting from gravatar...", user.ID, size)
+ resp, err := http.Get("https://www.gravatar.com/avatar/" + user.AvatarURL + "?s=" + sizeString + "&d=mp")
+ if err != nil {
+ return nil, "", err
+ }
+ avatarContent, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, "", err
+ }
+ avatars[cacheKey] = &avatar{
+ content: avatarContent,
+ loadedAt: time.Now(),
+ }
+ }
+ return avatars[cacheKey].content, "image/jpg", nil
+}
diff --git a/pkg/routes/api/v1/avatar.go b/pkg/routes/api/v1/avatar.go
new file mode 100644
index 000000000..7cace9472
--- /dev/null
+++ b/pkg/routes/api/v1/avatar.go
@@ -0,0 +1,64 @@
+// Vikunja is a to-do-list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package v1
+
+import (
+ "code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/modules/avatar/gravatar"
+ user2 "code.vikunja.io/api/pkg/user"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo/v4"
+ "net/http"
+ "strconv"
+)
+
+// GetAvatar returns a user's avatar
+func GetAvatar(c echo.Context) error {
+ // Get the username
+ username := c.Param("username")
+
+ // Get the user
+ user, err := user2.GetUserByUsername(username)
+ if err != nil {
+ log.Errorf("Error getting user for avatar: %v", err)
+ return handler.HandleHTTPError(err, c)
+ }
+
+ // Initialize the avatar provider
+ // For now, we only have one avatar provider, in the future there could be multiple which
+ // could be changed based on user settings etc.
+ avatarProvider := gravatar.Provider{}
+
+ size := c.QueryParam("size")
+ var sizeInt int64
+ if size != "" {
+ sizeInt, err = strconv.ParseInt(size, 10, 64)
+ if err != nil {
+ log.Errorf("Error parsing size: %v", err)
+ return handler.HandleHTTPError(err, c)
+ }
+ }
+
+ // Get the avatar
+ avatar, mimeType, err := avatarProvider.GetAvatar(user, sizeInt)
+ if err != nil {
+ log.Errorf("Error getting avatar for user %d: %v", user.ID, err)
+ return handler.HandleHTTPError(err, c)
+ }
+
+ return c.Blob(http.StatusOK, mimeType, avatar)
+}
diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go
index 142a479ee..eeae39dd7 100644
--- a/pkg/routes/routes.go
+++ b/pkg/routes/routes.go
@@ -181,6 +181,9 @@ func registerAPIRoutes(a *echo.Group) {
// Info endpoint
n.GET("/info", apiv1.Info)
+ // Avatar endpoint
+ n.GET("/:username/avatar", apiv1.GetAvatar)
+
// Link share auth
if config.ServiceEnableLinkSharing.GetBool() {
n.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare)