This commit is contained in:
parent
98191fd098
commit
d272fa90b4
20 changed files with 574 additions and 27 deletions
15
web/public/api/activitypub/activitypub.go
Normal file
15
web/public/api/activitypub/activitypub.go
Normal 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
|
||||
}
|
96
web/public/api/activitypub/user.go
Normal file
96
web/public/api/activitypub/user.go
Normal 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
|
||||
}
|
||||
*/
|
25
web/public/api/activitypub/util.go
Normal file
25
web/public/api/activitypub/util.go
Normal 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
20
web/public/api/api.go
Normal 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
167
web/public/api/webfinger.go
Normal 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)
|
||||
}
|
22
web/public/errorpages.go
Normal file
22
web/public/errorpages.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package webpublic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
)
|
||||
|
||||
var errorDescriptions = map[string]string{
|
||||
"webfinger-bad-resource": "The given format for the \"resource\" url parameter was missing or invalid. It must follow the form \"acct:<username>@<domain>\"",
|
||||
"db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time",
|
||||
}
|
||||
|
||||
func errorTypeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
errName := r.PathValue("name")
|
||||
if description, ok := errorDescriptions[errName]; ok {
|
||||
fmt.Fprint(w, description)
|
||||
} else {
|
||||
webutils.ProblemDetailsStatusOnly(w, 404)
|
||||
}
|
||||
}
|
|
@ -30,6 +30,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/web/public/api"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -42,8 +46,13 @@ func New(addr string) *Server {
|
|||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, "not implemented")
|
||||
})
|
||||
handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter()))
|
||||
handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger)
|
||||
handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo)
|
||||
handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo)
|
||||
handler.HandleFunc("GET /errors/{name}", errorTypeHandler)
|
||||
server := http.Server{
|
||||
Handler: handler,
|
||||
Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware),
|
||||
Addr: addr,
|
||||
}
|
||||
return &Server{&server}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue