Browse Source

collection: added collection support and refactored variously

Benton Edmondson 2 years ago
parent
commit
13e984a27a
11 changed files with 471 additions and 225 deletions
  1. 4 2
      go.mod
  2. 4 2
      go.sum
  3. 9 1
      kinds/actor.go
  4. 153 0
      kinds/collection.go
  5. 196 10
      kinds/construct.go
  6. 5 1
      kinds/content.go
  7. 39 7
      kinds/extractor.go
  8. 13 7
      kinds/link.go
  9. 34 12
      kinds/post.go
  10. 0 160
      kinds/request.go
  11. 14 23
      main.go

+ 4 - 2
go.mod

@@ -1,8 +1,10 @@
 module mimicry
 
-go 1.19
+go 1.20
 
 require (
 	github.com/yuin/goldmark v1.5.4
-	golang.org/x/net v0.5.0
+	golang.org/x/net v0.8.0
 )
+
+require golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0

+ 4 - 2
go.sum

@@ -1,4 +1,6 @@
 github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
 github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=

+ 9 - 1
kinds/actor.go

@@ -9,6 +9,10 @@ import (
 
 type Actor Dict
 
+func (a Actor) Raw() Dict {
+	return a
+}
+
 func (a Actor) Kind() (string, error) {
 	kind, err := Get[string](a, "type")
 	return strings.ToLower(kind), err
@@ -58,7 +62,7 @@ func (a Actor) Bio() (string, error) {
 	return render.Render(body, mediaType, 80)
 }
 
-func (a Actor) String() (string, error) {
+func (a Actor) String(width int) (string, error) {
 	output := ""
 
 	name, err := a.InlineName()
@@ -71,4 +75,8 @@ func (a Actor) String() (string, error) {
 		output += bio
 	}
 	return output, nil
+}
+
+func (a Actor) Preview() (string, error) {
+	return "todo", nil
 }

+ 153 - 0
kinds/collection.go

@@ -0,0 +1,153 @@
+package kinds
+
+import (
+	"strings"
+	"net/url"
+	"strconv"
+)
+
+type Collection struct {
+	page Dict
+
+	// index *within the current page*
+	index int
+}
+
+func (c Collection) Raw() Dict {
+	return c.page
+}
+
+func (c Collection) Kind() (string, error) {
+	kind, err := Get[string](c.page, "type")
+	return strings.ToLower(kind), err
+}
+
+func (c Collection) Category() string {
+	return "collection"
+}
+
+func (c Collection) Identifier() (*url.URL, error) {
+	return GetURL(c.page, "id")
+}
+
+func (c Collection) String(width int) (string, error) {
+	elements := []string{}
+
+	const elementsToShow = 3
+	for len(elements) < elementsToShow {
+
+		current, err := c.Current()
+		if current == nil && err == nil {
+			break
+		}
+
+		if err != nil {
+			c.Next()
+			continue
+		}
+
+		output, err := current.Preview()
+		if err != nil {
+			return "", err
+		}
+
+		elements = append(elements, output)
+
+		c.Next()
+	}
+	
+	return strings.Join(elements, "\n"), nil
+}
+
+func (c Collection) Size() (string, error) {
+	value, err := Get[float64](c.page, "totalItems")
+	if err != nil {
+		return "", err
+	}
+	return strconv.Itoa(int(value)), nil
+}
+
+func (c Collection) items() []any {
+	itemsList, itemsErr := Get[[]any](c.page, "items")
+	if itemsErr == nil {
+		return itemsList
+	}
+	orderedItemsList, orderedItemsErr := Get[[]any](c.page, "orderedItems")
+	if orderedItemsErr == nil {
+		return orderedItemsList
+	}
+
+	return []any{}
+}
+
+func (c *Collection) Next() (Content, error) {
+	c.index += 1
+	return c.Current()
+}
+
+func (c *Collection) Previous() (Content, error) {
+	c.index -= 1
+	return c.Current()
+}
+
+/* This return type is a Option<Result<Content>>
+   where nil, nil represents None (end of collection)
+   nil, err represents Some(Err()) (current item failed construction)
+   x, nil represent Some(x) (current item)
+   and x, err is invalid */
+func (c *Collection) Current() (Content, error) {
+	items := c.items()
+	if len(items) == 0 {
+		kind, kindErr := c.Kind()
+		if kindErr != nil {
+			return nil, nil
+		}
+
+		/* If it is a collection, get the first page */
+		if kind == "collection" || kind == "orderedcollection" {
+			first, firstErr := GetItem[Collection](c.page, "first")
+			if firstErr != nil {
+				return nil, nil
+			}
+			c.page = first.page
+			c.index = 0
+			return c.Current()
+		}
+	}
+
+	/* At this point we know items are present */
+
+	/* This means we are beyond the end of this page */
+	if c.index >= len(items) {
+		next, err := GetItem[Collection](c.page, "next")
+		if err != nil {
+			return nil, nil
+		}
+		c.page = next.page
+		c.index = 0
+		/* Call recursively because the next page may be empty */
+		return c.Current()
+	} else if c.index < 0 {
+		prev, err := GetItem[Collection](c.page, "prev")
+		if err != nil {
+			return nil, nil
+		}
+		c.page = prev.page
+		items := c.items()
+		/* If this new page is empty, this will be -1, and the
+		   call to Current will flip back yet another page, as
+		   intended */
+		c.index = len(items)-1
+		return c.Current()
+	}
+
+	/* At this point we know index is within items */ 
+
+	id, _ := c.Identifier()
+
+	return FetchUnknown(items[c.index], id)
+}
+
+func (c Collection) Preview() (string, error) {
+	return "I will get rid of this function", nil
+}

+ 196 - 10
kinds/construct.go

@@ -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:
+	// converts a string (url) or Dict into an Item
+	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
+
+	// converts user input (webfinger, url, or local file) into 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:
+		// TODO: detect the 3 `Public` identifiers and error on them
+		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) // `nil` is body
+	if err != nil {
+		return nil, err
+	}
+
+	// add the accept header, some servers only respond if the optional
+	// content type is included as well
+	// § 3.2
+	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)
+
+	// GNU Social servers return 202 (with a correct body) instead of 200
+	if resp.StatusCode != 200 && resp.StatusCode != 202 {
+		return nil, errors.New("The server returned a status code of " + resp.Status)
+	}
+
+	// TODO: delete the pointless first if right here
+	// TODO: for the sake of static servers, accept application/json
+	//		 as well iff it contains the @context key (but double check
+	//		 that it is absolutely necessary)
+	if contentType := resp.Header.Get("Content-Type"); contentType == "" {
+		return nil, errors.New("The server's response did not contain a content type")
+
+	// TODO: accept application/ld+json, application/json, and application/activity+json as responses
+	} 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)
+}
+
 // TODO, add a verbose debugging output mode
 // to debug problems that arise with this thing
 // looping too much and whatnot
