mirror of
https://gitlab.melroy.org/melroy/fediresolve.git
synced 2025-06-07 20:08:57 +00:00
Remove urlFormats from resolver.go
This commit is contained in:
parent
3f377e14c0
commit
5d08981a5f
2 changed files with 84 additions and 78 deletions
|
@ -515,3 +515,8 @@ func (r *Resolver) ResolveObjectOrNodeInfo(objectURL string) ([]byte, map[string
|
||||||
func FormatHelperResult(raw []byte, nodeinfo map[string]interface{}) (string, error) {
|
func FormatHelperResult(raw []byte, nodeinfo map[string]interface{}) (string, error) {
|
||||||
return formatter.Format(nodeinfo)
|
return formatter.Format(nodeinfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatCanonicalResultHelper is used by resolver.go to format ActivityPub objects without importing formatter there
|
||||||
|
func formatCanonicalResultHelper(jsonData []byte, data map[string]interface{}) (string, error) {
|
||||||
|
return formatter.Format(data)
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func (r *Resolver) Resolve(input string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
formatted, ferr := FormatHelperResult(raw, nodeinfo)
|
formatted, ferr := r.formatCanonicalResultHelper(raw, nodeinfo)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
return string(raw), nil
|
return string(raw), nil
|
||||||
}
|
}
|
||||||
|
@ -189,88 +189,89 @@ func (r *Resolver) resolveHandle(handle string) (string, error) {
|
||||||
|
|
||||||
// resolveURL resolves a Fediverse URL to its ActivityPub representation
|
// resolveURL resolves a Fediverse URL to its ActivityPub representation
|
||||||
func (r *Resolver) resolveURL(inputURL string) (string, error) {
|
func (r *Resolver) resolveURL(inputURL string) (string, error) {
|
||||||
// Parse the URL
|
// Always fetch the provided URL as-is, using ActivityPub Accept header and HTTP signatures
|
||||||
parsedURL, err := url.Parse(inputURL)
|
// 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)
|
||||||
|
jsonData, err := r.fetchActivityPubObjectWithSignatureRaw(objectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error parsing URL: %v", err)
|
return "", err
|
||||||
|
}
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal(jsonData, &data); err != nil {
|
||||||
|
return "", fmt.Errorf("error parsing ActivityPub JSON: %v", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
return r.formatCanonicalResultHelper(jsonData, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For cross-instance URLs, we'll skip the redirect check
|
// fetchActivityPubObjectWithSignatureRaw fetches an ActivityPub object and returns the raw JSON []byte (not formatted)
|
||||||
// because some instances (like Mastodon) have complex redirect systems
|
func (r *Resolver) fetchActivityPubObjectWithSignatureRaw(objectURL string) ([]byte, error) {
|
||||||
// that might not work reliably
|
fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL)
|
||||||
|
|
||||||
// Check if this is a cross-instance URL (e.g., https://mastodon.social/@user@another.instance/123)
|
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
|
||||||
username := parsedURL.Path
|
if err != nil {
|
||||||
if len(username) > 0 && username[0] == '/' {
|
return nil, fmt.Errorf("Could not extract actor URL: %v", err)
|
||||||
username = username[1:]
|
}
|
||||||
|
actorData, err := r.fetchActorData(actorURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not fetch actor data: %v", err)
|
||||||
|
}
|
||||||
|
keyID, _, err := r.extractPublicKey(actorData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not extract public key: %v", err)
|
||||||
|
}
|
||||||
|
privateKey, err := generateRSAKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not generate RSA key: %v", err)
|
||||||
|
}
|
||||||
|
// Sign and send the request
|
||||||
|
req, err := http.NewRequest("GET", objectURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating signed 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))
|
||||||
|
if err := signRequest(req, keyID, privateKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not sign request: %v", err)
|
||||||
|
}
|
||||||
|
resp, err := r.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error sending signed request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("signed 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the username contains an @ symbol (indicating a cross-instance URL)
|
// formatCanonicalResultHelper formats the ActivityPub object for display
|
||||||
if strings.HasPrefix(username, "@") && strings.Contains(username[1:], "@") {
|
func (r *Resolver) formatCanonicalResultHelper(jsonData []byte, data map[string]interface{}) (string, error) {
|
||||||
// This is a cross-instance URL
|
// Call the new helper method from helpers.go
|
||||||
fmt.Println("Detected cross-instance URL. Original instance:", strings.Split(username[1:], "/")[0])
|
return formatCanonicalResultHelper(jsonData, data)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not a cross-instance URL, fetch the ActivityPub object directly
|
|
||||||
return r.fetchActivityPubObject(inputURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchActivityPubObject fetches an ActivityPub object from a URL
|
// fetchActivityPubObject fetches an ActivityPub object from a URL
|
||||||
|
|
Loading…
Add table
Reference in a new issue