linstrom/activitypub/import.go
mstar 08f6de0bd7
All checks were successful
/ docker (push) Successful in 4m1s
Rename cavage singing func, add import for server
2025-04-15 14:51:07 +02:00

438 lines
13 KiB
Go

package activitypub
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
)
var interestingMetadata = []string{
"nodeDescription",
"maintainer",
"tosUrl",
"nodeAdmins",
"privacyPolicyUrl",
"inquiryUrl",
"impressumUrl",
"donationUrl",
"staffAccounts",
}
func ImportRemoteAccount(targetName string) (string, error) {
type InboundUserKey struct {
Id string `json:"id"`
Owner string `json:"owner"`
Pem string `json:"publicKeyPem"`
}
type InboundUserMedia struct {
Type string `json:"type"`
Url string `json:"url"`
MediaType string `json:"mediaType"`
}
type InboundUser struct {
Id string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
PublicKey *InboundUserKey `json:"publicKey"`
Published *time.Time `json:"published"`
DisplayName *string `json:"name"`
Description *string `json:"summary,omitempty"`
PublicUrl *string `json:"url"`
Icon *InboundUserMedia `json:"icon,omitempty"`
Banner *InboundUserMedia `json:"image,omitempty"`
Discoverable *bool `json:"discoverable"`
Location *string `json:"vcard:Address,omitempty"`
Birthday *string `json:"vcard:bday,omitempty"`
SpeakAsCat bool `json:"speakAsCat"`
IsCat bool `json:"isCat"`
RestrictedFollow *bool `json:"manuallyApprovesFollowers"`
}
// Get the target user's link first
webfinger, err := GetAccountWebfinger(targetName)
if err != nil {
return "", other.Error("activitypub", "webfinger request failed", err)
}
selfLinks := sliceutils.Filter(webfinger.Links, func(t LinkData) bool {
return t.Relation == "self"
})
if len(selfLinks) == 0 {
return "", errors.New("no self link")
}
APLink := selfLinks[0]
// Server actor key for signing
linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq("linstrom")).First()
if err != nil {
return "", other.Error("activitypub", "failed to get server actor", err)
}
var response *http.Response
response, err = webshared.RequestSignedCavage("GET", *APLink.Href, nil, linstromActor)
if err != nil {
return "", other.Error("activitypub", "failed to complete cavage signed request", err)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
log.Trace().
Int("status", response.StatusCode).
Bytes("body", body).
Any("headers", response.Header).
Msg("Response information")
if response.StatusCode != 200 {
return "", fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode)
}
var data InboundUser
err = json.Unmarshal(body, &data)
if err != nil {
return "", other.Error("activitypub", "failed to unmarshal response", err)
}
log.Debug().Any("received-data", data).Msg("Response data")
log.Info().Str("user", targetName).Msg("Import completed")
return "", nil
}
func ImportRemoteServer(host string) (uint, error) {
// Nodeinfo is not locked behind authentication, so don't do it
req, err := webshared.NewRequest("GET", "https://"+host+"/.well-known/nodeinfo", nil)
if err != nil {
return 0, other.Error("activitypub", "failed to create overview request", err)
}
response, err := webshared.RequestClient.Do(req)
if err != nil {
return 0, other.Error("activitypub", "overview request failed", err)
}
var nodeInfoOverview webshared.NodeInfoOverview
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&nodeInfoOverview)
if err != nil {
return 0, other.Error("activitypub", "overview unmarshal failed", err)
}
relevantInfos := sliceutils.Filter(nodeInfoOverview.Links, func(t webshared.NodeInfoLink) bool {
return strings.HasSuffix(t.Rel, "schema/2.0") || strings.HasSuffix(t.Rel, "schema/2.1")
})
var data webshared.NodeInfo2
v21Slice := sliceutils.Filter(relevantInfos, func(t webshared.NodeInfoLink) bool {
return strings.HasSuffix(t.Rel, "schema/2.1")
})
if len(v21Slice) > 0 {
req, err = webshared.NewRequest("GET", v21Slice[0].Href, nil)
if err != nil {
return 0, other.Error("activitypub", "info 2.1 request creation failed", err)
}
} else {
req, err = webshared.NewRequest("GET", relevantInfos[0].Href, nil)
if err != nil {
return 0, other.Error("activitypub", "info 2.0 request creation failed", err)
}
}
res, err := webshared.RequestClient.Do(req)
if err != nil {
return 0, other.Error("activitypub", "info request failed to complete", err)
}
decoder = json.NewDecoder(res.Body)
err = decoder.Decode(&data)
if err != nil {
return 0, other.Error("activitypub", "failed to unmarshal info", err)
}
log.Debug().Any("nodeinfo", data).Msg("Server info received")
rs := dbgen.RemoteServer
// rsm := dbgen.RemoteServerMetadata
serverModelType := webshared.MapNodeServerTypeToModelType(data.Software.Name)
existingEntry, err := rs.Where(rs.Domain.Eq(host)).Preload(rs.Metadata).First()
switch err {
case gorm.ErrRecordNotFound:
existingEntry = &models.RemoteServer{
ServerType: serverModelType,
Version: data.Version,
Domain: host,
SpecificType: data.Software.Name,
}
if err = rs.Create(existingEntry); err != nil {
return 0, other.Error("activitypub", "failed to create new server entry", err)
}
case nil:
default:
return 0, other.Error("activitypub", "db failure", err)
}
existingEntry.SpecificType = data.Software.Name
existingEntry.IsSelf = false
existingEntry.ServerType = serverModelType
existingEntry.Version = data.Version
if name, ok := data.Metadata["nodeName"].(string); ok {
existingEntry.Name = name
} else {
existingEntry.Name = host
}
// Cast without check for existence is ok here
// Default value for `Any` is `nil`, which fails to cast to a valid string
if description, ok := data.Metadata["nodeDescription"].(string); ok {
log.Debug().Msg("Description found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "description"
},
)
if len(targets) > 0 {
targets[0].Value = description
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: description,
Key: "description",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "description").
Str("value", description).
Msg("Failed to append new metadata to server")
}
}
}
if maintainer, ok := data.Metadata["maintainer"].(map[string]any); ok {
log.Debug().Msg("Maintainer found")
name, nameOk := maintainer["name"].(string)
email, emailOk := maintainer["email"].(string)
if nameOk && emailOk {
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "maintainer"
},
)
val := fmt.Sprintf("%s <%s>", name, email)
if len(targets) > 0 {
targets[0].Value = val
} else {
err = rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Key: "maintainer",
Value: val,
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "maintainer").
Str("value", val).
Msg("Failed to append new metadata to server")
}
}
}
}
if tosUrl, ok := data.Metadata["tosUrl"].(string); ok {
log.Debug().Msg("Tos url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "tosUrl"
},
)
if len(targets) > 0 {
targets[0].Value = tosUrl
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: tosUrl,
Key: "tosUrl",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "tosUrl").
Str("value", tosUrl).
Msg("Failed to append new metadata to server")
}
}
}
if nodeAdmins, ok := data.Metadata["nodeAdmins"].([]map[string]any); ok {
log.Debug().Msg("Node admins url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "nodeAdmins"
},
)
valueBuilder := strings.Builder{}
for _, v := range nodeAdmins {
name, nameOk := v["name"].(string)
email, emailOk := v["email"].(string)
if nameOk && emailOk {
valueBuilder.WriteString(name)
valueBuilder.WriteString(" <")
valueBuilder.WriteString(email)
valueBuilder.WriteString(">;")
}
}
if len(targets) > 0 {
targets[0].Value = valueBuilder.String()
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: valueBuilder.String(),
Key: "nodeAdmins",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "nodeAdmins").
Str("value", valueBuilder.String()).
Msg("Failed to append new metadata to server")
}
}
}
if privacyPolicyUrl, ok := data.Metadata["privacyPolicyUrl"].(string); ok {
log.Debug().Msg("Privacy policy url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "privacyPolicyUrl"
},
)
if len(targets) > 0 {
targets[0].Value = privacyPolicyUrl
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: privacyPolicyUrl,
Key: "privacyPolicyUrl",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "privacyPolicyUrl").
Str("value", privacyPolicyUrl).
Msg("Failed to append new metadata to server")
}
}
}
if inquiryUrl, ok := data.Metadata["inquiryUrl"].(string); ok {
log.Debug().Msg("Inquiry found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "inquiryUrl"
},
)
if len(targets) > 0 {
targets[0].Value = inquiryUrl
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: inquiryUrl,
Key: "inquiryUrl",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "inquiryUrl").
Str("value", inquiryUrl).
Msg("Failed to append new metadata to server")
}
}
}
if impressumUrl, ok := data.Metadata["impressumUrl"].(string); ok {
log.Debug().Msg("Impressum url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "impressumUrl"
},
)
if len(targets) > 0 {
targets[0].Value = impressumUrl
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: impressumUrl,
Key: "impressumUrl",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "impressumUrl").
Str("value", impressumUrl).
Msg("Failed to append new metadata to server")
}
}
}
if donationUrl, ok := data.Metadata["donationUrl"].(string); ok {
log.Debug().Msg("Donation url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "donationUrl"
},
)
if len(targets) > 0 {
targets[0].Value = donationUrl
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: donationUrl,
Key: "donationUrl",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "donationUrl").
Str("value", donationUrl).
Msg("Failed to append new metadata to server")
}
}
}
if staffAccounts, ok := data.Metadata["nodeAdmins"].([]any); ok {
log.Debug().Msg("Node admins url found")
targets := sliceutils.Filter(
existingEntry.Metadata,
func(t models.RemoteServerMetadata) bool {
return t.Key == "staffAccounts"
},
)
valueBuilder := strings.Builder{}
for _, v := range staffAccounts {
if acc, ok := v.(string); ok {
valueBuilder.WriteString(acc)
valueBuilder.WriteString(";")
}
}
if len(targets) > 0 {
targets[0].Value = valueBuilder.String()
} else {
err := rs.Metadata.Model(existingEntry).Append(&models.RemoteServerMetadata{
Value: valueBuilder.String(),
Key: "staffAccounts",
})
if err != nil {
log.Warn().
Err(err).
Uint("server-id", existingEntry.ID).
Str("key", "staffAccounts").
Str("value", valueBuilder.String()).
Msg("Failed to append new metadata to server")
}
}
}
id := existingEntry.ID
existingEntry.ID = 0
_, err = rs.Where(rs.ID.Eq(id)).UpdateColumns(existingEntry)
if err != nil {
return 0, other.Error("activitypub", "failed to store update in db", err)
}
return id, nil
}