573 lines
18 KiB
Go
573 lines
18 KiB
Go
package activitypub
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"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/shared"
|
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
|
)
|
|
|
|
type inboundImportUserKey struct {
|
|
Id string `json:"id"`
|
|
Owner string `json:"owner"`
|
|
Pem string `json:"publicKeyPem"`
|
|
}
|
|
type inboundImportUserMedia struct {
|
|
Type string `json:"type"`
|
|
Url string `json:"url"`
|
|
MediaType string `json:"mediaType"`
|
|
}
|
|
type inboundImportUser struct {
|
|
Id string `json:"id"`
|
|
Type string `json:"type"`
|
|
PreferredUsername string `json:"preferredUsername"`
|
|
Inbox string `json:"inbox"`
|
|
Outbox *string `json:"outbox"`
|
|
PublicKey *inboundImportUserKey `json:"publicKey"`
|
|
Published *time.Time `json:"published"`
|
|
DisplayName *string `json:"name"`
|
|
Description *string `json:"summary,omitempty"`
|
|
PublicUrl *string `json:"url"`
|
|
Icon *inboundImportUserMedia `json:"icon,omitempty"`
|
|
Banner *inboundImportUserMedia `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"`
|
|
}
|
|
|
|
func ImportRemoteAccount(targetName string) (string, error) {
|
|
// 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(shared.ServerActorName)).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 inboundImportUser
|
|
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")
|
|
|
|
// TODO: Store received user in db
|
|
_, host, _ := SplitFullHandle(targetName)
|
|
hostId, err := ImportRemoteServer(host)
|
|
if err != nil {
|
|
return "", other.Error("activitypub", "failed to import host of target user", err)
|
|
}
|
|
|
|
user, err := dbgen.User.
|
|
Where(dbgen.User.Username.Eq(targetName)).
|
|
Where(dbgen.User.ServerId.Eq(hostId)).
|
|
Preload(dbgen.User.RemoteInfo).
|
|
Preload(dbgen.User.InfoFields).
|
|
Preload(dbgen.User.BeingTypes).
|
|
Preload(dbgen.User.Roles).
|
|
FirstOrCreate()
|
|
if err != nil {
|
|
return "", other.Error("activitypub", "failed to find or create user in db", err)
|
|
}
|
|
user.Verified = true
|
|
user.FinishedRegistration = true
|
|
|
|
if !sliceutils.ContainsFunc(user.Roles, func(t models.UserToRole) bool {
|
|
return t.Role.ID == models.DefaultUserRole.ID
|
|
}) {
|
|
roleMapping := models.UserToRole{
|
|
Role: models.DefaultUserRole,
|
|
RoleId: models.DefaultUserRole.ID,
|
|
UserId: user.ID,
|
|
}
|
|
if err = dbgen.UserToRole.Create(&roleMapping); err != nil {
|
|
return "", other.Error(
|
|
"activitypub",
|
|
"failed to store default user role to imported account mapping",
|
|
err,
|
|
)
|
|
}
|
|
if err = dbgen.User.Roles.Model(user).Append(&roleMapping); err != nil {
|
|
return "", other.Error("activitypub", "failed to attach default role to user", err)
|
|
}
|
|
}
|
|
if user.RemoteInfo == nil {
|
|
user.RemoteInfo = &models.UserRemoteLinks{
|
|
UserId: user.ID,
|
|
ApLink: data.Id,
|
|
}
|
|
err = dbgen.UserRemoteLinks.Create(user.RemoteInfo)
|
|
if err != nil {
|
|
return "", other.Error("activitypub", "failed to create remote data for user", err)
|
|
}
|
|
err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo)
|
|
if err != nil {
|
|
return "", other.Error("activitypub", "failed to connect remote data to user", err)
|
|
}
|
|
user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID)
|
|
user.RemoteInfoId.Valid = true
|
|
}
|
|
if data.DisplayName != nil {
|
|
user.DisplayName = *data.DisplayName
|
|
}
|
|
if data.Outbox != nil {
|
|
user.RemoteInfo.OutboxLink.String = *data.Outbox
|
|
user.RemoteInfo.OutboxLink.Valid = true
|
|
}
|
|
user.RemoteInfo.InboxLink = data.Inbox
|
|
if data.PublicKey != nil {
|
|
pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem))
|
|
if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" {
|
|
return "", fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type)
|
|
}
|
|
user.PublicKeyRsa = pemBlock.Bytes
|
|
}
|
|
// Assume published day of user won't change
|
|
if data.Description != nil {
|
|
user.Description = *data.Description
|
|
}
|
|
if data.PublicUrl != nil {
|
|
user.RemoteInfo.ViewLink.String = *data.PublicUrl
|
|
user.RemoteInfo.ViewLink.Valid = true
|
|
}
|
|
if data.Discoverable != nil {
|
|
user.Indexable = *data.Discoverable
|
|
} else {
|
|
// Assume false per default
|
|
user.Indexable = false
|
|
}
|
|
if data.Location != nil {
|
|
user.Location.String = *data.Location
|
|
user.Location.Valid = true
|
|
}
|
|
if data.Birthday != nil {
|
|
user.Birthday.String = *data.Birthday
|
|
user.Birthday.Valid = true
|
|
}
|
|
if data.RestrictedFollow != nil {
|
|
user.RestrictedFollow = *data.RestrictedFollow
|
|
} else {
|
|
// Assume not restricted if not included in received data
|
|
user.RestrictedFollow = false
|
|
}
|
|
if data.IsCat {
|
|
if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
|
return t.Being == string(models.BEING_CAT)
|
|
}) {
|
|
log.Debug().Msg("user doesn't contain cat yet")
|
|
bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)}
|
|
if err = dbgen.UserToBeing.Create(&bt); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to append cat being type to imported user")
|
|
}
|
|
if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil {
|
|
log.Warn().
|
|
Err(err).
|
|
Msg("Failed to append cat being type to imported user relations")
|
|
}
|
|
user.BeingTypes = append(user.BeingTypes, bt)
|
|
}
|
|
} else {
|
|
if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
|
return t.Being == string(models.BEING_CAT)
|
|
}) {
|
|
_, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(string(models.BEING_CAT))).Delete()
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("Failed to remove cat being type from user")
|
|
}
|
|
}
|
|
}
|
|
// Don't handle SpeakAsCat yet, as not included in stored data yet
|
|
|
|
// Handle media last as more complicated
|
|
// Icon *inboundImportUserMedia `json:"icon,omitempty"`
|
|
// Banner *inboundImportUserMedia `json:"image,omitempty"`
|
|
|
|
id := user.ID
|
|
user.ID = ""
|
|
if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil {
|
|
return "", other.Error("activitypub", "failed to update imported user", err)
|
|
}
|
|
rlid := user.RemoteInfo.ID
|
|
user.RemoteInfo.ID = 0
|
|
if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil {
|
|
return "", other.Error("activitypub", "failed to update imported user's remote links", err)
|
|
}
|
|
|
|
log.Info().Str("user", targetName).Msg("Import completed")
|
|
return id, 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
|
|
}
|