180 lines
5.2 KiB
Go
180 lines
5.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"xorm.io/xorm"
|
|
|
|
"github.com/forgeo/forgebucket/internal/config"
|
|
"github.com/forgeo/forgebucket/internal/domain/federation"
|
|
"github.com/forgeo/forgebucket/internal/models"
|
|
)
|
|
|
|
const activityJSONType = "application/activity+json"
|
|
|
|
type FederationHandler struct {
|
|
db *xorm.Engine
|
|
cfg *config.Config
|
|
}
|
|
|
|
func NewFederationHandler(db *xorm.Engine, cfg *config.Config) *FederationHandler {
|
|
return &FederationHandler{db: db, cfg: cfg}
|
|
}
|
|
|
|
// WebFinger handles GET /.well-known/webfinger?resource=acct:user@domain
|
|
func (h *FederationHandler) WebFinger(w http.ResponseWriter, r *http.Request) {
|
|
resource := r.URL.Query().Get("resource")
|
|
if !strings.HasPrefix(resource, "acct:") {
|
|
http.Error(w, "resource must use acct: scheme", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// acct:username@domain — extract username
|
|
acct := strings.TrimPrefix(resource, "acct:")
|
|
username := strings.SplitN(acct, "@", 2)[0]
|
|
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
actorURL := federation.APID(h.cfg.InstanceURL, username)
|
|
resp := map[string]any{
|
|
"subject": resource,
|
|
"links": []map[string]any{
|
|
{
|
|
"rel": "self",
|
|
"type": activityJSONType,
|
|
"href": actorURL,
|
|
},
|
|
},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/jrd+json")
|
|
json.NewEncoder(w).Encode(resp) //nolint:errcheck
|
|
}
|
|
|
|
// Actor handles GET /users/{username} — returns the JSON-LD actor document.
|
|
func (h *FederationHandler) Actor(w http.ResponseWriter, r *http.Request) {
|
|
username := chi.URLParam(r, "username")
|
|
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
|
|
if err != nil {
|
|
http.Error(w, "could not get actor", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
doc := federation.ActorJSON(actor, username, username)
|
|
w.Header().Set("Content-Type", activityJSONType)
|
|
json.NewEncoder(w).Encode(doc) //nolint:errcheck
|
|
}
|
|
|
|
// Inbox handles POST /users/{username}/inbox — receive an ActivityPub activity.
|
|
func (h *FederationHandler) Inbox(w http.ResponseWriter, r *http.Request) {
|
|
username := chi.URLParam(r, "username")
|
|
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
|
|
if err != nil {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1 MB max
|
|
if err != nil {
|
|
http.Error(w, "could not read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify HTTP signature. In debug mode, skip verification so local testing works.
|
|
if !h.cfg.Debug {
|
|
if err := federation.Verify(r, h.db, h.cfg.InstanceURL); err != nil {
|
|
http.Error(w, "signature verification failed: "+err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := federation.Receive(h.db, actor, body); err != nil {
|
|
http.Error(w, "could not process activity: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
// OutboxGet handles GET /users/{username}/outbox — serve the activity collection.
|
|
func (h *FederationHandler) OutboxGet(w http.ResponseWriter, r *http.Request) {
|
|
username := chi.URLParam(r, "username")
|
|
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
|
|
if err != nil {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
coll := federation.Collection(h.db, actor.APID, actor.OutboxURL, page)
|
|
|
|
w.Header().Set("Content-Type", activityJSONType)
|
|
json.NewEncoder(w).Encode(coll) //nolint:errcheck
|
|
}
|
|
|
|
// Followers handles GET /users/{username}/followers
|
|
func (h *FederationHandler) Followers(w http.ResponseWriter, r *http.Request) {
|
|
username := chi.URLParam(r, "username")
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
|
|
if err != nil {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
coll := federation.StubCollection(actor.APID + "/followers")
|
|
w.Header().Set("Content-Type", activityJSONType)
|
|
json.NewEncoder(w).Encode(coll) //nolint:errcheck
|
|
}
|
|
|
|
// Following handles GET /users/{username}/following
|
|
func (h *FederationHandler) Following(w http.ResponseWriter, r *http.Request) {
|
|
username := chi.URLParam(r, "username")
|
|
var user models.User
|
|
if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
|
|
if err != nil {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
coll := federation.StubCollection(actor.APID + "/following")
|
|
w.Header().Set("Content-Type", activityJSONType)
|
|
json.NewEncoder(w).Encode(coll) //nolint:errcheck
|
|
}
|