package mf import ( "brainbaking.com/go-jamming/common" "fmt" "strings" "time" "willnorris.com/go/microformats" ) const ( DateFormatWithTimeZone = "2006-01-02T15:04:05-07:00" dateFormatWithAbsoluteTimeZone = "2006-01-02T15:04:05-0700" dateFormatWithTimeZoneSuffixed = "2006-01-02T15:04:05.000Z" dateFormatWithoutTimeZone = "2006-01-02T15:04:05" dateFormatWithSecondsWithoutTimeZone = "2006-01-02T15:04:05.00Z" dateFormatWithoutTime = "2006-01-02" Anonymous = "anonymous" ) var ( // This is similar to Hugo's string-to-date casting system // See https://github.com/spf13/cast/blob/master/caste.go supportedFormats = []string{ DateFormatWithTimeZone, dateFormatWithAbsoluteTimeZone, dateFormatWithTimeZoneSuffixed, dateFormatWithSecondsWithoutTimeZone, dateFormatWithoutTimeZone, dateFormatWithoutTime, } ) type IndiewebAuthor struct { Name string `json:"name"` Picture string `json:"picture"` } func (ia *IndiewebAuthor) AnonymizePicture() { ia.Picture = fmt.Sprintf("/pictures/%s", Anonymous) } func (ia *IndiewebAuthor) AnonymizeName() { ia.Name = "Anonymous" } type IndiewebDataResult struct { Status string `json:"status"` Data []*IndiewebData `json:"json"` } func ResultFailure(data []*IndiewebData) IndiewebDataResult { return emptyNilData(IndiewebDataResult{ Status: "failure", Data: data, }) } func ResultSuccess(data []*IndiewebData) IndiewebDataResult { return emptyNilData(IndiewebDataResult{ Status: "success", Data: data, }) } func emptyNilData(result IndiewebDataResult) IndiewebDataResult { if result.Data == nil { result.Data = make([]*IndiewebData, 0) } return result } type IndiewebData struct { Author IndiewebAuthor `json:"author"` Name string `json:"name"` Content string `json:"content"` Published string `json:"published"` Url string `json:"url"` IndiewebType MfType `json:"type"` Source string `json:"source"` Target string `json:"target"` } func (id *IndiewebData) PublishedDate() time.Time { return common.ToTime(id.Published, DateFormatWithTimeZone) } func (id *IndiewebData) AsMention() Mention { return Mention{ Source: id.Source, Target: id.Target, } } func (id *IndiewebData) IsEmpty() bool { return id.Url == "" } func PublishedNow() string { return common.Now().UTC().Format(DateFormatWithTimeZone) } // Go stuff: entry.Properties["name"][0].(string), // JS stuff: hEntry.properties?.name?.[0] // The problem: convoluted syntax and no optional chaining! func Str(mf *microformats.Microformat, key string) string { val := mf.Properties[key] if len(val) == 0 { return "" } str, ok := val[0].(string) if !ok { // in very weird cases, it could be a map holding a value, like in mf2's "photo" valMap, ok2 := val[0].(map[string]string) if !ok2 { return "" } return valMap["value"] } return str } func Map(mf *microformats.Microformat, key string) map[string]string { val := mf.Properties[key] if len(val) == 0 { return map[string]string{} } mapVal, ok := val[0].(map[string]string) if !ok { return map[string]string{} } return mapVal } func HEntry(data *microformats.Data) *microformats.Microformat { return hItemType(data, "h-entry") } func HCard(data *microformats.Data) *microformats.Microformat { return hItemType(data, "h-card") } func hItemType(data *microformats.Data, hType string) *microformats.Microformat { for _, itm := range data.Items { if common.Includes(itm.Type, hType) { return itm } } return nil } func mfEmpty() *microformats.Microformat { return µformats.Microformat{ Properties: map[string][]interface{}{}, } } func Prop(mf *microformats.Microformat, key string) *microformats.Microformat { val := mf.Properties[key] if len(val) == 0 { return mfEmpty() } for i := range val { conv, ok := val[i].(*microformats.Microformat) if ok { return conv } } return mfEmpty() } func Published(hEntry *microformats.Microformat) string { publishedDate := Str(hEntry, "published") if publishedDate == "" { return PublishedNow() } for _, format := range supportedFormats { formatted, err := time.Parse(format, publishedDate) if err != nil { continue } return formatted.Format(DateFormatWithTimeZone) } return PublishedNow() } func NewAuthor(hEntry *microformats.Microformat, hCard *microformats.Microformat) IndiewebAuthor { name := "" if hCard != nil { name = DetermineAuthorName(hCard) } if name == "" { name = DetermineAuthorName(hEntry) } picture := DetermineAuthorPhoto(hEntry) if picture == "" { picture = DetermineAuthorPhoto(hCard) } return IndiewebAuthor{ Picture: picture, Name: name, } } func DetermineAuthorPhoto(hEntry *microformats.Microformat) string { photo := Str(Prop(hEntry, "author"), "photo") if photo == "" { photo = Str(hEntry, "photo") } return photo } func DetermineAuthorName(hEntry *microformats.Microformat) string { authorName := Str(Prop(hEntry, "author"), "name") if authorName == "" { authorName = Prop(hEntry, "author").Value } if authorName == "" { authorName = Str(hEntry, "author") } if authorName == "" { authorName = Str(hEntry, "name") } return authorName } type MfType string const ( TypeLink MfType = "link" TypeReply MfType = "reply" TypeLike MfType = "like" TypeBookmark MfType = "bookmark" TypeMention MfType = "mention" ) func Type(hEntry *microformats.Microformat) MfType { likeOf := Str(hEntry, "like-of") if likeOf != "" { return TypeLike } bookmarkOf := Str(hEntry, "bookmark-of") if bookmarkOf != "" { return TypeBookmark } return TypeMention } // Mastodon uids start with "tag:server", but we do want indieweb uids from other sources func Url(hEntry *microformats.Microformat, source string) string { uid := Str(hEntry, "uid") if uid != "" && strings.HasPrefix(uid, "http") { return uid } url := Str(hEntry, "url") if url != "" { return url } return source } func Content(hEntry *microformats.Microformat) string { bridgyTwitterContent := Str(hEntry, "bridgy-twitter-content") if bridgyTwitterContent != "" { return common.Shorten(bridgyTwitterContent) } summary := Str(hEntry, "summary") if summary != "" { return common.Shorten(summary) } contentEntry := Map(hEntry, "content")["value"] return common.Shorten(contentEntry) }