Browse Source

have created all list parsing methods

Benton Edmondson 2 years ago
parent
commit
9029ed483c
9 changed files with 319 additions and 52 deletions
  1. 2 2
      kinds/actor.go
  2. 12 26
      kinds/construct.go
  3. 11 1
      kinds/content.go
  4. 126 13
      kinds/extractor.go
  5. 109 2
      kinds/link.go
  6. 40 3
      kinds/post.go
  7. 4 3
      kinds/request.go
  8. 2 1
      main.go
  9. 13 1
      style/style.go

+ 2 - 2
kinds/actor.go

@@ -51,7 +51,7 @@ func (a Actor) Bio() (string, error) {
 	return strings.TrimSpace(bio), err
 }
 
-func (a Actor) String() string {
+func (a Actor) String() (string, error) {
 	output := ""
 
 	name, err := a.InlineName()
@@ -68,5 +68,5 @@ func (a Actor) String() string {
 		output += "\n"
 		output += bio
 	}
-	return output
+	return output, nil
 }

+ 12 - 26
kinds/construct.go

@@ -18,32 +18,24 @@ func Construct(unstructured Dict, source *url.URL) (Content, error) {
 		return nil, err
 	}
 
+	// this requirement should be removed, and the below check
+	// should be checking if only type or only type and id
+	// are present on the element
+	hasIdentifier := true
 	id, err := GetURL(unstructured, "id")
 	if err != nil {
-		return nil, err
+		hasIdentifier = false
 	}
 
 	// if the JSON came from a source (e.g. inline in another collection), with a
 	// different hostname than its ID, refetch
 	// if the JSON only has two keys (type and id), refetch
-	if source != nil && source.Hostname() != id.Hostname() || len(unstructured) <= 2 {
+	if (source != nil && source.Hostname() != id.Hostname()) || (len(unstructured) <= 2 && hasIdentifier) {
 		return Fetch(id)
 	}
 
 	switch kind {
-	case "Article":
-		fallthrough
-	case "Audio":
-		fallthrough
-	case "Document":
-		fallthrough
-	case "Image":
-		fallthrough
-	case "Note":
-		fallthrough
-	case "Page":
-		fallthrough
-	case "Video":
+	case "Article", "Audio", "Document", "Image", "Note", "Page", "Video":
 		// TODO: figure out the way to do this directly
 		post := Post{}
 		post = unstructured
@@ -60,22 +52,16 @@ func Construct(unstructured Dict, source *url.URL) (Content, error) {
 	// case "Question":
 	// 	return Activity{unstructured}, nil
 
-	case "Application":
-		fallthrough
-	case "Group":
-		fallthrough
-	case "Organization":
-		fallthrough
-	case "Person":
-		fallthrough
-	case "Service":
+	case "Application", "Group", "Organization", "Person", "Service":
 		// TODO: nicer way to do this?
 		actor := Actor{}
 		actor = unstructured
 		return actor, nil
 
-	// case "Link":
-	// 	return Link{unstructured}, nil
+	case "Link":
+		link := Link{}
+		link = unstructured
+		return link, nil
 
 	// case "Collection":
 	// 	fallthrough

+ 11 - 1
kinds/content.go

@@ -1,7 +1,17 @@
 package kinds
 
+import (
+	"net/url"
+)
+
 type Content interface {
-	String() string
+	String() (string, error)
 	Kind() (string, error)
 	Category() string
+
+	// if the id field is absent or nil, then
+	// this should return (nil, nil),
+	// if it is present and malformed, then use
+	// an error
+	Identifier() (*url.URL, error)
 }

+ 126 - 13
kinds/extractor.go

@@ -63,24 +63,26 @@ func GetURL(o Dict, key string) (*url.URL, error) {
 	}
 }
 
-func GetContent[T Content](d Dict, key string) ([]T, error) {
-
-	value, ok := d["attributedTo"]
-	if !ok {
-		return []T{}, nil
-	}
-
-	list := []any{}
+// TODO: need to filter out the 3 public cases.
+/*
+	`GetContent`
+	For a given key, return all values of type T.
+	Strings are interpreted as URLs and fetched.
+	The Public address representations mentioned
+	in §5.6 are ignored.
 	
-	if valueList, isList := value.([]any); isList {
-		list = valueList
-	} else {
-		list = []any{value}
+	Used for `Post.attributedTo` and `Post.inReplyTo`,
+	for instance.
+*/
+func GetContent[T Content](d Dict, key string) ([]T, error) {
+	values, err := GetList(d, key)
+	if err != nil {
+		return nil, err
 	}
 	
 	output := []T{}
 	
-	for _, el := range list {
+	for _, el := range values {
 		switch narrowed := el.(type) {
 		case Dict:
 			// TODO: if source is absent, must refetch
@@ -92,6 +94,9 @@ func GetContent[T Content](d Dict, key string) ([]T, error) {
 			if !isT { continue }
 			output = append(output, asT)
 		case string:
+			// §5.6
+			if narrowed == "https://www.w3.org/ns/activitystreams#Public" ||
+				narrowed == "as:Public" || narrowed == "Public" { continue }
 			url, err := url.Parse(narrowed)
 			if err != nil { continue }
 			object, err := Fetch(url)
@@ -104,5 +109,113 @@ func GetContent[T Content](d Dict, key string) ([]T, error) {
 	}
 
 	return output, nil
+}
+
+/*
+	`GetList`
+	For a given key, return the value if it is a
+	slice, if not, make it a slice of size 1
+*/
+func GetList(d Dict, key string) ([]any, error) {
+	value, err := Get[any](d, key)
+	if err != nil { return []any{}, err }
+	if asList, isList := value.([]any); isList {
+		return asList, nil
+	} else {
+		return []any{value}, nil
+	}
+}
+
+/*
+	`GetLinks`
+	Returns a list
+	of Links. Strings are interpreted as Links and
+	are not fetched. If d.content is absent, d.mediaType
+	is interpreted as applying to these strings.
+	Non-string, non-Link elements are ignored.
+
+	Used exclusively for `Post.url`.
+*/
+func GetLinks(d Dict, key string) ([]Link, error) {
+	values, err := GetList(d, "url")
+	if err != nil {
+		return nil, err
+	}
 	
+	output := []Link{}
+
+	// if content is absent and mediaType is present,
+	// mediaType applies to the Links
+	// name applies to the Links
+	// nil/null represents absence
+	var defaultMediaType any // (string | nil)
+	mediaType, mediaTypeErr := Get[string](d, "mediaType")
+	_, contentErr := GetNatural(d, "content", "en")
+	if mediaTypeErr != nil || contentErr == nil {
+		defaultMediaType = nil
+	} else { defaultMediaType = mediaType }
+	var defaultName any // (string | nil)
+	if name, nameErr := Get[string](d, "name"); nameErr != nil {
+		defaultName = name
+	} else { defaultName = nil }
+
+	for _, el := range values {
+		switch narrowed := el.(type) {
+		case string:
+			output = append(output, Link{
+				"type": "Link",
+				"href": narrowed,
+				"name": defaultName,
+				"mediaType": defaultMediaType,
+			})
+		case Dict:
+			source, err := GetURL(d, "id")
+			constructed, err := Construct(narrowed, source)
+			if err != nil { continue }
+			switch narrowedConstructed := constructed.(type) {
+			case Link:
+				output = append(output, narrowedConstructed)
+			// TODO: ignore this case
+			case Post:
+				if postLink, err := narrowedConstructed.Link(); err != nil {
+					output = append(output, postLink)
+				} else { continue }
+			default: continue
+			}
+		default: continue
+		}
+	}
+
+	return output, nil
 }
+
+/*
+	`GetAsLinks`
+	Similar to `GetLinks`, but converts Posts
+	to Links instead of ignoring them, and treats
+	strings as URLs (not Links) and fetches them.
+
+	Used for `Post.attachment`, `Actor.icon`, etc.
+*/
+func GetAsLinks(d Dict, key string) ([]Link, error) {
+	values, err := GetContent[Content](d, key)
+	if err != nil {
+		return []Link{}, err
+	}
+
+	output := []Link{}
+
+	for _, el := range values {
+		switch narrowed := el.(type) {
+		case Link:
+			output = append(output, narrowed)
+		case Post:
+			if link, err := narrowed.Link(); err == nil {
+				output = append(output, link)
+			} else { continue }
+		default: continue
+		}
+	}
+
+	return output, nil
+}

+ 109 - 2
kinds/link.go

@@ -6,10 +6,11 @@ import (
 
 type Link Dict
 
+// one of these should be omitted so
+// Link isn't Content
 func (l Link) Kind() (string, error) {
 	return "link", nil
 }
-
 func (l Link) Category() string {
 	return "link"
 }
@@ -24,4 +25,110 @@ func (l Link) URL() (*url.URL, error) {
 
 func (l Link) Alt() (string, error) {
 	return Get[string](l, "name")
-}
+}
+
+func (l Link) Identifier() (*url.URL, error) {
+	return nil, nil
+}
+
+// TODO: update of course to be nice markup of some sort
+func (l Link) String() (string, error) {
+	if url, err := l.URL(); err == nil {
+		return url.String(), nil
+	} else {
+		return "", err
+	}
+}
+
+// guide:
+// Audio, Image, Video
+// filter for ones with audio/, image/, video/
+// as mime type, tiebreaker is resolution
+// otherwise just take what you can get
+// Article, Note, Page, Document
+// probably honestly just take the first one
+
+// probably provide the priorities as lists
+// then write a function that looks up the list
+
+// var priorities = map[string][]string{
+// 	"image": []string{""}
+// }
+
+// given a Post, find the best link
+// func GetLink(p Post) (Link, error) {
+// 	kind, err := p.Kind()
+// 	if err != nil {
+// 		return nil, err
+// 	}
+// 	switch kind {
+// 	// case "audio":
+// 	// 	fallthrough
+// 	// case "image":
+// 	// 	fallthrough
+// 	// case "video":
+// 	// 	return GetBestLink(p)
+// 	case "article":
+// 		fallthrough
+// 	case "document":
+// 		fallthrough
+// 	case "note":
+// 		fallthrough
+// 	case "page":
+// 		return GetFirstLink(p)
+// 	default:
+// 		return nil, errors.New("Link extraction is not supported for type " + kind)
+// 	}
+// }
+
+// pulls the link with the mime type that
+// matches the Kind of the post, used for
+// image, audio, video
+
+// the reason this can't use GetContent is because GetContent
+// treats strings as URLs used to find the end object,
+// whereas in this context strings are URLs that are the href
+// being the endpoint the Link represents
+// func GetBestLink(p Post) (Link, error) {
+
+// }
+
+// pulls the first link
+// func GetFirstLink(p Post) (Link, error) {
+// 	values, err := GetList(p, "url")
+// 	if err != nil {
+// 		return nil, err
+// 	}
+	
+// 	var individual any
+
+// 	if len(values) == 0 {
+// 		return nil, errors.New("Link is an empty list on the post")
+// 	} else {
+// 		individual = values[0]
+// 	}
+
+// 	switch narrowed := individual.(type) {
+// 	case string:
+// 		// here I should build the link out of the outer object
+// 		return Link{"type": "Link", "href": narrowed}, nil
+// 	case Dict:
+// 		return Construct(narrowed)
+// 	default:
+// 		return nil, errors.New("The first URL entry on the post is a non-string, non-object. What?")
+// 	}
+
+// }
+
+//
+// GetLinks(p Post)
+// similar to GetContent, but treats strings
+// as Link.href, not as a reference to an object
+// that should be fulfilled
+// so whereas GetContent uses networking, GetLink
+// does not
+
+
+// GetBestLink - uses mime types/resolutions to determine best link
+// of a list of Links
+

+ 40 - 3
kinds/post.go

@@ -6,6 +6,7 @@ import (
 	"time"
 	"mimicry/style"
 	"fmt"
+	"errors"
 )
 
 type Post Dict
@@ -29,7 +30,11 @@ func (p Post) Body() (string, error) {
 
 func (p Post) BodyPreview() (string, error) {
 	body, err := p.Body()
-	return fmt.Sprintf("%s…", string([]rune(body)[:280])), err
+	if len(body) > 280*2 { // pretty much arbitrary length >280
+		return fmt.Sprintf("%s…", string([]rune(body)[:280])), err
+	} else {
+		return body, err
+	}
 }
 
 func (p Post) Identifier() (*url.URL, error) {
@@ -52,7 +57,32 @@ func (p Post) Creators() ([]Actor, error) {
 	return GetContent[Actor](p, "attributedTo")
 }
 
-func (p Post) String() string {
+// func (p Post) bestLink() (Link, error) {
+
+// }
+
+func (p Post) Link() (Link, error) {
+	kind, err := p.Kind()
+	if err != nil {
+		return nil, err
+	}
+	switch kind {
+	// case "audio", "image", "video":
+	// 	return GetBestLink(p)
+	case "article", "document", "note", "page":
+		if links, err := GetLinks(p, "url"); err != nil {
+			return nil, err
+		} else if len(links) == 0 {
+			return nil, err
+		} else {
+			return links[0], nil
+		}
+	default:
+		return nil, errors.New("Link extraction is not supported for type " + kind)
+	}
+}
+
+func (p Post) String() (string, error) {
 	output := ""
 
 	if title, err := p.Title(); err == nil {
@@ -79,5 +109,12 @@ func (p Post) String() string {
 		}
 	}
 
-	return strings.TrimSpace(output)
+	if link, err := p.Link(); err == nil {
+		if linkStr, err := link.String(); err == nil {
+			output += "\n"
+			output += linkStr
+		}
+	}
+
+	return strings.TrimSpace(output), nil
 }

+ 4 - 3
kinds/request.go

@@ -34,8 +34,11 @@ func Fetch(url *url.URL) (Content, error) {
 		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 " + string(resp.StatusCode))
+		return nil, errors.New("The server returned a status code of " + resp.Status)
 	}
 
 	if contentType := resp.Header.Get("Content-Type"); contentType == "" {
@@ -44,8 +47,6 @@ func Fetch(url *url.URL) (Content, error) {
 		return nil, errors.New("The server responded with the invalid content type of " + contentType)
 	}
 
-	defer resp.Body.Close()
-	body, err := ioutil.ReadAll(resp.Body)
 	var unstructured map[string]any
 	if err := json.Unmarshal(body, &unstructured); err != nil {
 		return nil, err

+ 2 - 1
main.go

@@ -30,5 +30,6 @@ func main() {
 		return
 	}
 
-	fmt.Println(object.String())
+	str, _ := object.String()
+	fmt.Println(str)
 }

+ 13 - 1
style/style.go

@@ -4,10 +4,22 @@ import (
 	"fmt"
 )
 
+// const (
+// 	Bold = 
+// )
+
 func Display(text string, code int) string {
 	return fmt.Sprintf("\x1b[%dm%s\x1b[0m", code, text)
 }
 
 func Bold(text string) string {
 	return Display(text, 1)
-}
+}
+
+// func Underline(text string) string {
+// 	return Display(text, )
+// }
+
+// func Anchor(text string) string {
+
+// }