|
@@ -3,8 +3,102 @@ package kinds
|
|
|
import (
|
|
|
"errors"
|
|
|
"net/url"
|
|
|
+ "strings"
|
|
|
+ "net/http"
|
|
|
+ "io/ioutil"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
)
|
|
|
|
|
|
+
|
|
|
+ TODO: updated plan:
|
|
|
+ I need a function which accepts a string (url) or dict and converts
|
|
|
+ it into an Item (Currently under GetContent)
|
|
|
+
|
|
|
+ I need another function which accepts a string (webfinger or url) and converts
|
|
|
+ it into an Item (currently under FetchUnkown)
|
|
|
+
|
|
|
+ Namings:
|
|
|
+
|
|
|
+ FetchUnknown: any (a url.URL or Dict) -> Item
|
|
|
+ If input is a string, converts it via:
|
|
|
+ return FetchURL: url.URL -> Item
|
|
|
+ return Construct: Dict -> Item
|
|
|
+
|
|
|
+
|
|
|
+ FetchUserInput: string -> Item
|
|
|
+ If input starts with @, converts it via:
|
|
|
+ ResolveWebfinger: string -> url.URL
|
|
|
+ return FetchURL: url.URL -> Item
|
|
|
+*/
|
|
|
+
|
|
|
+var client = &http.Client{}
|
|
|
+
|
|
|
+const requiredContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
|
|
+const optionalContentType = "application/activity+json"
|
|
|
+
|
|
|
+func FetchUnknown(input any, source *url.URL) (Content, error) {
|
|
|
+ switch narrowed := input.(type) {
|
|
|
+ case string:
|
|
|
+
|
|
|
+ url, err := url.Parse(narrowed)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return FetchURL(url)
|
|
|
+ case Dict:
|
|
|
+ return Construct(narrowed, source)
|
|
|
+ default:
|
|
|
+ return nil, errors.New("Can't resolve non-string, non-Dict into Item.")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func FetchURL(url *url.URL) (Content, error) {
|
|
|
+ link := url.String()
|
|
|
+
|
|
|
+ req, err := http.NewRequest("GET", link, nil)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ req.Header.Add("Accept", fmt.Sprintf("%s, %s", requiredContentType, optionalContentType))
|
|
|
+
|
|
|
+ resp, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ defer resp.Body.Close()
|
|
|
+ body, err := ioutil.ReadAll(resp.Body)
|
|
|
+
|
|
|
+
|
|
|
+ if resp.StatusCode != 200 && resp.StatusCode != 202 {
|
|
|
+ return nil, errors.New("The server returned a status code of " + resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if contentType := resp.Header.Get("Content-Type"); contentType == "" {
|
|
|
+ return nil, errors.New("The server's response did not contain a content type")
|
|
|
+
|
|
|
+
|
|
|
+ } else if !strings.Contains(contentType, requiredContentType) && !strings.Contains(contentType, optionalContentType) {
|
|
|
+ return nil, errors.New("The server responded with the invalid content type of " + contentType)
|
|
|
+ }
|
|
|
+
|
|
|
+ var unstructured map[string]any
|
|
|
+ if err := json.Unmarshal(body, &unstructured); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return Construct(unstructured, url)
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
@@ -32,7 +126,7 @@ func Construct(unstructured Dict, source *url.URL) (Content, error) {
|
|
|
|
|
|
if (source != nil && id != nil) {
|
|
|
if (source.Hostname() != id.Hostname()) || (len(unstructured) <= 2 && hasIdentifier) {
|
|
|
- return Fetch(id)
|
|
|
+ return FetchURL(id)
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -65,17 +159,109 @@ func Construct(unstructured Dict, source *url.URL) (Content, error) {
|
|
|
link = unstructured
|
|
|
return link, nil
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+ case "Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage":
|
|
|
+ collection := Collection{}
|
|
|
+ collection = Collection{unstructured, 0}
|
|
|
+ return collection, nil
|
|
|
|
|
|
default:
|
|
|
return nil, errors.New("Object of Type " + kind + " unsupported")
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+func FetchUserInput(text string) (Content, error) {
|
|
|
+ if strings.HasPrefix(text, "@") {
|
|
|
+ link, err := ResolveWebfinger(text)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return FetchURL(link)
|
|
|
+ } else {
|
|
|
+ link, err := url.Parse(text)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return FetchURL(link)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func ResolveWebfinger(username string) (*url.URL, error) {
|
|
|
+
|
|
|
+
|
|
|
+ username = strings.TrimPrefix(username, "@")
|
|
|
+
|
|
|
+ split := strings.Split(username, "@")
|
|
|
+ var account, domain string
|
|
|
+ if len(split) != 2 {
|
|
|
+ return nil, errors.New("webfinger address must have a separating @ symbol")
|
|
|
+ } else {
|
|
|
+ account = split[0]
|
|
|
+ domain = split[1]
|
|
|
+ }
|
|
|
+
|
|
|
+ query := url.Values{}
|
|
|
+ query.Add("resource", fmt.Sprintf("acct:%s@%s", account, domain))
|
|
|
+ query.Add("rel", "self")
|
|
|
+
|
|
|
+ link := url.URL{
|
|
|
+ Scheme: "https",
|
|
|
+ Host: domain,
|
|
|
+ Path: "/.well-known/webfinger",
|
|
|
+ RawQuery: query.Encode(),
|
|
|
+ }
|
|
|
+
|
|
|
+ req, err := http.NewRequest("GET", link.String(), nil)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := client.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ defer resp.Body.Close()
|
|
|
+ body, err := ioutil.ReadAll(resp.Body)
|
|
|
+
|
|
|
+ if resp.StatusCode != 200 {
|
|
|
+ return nil, errors.New(fmt.Sprintf("the server responded to the WebFinger query %s with %s", link.String(), resp.Status))
|
|
|
+ } else if contentType := resp.Header.Get("Content-Type"); !strings.Contains(contentType, "application/jrd+json") && !strings.Contains(contentType, "application/json") {
|
|
|
+ return nil, errors.New("the server responded to the WebFinger query with invalid Content-Type " + contentType)
|
|
|
+ }
|
|
|
+
|
|
|
+ var jrd Dict
|
|
|
+ if err := json.Unmarshal(body, &jrd); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ jrdLinks, err := GetList(jrd, "links")
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ var underlyingLink *url.URL = nil
|
|
|
+
|
|
|
+ for _, el := range jrdLinks {
|
|
|
+ jrdLink, ok := el.(Dict)
|
|
|
+ if ok {
|
|
|
+ rel, err := Get[string](jrdLink, "rel")
|
|
|
+ if err != nil { continue }
|
|
|
+ if rel != "self" { continue }
|
|
|
+ mediaType, err := Get[string](jrdLink, "type")
|
|
|
+ if err != nil { continue }
|
|
|
+ if !strings.Contains(mediaType, requiredContentType) && !strings.Contains(mediaType, optionalContentType) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ href, err := GetURL(jrdLink, "href")
|
|
|
+ if err != nil { continue }
|
|
|
+ underlyingLink = href
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if underlyingLink == nil {
|
|
|
+ return nil, errors.New("no matching href was found in the links array of " + link.String())
|
|
|
+ }
|
|
|
+
|
|
|
+ return underlyingLink, nil
|
|
|
+}
|