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  }