implemented federation

This commit is contained in:
2026-05-12 20:55:13 +02:00
parent e360f3697e
commit ab94775162
13 changed files with 874 additions and 30 deletions
+179
View File
@@ -0,0 +1,179 @@
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
}