@@ -32,7 +126,7 @@ func Construct(unstructured Dict, source *url.URL) (Content, error) {
 	// if the JSON only has two keys (type and id), refetch
 	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":
-	// 	fallthrough
-	// case "OrderedCollection":
-	// 	return Collection{unstructured}, nil
-
-	// case "CollectionPage":
-	// 	fallthrough
-	// case "OrderedCollectionPage":
-	// 	return CollectionPage{unstructured}, 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) {
+	// description of WebFinger: https://www.rfc-editor.org/rfc/rfc7033.html
+
+	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) // `nil` is body
+	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
+}

+ 5 - 1
kinds/content.go

@@ -4,8 +4,11 @@ import (
 	"net/url"
 )
 
+// TODO: rename to Item
+// TODO: a collection should probably not be an item
 type Content interface {
-	String() (string, error)
+	String(width int) (string, error)
+	Preview() (string, error)
 	Kind() (string, error)
 	Category() string
 
@@ -14,4 +17,5 @@ type Content interface {
 	// if it is present and malformed, then use
 	// an error
 	Identifier() (*url.URL, error)
+	Raw() Dict
 }

+ 39 - 7
kinds/extractor.go

@@ -6,18 +6,29 @@ import (
 	"time"
 )
 
-// TODO throughout this file: attach the problematic object to the error
-// make these all methods on Dictionary
+// TODO: rename this file to dictionary.go
+// TODO: rename Dict to Dictionary
+
+// TODO: add a HasContent method. This is used when checking if
+// content exists, so Actors can apply mediaType to summary,
+// and Posts can apply it to url. In other situations (items vs
+// orderedItems, name vs preferredName) attempting to fallback
+// is always better than just failing, and for (absence of next/prev)
+// if I don't have next, I have no other option, so it is 
+// effectively the end of the line
+
+// TODO: read through the spec's discussion on security
+
 type Dict = map[string]any
 
 func Get[T any](o Dict, key string) (T, error) {
 	var zero T
 	if value, ok := o[key]; !ok {
 		return zero, errors.New("Object does not contain key " + key)
-	} else if value, ok := value.(T); !ok {
+	} else if narrowed, ok := value.(T); !ok {
 		return zero, errors.New("Key " + key + " is not of the desired type")
 	} else {
-		return value, nil
+		return narrowed, nil
 	}
 }
 
@@ -63,7 +74,7 @@ func GetURL(o Dict, key string) (*url.URL, error) {
 	}
 }
 
