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 }