mirror of
https://gitlab.melroy.org/melroy/fediresolve.git
synced 2025-06-07 20:08:57 +00:00
Add support for instance info
This commit is contained in:
parent
c7828633c8
commit
6393d94db3
3 changed files with 155 additions and 1 deletions
|
@ -31,8 +31,13 @@ func Format(data map[string]interface{}) (string, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSummary generates a human-readable summary of the ActivityPub object
|
// createSummary generates a human-readable summary of the ActivityPub object or nodeinfo
|
||||||
func createSummary(jsonStr string) string {
|
func createSummary(jsonStr string) string {
|
||||||
|
// Try to detect nodeinfo
|
||||||
|
if gjson.Get(jsonStr, "software.name").Exists() && gjson.Get(jsonStr, "version").Exists() {
|
||||||
|
return nodeInfoSummary(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
objectType := gjson.Get(jsonStr, "type").String()
|
objectType := gjson.Get(jsonStr, "type").String()
|
||||||
|
|
||||||
// Build a header with the object type
|
// Build a header with the object type
|
||||||
|
@ -74,6 +79,49 @@ func createSummary(jsonStr string) string {
|
||||||
return strings.Join(summaryParts, "\n")
|
return strings.Join(summaryParts, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodeInfoSummary generates a summary for nodeinfo objects
|
||||||
|
func nodeInfoSummary(jsonStr string) string {
|
||||||
|
bold := color.New(color.Bold).SprintFunc()
|
||||||
|
cyan := color.New(color.FgCyan).SprintFunc()
|
||||||
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
|
yellow := color.New(color.FgYellow).SprintFunc()
|
||||||
|
|
||||||
|
parts := []string{}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s", bold("NodeInfo Version"), cyan(gjson.Get(jsonStr, "version").String())))
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s %s", bold("Software"), green(gjson.Get(jsonStr, "software.name").String()), yellow(gjson.Get(jsonStr, "software.version").String())))
|
||||||
|
if repo := gjson.Get(jsonStr, "software.repository").String(); repo != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s", bold("Repository"), repo))
|
||||||
|
}
|
||||||
|
if protocols := gjson.Get(jsonStr, "protocols").Array(); len(protocols) > 0 {
|
||||||
|
var plist []string
|
||||||
|
for _, p := range protocols {
|
||||||
|
plist = append(plist, p.String())
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s", bold("Protocols"), strings.Join(plist, ", ")))
|
||||||
|
}
|
||||||
|
if users := gjson.Get(jsonStr, "usage.users.total").Int(); users > 0 {
|
||||||
|
activeMonth := gjson.Get(jsonStr, "usage.users.activeMonth").Int()
|
||||||
|
activeHalfyear := gjson.Get(jsonStr, "usage.users.activeHalfyear").Int()
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %d (active month: %d, halfyear: %d)", bold("Users"), users, activeMonth, activeHalfyear))
|
||||||
|
}
|
||||||
|
if posts := gjson.Get(jsonStr, "usage.localPosts").Int(); posts > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %d", bold("Local Posts"), posts))
|
||||||
|
}
|
||||||
|
if comments := gjson.Get(jsonStr, "usage.localComments").Int(); comments > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %d", bold("Local Comments"), comments))
|
||||||
|
}
|
||||||
|
if open := gjson.Get(jsonStr, "openRegistrations").Exists(); open {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %v", bold("Open Registrations"), gjson.Get(jsonStr, "openRegistrations").Bool()))
|
||||||
|
}
|
||||||
|
if nodeName := gjson.Get(jsonStr, "metadata.nodeName").String(); nodeName != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s", bold("Node Name"), cyan(nodeName)))
|
||||||
|
}
|
||||||
|
if nodeDesc := gjson.Get(jsonStr, "metadata.nodeDescription").String(); nodeDesc != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s:\n%s", bold("Node Description"), nodeDesc))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// formatActor formats actor-type objects (Person, Service, etc.)
|
// formatActor formats actor-type objects (Person, Service, etc.)
|
||||||
func formatActor(jsonStr string, parts []string, bold, cyan, green, red, yellow func(a ...interface{}) string) []string {
|
func formatActor(jsonStr string, parts []string, bold, cyan, green, red, yellow func(a ...interface{}) string) []string {
|
||||||
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
||||||
|
|
|
@ -506,3 +506,95 @@ func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, er
|
||||||
|
|
||||||
return actorURL, nil
|
return actorURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchNodeInfo fetches nodeinfo from the given domain, returning the raw JSON and parsed data
|
||||||
|
func (r *Resolver) fetchNodeInfo(domain string) ([]byte, map[string]interface{}, error) {
|
||||||
|
nodeinfoURL := "https://" + domain + "/.well-known/nodeinfo"
|
||||||
|
fmt.Printf("Fetching nodeinfo discovery from: %s\n", nodeinfoURL)
|
||||||
|
|
||||||
|
resp, err := r.client.Get(nodeinfoURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error fetching nodeinfo discovery: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, nil, fmt.Errorf("nodeinfo discovery failed with status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error reading nodeinfo discovery: %v", err)
|
||||||
|
}
|
||||||
|
var discovery struct {
|
||||||
|
Links []struct {
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
} `json:"links"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &discovery); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error parsing nodeinfo discovery: %v", err)
|
||||||
|
}
|
||||||
|
var nodeinfoHref string
|
||||||
|
for _, link := range discovery.Links {
|
||||||
|
if strings.HasSuffix(link.Rel, "/schema/2.1") {
|
||||||
|
nodeinfoHref = link.Href
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodeinfoHref == "" {
|
||||||
|
for _, link := range discovery.Links {
|
||||||
|
if strings.HasSuffix(link.Rel, "/schema/2.0") {
|
||||||
|
nodeinfoHref = link.Href
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodeinfoHref == "" {
|
||||||
|
return nil, nil, fmt.Errorf("no nodeinfo schema 2.1 or 2.0 found")
|
||||||
|
}
|
||||||
|
fmt.Printf("Fetching nodeinfo from: %s\n", nodeinfoHref)
|
||||||
|
resp2, err := r.client.Get(nodeinfoHref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error fetching nodeinfo: %v", err)
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
if resp2.StatusCode != http.StatusOK {
|
||||||
|
return nil, nil, fmt.Errorf("nodeinfo fetch failed with status: %s", resp2.Status)
|
||||||
|
}
|
||||||
|
raw, err := io.ReadAll(resp2.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error reading nodeinfo: %v", err)
|
||||||
|
}
|
||||||
|
var nodeinfo map[string]interface{}
|
||||||
|
if err := json.Unmarshal(raw, &nodeinfo); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error parsing nodeinfo: %v", err)
|
||||||
|
}
|
||||||
|
return raw, nodeinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract actor, else try nodeinfo fallback for top-level domains
|
||||||
|
func (r *Resolver) ResolveObjectOrNodeInfo(objectURL string) ([]byte, map[string]interface{}, string, error) {
|
||||||
|
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
|
||||||
|
if err == nil && actorURL != "" {
|
||||||
|
actorData, err := r.fetchActorData(actorURL)
|
||||||
|
if err == nil && actorData != nil {
|
||||||
|
jsonData, _ := json.MarshalIndent(actorData, "", " ")
|
||||||
|
return jsonData, actorData, "actor", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If actor resolution fails, try nodeinfo
|
||||||
|
parts := strings.Split(objectURL, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, nil, "", fmt.Errorf("invalid object URL: %s", objectURL)
|
||||||
|
}
|
||||||
|
domain := parts[2]
|
||||||
|
raw, nodeinfo, err := r.fetchNodeInfo(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "", fmt.Errorf("could not fetch nodeinfo: %v", err)
|
||||||
|
}
|
||||||
|
return raw, nodeinfo, "nodeinfo", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatHelperResult wraps formatter.Format for use by resolver.go, keeping formatter import out of resolver.go
|
||||||
|
func FormatHelperResult(raw []byte, nodeinfo map[string]interface{}) (string, error) {
|
||||||
|
return formatter.Format(nodeinfo)
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,20 @@ func (r *Resolver) Resolve(input string) (string, error) {
|
||||||
// Check if input looks like a URL
|
// Check if input looks like a URL
|
||||||
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
|
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
|
||||||
fmt.Println("Detected URL, attempting direct resolution")
|
fmt.Println("Detected URL, attempting direct resolution")
|
||||||
|
// Special case: if input is just a root domain (no path or only "/"), use nodeinfo fallback
|
||||||
|
parsedURL, err := url.Parse(input)
|
||||||
|
if err == nil && (parsedURL.Path == "" || parsedURL.Path == "/") {
|
||||||
|
raw, nodeinfo, _, err := r.ResolveObjectOrNodeInfo(input)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Format using the formatter (in helpers.go)
|
||||||
|
formatted, ferr := FormatHelperResult(raw, nodeinfo)
|
||||||
|
if ferr != nil {
|
||||||
|
return string(raw), nil
|
||||||
|
}
|
||||||
|
return formatted, nil
|
||||||
|
}
|
||||||
return r.resolveURL(input)
|
return r.resolveURL(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue