Files

191 lines
5.5 KiB
Go

package handlers
import (
"log/slog"
"net/http"
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/middleware"
"atcr.io/pkg/atproto"
"github.com/go-chi/chi/v5"
)
// UserPageHandler handles the public user page showing all images for a user
type UserPageHandler struct {
BaseUIHandler
}
func (h *UserPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
identifier := chi.URLParam(r, "handle")
// Resolve identifier (handle or DID) to canonical DID and current handle
did, resolvedHandle, pdsEndpoint, err := atproto.ResolveIdentity(r.Context(), identifier)
if err != nil {
RenderNotFound(w, r, &h.BaseUIHandler)
return
}
// Look up user by DID
viewedUser, err := db.GetUserByDID(h.ReadOnlyDB, did)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
hasProfile := true
if viewedUser == nil {
// Valid ATProto user but hasn't set up ATCR profile
hasProfile = false
viewedUser = &db.User{
DID: did,
Handle: resolvedHandle,
PDSEndpoint: pdsEndpoint,
// Avatar intentionally empty - template shows '?' placeholder
}
} else if viewedUser.Handle != resolvedHandle {
// Opportunistically update cached handle if it changed
_ = db.UpdateUserHandle(h.DB, did, resolvedHandle)
viewedUser.Handle = resolvedHandle
}
// Get current user DID for star state (empty string if not logged in)
var currentUserDID string
if user := middleware.GetUser(r); user != nil {
currentUserDID = user.DID
}
// Fetch repository cards. Track the error separately so the template can
// render a distinct error state ("couldn't load their images") rather
// than the empty profile copy ("no images yet"), which implies no push
// has ever happened.
var cardsErr bool
cards, err := db.GetUserRepoCards(h.ReadOnlyDB, viewedUser.DID, currentUserDID)
if err != nil {
slog.Error("user: fetch repo cards", "did", viewedUser.DID, "err", err)
cards = []db.RepoCardData{}
cardsErr = true
}
db.SetRegistryURL(cards, h.RegistryURL)
pageData := NewPageData(r, &h.BaseUIHandler)
db.SetOciClient(cards, pageData.OciClient)
// Check for supporter badge based on billing subscription
supporterBadge := h.BillingManager.GetSupporterBadge(viewedUser.DID)
// Build page meta
meta := NewPageMeta(
viewedUser.Handle+" - "+h.ClientShortName,
"Container images by "+viewedUser.Handle+" on "+h.ClientShortName+", the decentralized container registry",
).
WithCanonical("https://" + h.SiteURL + "/u/" + viewedUser.Handle).
WithOGImage("https://" + h.SiteURL + "/og/u/" + viewedUser.Handle).
WithOGType("profile").
WithSiteName(h.ClientShortName).
WithJSONLD(NewJSONLDProfilePage(h.SiteURL, viewedUser.Handle, viewedUser.Avatar))
data := struct {
PageData
Meta *PageMeta
ViewedUser *db.User // User whose page we're viewing
Repositories []db.RepoCardData
HasProfile bool
SupporterBadge string
HasError bool
}{
PageData: pageData,
Meta: meta,
ViewedUser: viewedUser,
Repositories: cards,
HasProfile: hasProfile,
SupporterBadge: supporterBadge,
HasError: cardsErr,
}
if err := h.Templates.ExecuteTemplate(w, "user", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// StarredRepositoriesHandler renders /u/{handle}/starred — the list of repos
// the viewed user has starred. Stars whose target repo no longer exists or is
// inaccessible are silently dropped by the underlying query.
type StarredRepositoriesHandler struct {
BaseUIHandler
}
func (h *StarredRepositoriesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
identifier := chi.URLParam(r, "handle")
did, resolvedHandle, pdsEndpoint, err := atproto.ResolveIdentity(r.Context(), identifier)
if err != nil {
RenderNotFound(w, r, &h.BaseUIHandler)
return
}
viewedUser, err := db.GetUserByDID(h.ReadOnlyDB, did)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
hasProfile := true
if viewedUser == nil {
hasProfile = false
viewedUser = &db.User{
DID: did,
Handle: resolvedHandle,
PDSEndpoint: pdsEndpoint,
}
} else if viewedUser.Handle != resolvedHandle {
_ = db.UpdateUserHandle(h.DB, did, resolvedHandle)
viewedUser.Handle = resolvedHandle
}
var currentUserDID string
if user := middleware.GetUser(r); user != nil {
currentUserDID = user.DID
}
var cardsErr bool
cards, err := db.GetStarredRepoCards(h.ReadOnlyDB, viewedUser.DID, currentUserDID)
if err != nil {
slog.Error("starred: fetch repo cards", "did", viewedUser.DID, "err", err)
cards = []db.RepoCardData{}
cardsErr = true
}
db.SetRegistryURL(cards, h.RegistryURL)
pageData := NewPageData(r, &h.BaseUIHandler)
db.SetOciClient(cards, pageData.OciClient)
meta := NewPageMeta(
viewedUser.Handle+"'s stars - "+h.ClientShortName,
"Repositories starred by "+viewedUser.Handle+" on "+h.ClientShortName+", the decentralized container registry",
).
WithCanonical("https://" + h.SiteURL + "/u/" + viewedUser.Handle + "/starred").
WithSiteName(h.ClientShortName)
data := struct {
PageData
Meta *PageMeta
ViewedUser *db.User
Repositories []db.RepoCardData
HasProfile bool
HasError bool
}{
PageData: pageData,
Meta: meta,
ViewedUser: viewedUser,
Repositories: cards,
HasProfile: hasProfile,
HasError: cardsErr,
}
if err := h.Templates.ExecuteTemplate(w, "user-starred", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}