-// TODO: need to filter out the 3 public cases.
+// TODO: this needs to be switched over to using `GetItem`
 /*
 	`GetContent`
 	For a given key, return all values of type T.
@@ -99,7 +110,7 @@ func GetContent[T Content](d Dict, key string) ([]T, error) {
 				narrowed == "as:Public" || narrowed == "Public" { continue }
 			url, err := url.Parse(narrowed)
 			if err != nil { continue }
-			object, err := Fetch(url)
+			object, err := FetchURL(url)
 			if err != nil { continue }
 			asT, isT := object.(T)
 			if !isT { continue }
@@ -111,6 +122,26 @@ func GetContent[T Content](d Dict, key string) ([]T, error) {
 	return output, nil
 }
 
+func GetItem[T Content](d Dict, key string) (T, error) {
+	value, err := Get[any](d, key)
+	if err != nil {
+		var dummy T
+		return dummy, err
+	}
+
+	source, _ := GetURL(d, "id")
+
+	fetched, err := FetchUnknown(value, source)
+
+	asT, isT := fetched.(T)
+
+	if !isT {
+		errors.New("Fetched " + key + " on " + source.String() + " is not of the desired type")
+	}
+
+	return asT, nil
+}
+
 /*
 	`GetList`
 	For a given key, return the value if it is a
@@ -172,6 +203,7 @@ func GetLinksStrict(d Dict, key string) ([]Link, error) {
 				"mediaType": defaultMediaType,
 			})
 		case Dict:
+			// TODO: need to check this error?
 			source, err := GetURL(d, "id")
 			constructed, err := Construct(narrowed, source)
 			if err != nil { continue }
@@ -221,4 +253,4 @@ func GetLinksLenient(d Dict, key string) ([]Link, error) {
 	}
 
 	return output, nil
-}
+}

+ 13 - 7
kinds/link.go

@@ -8,8 +8,10 @@ import (
 
 type Link Dict
 
-// one of these should be omitted so
-// Link isn't Content
+func (l Link) Raw() Dict {
+	return l
+}
+
 func (l Link) Kind() (string, error) {
 	return "link", nil
 }
@@ -50,16 +52,15 @@ func (l Link) Identifier() (*url.URL, error) {
 
 // used for link prioritization, roughly
 // related to resolution
-func (l Link) rating() int {
-	height, err := Get[int](l, "height")
+func (l Link) rating() float64 {
+	height, err := Get[float64](l, "height")
 	if err != nil { height = 1 }
-	width, err := Get[int](l, "width")
+	width, err := Get[float64](l, "width")
 	if err != nil { width = 1 }
 	return height * width
 }
 
-// TODO: update of course to be nice markup of some sort
-func (l Link) String() (string, error) {
+func (l Link) String(width int) (string, error) {
 	output := ""
 
 	if alt, err := l.Alt(); err == nil {
@@ -75,7 +76,12 @@ func (l Link) String() (string, error) {
 	return output, nil
 }
 
+func (l Link) Preview() (string, error) {
+	return "todo", nil
+}
+
 // TODO: must test when list only has 1 link (probably works)
+// TODO: pass in *MediaType instead of supertype
 func SelectBestLink(links []Link, supertype string) (Link, error) {
 	if len(links) == 0 {
 		return nil, errors.New("Can't select best link of type " + supertype + "/* from an empty list")

+ 34 - 12
kinds/post.go

@@ -5,7 +5,6 @@ import (
 	"strings"
 	"time"
 	"mimicry/style"
-	"errors"
 	"mimicry/render"
 	"mimicry/ansi"
 )
@@ -71,6 +70,18 @@ func (p Post) Attachments() ([]Link, error) {
 	return GetLinksLenient(p, "attachment")
 }
 
+func (p Post) Comments() (Collection, error) {
+	replies, repliesErr := GetItem[Collection](p, "replies")
+	if repliesErr != nil {
+		comments, commentsErr := GetItem[Collection](p, "comments")
+		if commentsErr != nil {
+			return Collection{}, repliesErr
+		}
+		replies = comments
+	}
+	return replies, nil
+}
+
 func (p Post) Link() (Link, error) {
 	kind, err := p.Kind()
 	if err != nil {
@@ -85,10 +96,8 @@ func (p Post) Link() (Link, error) {
 	switch kind {
 	case "audio", "image", "video":
 		return SelectBestLink(links, kind)
-	case "article", "document", "note", "page":
+	default: // "article", "document", "note", "page":
 		return SelectFirstLink(links)
-	default:
-		return nil, errors.New("Link extraction is not supported for type " + kind)
 	}
 }
 
@@ -130,24 +139,23 @@ func (p Post) header(width int) (string, error) {
 	if created, err := p.Created(); err == nil {
 		const timeFormat = "3:04 pm on 2 Jan 2006"
 		output += " at " + style.Color(created.Format(timeFormat))
-		if edited, err := p.Updated(); err == nil {
-			output += "(edited at " + style.Color(edited.Format(timeFormat) + ")")
-		}
+		// if edited, err := p.Updated(); err == nil {
+		// 	output += " (edited at " + style.Color(edited.Format(timeFormat)) + ")"
+		// }
 	}
 
 	return ansi.Wrap(output, width), nil
 }
 
-func (p Post) String() (string, error) {
+func (p Post) String(width int) (string, error) {
 	output := ""
-	width := 100
 
-	if header, err := p.header(width - 2); err == nil {
+	if header, err := p.header(width - 4); err == nil {
 		output += ansi.Indent(header, "  ", true)
 		output += "\n\n"
 	}
 
-	if body, err := p.Body(width - 4); err == nil {
+	if body, err := p.Body(width - 8); err == nil {
 		output += ansi.Indent(body, "    ", true)
 		output += "\n\n"
 	}
@@ -157,7 +165,7 @@ func (p Post) String() (string, error) {
 			section := "Attachments:\n"
 			names := []string{}
 			for _, attachment := range attachments {
-				if name, err := attachment.String(); err == nil {
+				if name, err := attachment.String(width); err == nil {
 					names = append(names, style.Link(name))
 				}
 			}
@@ -168,6 +176,20 @@ func (p Post) String() (string, error) {
 		}
 	}
 
+	if comments, err := p.Comments(); err == nil {
+		if size, err := comments.Size(); err == nil {
+			output += ansi.Indent(ansi.Wrap("with " + style.Color(size + " comments"), width - 2), "  ", true)
+			output += "\n\n"
+		}
+		if section, err := comments.String(width); err == nil {
+			output += section + "\n"
+		} else {
+			return "", err
+		}
+	} else {
+		return "", err
+	}
+
 	return output, nil
 }
 

+ 0 - 160
kinds/request.go

@@ -1,160 +0,0 @@
-package kinds
-
-import (
-	"strings"
-	"net/http"
-	"net/url"
-	"errors"
-	"io/ioutil"
-	"encoding/json"
-	"fmt"
-)
-
-var client = &http.Client{}
-//var cache = TODO
-
-const requiredContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
-const optionalContentType = "application/activity+json"
-
-func Fetch(url *url.URL) (Content, error) {
-	link := url.String()
-
-	req, err := http.NewRequest("GET", link, nil) // `nil` is body
-	if err != nil {
-		return nil, err
-	}
-
-	// add the accept header, some servers only respond if the optional
-	// content type is included as well
-	// § 3.2
-	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 {
-		return nil, errors.New("The server returned a status code of " + resp.Status)
-	}
-
-	// TODO: delete the pointless first if right here
-	// TODO: for the sake of static servers, accept application/json
-	//		 as well iff it contains the @context key (but double check
-	//		 that it is absolutely necessary)
-	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)
-}
-
-func FetchWebFinger(username string) (Actor, error) {
-	// description of WebFinger: https://www.rfc-editor.org/rfc/rfc7033.html
-
-	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) // `nil` is body
-	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())
-	}
-
-	content, err := Fetch(underlyingLink)
-	if err != nil { return nil, err }
-
-	actor, ok := content.(Actor)
-	if !ok { return nil, errors.New("content returned by the WebFinger request was not an Actor") }
-
-	return actor, nil
-}
-
-func FetchUnknown(unknown string) (Content, error) {
-	if strings.HasPrefix(unknown, "@") {
-		return FetchWebFinger(unknown)
-	}
-
-	url, err := url.Parse(unknown)
-	if err != nil {
-		return nil, err
-	}
-
-	return Fetch(url)
-}

+ 14 - 23
main.go

@@ -9,49 +9,40 @@ import (
 	// "mimicry/render"
 )
 
-// TODO: even if only supported in few terminals,
-// consider using the proportional spacing codes when possible
-
 // TODO: when returning errors, use zero value for return
 // also change all error messages to using sprintf-style
 // formatting, all lowercase, and no punctuation
 
-func main() {
-	// fmt.Println(style.Bold("Bold") + "\tNot Bold")
-	// fmt.Println(style.Strikethrough("Strikethrough") + "\tNot Strikethrough")
-	// fmt.Println(style.Underline("Underline") + "\tNot Underline")
-	// fmt.Println(style.Italic("Italic") + "\tNot Italic")
-	// fmt.Println(style.Code("Code") + "\tNot Code")
-	// fmt.Println(style.Highlight("Highlight") + "\tNot Highlight")
-
-	// fmt.Println(style.Highlight("Stuff here " + style.Code("CODE") + " more here"))
-	// fmt.Println(style.Bold("struff " + style.Strikethrough("bad") + " more stuff"))
-
-	// fmt.Println(style.Linkify("Hello!"))
+// TODO: get rid of Raw, just use jtp.Get and then stringify the result
 
-	// output, err := render.Render("<p>Hello<code>hi</code> Everyone</p><i>@everyone</i> <blockquote>please<br>don't!</blockquote>", "text/html")
-	// if err != nil {
-	// 	panic(err)
-	// }
-	// fmt.Println(output)
+func main() {
 
 	link := os.Args[len(os.Args)-1]
 	command := os.Args[1]
 
-	content, err := kinds.FetchUnknown(link)
+	content, err := kinds.FetchUserInput(link)
 	if err != nil {
 		panic(err)
 	}
 
 	if command == "raw" {
 		enc := json.NewEncoder(os.Stdout)
-		if err := enc.Encode(content); err != nil {
+		if err := enc.Encode(content.Raw()); err != nil {
 			panic(err)
 		}
 		return
 	}
 
-	if str, err := content.String(); err != nil {
+	// if narrowed, ok := content.(kinds.Post); ok {
+	// 	if str, err := narrowed.Preview(); err != nil {
+	// 		panic(err)
+	// 	} else {
+	// 		fmt.Print(str)
+	// 	}
+	// 	return
+	// }
+
+	if str, err := content.String(90); err != nil {
 		panic(err)
 	} else {
 		fmt.Print(str)