implemented federation
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user