git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/feeds/atom.go (about) 1 package feeds 2 3 import ( 4 "encoding/xml" 5 "fmt" 6 "net/url" 7 "time" 8 9 "git.sr.ht/~pingoo/stdx/uuid" 10 ) 11 12 // Generates Atom feed as XML 13 14 const ns = "http://www.w3.org/2005/Atom" 15 16 type AtomPerson struct { 17 Name string `xml:"name,omitempty"` 18 Uri string `xml:"uri,omitempty"` 19 Email string `xml:"email,omitempty"` 20 } 21 22 type AtomSummary struct { 23 XMLName xml.Name `xml:"summary"` 24 Content string `xml:",chardata"` 25 Type string `xml:"type,attr"` 26 } 27 28 type AtomContent struct { 29 XMLName xml.Name `xml:"content"` 30 Content string `xml:",chardata"` 31 Type string `xml:"type,attr"` 32 } 33 34 type AtomAuthor struct { 35 XMLName xml.Name `xml:"author"` 36 AtomPerson 37 } 38 39 type AtomContributor struct { 40 XMLName xml.Name `xml:"contributor"` 41 AtomPerson 42 } 43 44 type AtomEntry struct { 45 XMLName xml.Name `xml:"entry"` 46 Xmlns string `xml:"xmlns,attr,omitempty"` 47 Title string `xml:"title"` // required 48 Updated string `xml:"updated"` // required 49 Id string `xml:"id"` // required 50 Category string `xml:"category,omitempty"` 51 Content *AtomContent 52 Rights string `xml:"rights,omitempty"` 53 Source string `xml:"source,omitempty"` 54 Published string `xml:"published,omitempty"` 55 Contributor *AtomContributor 56 Links []AtomLink // required if no child 'content' elements 57 Summary *AtomSummary // required if content has src or content is base64 58 Author *AtomAuthor // required if feed lacks an author 59 } 60 61 // Multiple links with different rel can coexist 62 type AtomLink struct { 63 //Atom 1.0 <link rel="enclosure" type="audio/mpeg" title="MP3" href="http://www.example.org/myaudiofile.mp3" length="1234" /> 64 XMLName xml.Name `xml:"link"` 65 Href string `xml:"href,attr"` 66 Rel string `xml:"rel,attr,omitempty"` 67 Type string `xml:"type,attr,omitempty"` 68 Length string `xml:"length,attr,omitempty"` 69 } 70 71 type AtomFeed struct { 72 XMLName xml.Name `xml:"feed"` 73 Xmlns string `xml:"xmlns,attr"` 74 Title string `xml:"title"` // required 75 Id string `xml:"id"` // required 76 Updated string `xml:"updated"` // required 77 Category string `xml:"category,omitempty"` 78 Icon string `xml:"icon,omitempty"` 79 Logo string `xml:"logo,omitempty"` 80 Rights string `xml:"rights,omitempty"` // copyright used 81 Subtitle string `xml:"subtitle,omitempty"` 82 Link *AtomLink 83 Author *AtomAuthor `xml:"author,omitempty"` 84 Contributor *AtomContributor 85 Entries []*AtomEntry `xml:"entry"` 86 } 87 88 type Atom struct { 89 *Feed 90 } 91 92 func newAtomEntry(i *Item) *AtomEntry { 93 id := i.Id 94 // assume the description is html 95 s := &AtomSummary{Content: i.Description, Type: "html"} 96 97 if len(id) == 0 { 98 // if there's no id set, try to create one, either from data or just a uuid 99 if len(i.Link.Href) > 0 && (!i.Created.IsZero() || !i.Updated.IsZero()) { 100 dateStr := anyTimeFormat("2006-01-02", i.Updated, i.Created) 101 host, path := i.Link.Href, "/invalid.html" 102 if url, err := url.Parse(i.Link.Href); err == nil { 103 host, path = url.Host, url.Path 104 } 105 id = fmt.Sprintf("tag:%s,%s:%s", host, dateStr, path) 106 } else { 107 id = "urn:uuid:" + uuid.NewV4().String() 108 } 109 } 110 var name, email string 111 if i.Author != nil { 112 name, email = i.Author.Name, i.Author.Email 113 } 114 115 link_rel := i.Link.Rel 116 if link_rel == "" { 117 link_rel = "alternate" 118 } 119 x := &AtomEntry{ 120 Title: i.Title, 121 Links: []AtomLink{{Href: i.Link.Href, Rel: link_rel, Type: i.Link.Type}}, 122 Id: id, 123 Updated: anyTimeFormat(time.RFC3339, i.Updated, i.Created), 124 Summary: s, 125 } 126 127 // if there's a content, assume it's html 128 if len(i.Content) > 0 { 129 x.Content = &AtomContent{Content: i.Content, Type: "html"} 130 } 131 132 if i.Enclosure != nil && link_rel != "enclosure" { 133 x.Links = append(x.Links, AtomLink{Href: i.Enclosure.Url, Rel: "enclosure", Type: i.Enclosure.Type, Length: i.Enclosure.Length}) 134 } 135 136 if len(name) > 0 || len(email) > 0 { 137 x.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: name, Email: email}} 138 } 139 return x 140 } 141 142 // create a new AtomFeed with a generic Feed struct's data 143 func (a *Atom) AtomFeed() *AtomFeed { 144 updated := anyTimeFormat(time.RFC3339, a.Updated, a.Created) 145 feed := &AtomFeed{ 146 Xmlns: ns, 147 Title: a.Title, 148 Link: &AtomLink{Href: a.Link.Href, Rel: a.Link.Rel}, 149 Subtitle: a.Description, 150 Id: a.Link.Href, 151 Updated: updated, 152 Rights: a.Copyright, 153 } 154 if a.Author != nil { 155 feed.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: a.Author.Name, Email: a.Author.Email}} 156 } 157 for _, e := range a.Items { 158 feed.Entries = append(feed.Entries, newAtomEntry(e)) 159 } 160 return feed 161 } 162 163 // FeedXml returns an XML-Ready object for an Atom object 164 func (a *Atom) FeedXml() interface{} { 165 return a.AtomFeed() 166 } 167 168 // FeedXml returns an XML-ready object for an AtomFeed object 169 func (a *AtomFeed) FeedXml() interface{} { 170 return a 171 }