AP stuff almost works
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Melody Becker 2025-04-09 17:35:31 +02:00
parent 98191fd098
commit d272fa90b4
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
20 changed files with 574 additions and 27 deletions

View file

@ -0,0 +1,15 @@
package activitypub
import (
"fmt"
"net/http"
)
func BuildActivitypubRouter() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/user/{id}", users)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "in ap")
})
return router
}

View file

@ -0,0 +1,96 @@
package activitypub
import (
"encoding/json"
"fmt"
"net/http"
webutils "git.mstar.dev/mstar/goutils/http"
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
)
var baseLdContext = []any{
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
}
func users(w http.ResponseWriter, r *http.Request) {
type OutboundKey struct {
Id string `json:"id"`
Owner string `json:"owner"`
Pem string `json:"publicKeyPem"`
}
type Outbound struct {
Context []any `json:"@context"`
Id string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
// FIXME: Public key stuff is borken. Focus on fixing
// PublicKey OutboundKey `json:"publicKey"`
}
userId := r.PathValue("id")
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
// fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey))
apUrl := userIdToApUrl(user.ID)
data := Outbound{
Context: baseLdContext,
Id: apUrl,
Type: "Person",
PreferredUsername: user.DisplayName,
Inbox: apUrl + "/inbox",
// PublicKey: OutboundKey{
// Id: apUrl + "#main-key",
// Owner: apUrl,
// Pem: keyBytesToPem(user.PublicKey),
// },
}
encoded, err := json.Marshal(data)
w.Header().Add("Content-Type", "application/activity+json")
fmt.Fprint(w, string(encoded))
}
/*
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
Fuck you.
If anyone wants to get this shit working *the propper way* with JsonLD, here's the
original code
var chain goap.BaseApChain = &goap.EmptyBaseObject{}
chain = goap.AppendUDIdData(chain, apUrl)
chain = goap.AppendUDTypeData(chain, "Person")
chain = goap.AppendASPreferredNameData(chain, goap.ValueValue[string]{Value: user.DisplayName})
chain = goap.AppendW3SecurityPublicKeyData(
chain,
apUrl+"#main-key",
apUrl,
keyBytesToPem(user.PublicKey),
)
chainMap := chain.MarshalToMap()
proc := ld.NewJsonLdProcessor()
options := ld.NewJsonLdOptions("")
tmp, tmperr := json.Marshal(chainMap)
fmt.Println(string(tmp), tmperr)
data, err := proc.Compact(chainMap, baseLdContext, options)
// data, err := goap.Compact(chain, baseLdContext)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal ap chain")
webutils.ProblemDetailsStatusOnly(w, 500)
return
}
*/

View file

@ -0,0 +1,25 @@
package activitypub
import (
"encoding/pem"
"fmt"
"git.mstar.dev/mstar/linstrom/config"
)
func userIdToApUrl(id string) string {
return fmt.Sprintf(
"%s/api/ap/users/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
id,
)
}
func keyBytesToPem(bytes []byte) string {
block := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: bytes,
}
return string(pem.EncodeToMemory(&block))
}

20
web/public/api/api.go Normal file
View file

@ -0,0 +1,20 @@
package api
import (
"fmt"
"net/http"
"git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
)
func BuildApiRouter() http.Handler {
router := http.NewServeMux()
router.Handle(
"/activitypub/",
http.StripPrefix("/activitypub", activitypub.BuildActivitypubRouter()),
)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "in api")
})
return router
}

167
web/public/api/webfinger.go Normal file
View file

@ -0,0 +1,167 @@
package api
import (
"errors"
"fmt"
"net/http"
"regexp"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
)
var webfingerResourceRegex = regexp.MustCompile(`acct:(?P<username>[\w-]+)@(?<domain>[\w\.-]+)`)
func WellKnownWebfinger(w http.ResponseWriter, r *http.Request) {
type OutboundLink struct {
Relation string `json:"rel"`
Type string `json:"type"`
Href *string `json:"href,omitempty"`
}
type Outbound struct {
Subject string `json:"subject"`
Links []OutboundLink `json:"links"`
Aliases []string `json:"aliases,omitempty"`
}
requestedResource := r.FormValue("resource")
matches := webfingerResourceRegex.FindStringSubmatch(requestedResource)
if len(matches) == 0 {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/webfinger-bad-resource",
"Bad resource format",
other.IntoPointer(
`The "resource" parameter must be available and of the form "acct:<username>@<domain>`,
),
nil,
)
return
}
// NOTE: Safe to access since, if the regex matches, it must include both groups
// Index 0 is the full matching string
// Index 1 is the username
// Index 2 is something vaguely domain-like
username := matches[1]
domain := matches[2]
// Fail if requested user is a different domain
// TODO: Decide whether to include the info that it's a different domain
if domain != config.GlobalConfig.General.GetFullDomain() {
webutils.ProblemDetailsStatusOnly(w, 404)
return
}
user, err := dbgen.User.GetByUsername(username)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
webutils.ProblemDetailsStatusOnly(w, 404)
} else {
// Fail the request, then attempt to reconnect
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get user from db")
}
}
return
}
data := Outbound{
Subject: matches[0],
Links: []OutboundLink{
{
Relation: "self",
Type: "application/activity+json",
Href: other.IntoPointer(
fmt.Sprintf(
"%s/api/activitypub/user/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
user.ID,
),
),
},
{
Relation: "http://webfinger.net/rel/profile-page",
Type: "text/html",
Href: other.IntoPointer(
fmt.Sprintf(
"%s/user/%s",
config.GlobalConfig.General.GetFullPublicUrl(),
user.ID,
),
),
},
},
}
webutils.SendJson(w, &data)
}
func Nodeinfo(w http.ResponseWriter, r *http.Request) {
u := dbgen.User
userCount, err := u.Where(u.DeletedAt.IsNull(), u.Verified.Is(true)).Count()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
n := dbgen.Note
noteCount, err := n.Where(n.DeletedAt.IsNull(), n.OriginId.Eq(1)).Count()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
} else {
log.Error().Err(err).Msg("Failed to get total user count from db")
}
return
}
data := map[string]any{
"version": "2.1",
"software": map[string]string{
"name": "linstrom",
"version": shared.Version,
"homepage": "https://git.mstar.dev/mstar/linstrom",
"repository": "https://git.mstar.dev/mstar/linstrom",
},
"protocols": []string{"activitypub"},
"services": map[string]any{
"inbound": []string{},
"outbound": []string{},
},
"openRegistrations": config.GlobalConfig.Admin.AllowRegistration,
"usage": map[string]any{
"users": map[string]any{
"total": userCount,
"activeHalfyear": nil,
"activeMonth": nil,
},
"localPosts": noteCount,
"localComments": 0,
},
"metadata": map[string]any{},
}
webutils.SendJson(w, data)
}
func WellKnownNodeinfo(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"links": []map[string]any{
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": config.GlobalConfig.General.GetFullPublicUrl() + "/nodeinfo/2.1",
},
},
}
webutils.SendJson(w, data)
}