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)