mirror of
https://gitlab.melroy.org/melroy/fediresolve.git
synced 2025-07-24 13:38:53 +00:00
Compare commits
19 commits
07140b16ef
...
7fe3ce2234
Author | SHA1 | Date | |
---|---|---|---|
|
7fe3ce2234 | ||
|
e56d6c9c41 | ||
|
8e5a08ab5f | ||
|
d85cf4bd11 | ||
|
1c8dad5500 | ||
|
b951b920ac | ||
|
f0399df34a | ||
|
a802483eb6 | ||
|
f2ffead88b | ||
|
02853f346d | ||
|
5d08981a5f | ||
|
3f377e14c0 | ||
|
416e4cded4 | ||
|
9203ac81b7 | ||
|
6393d94db3 | ||
|
c7828633c8 | ||
|
9d787d3115 | ||
|
508ec81ae1 | ||
|
be360d6475 |
8 changed files with 427 additions and 591 deletions
|
@ -30,7 +30,7 @@ go-mod-tidy:
|
|||
build:
|
||||
stage: build
|
||||
script:
|
||||
- go build
|
||||
- go build -ldflags "-s -w"
|
||||
- echo "ARTIFACT_URL=${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/fediresolve" >> variables.env
|
||||
artifacts:
|
||||
paths:
|
||||
|
|
|
@ -34,6 +34,13 @@ cd fediresolve
|
|||
go build
|
||||
```
|
||||
|
||||
Optionally, install the binary by executing: `go install`
|
||||
|
||||
Which depending on the OS installs to the installation directory, under Linux that is: `$HOME/go/bin`.
|
||||
Be sure to add `$HOME/go/bin` path to your `$PATH` in your shell, [more info](https://go.dev/doc/tutorial/compile-install).
|
||||
|
||||
Once installed (assuming the `$HOME/go/bin` path is in your `$PATH`) you can just execute the `fediresolve` binary without `./` of course.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic usage
|
||||
|
|
|
@ -7,33 +7,42 @@ import (
|
|||
"time"
|
||||
|
||||
h2m "github.com/JohannesKaufmann/html-to-markdown"
|
||||
markdown "github.com/Klaus-Tockloth/go-term-markdown"
|
||||
"github.com/fatih/color"
|
||||
"github.com/tidwall/gjson"
|
||||
markdown "github.com/vlanse/go-term-markdown"
|
||||
)
|
||||
|
||||
// Format takes ActivityPub data and returns a formatted string representation
|
||||
func Format(data map[string]interface{}) (string, error) {
|
||||
// First, get the beautified JSON
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error formatting JSON: %v", err)
|
||||
func Format(jsonData []byte) (string, error) {
|
||||
// Create a summary based on the object type
|
||||
summary := createSummary(jsonData)
|
||||
|
||||
// This might look unnecessary, but it is not in order to beautify the JSON.
|
||||
// First Unmarkshall to get a map[string]interface{}
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(jsonData, &data); err != nil {
|
||||
return "", fmt.Errorf("error parsing JSON: %v", err)
|
||||
}
|
||||
|
||||
// Convert to string for gjson parsing
|
||||
jsonStr := string(jsonData)
|
||||
|
||||
// Create a summary based on the object type
|
||||
summary := createSummary(jsonStr)
|
||||
// Marshal with indentation to beautify the output
|
||||
pretty, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error beautifying JSON: %v", err)
|
||||
}
|
||||
|
||||
// Combine the full JSON first, followed by the summary at the bottom
|
||||
result := fmt.Sprintf("%s\n\n%s", string(jsonData), summary)
|
||||
result := fmt.Sprintf("%s\n\n%s", pretty, summary)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createSummary generates a human-readable summary of the ActivityPub object
|
||||
func createSummary(jsonStr string) string {
|
||||
objectType := gjson.Get(jsonStr, "type").String()
|
||||
// createSummary generates a human-readable summary of the ActivityPub object or nodeinfo
|
||||
func createSummary(jsonStr []byte) string {
|
||||
// Try to detect nodeinfo
|
||||
if gjson.GetBytes(jsonStr, "software.name").Exists() && gjson.GetBytes(jsonStr, "version").Exists() {
|
||||
return nodeInfoSummary(jsonStr)
|
||||
}
|
||||
|
||||
objectType := gjson.GetBytes(jsonStr, "type").String()
|
||||
|
||||
// Build a header with the object type
|
||||
bold := color.New(color.Bold).SprintFunc()
|
||||
|
@ -44,12 +53,17 @@ func createSummary(jsonStr string) string {
|
|||
|
||||
header := fmt.Sprintf("%s: %s\n", bold("Type"), cyan(objectType))
|
||||
|
||||
// Add sensitive content warning if present
|
||||
if gjson.GetBytes(jsonStr, "sensitive").Bool() {
|
||||
header += fmt.Sprintf("%s: %s\n", red(bold("WARNING")), red("Sensitive Content!"))
|
||||
}
|
||||
|
||||
// Add common fields
|
||||
var summaryParts []string
|
||||
summaryParts = append(summaryParts, header)
|
||||
|
||||
// Add ID if available
|
||||
if id := gjson.Get(jsonStr, "id").String(); id != "" {
|
||||
if id := gjson.GetBytes(jsonStr, "id").String(); id != "" {
|
||||
summaryParts = append(summaryParts, fmt.Sprintf("%s: %s", bold("Original URL"), green(id)))
|
||||
}
|
||||
|
||||
|
@ -74,39 +88,95 @@ func createSummary(jsonStr string) string {
|
|||
return strings.Join(summaryParts, "\n")
|
||||
}
|
||||
|
||||
// nodeInfoSummary generates a summary for nodeinfo objects
|
||||
func nodeInfoSummary(jsonStr []byte) 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()
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
|
||||
parts := []string{}
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("NodeInfo Version"), cyan(gjson.GetBytes(jsonStr, "version").String())))
|
||||
parts = append(parts, fmt.Sprintf("%s: %s %s", bold("Software"), green(gjson.GetBytes(jsonStr, "software.name").String()), yellow(gjson.GetBytes(jsonStr, "software.version").String())))
|
||||
if repo := gjson.GetBytes(jsonStr, "software.repository").String(); repo != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Repository"), green(repo)))
|
||||
}
|
||||
|
||||
// Color openRegistrations green if true, red if false
|
||||
openReg := gjson.GetBytes(jsonStr, "openRegistrations")
|
||||
openRegStr := openReg.String()
|
||||
var openRegColored string
|
||||
if openReg.Exists() {
|
||||
if openReg.Bool() {
|
||||
openRegColored = green(openRegStr)
|
||||
} else {
|
||||
openRegColored = red(openRegStr)
|
||||
}
|
||||
} else {
|
||||
openRegColored = openRegStr
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Open Registrations"), openRegColored))
|
||||
if protocols := gjson.GetBytes(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.GetBytes(jsonStr, "usage.users.total").Int(); users > 0 {
|
||||
activeMonth := gjson.GetBytes(jsonStr, "usage.users.activeMonth").Int()
|
||||
activeHalfyear := gjson.GetBytes(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.GetBytes(jsonStr, "usage.localPosts").Int(); posts > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s: %d", bold("Local Posts"), posts))
|
||||
}
|
||||
if comments := gjson.GetBytes(jsonStr, "usage.localComments").Int(); comments > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s: %d", bold("Local Comments"), comments))
|
||||
}
|
||||
if nodeName := gjson.GetBytes(jsonStr, "metadata.nodeName").String(); nodeName != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Node Name"), cyan(nodeName)))
|
||||
}
|
||||
if nodeDesc := gjson.GetBytes(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.)
|
||||
func formatActor(jsonStr string, parts []string, bold, cyan, green, red, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
||||
func formatActor(jsonStr []byte, parts []string, bold, cyan, green, red, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.GetBytes(jsonStr, "name").String(); name != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Name"), cyan(name)))
|
||||
}
|
||||
|
||||
if preferredUsername := gjson.Get(jsonStr, "preferredUsername").String(); preferredUsername != "" {
|
||||
if preferredUsername := gjson.GetBytes(jsonStr, "preferredUsername").String(); preferredUsername != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Username"), red(preferredUsername)))
|
||||
}
|
||||
|
||||
if url := gjson.Get(jsonStr, "url").String(); url != "" {
|
||||
if url := gjson.GetBytes(jsonStr, "url").String(); url != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("URL"), green(url)))
|
||||
}
|
||||
|
||||
iconUrl := gjson.Get(jsonStr, "icon.url").String()
|
||||
iconUrl := gjson.GetBytes(jsonStr, "icon.url").String()
|
||||
if iconUrl != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Avatar"), green(iconUrl)))
|
||||
}
|
||||
|
||||
if summary := gjson.Get(jsonStr, "summary").String(); summary != "" {
|
||||
if summary := gjson.GetBytes(jsonStr, "summary").String(); summary != "" {
|
||||
md := htmlToMarkdown(summary)
|
||||
parts = append(parts, fmt.Sprintf("%s:\n%s", bold("Summary"), renderMarkdown(md)))
|
||||
}
|
||||
|
||||
if published := gjson.Get(jsonStr, "published").String(); published != "" {
|
||||
if published := gjson.GetBytes(jsonStr, "published").String(); published != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Published"), yellow(formatDate(published))))
|
||||
}
|
||||
|
||||
if followers := gjson.Get(jsonStr, "followers").String(); followers != "" {
|
||||
if followers := gjson.GetBytes(jsonStr, "followers").String(); followers != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Followers"), green(followers)))
|
||||
}
|
||||
|
||||
if following := gjson.Get(jsonStr, "following").String(); following != "" {
|
||||
if following := gjson.GetBytes(jsonStr, "following").String(); following != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Following"), green(following)))
|
||||
}
|
||||
|
||||
|
@ -114,13 +184,13 @@ func formatActor(jsonStr string, parts []string, bold, cyan, green, red, yellow
|
|||
}
|
||||
|
||||
// formatContent formats content-type objects (Note, Article, Page, etc.)
|
||||
func formatContent(jsonStr string, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
func formatContent(jsonStr []byte, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
// Show the name/title if present (especially for Page/thread)
|
||||
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
||||
if name := gjson.GetBytes(jsonStr, "name").String(); name != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Title"), name))
|
||||
}
|
||||
|
||||
if content := gjson.Get(jsonStr, "content").String(); content != "" {
|
||||
if content := gjson.GetBytes(jsonStr, "content").String(); content != "" {
|
||||
md := htmlToMarkdown(content)
|
||||
// Truncate the content if its too big.
|
||||
if len(md) > 1200 {
|
||||
|
@ -130,7 +200,7 @@ func formatContent(jsonStr string, parts []string, bold, green, yellow func(a ..
|
|||
}
|
||||
|
||||
// Check for attachments (images, videos, etc.)
|
||||
attachments := gjson.Get(jsonStr, "attachment").Array()
|
||||
attachments := gjson.GetBytes(jsonStr, "attachment").Array()
|
||||
if len(attachments) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s:", bold("Attachments")))
|
||||
for i, attachment := range attachments {
|
||||
|
@ -155,7 +225,7 @@ func formatContent(jsonStr string, parts []string, bold, green, yellow func(a ..
|
|||
parts = append(parts, attachmentInfo)
|
||||
|
||||
// For type Page and attachment type Link, show href if present
|
||||
objectType := gjson.Get(jsonStr, "type").String()
|
||||
objectType := gjson.GetBytes(jsonStr, "type").String()
|
||||
if objectType == "Page" && attachmentType == "Link" && href != "" {
|
||||
parts = append(parts, fmt.Sprintf(" URL: %s", green(href)))
|
||||
} else if url != "" {
|
||||
|
@ -164,39 +234,39 @@ func formatContent(jsonStr string, parts []string, bold, green, yellow func(a ..
|
|||
}
|
||||
}
|
||||
|
||||
if published := gjson.Get(jsonStr, "published").String(); published != "" {
|
||||
if published := gjson.GetBytes(jsonStr, "published").String(); published != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Published"), yellow(formatDate(published))))
|
||||
}
|
||||
|
||||
if updated := gjson.Get(jsonStr, "updated").String(); updated != "" {
|
||||
if updated := gjson.GetBytes(jsonStr, "updated").String(); updated != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Updated"), yellow(formatDate(updated))))
|
||||
}
|
||||
|
||||
if attributedTo := gjson.Get(jsonStr, "attributedTo").String(); attributedTo != "" {
|
||||
if attributedTo := gjson.GetBytes(jsonStr, "attributedTo").String(); attributedTo != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Author"), green(attributedTo)))
|
||||
}
|
||||
|
||||
if to := gjson.Get(jsonStr, "to").Array(); len(to) > 0 {
|
||||
if to := gjson.GetBytes(jsonStr, "to").Array(); len(to) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("To"), green(formatArray(to))))
|
||||
}
|
||||
|
||||
if cc := gjson.Get(jsonStr, "cc").Array(); len(cc) > 0 {
|
||||
if cc := gjson.GetBytes(jsonStr, "cc").Array(); len(cc) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("CC"), green(formatArray(cc))))
|
||||
}
|
||||
|
||||
if inReplyTo := gjson.Get(jsonStr, "inReplyTo").String(); inReplyTo != "" {
|
||||
if inReplyTo := gjson.GetBytes(jsonStr, "inReplyTo").String(); inReplyTo != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("In Reply To"), green(inReplyTo)))
|
||||
}
|
||||
|
||||
// Include endTime for Question type
|
||||
if endTime := gjson.Get(jsonStr, "endTime").String(); endTime != "" {
|
||||
if endTime := gjson.GetBytes(jsonStr, "endTime").String(); endTime != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("End Time"), yellow(formatDate(endTime))))
|
||||
}
|
||||
|
||||
// Include options (oneOf/anyOf) for Question type
|
||||
options := gjson.Get(jsonStr, "oneOf").Array()
|
||||
options := gjson.GetBytes(jsonStr, "oneOf").Array()
|
||||
if len(options) == 0 {
|
||||
options = gjson.Get(jsonStr, "anyOf").Array()
|
||||
options = gjson.GetBytes(jsonStr, "anyOf").Array()
|
||||
}
|
||||
if len(options) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s:", bold("Poll Options")))
|
||||
|
@ -210,24 +280,24 @@ func formatContent(jsonStr string, parts []string, bold, green, yellow func(a ..
|
|||
}
|
||||
|
||||
// formatActivity formats activity-type objects (Create, Like, etc.)
|
||||
func formatActivity(jsonStr string, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if actor := gjson.Get(jsonStr, "actor").String(); actor != "" {
|
||||
func formatActivity(jsonStr []byte, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if actor := gjson.GetBytes(jsonStr, "actor").String(); actor != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Actor"), actor))
|
||||
}
|
||||
|
||||
if object := gjson.Get(jsonStr, "object").String(); object != "" {
|
||||
if object := gjson.GetBytes(jsonStr, "object").String(); object != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Object"), object))
|
||||
} else if gjson.Get(jsonStr, "object").IsObject() {
|
||||
objectType := gjson.Get(jsonStr, "object.type").String()
|
||||
} else if gjson.GetBytes(jsonStr, "object").IsObject() {
|
||||
objectType := gjson.GetBytes(jsonStr, "object.type").String()
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Object Type"), yellow(objectType)))
|
||||
|
||||
if content := gjson.Get(jsonStr, "object.content").String(); content != "" {
|
||||
if content := gjson.GetBytes(jsonStr, "object.content").String(); content != "" {
|
||||
md := htmlToMarkdown(content)
|
||||
parts = append(parts, fmt.Sprintf("%s:\n%s", bold("Content"), renderMarkdown(md)))
|
||||
}
|
||||
|
||||
// Check for attachments in the object
|
||||
attachments := gjson.Get(jsonStr, "object.attachment").Array()
|
||||
attachments := gjson.GetBytes(jsonStr, "object.attachment").Array()
|
||||
if len(attachments) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s:", bold("Attachments")))
|
||||
for i, attachment := range attachments {
|
||||
|
@ -257,11 +327,11 @@ func formatActivity(jsonStr string, parts []string, bold, green, yellow func(a .
|
|||
}
|
||||
}
|
||||
|
||||
if published := gjson.Get(jsonStr, "published").String(); published != "" {
|
||||
if published := gjson.GetBytes(jsonStr, "published").String(); published != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Published"), yellow(formatDate(published))))
|
||||
}
|
||||
|
||||
if target := gjson.Get(jsonStr, "target").String(); target != "" {
|
||||
if target := gjson.GetBytes(jsonStr, "target").String(); target != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Target"), target))
|
||||
}
|
||||
|
||||
|
@ -269,15 +339,15 @@ func formatActivity(jsonStr string, parts []string, bold, green, yellow func(a .
|
|||
}
|
||||
|
||||
// formatCollection formats collection-type objects
|
||||
func formatCollection(jsonStr string, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if totalItems := gjson.Get(jsonStr, "totalItems").Int(); totalItems > 0 {
|
||||
func formatCollection(jsonStr []byte, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if totalItems := gjson.GetBytes(jsonStr, "totalItems").Int(); totalItems > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s: %d", bold("Total Items"), totalItems))
|
||||
}
|
||||
|
||||
// Show first few items if available
|
||||
items := gjson.Get(jsonStr, "items").Array()
|
||||
items := gjson.GetBytes(jsonStr, "items").Array()
|
||||
if len(items) == 0 {
|
||||
items = gjson.Get(jsonStr, "orderedItems").Array()
|
||||
items = gjson.GetBytes(jsonStr, "orderedItems").Array()
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
|
@ -300,7 +370,7 @@ func formatCollection(jsonStr string, parts []string, bold, green, yellow func(a
|
|||
}
|
||||
}
|
||||
|
||||
if published := gjson.Get(jsonStr, "published").String(); published != "" {
|
||||
if published := gjson.GetBytes(jsonStr, "published").String(); published != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Published"), yellow(formatDate(published))))
|
||||
}
|
||||
|
||||
|
@ -308,24 +378,24 @@ func formatCollection(jsonStr string, parts []string, bold, green, yellow func(a
|
|||
}
|
||||
|
||||
// formatMedia formats media-type objects (Image, Video, etc.)
|
||||
func formatMedia(jsonStr string, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
||||
func formatMedia(jsonStr []byte, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.GetBytes(jsonStr, "name").String(); name != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Title"), name))
|
||||
}
|
||||
|
||||
if url := gjson.Get(jsonStr, "url").String(); url != "" {
|
||||
if url := gjson.GetBytes(jsonStr, "url").String(); url != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("URL"), green(url)))
|
||||
}
|
||||
|
||||
if duration := gjson.Get(jsonStr, "duration").String(); duration != "" {
|
||||
if duration := gjson.GetBytes(jsonStr, "duration").String(); duration != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Duration"), duration))
|
||||
}
|
||||
|
||||
if published := gjson.Get(jsonStr, "published").String(); published != "" {
|
||||
if published := gjson.GetBytes(jsonStr, "published").String(); published != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Published"), yellow(formatDate(published))))
|
||||
}
|
||||
|
||||
if attributedTo := gjson.Get(jsonStr, "attributedTo").String(); attributedTo != "" {
|
||||
if attributedTo := gjson.GetBytes(jsonStr, "attributedTo").String(); attributedTo != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Author"), attributedTo))
|
||||
}
|
||||
|
||||
|
@ -333,25 +403,25 @@ func formatMedia(jsonStr string, parts []string, bold, green, yellow func(a ...i
|
|||
}
|
||||
|
||||
// formatEvent formats event-type objects
|
||||
func formatEvent(jsonStr string, parts []string, bold, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.Get(jsonStr, "name").String(); name != "" {
|
||||
func formatEvent(jsonStr []byte, parts []string, bold, yellow func(a ...interface{}) string) []string {
|
||||
if name := gjson.GetBytes(jsonStr, "name").String(); name != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Title"), name))
|
||||
}
|
||||
|
||||
if content := gjson.Get(jsonStr, "content").String(); content != "" {
|
||||
if content := gjson.GetBytes(jsonStr, "content").String(); content != "" {
|
||||
md := htmlToMarkdown(content)
|
||||
parts = append(parts, fmt.Sprintf("%s:\n%s", bold("Description"), renderMarkdown(md)))
|
||||
}
|
||||
|
||||
if startTime := gjson.Get(jsonStr, "startTime").String(); startTime != "" {
|
||||
if startTime := gjson.GetBytes(jsonStr, "startTime").String(); startTime != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Start Time"), yellow(formatDate(startTime))))
|
||||
}
|
||||
|
||||
if endTime := gjson.Get(jsonStr, "endTime").String(); endTime != "" {
|
||||
if endTime := gjson.GetBytes(jsonStr, "endTime").String(); endTime != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("End Time"), yellow(formatDate(endTime))))
|
||||
}
|
||||
|
||||
if location := gjson.Get(jsonStr, "location").String(); location != "" {
|
||||
if location := gjson.GetBytes(jsonStr, "location").String(); location != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Location"), location))
|
||||
}
|
||||
|
||||
|
@ -359,12 +429,12 @@ func formatEvent(jsonStr string, parts []string, bold, yellow func(a ...interfac
|
|||
}
|
||||
|
||||
// formatTombstone formats tombstone-type objects
|
||||
func formatTombstone(jsonStr string, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if formerType := gjson.Get(jsonStr, "formerType").String(); formerType != "" {
|
||||
func formatTombstone(jsonStr []byte, parts []string, bold, green, yellow func(a ...interface{}) string) []string {
|
||||
if formerType := gjson.GetBytes(jsonStr, "formerType").String(); formerType != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Former Type"), formerType))
|
||||
}
|
||||
|
||||
if deleted := gjson.Get(jsonStr, "deleted").String(); deleted != "" {
|
||||
if deleted := gjson.GetBytes(jsonStr, "deleted").String(); deleted != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", bold("Deleted"), yellow(formatDate(deleted))))
|
||||
}
|
||||
|
||||
|
|
38
go.mod
38
go.mod
|
@ -1,37 +1,39 @@
|
|||
module gitlab.melroy.org/melroy/fediresolve
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/Klaus-Tockloth/go-term-markdown v0.0.0-20250129073703-91600624167c
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/vlanse/go-term-markdown v0.0.1-dev2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/MichaelMure/go-term-text v0.3.1 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/eliukblau/pixterm v1.3.1 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/eliukblau/pixterm v1.3.2 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kyokomi/emoji/v2 v2.2.12 // indirect
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/image v0.26.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
|
|
98
go.sum
98
go.sum
|
@ -1,48 +1,51 @@
|
|||
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
||||
github.com/Klaus-Tockloth/go-term-markdown v0.0.0-20250129073703-91600624167c h1:8vDVIfpK0DOO4QqWKZiNeDKJXlyI06YmE8dubIsx5wU=
|
||||
github.com/Klaus-Tockloth/go-term-markdown v0.0.0-20250129073703-91600624167c/go.mod h1:N/I7QI6a0FQEzwYCFXKaeCbNupI4uAdmRS+QRI/Z0ho=
|
||||
github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
|
||||
github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/eliukblau/pixterm v1.3.1 h1:XeouQViH+lmzCa7sMUoK2cd7qlgHYGLIjwRKaOdJbKA=
|
||||
github.com/eliukblau/pixterm v1.3.1/go.mod h1:on5ueknFt+ZFVvIVVzQ7/JXwPjv5fJd8Q1Ybh7XixfU=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/eliukblau/pixterm v1.3.2 h1:kAF9qvbaDV3emb9LPHw1Bvd9D5o4y28U0e8Q9vfl24I=
|
||||
github.com/eliukblau/pixterm v1.3.2/go.mod h1:CgaInx2l92Xo3GTldly4UQeNghSFXmIQNk3zL77Xo/A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60=
|
||||
github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -56,42 +59,44 @@ github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK
|
|||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/vlanse/go-term-markdown v0.0.1-dev2 h1:sisNMYZSc2zdetAo7/kK5DRqzwfShlbuMdXEPAYlviQ=
|
||||
github.com/vlanse/go-term-markdown v0.0.1-dev2/go.mod h1:ujQ7UdQuyzdk827VWflQknUMr7qyQHPHIQA0wDgVWwc=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
|
@ -99,48 +104,65 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -3,7 +3,6 @@ package resolver
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -18,112 +17,76 @@ import (
|
|||
// Define common constants
|
||||
const (
|
||||
// UserAgent is the user agent string used for all HTTP requests
|
||||
UserAgent = "FediResolve/1.0 (https://melroy.org)"
|
||||
UserAgent = "FediResolve/1.0 (https://github.com/melroy89/FediResolve)"
|
||||
AcceptHeader = "application/activity+json, application/ld+json"
|
||||
)
|
||||
|
||||
// fetchActivityPubObjectWithSignature is a helper function that always signs HTTP requests
|
||||
// This is the preferred way to fetch ActivityPub content as many instances require signatures
|
||||
func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string, error) {
|
||||
func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) ([]byte, error) {
|
||||
fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL)
|
||||
|
||||
// First, we need to extract the actor URL from the object URL
|
||||
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
|
||||
// Fetch the object itself
|
||||
data, err := r.fetchActivityPubObjectDirect(objectURL)
|
||||
if err != nil {
|
||||
// If we can't extract the actor URL, fall back to a direct request
|
||||
fmt.Printf("Could not extract actor URL: %v, falling back to direct request\n", err)
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
}
|
||||
|
||||
// Then, we need to fetch the actor data to get the public key
|
||||
actorData, err := r.fetchActorData(actorURL)
|
||||
if err != nil {
|
||||
// If we can't fetch the actor data, fall back to a direct request
|
||||
fmt.Printf("Could not fetch actor data: %v, falling back to direct request\n", err)
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the public key ID
|
||||
keyID, _, err := r.extractPublicKey(actorData)
|
||||
keyID, err := r.extractPublicKey(data)
|
||||
if err != nil {
|
||||
// If we can't extract the public key, fall back to a direct request
|
||||
fmt.Printf("Could not extract public key: %v, falling back to direct request\n", err)
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
return nil, fmt.Errorf("could not extract public key: %v", err)
|
||||
}
|
||||
|
||||
// Create a new private key for signing (in a real app, we would use a persistent key)
|
||||
privateKey, err := generateRSAKey()
|
||||
if err != nil {
|
||||
// If we can't generate a key, fall back to a direct request
|
||||
fmt.Printf("Could not generate RSA key: %v, falling back to direct request\n", err)
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
return nil, fmt.Errorf("could not generate RSA key: %v", err)
|
||||
}
|
||||
|
||||
// Now, sign and send the request
|
||||
req, err := http.NewRequest("GET", objectURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating signed request: %v", err)
|
||||
return nil, fmt.Errorf("error creating signed request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
|
||||
req.Header.Set("Accept", AcceptHeader)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
// Sign the request
|
||||
if err := signRequest(req, keyID, privateKey); err != nil {
|
||||
// If we can't sign the request, fall back to a direct request
|
||||
fmt.Printf("Could not sign request: %v, falling back to direct request\n", err)
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
return nil, fmt.Errorf("could not sign request: %v", err)
|
||||
}
|
||||
|
||||
// Send the request
|
||||
fmt.Printf("Sending signed request with headers: %v\n", req.Header)
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error sending signed request: %v", err)
|
||||
return nil, fmt.Errorf("error sending signed request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Printf("Received response with status: %s\n", resp.Status)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// If the signed request fails, try a direct request as a fallback
|
||||
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
||||
fmt.Println("Signed request failed with auth error, trying direct request as fallback")
|
||||
return r.fetchActivityPubObjectDirect(objectURL)
|
||||
}
|
||||
|
||||
// Read body for error info
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
return nil, fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
// Read and parse the response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response: %v", err)
|
||||
return nil, fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
if len(bodyBytes) == 0 {
|
||||
return nil, fmt.Errorf("received empty response body")
|
||||
}
|
||||
|
||||
// Debug output
|
||||
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
|
||||
|
||||
// Check if the response is empty
|
||||
if len(body) == 0 {
|
||||
return "", fmt.Errorf("received empty response body")
|
||||
}
|
||||
|
||||
// Try to decode the JSON response
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return "", fmt.Errorf("error decoding response: %v", err)
|
||||
}
|
||||
|
||||
// Format the result
|
||||
return formatter.Format(data)
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
// fetchActivityPubObjectDirect is a helper function to fetch content without signatures
|
||||
// This is used as a fallback when signing fails
|
||||
func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error) {
|
||||
func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) ([]byte, error) {
|
||||
fmt.Printf("Fetching ActivityPub object directly from: %s\n", objectURL)
|
||||
|
||||
// Create a custom client that doesn't follow redirects automatically
|
||||
|
@ -137,18 +100,18 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
|
|||
// Create the request
|
||||
req, err := http.NewRequest("GET", objectURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %v", err)
|
||||
return nil, fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set Accept headers to request ActivityPub data
|
||||
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
|
||||
req.Header.Set("Accept", AcceptHeader)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
// Perform the request
|
||||
fmt.Printf("Sending direct request with headers: %v\n", req.Header)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching content: %v", err)
|
||||
return nil, fmt.Errorf("error fetching content: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
@ -169,186 +132,28 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
|
|||
if resp.StatusCode != http.StatusOK {
|
||||
// Read body for error info
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
return nil, fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
// Read and parse the response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response: %v", err)
|
||||
return nil, fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
|
||||
// Debug output
|
||||
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
|
||||
|
||||
// Check if the response is empty
|
||||
if len(body) == 0 {
|
||||
return "", fmt.Errorf("received empty response body")
|
||||
if len(bodyBytes) == 0 {
|
||||
return nil, fmt.Errorf("received empty response body")
|
||||
}
|
||||
|
||||
// Try to decode the JSON response
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return "", fmt.Errorf("error decoding response: %v", err)
|
||||
}
|
||||
|
||||
// Format the result
|
||||
return formatter.Format(data)
|
||||
}
|
||||
|
||||
// fetchWithSignature fetches ActivityPub content using HTTP Signatures
|
||||
func (r *Resolver) fetchWithSignature(objectURL string) (string, error) {
|
||||
fmt.Printf("Fetching with HTTP signatures from: %s\n", objectURL)
|
||||
|
||||
// First, we need to extract the actor URL from the object URL
|
||||
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error extracting actor URL: %v", err)
|
||||
}
|
||||
|
||||
// Then, we need to fetch the actor data to get the public key
|
||||
actorData, err := r.fetchActorData(actorURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching actor data: %v", err)
|
||||
}
|
||||
|
||||
// Extract the public key ID
|
||||
keyID, _, err := r.extractPublicKey(actorData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error extracting public key: %v", err)
|
||||
}
|
||||
|
||||
// Create a new private key for signing (in a real app, we would use a persistent key)
|
||||
privateKey, err := generateRSAKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error generating RSA key: %v", err)
|
||||
}
|
||||
|
||||
// Now, sign and send the request
|
||||
req, err := http.NewRequest("GET", objectURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating signed request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
// Sign the request
|
||||
if err := signRequest(req, keyID, privateKey); err != nil {
|
||||
return "", fmt.Errorf("error signing request: %v", err)
|
||||
}
|
||||
|
||||
// Send the request
|
||||
fmt.Printf("Sending signed request with headers: %v\n", req.Header)
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error sending signed request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Printf("Received response with status: %s\n", resp.Status)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Read body for error info
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
// Read and parse the response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
|
||||
// Debug output
|
||||
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
|
||||
|
||||
// Check if the response is empty
|
||||
if len(body) == 0 {
|
||||
return "", fmt.Errorf("received empty response body")
|
||||
}
|
||||
|
||||
// Try to decode the JSON response
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return "", fmt.Errorf("error decoding response: %v", err)
|
||||
}
|
||||
|
||||
// Format the result
|
||||
return formatter.Format(data)
|
||||
}
|
||||
|
||||
// extractActorURLFromObjectURL extracts the actor URL from an object URL
|
||||
func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error) {
|
||||
// This is a simplified approach - in a real app, we would parse the object URL properly
|
||||
// For now, we'll assume the actor URL is the base domain with the username
|
||||
|
||||
// Basic URL pattern: https://domain.tld/@username/postid
|
||||
parts := strings.Split(objectURL, "/")
|
||||
if len(parts) < 4 {
|
||||
return "", fmt.Errorf("invalid object URL format: %s", objectURL)
|
||||
}
|
||||
|
||||
// Extract domain and username
|
||||
domain := parts[2]
|
||||
username := parts[3]
|
||||
|
||||
// Handle different URL formats
|
||||
if strings.HasPrefix(username, "@") {
|
||||
// Format: https://domain.tld/@username/postid
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
|
||||
// Check for cross-instance handles like @user@domain.tld
|
||||
if strings.Contains(username, "@") {
|
||||
userParts := strings.Split(username, "@")
|
||||
if len(userParts) == 2 {
|
||||
username = userParts[0]
|
||||
domain = userParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Try common URL patterns
|
||||
actorURLs := []string{
|
||||
fmt.Sprintf("https://%s/users/%s", domain, username),
|
||||
fmt.Sprintf("https://%s/@%s", domain, username),
|
||||
fmt.Sprintf("https://%s/user/%s", domain, username),
|
||||
fmt.Sprintf("https://%s/accounts/%s", domain, username),
|
||||
fmt.Sprintf("https://%s/profile/%s", domain, username),
|
||||
}
|
||||
|
||||
// Try each URL pattern
|
||||
for _, actorURL := range actorURLs {
|
||||
fmt.Printf("Trying potential actor URL: %s\n", actorURL)
|
||||
// Check if this URL returns a valid actor
|
||||
actorData, err := r.fetchActorData(actorURL)
|
||||
if err == nil && actorData != nil {
|
||||
return actorURL, nil
|
||||
}
|
||||
|
||||
// Add a small delay between requests to avoid rate limiting
|
||||
fmt.Println("Waiting 1 second before trying next actor URL...")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// If we couldn't find a valid actor URL, try WebFinger
|
||||
fmt.Printf("Trying WebFinger resolution for: %s@%s\n", username, domain)
|
||||
return r.resolveActorViaWebFinger(username, domain)
|
||||
} else if username == "users" || username == "user" || username == "accounts" || username == "profile" {
|
||||
// Format: https://domain.tld/users/username/postid
|
||||
if len(parts) < 5 {
|
||||
return "", fmt.Errorf("invalid user URL format: %s", objectURL)
|
||||
}
|
||||
actorURL := fmt.Sprintf("https://%s/%s/%s", domain, username, parts[4])
|
||||
return actorURL, nil
|
||||
}
|
||||
|
||||
// If we get here, we couldn't determine the actor URL
|
||||
return "", fmt.Errorf("could not determine actor URL from: %s", objectURL)
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
// fetchActorData fetches actor data from an actor URL
|
||||
func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, error) {
|
||||
func (r *Resolver) fetchActorData(actorURL string) ([]byte, error) {
|
||||
fmt.Printf("Fetching actor data from: %s\n", actorURL)
|
||||
|
||||
// Create the request
|
||||
|
@ -358,7 +163,7 @@ func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, erro
|
|||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
|
||||
req.Header.Set("Accept", AcceptHeader)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
// Send the request
|
||||
|
@ -373,43 +178,48 @@ func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, erro
|
|||
}
|
||||
|
||||
// Read and parse the response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading actor response: %v", err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("error parsing actor data: %v", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
// extractPublicKey extracts the public key ID from actor data
|
||||
func (r *Resolver) extractPublicKey(actorData map[string]interface{}) (string, string, error) {
|
||||
// Convert to JSON string for easier parsing with gjson
|
||||
actorJSON, err := json.Marshal(actorData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error marshaling actor data: %v", err)
|
||||
}
|
||||
// TODO: We are actually now extracting the ID not the public key pem....
|
||||
// Lets see if we can improve this, without breaking the signing.
|
||||
func (r *Resolver) extractPublicKey(data []byte) (string, error) {
|
||||
// Try to find the attributedTo URL
|
||||
actorURL := gjson.GetBytes(data, "attributedTo").String()
|
||||
|
||||
// Extract key ID
|
||||
keyID := gjson.GetBytes(actorJSON, "publicKey.id").String()
|
||||
if keyID == "" {
|
||||
// Try alternate formats
|
||||
keyID = gjson.GetBytes(actorJSON, "publicKey.0.id").String()
|
||||
}
|
||||
if keyID == "" {
|
||||
return "", "", fmt.Errorf("could not find public key ID in actor data")
|
||||
}
|
||||
if actorURL == "" {
|
||||
fmt.Printf("Could not find attributedTo in object\n")
|
||||
// Try to find key in the object itself
|
||||
keyID := gjson.GetBytes(data, "publicKey.id").String()
|
||||
|
||||
// For future implementation, we might need to parse and use the public key
|
||||
// But for now, we just return a dummy value since we're focused on signing
|
||||
dummyPEM := "dummy-key"
|
||||
if keyID == "" {
|
||||
return "", fmt.Errorf("could not find public key ID in object")
|
||||
}
|
||||
return keyID, nil
|
||||
} else {
|
||||
actorData, err := r.fetchActorData(actorURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching actor data: %v", err)
|
||||
}
|
||||
|
||||
return keyID, dummyPEM, nil
|
||||
// Extract key ID
|
||||
keyID := gjson.GetBytes(actorData, "publicKey.id").String()
|
||||
if keyID == "" {
|
||||
// Try alternate formats
|
||||
keyID = gjson.GetBytes(actorData, "publicKey.0.id").String()
|
||||
}
|
||||
if keyID == "" {
|
||||
fmt.Printf("could not find public key ID in actor data")
|
||||
return "dummy", nil
|
||||
}
|
||||
return keyID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateRSAKey generates a new RSA key pair for signing requests
|
||||
|
@ -448,61 +258,68 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er
|
|||
return signer.SignRequest(privateKey, keyID, req, nil)
|
||||
}
|
||||
|
||||
// resolveActorViaWebFinger resolves an actor URL via WebFinger protocol
|
||||
func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, error) {
|
||||
// WebFinger URL format: https://domain.tld/.well-known/webfinger?resource=acct:username@domain.tld
|
||||
webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s",
|
||||
domain, username, domain)
|
||||
// fetchNodeInfo fetches nodeinfo from the given domain, returning the raw JSON
|
||||
func (r *Resolver) fetchNodeInfo(domain string) ([]byte, error) {
|
||||
nodeinfoURL := "https://" + domain + "/.well-known/nodeinfo"
|
||||
fmt.Printf("Fetching nodeinfo discovery from: %s\n", nodeinfoURL)
|
||||
|
||||
fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL)
|
||||
|
||||
// Create the request
|
||||
req, err := http.NewRequest("GET", webfingerURL, nil)
|
||||
resp, err := r.client.Get(nodeinfoURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating WebFinger request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Accept", "application/jrd+json, application/json")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
// Send the request
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching WebFinger data: %v", err)
|
||||
return nil, fmt.Errorf("error fetching nodeinfo discovery: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("WebFinger request failed with status: %s", resp.Status)
|
||||
return nil, fmt.Errorf("nodeinfo discovery failed with status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Read and parse the response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading WebFinger response: %v", err)
|
||||
return nil, fmt.Errorf("error reading nodeinfo discovery: %v", err)
|
||||
}
|
||||
|
||||
// Find the ActivityPub actor URL in the WebFinger response
|
||||
actorURL := ""
|
||||
webfingerData := gjson.ParseBytes(body)
|
||||
links := webfingerData.Get("links").Array()
|
||||
for _, link := range links {
|
||||
rel := link.Get("rel").String()
|
||||
typ := link.Get("type").String()
|
||||
href := link.Get("href").String()
|
||||
|
||||
if rel == "self" && (typ == "application/activity+json" ||
|
||||
typ == "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ||
|
||||
strings.Contains(typ, "activity+json")) {
|
||||
actorURL = href
|
||||
break
|
||||
}
|
||||
var nodeinfoHref string
|
||||
result := gjson.GetBytes(body, `links.#(rel%"*/schema/2.1").href`)
|
||||
if !result.Exists() {
|
||||
result = gjson.GetBytes(body, `links.#(rel%"*/schema/2.0").href`)
|
||||
}
|
||||
if result.Exists() {
|
||||
nodeinfoHref = result.String()
|
||||
}
|
||||
if nodeinfoHref == "" {
|
||||
return nil, fmt.Errorf("no nodeinfo schema 2.1 or 2.0 found")
|
||||
}
|
||||
|
||||
if actorURL == "" {
|
||||
return "", fmt.Errorf("could not find ActivityPub actor URL in WebFinger response")
|
||||
fmt.Printf("Fetching nodeinfo from: %s\n", nodeinfoHref)
|
||||
resp2, err := r.client.Get(nodeinfoHref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching nodeinfo: %v", err)
|
||||
}
|
||||
|
||||
return actorURL, nil
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("nodeinfo fetch failed with status: %s", resp2.Status)
|
||||
}
|
||||
raw, err := io.ReadAll(resp2.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading nodeinfo: %v", err)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Try to extract actor, else try nodeinfo fallback for top-level domains
|
||||
func (r *Resolver) ResolveObjectOrNodeInfo(objectURL string) ([]byte, error) {
|
||||
// If actor resolution fails, try nodeinfo
|
||||
parts := strings.Split(objectURL, "/")
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("invalid object URL: %s", objectURL)
|
||||
}
|
||||
domain := parts[2]
|
||||
body, err := r.fetchNodeInfo(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch nodeinfo: %v", err)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Format result
|
||||
func formatResult(raw []byte) (string, error) {
|
||||
return formatter.Format(raw)
|
||||
}
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// checkForRedirect checks if a URL redirects to another URL
|
||||
// and returns the final redirect URL after following all redirects
|
||||
func (r *Resolver) checkForRedirect(inputURL string) (string, error) {
|
||||
// Create a custom client that doesn't follow redirects automatically
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// Follow up to 10 redirects (to prevent infinite loops)
|
||||
currentURL := inputURL
|
||||
for i := 0; i < 10; i++ {
|
||||
// Create the request
|
||||
req, err := http.NewRequest("GET", currentURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating redirect check request: %v", err)
|
||||
}
|
||||
|
||||
// Set standard browser-like headers
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml")
|
||||
|
||||
// Perform the request
|
||||
fmt.Printf("Checking for redirects from: %s\n", currentURL)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error checking for redirects: %v", err)
|
||||
}
|
||||
|
||||
// Check if we got a redirect (302, 301, etc.)
|
||||
if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently ||
|
||||
resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect {
|
||||
// Get the redirect URL from the Location header
|
||||
redirectURL := resp.Header.Get("Location")
|
||||
resp.Body.Close() // Close the response body before continuing
|
||||
|
||||
if redirectURL != "" {
|
||||
fmt.Printf("Found redirect to: %s\n", redirectURL)
|
||||
|
||||
// Handle relative URLs
|
||||
if redirectURL[0] == '/' {
|
||||
// This is a relative URL, so we need to resolve it against the current URL
|
||||
baseURL, err := url.Parse(currentURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing base URL: %v", err)
|
||||
}
|
||||
relativeURL, err := url.Parse(redirectURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing relative URL: %v", err)
|
||||
}
|
||||
resolvedURL := baseURL.ResolveReference(relativeURL)
|
||||
redirectURL = resolvedURL.String()
|
||||
fmt.Printf("Resolved relative URL to: %s\n", redirectURL)
|
||||
}
|
||||
|
||||
// Update the current URL and continue following redirects
|
||||
currentURL = redirectURL
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't get a redirect, we're done
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
|
||||
// If we followed redirects and ended up at a different URL, return it
|
||||
if currentURL != inputURL {
|
||||
return currentURL, nil
|
||||
}
|
||||
|
||||
// No redirect found or we ended up back at the original URL
|
||||
return "", nil
|
||||
}
|
|
@ -32,25 +32,40 @@ func ResolveInput(input string) (string, error) {
|
|||
|
||||
// Resolve takes a URL or handle and resolves it to a formatted result
|
||||
func (r *Resolver) Resolve(input string) (string, error) {
|
||||
// Check if input looks like a URL
|
||||
// Always prepend https:// if missing and not a handle
|
||||
inputNorm := input
|
||||
if !strings.HasPrefix(input, "http://") && !strings.HasPrefix(input, "https://") && !strings.Contains(input, "@") {
|
||||
inputNorm = "https://" + input
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(inputNorm)
|
||||
if err == nil && parsedURL.Host != "" && (parsedURL.Path == "" || parsedURL.Path == "/") && parsedURL.RawQuery == "" && parsedURL.Fragment == "" {
|
||||
// Looks like a root domain (with or without scheme), fetch nodeinfo
|
||||
raw, err := r.ResolveObjectOrNodeInfo(parsedURL.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching nodeinfo: %v", err)
|
||||
}
|
||||
formatted, err := formatResult(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error formatting nodeinfo: %v", err)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
// If not a root domain, proceed with other checks
|
||||
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
|
||||
fmt.Println("Detected URL, attempting direct resolution")
|
||||
return r.resolveURL(input)
|
||||
}
|
||||
|
||||
// Check if input looks like a Fediverse handle (@username@domain.tld)
|
||||
if strings.Contains(input, "@") {
|
||||
// Handle format should be either @username@domain.tld or username@domain.tld
|
||||
// and should not contain any slashes or other URL-like characters
|
||||
if !strings.Contains(input, "/") && !strings.Contains(input, ":") {
|
||||
if strings.HasPrefix(input, "@") {
|
||||
// Format: @username@domain.tld
|
||||
if strings.Count(input, "@") == 2 {
|
||||
fmt.Println("Detected Fediverse handle, using WebFinger resolution")
|
||||
return r.resolveHandle(input)
|
||||
}
|
||||
} else {
|
||||
// Format: username@domain.tld
|
||||
if strings.Count(input, "@") == 1 {
|
||||
fmt.Println("Detected Fediverse handle, using WebFinger resolution")
|
||||
return r.resolveHandle(input)
|
||||
|
@ -59,7 +74,6 @@ func (r *Resolver) Resolve(input string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// If we're not sure, try to treat it as a URL
|
||||
fmt.Println("Input format unclear, attempting URL resolution")
|
||||
return r.resolveURL(input)
|
||||
}
|
||||
|
@ -175,88 +189,67 @@ func (r *Resolver) resolveHandle(handle string) (string, error) {
|
|||
|
||||
// resolveURL resolves a Fediverse URL to its ActivityPub representation
|
||||
func (r *Resolver) resolveURL(inputURL string) (string, error) {
|
||||
// Parse the URL
|
||||
parsedURL, err := url.Parse(inputURL)
|
||||
// Always fetch the provided URL as-is, using ActivityPub Accept header and HTTP signatures
|
||||
// Then, if the response contains an `id` field that differs from the requested URL, fetch that recursively
|
||||
return r.resolveCanonicalActivityPub(inputURL, 0)
|
||||
}
|
||||
|
||||
// resolveCanonicalActivityPub fetches the ActivityPub object at the given URL, and if the response contains an `id` field
|
||||
// that differs from the requested URL, recursively fetches that canonical URL. Max depth is used to prevent infinite loops.
|
||||
func (r *Resolver) resolveCanonicalActivityPub(objectURL string, depth int) (string, error) {
|
||||
if depth > 3 {
|
||||
return "", fmt.Errorf("too many canonical redirects (possible loop)")
|
||||
}
|
||||
fmt.Printf("Fetching ActivityPub object for canonical resolution: %s\n", objectURL)
|
||||
raw, err := r.fetchActivityPubObjectRaw(objectURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing URL: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// For cross-instance URLs, we'll skip the redirect check
|
||||
// because some instances (like Mastodon) have complex redirect systems
|
||||
// that might not work reliably
|
||||
|
||||
// Check if this is a cross-instance URL (e.g., https://mastodon.social/@user@another.instance/123)
|
||||
username := parsedURL.Path
|
||||
if len(username) > 0 && username[0] == '/' {
|
||||
username = username[1:]
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return "", fmt.Errorf("error parsing ActivityPub JSON: %v", err)
|
||||
}
|
||||
|
||||
// Check if the username contains an @ symbol (indicating a cross-instance URL)
|
||||
if strings.HasPrefix(username, "@") && strings.Contains(username[1:], "@") {
|
||||
// This is a cross-instance URL
|
||||
fmt.Println("Detected cross-instance URL. Original instance:", strings.Split(username[1:], "/")[0])
|
||||
|
||||
// Extract the original instance, username, and post ID
|
||||
parts := strings.Split(username, "/")
|
||||
if len(parts) >= 2 {
|
||||
userParts := strings.Split(parts[0][1:], "@") // Remove the leading @ and split by @
|
||||
if len(userParts) == 2 {
|
||||
username := userParts[0]
|
||||
originalDomain := userParts[1]
|
||||
postID := parts[1]
|
||||
|
||||
fmt.Printf("Detected cross-instance URL. Original instance: %s, username: %s, post ID: %s\n",
|
||||
originalDomain, username, postID)
|
||||
|
||||
// Try different URL formats that are commonly used by different Fediverse platforms
|
||||
urlFormats := []string{
|
||||
// Mastodon format
|
||||
"https://%s/@%s/%s",
|
||||
"https://%s/users/%s/statuses/%s",
|
||||
// Pleroma format
|
||||
"https://%s/notice/%s",
|
||||
// Misskey format
|
||||
"https://%s/notes/%s",
|
||||
// Friendica format
|
||||
"https://%s/display/%s",
|
||||
// Hubzilla format
|
||||
"https://%s/item/%s",
|
||||
}
|
||||
|
||||
// Try each URL format
|
||||
for _, format := range urlFormats {
|
||||
var targetURL string
|
||||
if strings.Count(format, "%s") == 3 {
|
||||
// Format with username
|
||||
targetURL = fmt.Sprintf(format, originalDomain, username, postID)
|
||||
} else {
|
||||
// Format without username (just domain and ID)
|
||||
targetURL = fmt.Sprintf(format, originalDomain, postID)
|
||||
}
|
||||
|
||||
fmt.Printf("Trying URL format: %s\n", targetURL)
|
||||
|
||||
// Try to fetch with our signature-first approach
|
||||
result, err := r.fetchActivityPubObject(targetURL)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
fmt.Printf("Failed with error: %v\n", err)
|
||||
|
||||
// Add a delay between requests to avoid rate limiting
|
||||
fmt.Println("Waiting 2 seconds before trying next URL format...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// If all formats fail, return the last error
|
||||
return "", fmt.Errorf("failed to fetch content from original instance %s: all URL formats tried", originalDomain)
|
||||
}
|
||||
}
|
||||
idVal, ok := data["id"].(string)
|
||||
if ok && idVal != "" && idVal != objectURL {
|
||||
fmt.Printf("Found canonical id: %s (different from requested URL), following...\n", idVal)
|
||||
return r.resolveCanonicalActivityPub(idVal, depth+1)
|
||||
}
|
||||
// If no id or already canonical, format and return using helpers.go
|
||||
formatted, err := formatResult(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error formatting ActivityPub object: %v", err)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
// If not a cross-instance URL, fetch the ActivityPub object directly
|
||||
return r.fetchActivityPubObject(inputURL)
|
||||
// fetchActivityPubObjectRaw fetches an ActivityPub object and returns the raw JSON []byte (not formatted)
|
||||
func (r *Resolver) fetchActivityPubObjectRaw(objectURL string) ([]byte, error) {
|
||||
fmt.Printf("Fetching ActivityPub object with HTTP from: %s\n", objectURL)
|
||||
|
||||
req, err := http.NewRequest("GET", objectURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating get request: %v", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/ld+json, application/activity+json")
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error sending get request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("get request failed with status: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, fmt.Errorf("received empty response body")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// fetchActivityPubObject fetches an ActivityPub object from a URL
|
||||
|
@ -276,5 +269,13 @@ func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) {
|
|||
}
|
||||
|
||||
// Use our signature-first approach by default
|
||||
return r.fetchActivityPubObjectWithSignature(objectURL)
|
||||
raw, err := r.fetchActivityPubObjectWithSignature(objectURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching ActivityPub object: %v", err)
|
||||
}
|
||||
formatted, err := formatResult(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error formatting ActivityPub object: %v", err)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue