| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- package handlers
- import (
- "encoding/hex"
- "errors"
- "log/slog"
- "net/http"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // ExpressionsHandler handles Web AIM API expressions/buddy icon endpoints.
- type ExpressionsHandler struct {
- IconSource BuddyIconSource
- Logger *slog.Logger
- }
- // NewExpressionsHandler creates a new ExpressionsHandler.
- func NewExpressionsHandler(iconSource BuddyIconSource, logger *slog.Logger) *ExpressionsHandler {
- return &ExpressionsHandler{
- IconSource: iconSource,
- Logger: logger,
- }
- }
- // Get handles GET /expressions/get requests for buddy icons and expressions.
- //
- // The AIM client calls this endpoint two different ways:
- //
- // - With type=buddyIcon it fetches the image itself. The buddyIcon URL
- // published in presence, buddylist and myInfo payloads points here, and the
- // client renders it directly as an <img> source.
- // - With no type it asks for the user's expressions as JSON, then scans the
- // returned array for the entry typed bigBuddyIcon and uses its url to render
- // hovercards and other large views. We have only one icon per user, so it is
- // offered as both.
- func (h *ExpressionsHandler) Get(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- target := r.URL.Query().Get("t")
- if target == "" {
- SendError(w, http.StatusBadRequest, "missing target")
- return
- }
- screenName := state.NewIdentScreenName(target)
- switch r.URL.Query().Get("type") {
- case "buddyIcon", "bigBuddyIcon":
- h.serveIcon(w, r, screenName)
- return
- }
- iconURL := h.IconSource.URL(ctx, baseURLFromRequest(r), screenName)
- // f=redirect asks for the icon itself rather than a description of it.
- if r.URL.Query().Get("f") == "redirect" {
- if iconURL == "" {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- http.Redirect(w, r, iconURL, http.StatusFound)
- return
- }
- expressions := []any{}
- if iconURL != "" {
- expressions = append(expressions, map[string]any{
- "type": "bigBuddyIcon",
- "url": iconURL,
- })
- }
- resp := BaseResponse{}
- resp.Response.StatusCode = 200
- resp.Response.StatusText = "OK"
- resp.Response.Data = map[string]any{"expressions": expressions}
- SendResponse(w, r, resp, h.Logger)
- }
- // serveIcon writes a user's buddy icon image.
- //
- // A bartId names an exact image by content hash, so the endpoint serves that
- // image regardless of the user's current icon and lets browsers cache it
- // forever. Without a bartId it serves whatever the user's icon is now — which
- // keeps changing — so that response is not cacheable, and a user with no icon
- // gets the blank placeholder so the client's <img> still renders.
- func (h *ExpressionsHandler) serveIcon(w http.ResponseWriter, r *http.Request, screenName state.IdentScreenName) {
- ctx := r.Context()
- var (
- icon []byte
- err error
- immutable bool
- )
- if raw := r.URL.Query().Get("bartId"); raw != "" {
- hash, decodeErr := hex.DecodeString(raw)
- if decodeErr != nil || len(hash) == 0 {
- http.Error(w, "invalid bartId", http.StatusBadRequest)
- return
- }
- icon, err = h.IconSource.ImageForHash(ctx, screenName, hash)
- immutable = true
- } else {
- icon, err = h.IconSource.Image(ctx, screenName)
- if errors.Is(err, ErrNoBuddyIcon) {
- // Serve the blank placeholder rather than 404 so a cleared icon still
- // renders something and the client stops showing the previous one.
- icon, err = h.IconSource.ImageForHash(ctx, screenName, wire.GetClearIconHash())
- }
- }
- switch {
- case errors.Is(err, ErrNoBuddyIcon):
- // The client swaps in its own placeholder when the icon fails to load.
- http.Error(w, "icon not found", http.StatusNotFound)
- return
- case err != nil:
- h.Logger.ErrorContext(ctx, "failed to retrieve buddy icon",
- "screenName", screenName.String(), "err", err.Error())
- http.Error(w, "internal server error", http.StatusInternalServerError)
- return
- }
- if immutable {
- w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
- } else {
- w.Header().Set("Cache-Control", "no-cache")
- }
- w.Header().Set("Content-Type", http.DetectContentType(icon))
- _, _ = w.Write(icon)
- }
|