feat: export AvatarURL for the display picture at any size
- AvatarURL returns the avatar thumbnail, the file path when the thumbnail fails, or the gravatar URL, the rule of the user payload - applyAvatar takes avatar_url from it; the payload is unchanged
This commit is contained in:
@@ -172,6 +172,7 @@ Packages:
|
|||||||
- `ActivateUsers`, `DeactivateUsers`, `RestoreUsers`, `BanUsers`, `UnbanUsers`, `UnsuspendUser`, `ThrottleStates` and `ForceDeleteCleanup`, the user actions the admin runs. Each takes the handle of the caller's transaction.
|
- `ActivateUsers`, `DeactivateUsers`, `RestoreUsers`, `BanUsers`, `UnbanUsers`, `UnsuspendUser`, `ThrottleStates` and `ForceDeleteCleanup`, the user actions the admin runs. Each takes the handle of the caller's transaction.
|
||||||
- `TouchLastSeen(ctx, db, userID)`.
|
- `TouchLastSeen(ctx, db, userID)`.
|
||||||
- `RequestUser(ctx, app, r)` and `EndRequestSession(ctx, app, w, r)`, for handlers that serve signed-in and anonymous browsers on one route (an OAuth provider's authorize page). `RequestUser` reads and checks the session JWT exactly as the `jwt` guard does (bearer header, then the `token` and `auth_token` cookies, the blacklist, `tokens_valid_after`) and returns the user, or `nil` for an anonymous, invalid, expired or blacklisted request and for a soft-deleted user; only an infrastructure failure is an error. `EndRequestSession` blacklists the request's JWT until it expires (as `POST /logout` does) and writes the forget cookie of `bouncer.LoadCookieConfig`, best effort: a blacklist failure is logged, never returned.
|
- `RequestUser(ctx, app, r)` and `EndRequestSession(ctx, app, w, r)`, for handlers that serve signed-in and anonymous browsers on one route (an OAuth provider's authorize page). `RequestUser` reads and checks the session JWT exactly as the `jwt` guard does (bearer header, then the `token` and `auth_token` cookies, the blacklist, `tokens_valid_after`) and returns the user, or `nil` for an anonymous, invalid, expired or blacklisted request and for a soft-deleted user; only an infrastructure failure is an error. `EndRequestSession` blacklists the request's JWT until it expires (as `POST /logout` does) and writes the forget cookie of `bouncer.LoadCookieConfig`, best effort: a blacklist failure is logged, never returned.
|
||||||
|
- `AvatarURL(ctx, app, user, size)`, the display picture rule of the user payload at any size: the avatar thumbnail (size by size, mode auto) when the user has an avatar and a file bucket is published, the file path when the thumbnail fails, otherwise the gravatar URL. The URL may be protocol-relative or relative. The payload's `avatar_url` comes from it at 128.
|
||||||
- `HandleGenerator`, `AssignHandle(ctx, db, user)`, `CreatedEvent` / `CreatedEventName`, `DeletingEvent` / `DeletingEventName`, `AuthorizeUser` and `RegisterUserAuthorizer`.
|
- `HandleGenerator`, `AssignHandle(ctx, db, user)`, `CreatedEvent` / `CreatedEventName`, `DeletingEvent` / `DeletingEventName`, `AuthorizeUser` and `RegisterUserAuthorizer`.
|
||||||
- `middleware`: `APITokenGuard`, `Scope`, `Permission` and `BearerToken`.
|
- `middleware`: `APITokenGuard`, `Scope`, `Permission` and `BearerToken`.
|
||||||
- `models`: `User`, `Throttle`, `Organisation`, `UserGroup`, `UsersGroup` (the `users_groups` pivot), `APIToken`, `OAuthIdentity`, `FrontendPermission` and `PermissionSet`. `PermissionSet` reads a stored permission column tolerantly (NULL, an empty string, `[]`, numeric strings) and writes a JSON object of integers; `ParsePermissionSet` and `PermissionSet.Text` do the same for a column that is a plain string on its model. `UserGroup.UsersCount` is filled only by a query that selects `users_count` (the admin list does). `Organisation.Members` is the relation behind the members manager, `Organisation.AttachRelations` declares its avatar (`OrganisationAvatarField`), and `Slugify` is the slug rule. `User.FilterScope` answers the admin list scope `FilterByGroup`, and `User.AttachRelations` declares the avatar (`AvatarField`).
|
- `models`: `User`, `Throttle`, `Organisation`, `UserGroup`, `UsersGroup` (the `users_groups` pivot), `APIToken`, `OAuthIdentity`, `FrontendPermission` and `PermissionSet`. `PermissionSet` reads a stored permission column tolerantly (NULL, an empty string, `[]`, numeric strings) and writes a JSON object of integers; `ParsePermissionSet` and `PermissionSet.Text` do the same for a column that is a plain string on its model. `UserGroup.UsersCount` is filled only by a query that selects `users_count` (the admin list does). `Organisation.Members` is the relation behind the members manager, `Organisation.AttachRelations` declares its avatar (`OrganisationAvatarField`), and `Slugify` is the slug rule. `User.FilterScope` answers the admin list scope `FilterByGroup`, and `User.AttachRelations` declares the avatar (`AvatarField`).
|
||||||
|
|||||||
65
classes/avatar.go
Normal file
65
classes/avatar.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package classes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
||||||
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||||
|
"gocloud.dev/blob"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvatarURL returns the display picture URL of user at size pixels, the
|
||||||
|
// rule the user API payload follows: the thumbnail (size by size, mode
|
||||||
|
// auto) of the user's avatar attachment when there is one and a file
|
||||||
|
// bucket is published, the attachment's file path when the thumbnail
|
||||||
|
// fails (logged), and otherwise the gravatar URL
|
||||||
|
// //www.gravatar.com/avatar/{md5 of the email}?s={size}&d=mm. The URL may
|
||||||
|
// be protocol-relative or relative; a caller that needs an absolute URL
|
||||||
|
// resolves it. Only a failing database lookup is an error.
|
||||||
|
func AvatarURL(ctx context.Context, app *backpack.App, user *models.User, size int) (string, error) {
|
||||||
|
if user == nil {
|
||||||
|
return "", errors.New("golem15.user: avatar needs a user")
|
||||||
|
}
|
||||||
|
fallback := gravatar(user.Email, size)
|
||||||
|
if app == nil || user.ID == 0 {
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
gdb, ok := app.Lookup[*gorm.DB]()
|
||||||
|
if !ok || gdb == nil {
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
var f attach.File
|
||||||
|
err := gdb.WithContext(ctx).
|
||||||
|
Where("attachment_type = ? AND attachment_id = ? AND field = ?", user.MorphName(), strconv.FormatUint(uint64(user.ID), 10), models.AvatarField).
|
||||||
|
Take(&f).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
bucket, ok := app.Lookup[*blob.Bucket]()
|
||||||
|
if !ok || bucket == nil {
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
url, err := f.Thumb(ctx, bucket, size, size, "auto")
|
||||||
|
if err != nil {
|
||||||
|
slog.Default().Error("golem15.user: avatar thumb", "err", err)
|
||||||
|
return "/" + attach.BlobKey(f.DiskName), nil
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gravatar is the gravatar URL of an email at size pixels.
|
||||||
|
func gravatar(email string, size int) string {
|
||||||
|
sum := md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))
|
||||||
|
return fmt.Sprintf("//www.gravatar.com/avatar/%x?s=%d&d=mm", sum, size)
|
||||||
|
}
|
||||||
@@ -1283,17 +1283,9 @@ func applyAvatar(ctx context.Context, app *backpack.App, user *models.User, payl
|
|||||||
}
|
}
|
||||||
payload["has_avatar"] = true
|
payload["has_avatar"] = true
|
||||||
payload["avatar"] = f.DiskName
|
payload["avatar"] = f.DiskName
|
||||||
bucket, ok := app.Lookup[*blob.Bucket]()
|
if url, err := classes.AvatarURL(ctx, app, user, 128); err == nil && url != "" {
|
||||||
if !ok || bucket == nil {
|
payload["avatar_url"] = url
|
||||||
return
|
|
||||||
}
|
}
|
||||||
url, err := f.Thumb(ctx, bucket, 128, 128, "auto")
|
|
||||||
if err != nil {
|
|
||||||
slog.Default().Error("golem15.user: avatar thumb", "err", err)
|
|
||||||
payload["avatar_url"] = "/" + attach.BlobKey(f.DiskName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload["avatar_url"] = url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func avatarExt(mime string) (string, bool) {
|
func avatarExt(mime string) (string, bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user