Add RSS/Atom feed support for user actions (#16002)
Return rss/atom feed for user based on rss url suffix or Content-Type header.tokarchuk/v1.17
parent
8edda8b446
commit
3728f1daa0
@ -0,0 +1,154 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package feed |
||||
|
||||
import ( |
||||
"fmt" |
||||
"html" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
"code.gitea.io/gitea/modules/templates" |
||||
|
||||
"github.com/gorilla/feeds" |
||||
) |
||||
|
||||
// feedActionsToFeedItems convert gitea's Action feed to feeds Item
|
||||
func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (items []*feeds.Item, err error) { |
||||
for _, act := range actions { |
||||
act.LoadActUser() |
||||
|
||||
content, desc, title := "", "", "" |
||||
|
||||
link := &feeds.Link{Href: act.GetCommentLink()} |
||||
|
||||
// title
|
||||
title = act.ActUser.DisplayName() + " " |
||||
switch act.OpType { |
||||
case models.ActionCreateRepo: |
||||
title += ctx.Tr("action.create_repo", act.GetRepoLink(), act.ShortRepoPath()) |
||||
case models.ActionRenameRepo: |
||||
title += ctx.Tr("action.rename_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) |
||||
case models.ActionCommitRepo: |
||||
branchLink := act.GetBranch() |
||||
if len(act.Content) != 0 { |
||||
title += ctx.Tr("action.commit_repo", act.GetRepoLink(), branchLink, act.GetBranch(), act.ShortRepoPath()) |
||||
} else { |
||||
title += ctx.Tr("action.create_branch", act.GetRepoLink(), branchLink, act.GetBranch(), act.ShortRepoPath()) |
||||
} |
||||
case models.ActionCreateIssue: |
||||
title += ctx.Tr("action.create_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionCreatePullRequest: |
||||
title += ctx.Tr("action.create_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionTransferRepo: |
||||
title += ctx.Tr("action.transfer_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath()) |
||||
case models.ActionPushTag: |
||||
title += ctx.Tr("action.push_tag", act.GetRepoLink(), url.QueryEscape(act.GetTag()), act.ShortRepoPath()) |
||||
case models.ActionCommentIssue: |
||||
title += ctx.Tr("action.comment_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionMergePullRequest: |
||||
title += ctx.Tr("action.merge_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionCloseIssue: |
||||
title += ctx.Tr("action.close_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionReopenIssue: |
||||
title += ctx.Tr("action.reopen_issue", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionClosePullRequest: |
||||
title += ctx.Tr("action.close_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionReopenPullRequest: |
||||
title += ctx.Tr("action.reopen_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath) |
||||
case models.ActionDeleteTag: |
||||
title += ctx.Tr("action.delete_tag", act.GetRepoLink(), html.EscapeString(act.GetTag()), act.ShortRepoPath()) |
||||
case models.ActionDeleteBranch: |
||||
title += ctx.Tr("action.delete_branch", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) |
||||
case models.ActionMirrorSyncPush: |
||||
title += ctx.Tr("action.mirror_sync_push", act.GetRepoLink(), url.QueryEscape(act.GetBranch()), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) |
||||
case models.ActionMirrorSyncCreate: |
||||
title += ctx.Tr("action.mirror_sync_create", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) |
||||
case models.ActionMirrorSyncDelete: |
||||
title += ctx.Tr("action.mirror_sync_delete", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath()) |
||||
case models.ActionApprovePullRequest: |
||||
title += ctx.Tr("action.approve_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionRejectPullRequest: |
||||
title += ctx.Tr("action.reject_pull_request", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionCommentPull: |
||||
title += ctx.Tr("action.comment_pull", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath()) |
||||
case models.ActionPublishRelease: |
||||
title += ctx.Tr("action.publish_release", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath(), act.Content) |
||||
case models.ActionPullReviewDismissed: |
||||
title += ctx.Tr("action.review_dismissed", act.GetRepoLink(), act.GetIssueInfos()[0], act.ShortRepoPath(), act.GetIssueInfos()[1]) |
||||
case models.ActionStarRepo: |
||||
title += ctx.Tr("action.stared_repo", act.GetRepoLink(), act.GetRepoPath()) |
||||
link = &feeds.Link{Href: act.GetRepoLink()} |
||||
case models.ActionWatchRepo: |
||||
title += ctx.Tr("action.watched_repo", act.GetRepoLink(), act.GetRepoPath()) |
||||
link = &feeds.Link{Href: act.GetRepoLink()} |
||||
default: |
||||
return nil, fmt.Errorf("unknown action type: %v", act.OpType) |
||||
} |
||||
|
||||
// description & content
|
||||
{ |
||||
switch act.OpType { |
||||
case models.ActionCommitRepo, models.ActionMirrorSyncPush: |
||||
push := templates.ActionContent2Commits(act) |
||||
repoLink := act.GetRepoLink() |
||||
|
||||
for _, commit := range push.Commits { |
||||
if len(desc) != 0 { |
||||
desc += "\n\n" |
||||
} |
||||
desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s", |
||||
fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), commit.Sha1), |
||||
commit.Sha1, |
||||
templates.RenderCommitMessage(commit.Message, repoLink, nil), |
||||
) |
||||
} |
||||
|
||||
if push.Len > 1 { |
||||
link = &feeds.Link{Href: fmt.Sprintf("%s/%s", setting.AppSubURL, push.CompareURL)} |
||||
} else if push.Len == 1 { |
||||
link = &feeds.Link{Href: fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), push.Commits[0].Sha1)} |
||||
} |
||||
|
||||
case models.ActionCreateIssue, models.ActionCreatePullRequest: |
||||
desc = strings.Join(act.GetIssueInfos(), "#") |
||||
content = act.GetIssueContent() |
||||
case models.ActionCommentIssue, models.ActionApprovePullRequest, models.ActionRejectPullRequest, models.ActionCommentPull: |
||||
desc = act.GetIssueTitle() |
||||
comment := act.GetIssueInfos()[1] |
||||
if len(comment) != 0 { |
||||
desc += "\n\n" + comment |
||||
} |
||||
case models.ActionMergePullRequest: |
||||
desc = act.GetIssueInfos()[1] |
||||
case models.ActionCloseIssue, models.ActionReopenIssue, models.ActionClosePullRequest, models.ActionReopenPullRequest: |
||||
desc = act.GetIssueTitle() |
||||
case models.ActionPullReviewDismissed: |
||||
desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2] |
||||
} |
||||
} |
||||
if len(content) == 0 { |
||||
content = desc |
||||
} |
||||
|
||||
items = append(items, &feeds.Item{ |
||||
Title: title, |
||||
Link: link, |
||||
Description: desc, |
||||
Author: &feeds.Author{ |
||||
Name: act.ActUser.DisplayName(), |
||||
Email: act.ActUser.GetEmail(), |
||||
}, |
||||
Id: strconv.FormatInt(act.ID, 10), |
||||
Created: act.CreatedUnix.AsTime(), |
||||
Content: content, |
||||
}) |
||||
} |
||||
return |
||||
} |
@ -0,0 +1,98 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package feed |
||||
|
||||
import ( |
||||
"net/http" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/context" |
||||
|
||||
"github.com/gorilla/feeds" |
||||
) |
||||
|
||||
// RetrieveFeeds loads feeds for the specified user
|
||||
func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*models.Action { |
||||
actions, err := models.GetFeeds(options) |
||||
if err != nil { |
||||
ctx.ServerError("GetFeeds", err) |
||||
return nil |
||||
} |
||||
|
||||
userCache := map[int64]*models.User{options.RequestedUser.ID: options.RequestedUser} |
||||
if ctx.User != nil { |
||||
userCache[ctx.User.ID] = ctx.User |
||||
} |
||||
for _, act := range actions { |
||||
if act.ActUser != nil { |
||||
userCache[act.ActUserID] = act.ActUser |
||||
} |
||||
} |
||||
|
||||
for _, act := range actions { |
||||
repoOwner, ok := userCache[act.Repo.OwnerID] |
||||
if !ok { |
||||
repoOwner, err = models.GetUserByID(act.Repo.OwnerID) |
||||
if err != nil { |
||||
if models.IsErrUserNotExist(err) { |
||||
continue |
||||
} |
||||
ctx.ServerError("GetUserByID", err) |
||||
return nil |
||||
} |
||||
userCache[repoOwner.ID] = repoOwner |
||||
} |
||||
act.Repo.Owner = repoOwner |
||||
} |
||||
return actions |
||||
} |
||||
|
||||
// ShowUserFeed show user activity as RSS / Atom feed
|
||||
func ShowUserFeed(ctx *context.Context, ctxUser *models.User, formatType string) { |
||||
actions := RetrieveFeeds(ctx, models.GetFeedsOptions{ |
||||
RequestedUser: ctxUser, |
||||
Actor: ctx.User, |
||||
IncludePrivate: false, |
||||
OnlyPerformedBy: true, |
||||
IncludeDeleted: false, |
||||
Date: ctx.FormString("date"), |
||||
}) |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
|
||||
feed := &feeds.Feed{ |
||||
Title: ctx.Tr("home.feed_of", ctxUser.DisplayName()), |
||||
Link: &feeds.Link{Href: ctxUser.HTMLURL()}, |
||||
Description: ctxUser.Description, |
||||
Created: time.Now(), |
||||
} |
||||
|
||||
var err error |
||||
feed.Items, err = feedActionsToFeedItems(ctx, actions) |
||||
if err != nil { |
||||
ctx.ServerError("convert feed", err) |
||||
return |
||||
} |
||||
|
||||
writeFeed(ctx, feed, formatType) |
||||
} |
||||
|
||||
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
|
||||
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) { |
||||
ctx.Resp.WriteHeader(http.StatusOK) |
||||
if formatType == "atom" { |
||||
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8") |
||||
if err := feed.WriteAtom(ctx.Resp); err != nil { |
||||
ctx.ServerError("Render Atom failed", err) |
||||
} |
||||
} else { |
||||
ctx.Resp.Header().Set("Content-Type", "application/rss+xml;charset=utf-8") |
||||
if err := feed.WriteRss(ctx.Resp); err != nil { |
||||
ctx.ServerError("Render RSS failed", err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
language: go |
||||
sudo: false |
||||
matrix: |
||||
include: |
||||
- go: 1.8 |
||||
- go: 1.9 |
||||
- go: "1.10" |
||||
- go: 1.x |
||||
- go: tip |
||||
allow_failures: |
||||
- go: tip |
||||
script: |
||||
- go get -t -v ./... |
||||
- diff -u <(echo -n) <(gofmt -d -s .) |
||||
- go vet . |
||||
- go test -v -race ./... |
@ -0,0 +1,29 @@ |
||||
# This is the official list of gorilla/feeds authors for copyright purposes. |
||||
# Please keep the list sorted. |
||||
|
||||
Dmitry Chestnykh <dmitry@codingrobots.com> |
||||
Eddie Scholtz <eascholtz@gmail.com> |
||||
Gabriel Simmer <bladesimmer@gmail.com> |
||||
Google LLC (https://opensource.google.com/) |
||||
honky <honky@defendtheplanet.net> |
||||
James Gregory <james@jagregory.com> |
||||
Jason Hall <imjasonh@gmail.com> |
||||
Jason Moiron <jmoiron@jmoiron.net> |
||||
Kamil Kisiel <kamil@kamilkisiel.net> |
||||
Kevin Stock <kevinstock@tantalic.com> |
||||
Markus Zimmermann <markus.zimmermann@nethead.at> |
||||
Matt Silverlock <matt@eatsleeprepeat.net> |
||||
Matthew Dawson <matthew@mjdsystems.ca> |
||||
Milan Aleksic <milanaleksic@gmail.com> |
||||
Milan Aleksić <milanaleksic@gmail.com> |
||||
nlimpid <jshuangzl@gmail.com> |
||||
Paul Petring <paul@defendtheplanet.net> |
||||
Sean Enck <enckse@users.noreply.github.com> |
||||
Sue Spence <virtuallysue@gmail.com> |
||||
Supermighty <ukiah@faction.com> |
||||
Toru Fukui <fukuimone@gmail.com> |
||||
Vabd <vabd@anon.acme> |
||||
Volker <lists.volker@gmail.com> |
||||
ZhiFeng Hu <hufeng1987@gmail.com> |
||||
weberc2 <weberc2@gmail.com> |
||||
|
@ -0,0 +1,22 @@ |
||||
Copyright (c) 2013-2018 The Gorilla Feeds Authors. All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are met: |
||||
|
||||
Redistributions of source code must retain the above copyright notice, this |
||||
list of conditions and the following disclaimer. |
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, |
||||
this list of conditions and the following disclaimer in the documentation |
||||
and/or other materials provided with the distribution. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,185 @@ |
||||
## gorilla/feeds |
||||
[![GoDoc](https://godoc.org/github.com/gorilla/feeds?status.svg)](https://godoc.org/github.com/gorilla/feeds) |
||||
[![Build Status](https://travis-ci.org/gorilla/feeds.svg?branch=master)](https://travis-ci.org/gorilla/feeds) |
||||
|
||||
feeds is a web feed generator library for generating RSS, Atom and JSON feeds from Go |
||||
applications. |
||||
|
||||
### Goals |
||||
|
||||
* Provide a simple interface to create both Atom & RSS 2.0 feeds |
||||
* Full support for [Atom][atom], [RSS 2.0][rss], and [JSON Feed Version 1][jsonfeed] spec elements |
||||
* Ability to modify particulars for each spec |
||||
|
||||
[atom]: https://tools.ietf.org/html/rfc4287 |
||||
[rss]: http://www.rssboard.org/rss-specification |
||||
[jsonfeed]: https://jsonfeed.org/version/1 |
||||
|
||||
### Usage |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"time" |
||||
"github.com/gorilla/feeds" |
||||
) |
||||
|
||||
func main() { |
||||
now := time.Now() |
||||
feed := &feeds.Feed{ |
||||
Title: "jmoiron.net blog", |
||||
Link: &feeds.Link{Href: "http://jmoiron.net/blog"}, |
||||
Description: "discussion about tech, footie, photos", |
||||
Author: &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"}, |
||||
Created: now, |
||||
} |
||||
|
||||
feed.Items = []*feeds.Item{ |
||||
&feeds.Item{ |
||||
Title: "Limiting Concurrency in Go", |
||||
Link: &feeds.Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"}, |
||||
Description: "A discussion on controlled parallelism in golang", |
||||
Author: &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"}, |
||||
Created: now, |
||||
}, |
||||
&feeds.Item{ |
||||
Title: "Logic-less Template Redux", |
||||
Link: &feeds.Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"}, |
||||
Description: "More thoughts on logicless templates", |
||||
Created: now, |
||||
}, |
||||
&feeds.Item{ |
||||
Title: "Idiomatic Code Reuse in Go", |
||||
Link: &feeds.Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"}, |
||||
Description: "How to use interfaces <em>effectively</em>", |
||||
Created: now, |
||||
}, |
||||
} |
||||
|
||||
atom, err := feed.ToAtom() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
rss, err := feed.ToRss() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
json, err := feed.ToJSON() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
fmt.Println(atom, "\n", rss, "\n", json) |
||||
} |
||||
``` |
||||
|
||||
Outputs: |
||||
|
||||
```xml |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<feed xmlns="http://www.w3.org/2005/Atom"> |
||||
<title>jmoiron.net blog</title> |
||||
<link href="http://jmoiron.net/blog"></link> |
||||
<id>http://jmoiron.net/blog</id> |
||||
<updated>2013-01-16T03:26:01-05:00</updated> |
||||
<summary>discussion about tech, footie, photos</summary> |
||||
<entry> |
||||
<title>Limiting Concurrency in Go</title> |
||||
<link href="http://jmoiron.net/blog/limiting-concurrency-in-go/"></link> |
||||
<updated>2013-01-16T03:26:01-05:00</updated> |
||||
<id>tag:jmoiron.net,2013-01-16:/blog/limiting-concurrency-in-go/</id> |
||||
<summary type="html">A discussion on controlled parallelism in golang</summary> |
||||
<author> |
||||
<name>Jason Moiron</name> |
||||
<email>jmoiron@jmoiron.net</email> |
||||
</author> |
||||
</entry> |
||||
<entry> |
||||
<title>Logic-less Template Redux</title> |
||||
<link href="http://jmoiron.net/blog/logicless-template-redux/"></link> |
||||
<updated>2013-01-16T03:26:01-05:00</updated> |
||||
<id>tag:jmoiron.net,2013-01-16:/blog/logicless-template-redux/</id> |
||||
<summary type="html">More thoughts on logicless templates</summary> |
||||
<author></author> |
||||
</entry> |
||||
<entry> |
||||
<title>Idiomatic Code Reuse in Go</title> |
||||
<link href="http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"></link> |
||||
<updated>2013-01-16T03:26:01-05:00</updated> |
||||
<id>tag:jmoiron.net,2013-01-16:/blog/idiomatic-code-reuse-in-go/</id> |
||||
<summary type="html">How to use interfaces <em>effectively</em></summary> |
||||
<author></author> |
||||
</entry> |
||||
</feed> |
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<rss version="2.0"> |
||||
<channel> |
||||
<title>jmoiron.net blog</title> |
||||
<link>http://jmoiron.net/blog</link> |
||||
<description>discussion about tech, footie, photos</description> |
||||
<managingEditor>jmoiron@jmoiron.net (Jason Moiron)</managingEditor> |
||||
<pubDate>2013-01-16T03:22:24-05:00</pubDate> |
||||
<item> |
||||
<title>Limiting Concurrency in Go</title> |
||||
<link>http://jmoiron.net/blog/limiting-concurrency-in-go/</link> |
||||
<description>A discussion on controlled parallelism in golang</description> |
||||
<pubDate>2013-01-16T03:22:24-05:00</pubDate> |
||||
</item> |
||||
<item> |
||||
<title>Logic-less Template Redux</title> |
||||
<link>http://jmoiron.net/blog/logicless-template-redux/</link> |
||||
<description>More thoughts on logicless templates</description> |
||||
<pubDate>2013-01-16T03:22:24-05:00</pubDate> |
||||
</item> |
||||
<item> |
||||
<title>Idiomatic Code Reuse in Go</title> |
||||
<link>http://jmoiron.net/blog/idiomatic-code-reuse-in-go/</link> |
||||
<description>How to use interfaces <em>effectively</em></description> |
||||
<pubDate>2013-01-16T03:22:24-05:00</pubDate> |
||||
</item> |
||||
</channel> |
||||
</rss> |
||||
|
||||
{ |
||||
"version": "https://jsonfeed.org/version/1", |
||||
"title": "jmoiron.net blog", |
||||
"home_page_url": "http://jmoiron.net/blog", |
||||
"description": "discussion about tech, footie, photos", |
||||
"author": { |
||||
"name": "Jason Moiron" |
||||
}, |
||||
"items": [ |
||||
{ |
||||
"id": "", |
||||
"url": "http://jmoiron.net/blog/limiting-concurrency-in-go/", |
||||
"title": "Limiting Concurrency in Go", |
||||
"summary": "A discussion on controlled parallelism in golang", |
||||
"date_published": "2013-01-16T03:22:24.530817846-05:00", |
||||
"author": { |
||||
"name": "Jason Moiron" |
||||
} |
||||
}, |
||||
{ |
||||
"id": "", |
||||
"url": "http://jmoiron.net/blog/logicless-template-redux/", |
||||
"title": "Logic-less Template Redux", |
||||
"summary": "More thoughts on logicless templates", |
||||
"date_published": "2013-01-16T03:22:24.530817846-05:00" |
||||
}, |
||||
{ |
||||
"id": "", |
||||
"url": "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/", |
||||
"title": "Idiomatic Code Reuse in Go", |
||||
"summary": "How to use interfaces \u003cem\u003eeffectively\u003c/em\u003e", |
||||
"date_published": "2013-01-16T03:22:24.530817846-05:00" |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
@ -0,0 +1,169 @@ |
||||
package feeds |
||||
|
||||
import ( |
||||
"encoding/xml" |
||||
"fmt" |
||||
"net/url" |
||||
"time" |
||||
) |
||||
|
||||
// Generates Atom feed as XML
|
||||
|
||||
const ns = "http://www.w3.org/2005/Atom" |
||||
|
||||
type AtomPerson struct { |
||||
Name string `xml:"name,omitempty"` |
||||
Uri string `xml:"uri,omitempty"` |
||||
Email string `xml:"email,omitempty"` |
||||
} |
||||
|
||||
type AtomSummary struct { |
||||
XMLName xml.Name `xml:"summary"` |
||||
Content string `xml:",chardata"` |
||||
Type string `xml:"type,attr"` |
||||
} |
||||
|
||||
type AtomContent struct { |
||||
XMLName xml.Name `xml:"content"` |
||||
Content string `xml:",chardata"` |
||||
Type string `xml:"type,attr"` |
||||
} |
||||
|
||||
type AtomAuthor struct { |
||||
XMLName xml.Name `xml:"author"` |
||||
AtomPerson |
||||
} |
||||
|
||||
type AtomContributor struct { |
||||
XMLName xml.Name `xml:"contributor"` |
||||
AtomPerson |
||||
} |
||||
|
||||
type AtomEntry struct { |
||||
XMLName xml.Name `xml:"entry"` |
||||
Xmlns string `xml:"xmlns,attr,omitempty"` |
||||
Title string `xml:"title"` // required
|
||||
Updated string `xml:"updated"` // required
|
||||
Id string `xml:"id"` // required
|
||||
Category string `xml:"category,omitempty"` |
||||
Content *AtomContent |
||||
Rights string `xml:"rights,omitempty"` |
||||
Source string `xml:"source,omitempty"` |
||||
Published string `xml:"published,omitempty"` |
||||
Contributor *AtomContributor |
||||
Links []AtomLink // required if no child 'content' elements
|
||||
Summary *AtomSummary // required if content has src or content is base64
|
||||
Author *AtomAuthor // required if feed lacks an author
|
||||
} |
||||
|
||||
// Multiple links with different rel can coexist
|
||||
type AtomLink struct { |
||||
//Atom 1.0 <link rel="enclosure" type="audio/mpeg" title="MP3" href="http://www.example.org/myaudiofile.mp3" length="1234" />
|
||||
XMLName xml.Name `xml:"link"` |
||||
Href string `xml:"href,attr"` |
||||
Rel string `xml:"rel,attr,omitempty"` |
||||
Type string `xml:"type,attr,omitempty"` |
||||
Length string `xml:"length,attr,omitempty"` |
||||
} |
||||
|
||||
type AtomFeed struct { |
||||
XMLName xml.Name `xml:"feed"` |
||||
Xmlns string `xml:"xmlns,attr"` |
||||
Title string `xml:"title"` // required
|
||||
Id string `xml:"id"` // required
|
||||
Updated string `xml:"updated"` // required
|
||||
Category string `xml:"category,omitempty"` |
||||
Icon string `xml:"icon,omitempty"` |
||||
Logo string `xml:"logo,omitempty"` |
||||
Rights string `xml:"rights,omitempty"` // copyright used
|
||||
Subtitle string `xml:"subtitle,omitempty"` |
||||
Link *AtomLink |
||||
Author *AtomAuthor `xml:"author,omitempty"` |
||||
Contributor *AtomContributor |
||||
Entries []*AtomEntry `xml:"entry"` |
||||
} |
||||
|
||||
type Atom struct { |
||||
*Feed |
||||
} |
||||
|
||||
func newAtomEntry(i *Item) *AtomEntry { |
||||
id := i.Id |
||||
// assume the description is html
|
||||
s := &AtomSummary{Content: i.Description, Type: "html"} |
||||
|
||||
if len(id) == 0 { |
||||
// if there's no id set, try to create one, either from data or just a uuid
|
||||
if len(i.Link.Href) > 0 && (!i.Created.IsZero() || !i.Updated.IsZero()) { |
||||
dateStr := anyTimeFormat("2006-01-02", i.Updated, i.Created) |
||||
host, path := i.Link.Href, "/invalid.html" |
||||
if url, err := url.Parse(i.Link.Href); err == nil { |
||||
host, path = url.Host, url.Path |
||||
} |
||||
id = fmt.Sprintf("tag:%s,%s:%s", host, dateStr, path) |
||||
} else { |
||||
id = "urn:uuid:" + NewUUID().String() |
||||
} |
||||
} |
||||
var name, email string |
||||
if i.Author != nil { |
||||
name, email = i.Author.Name, i.Author.Email |
||||
} |
||||
|
||||
link_rel := i.Link.Rel |
||||
if link_rel == "" { |
||||
link_rel = "alternate" |
||||
} |
||||
x := &AtomEntry{ |
||||
Title: i.Title, |
||||
Links: []AtomLink{{Href: i.Link.Href, Rel: link_rel, Type: i.Link.Type}}, |
||||
Id: id, |
||||
Updated: anyTimeFormat(time.RFC3339, i.Updated, i.Created), |
||||
Summary: s, |
||||
} |
||||
|
||||
// if there's a content, assume it's html
|
||||
if len(i.Content) > 0 { |
||||
x.Content = &AtomContent{Content: i.Content, Type: "html"} |
||||
} |
||||
|
||||
if i.Enclosure != nil && link_rel != "enclosure" { |
||||
x.Links = append(x.Links, AtomLink{Href: i.Enclosure.Url, Rel: "enclosure", Type: i.Enclosure.Type, Length: i.Enclosure.Length}) |
||||
} |
||||
|
||||
if len(name) > 0 || len(email) > 0 { |
||||
x.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: name, Email: email}} |
||||
} |
||||
return x |
||||
} |
||||
|
||||
// create a new AtomFeed with a generic Feed struct's data
|
||||
func (a *Atom) AtomFeed() *AtomFeed { |
||||
updated := anyTimeFormat(time.RFC3339, a.Updated, a.Created) |
||||
feed := &AtomFeed{ |
||||
Xmlns: ns, |
||||
Title: a.Title, |
||||
Link: &AtomLink{Href: a.Link.Href, Rel: a.Link.Rel}, |
||||
Subtitle: a.Description, |
||||
Id: a.Link.Href, |
||||
Updated: updated, |
||||
Rights: a.Copyright, |
||||
} |
||||
if a.Author != nil { |
||||
feed.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: a.Author.Name, Email: a.Author.Email}} |
||||
} |
||||
for _, e := range a.Items { |
||||
feed.Entries = append(feed.Entries, newAtomEntry(e)) |
||||
} |
||||
return feed |
||||
} |
||||
|
||||
// FeedXml returns an XML-Ready object for an Atom object
|
||||
func (a *Atom) FeedXml() interface{} { |
||||
return a.AtomFeed() |
||||
} |
||||
|
||||
// FeedXml returns an XML-ready object for an AtomFeed object
|
||||
func (a *AtomFeed) FeedXml() interface{} { |
||||
return a |
||||
} |
@ -0,0 +1,73 @@ |
||||
/* |
||||
Syndication (feed) generator library for golang. |
||||
|
||||
Installing |
||||
|
||||
go get github.com/gorilla/feeds |
||||
|
||||
Feeds provides a simple, generic Feed interface with a generic Item object as well as RSS, Atom and JSON Feed specific RssFeed, AtomFeed and JSONFeed objects which allow access to all of each spec's defined elements. |
||||
|
||||
Examples |
||||
|
||||
Create a Feed and some Items in that feed using the generic interfaces: |
||||
|
||||
import ( |
||||
"time" |
||||
. "github.com/gorilla/feeds" |
||||
) |
||||
|
||||
now = time.Now() |
||||
|
||||
feed := &Feed{ |
||||
Title: "jmoiron.net blog", |
||||
Link: &Link{Href: "http://jmoiron.net/blog"}, |
||||
Description: "discussion about tech, footie, photos", |
||||
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"}, |
||||
Created: now, |
||||
Copyright: "This work is copyright © Benjamin Button", |
||||
} |
||||
|
||||
feed.Items = []*Item{ |
||||
&Item{ |
||||
Title: "Limiting Concurrency in Go", |
||||
Link: &Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"}, |
||||
Description: "A discussion on controlled parallelism in golang", |
||||
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"}, |
||||
Created: now, |
||||
}, |
||||
&Item{ |
||||
Title: "Logic-less Template Redux", |
||||
Link: &Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"}, |
||||
Description: "More thoughts on logicless templates", |
||||
Created: now, |
||||
}, |
||||
&Item{ |
||||
Title: "Idiomatic Code Reuse in Go", |
||||
Link: &Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"}, |
||||
Description: "How to use interfaces <em>effectively</em>", |
||||
Created: now, |
||||
}, |
||||
} |
||||
|
||||
From here, you can output Atom, RSS, or JSON Feed versions of this feed easily |
||||
|
||||
atom, err := feed.ToAtom() |
||||
rss, err := feed.ToRss() |
||||
json, err := feed.ToJSON() |
||||
|
||||
You can also get access to the underlying objects that feeds uses to export its XML |
||||
|
||||
atomFeed := (&Atom{Feed: feed}).AtomFeed() |
||||
rssFeed := (&Rss{Feed: feed}).RssFeed() |
||||
jsonFeed := (&JSON{Feed: feed}).JSONFeed() |
||||
|
||||
From here, you can modify or add each syndication's specific fields before outputting |
||||
|
||||
atomFeed.Subtitle = "plays the blues" |
||||
atom, err := ToXML(atomFeed) |
||||
rssFeed.Generator = "gorilla/feeds v1.0 (github.com/gorilla/feeds)" |
||||
rss, err := ToXML(rssFeed) |
||||
jsonFeed.NextUrl = "https://www.example.com/feed.json?page=2" |
||||
json, err := jsonFeed.ToJSON() |
||||
*/ |
||||
package feeds |
@ -0,0 +1,145 @@ |
||||
package feeds |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"encoding/xml" |
||||
"io" |
||||
"sort" |
||||
"time" |
||||
) |
||||
|
||||
type Link struct { |
||||
Href, Rel, Type, Length string |
||||
} |
||||
|
||||
type Author struct { |
||||
Name, Email string |
||||
} |
||||
|
||||
type Image struct { |
||||
Url, Title, Link string |
||||
Width, Height int |
||||
} |
||||
|
||||
type Enclosure struct { |
||||
Url, Length, Type string |
||||
} |
||||
|
||||
type Item struct { |
||||
Title string |
||||
Link *Link |
||||
Source *Link |
||||
Author *Author |
||||
Description string // used as description in rss, summary in atom
|
||||
Id string // used as guid in rss, id in atom
|
||||
Updated time.Time |
||||
Created time.Time |
||||
Enclosure *Enclosure |
||||
Content string |
||||
} |
||||
|
||||
type Feed struct { |
||||
Title string |
||||
Link *Link |
||||
Description string |
||||
Author *Author |
||||
Updated time.Time |
||||
Created time.Time |
||||
Id string |
||||
Subtitle string |
||||
Items []*Item |
||||
Copyright string |
||||
Image *Image |
||||
} |
||||
|
||||
// add a new Item to a Feed
|
||||
func (f *Feed) Add(item *Item) { |
||||
f.Items = append(f.Items, item) |
||||
} |
||||
|
||||
// returns the first non-zero time formatted as a string or ""
|
||||
func anyTimeFormat(format string, times ...time.Time) string { |
||||
for _, t := range times { |
||||
if !t.IsZero() { |
||||
return t.Format(format) |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
// interface used by ToXML to get a object suitable for exporting XML.
|
||||
type XmlFeed interface { |
||||
FeedXml() interface{} |
||||
} |
||||
|
||||
// turn a feed object (either a Feed, AtomFeed, or RssFeed) into xml
|
||||
// returns an error if xml marshaling fails
|
||||
func ToXML(feed XmlFeed) (string, error) { |
||||
x := feed.FeedXml() |
||||
data, err := xml.MarshalIndent(x, "", " ") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
// strip empty line from default xml header
|
||||
s := xml.Header[:len(xml.Header)-1] + string(data) |
||||
return s, nil |
||||
} |
||||
|
||||
// WriteXML writes a feed object (either a Feed, AtomFeed, or RssFeed) as XML into
|
||||
// the writer. Returns an error if XML marshaling fails.
|
||||
func WriteXML(feed XmlFeed, w io.Writer) error { |
||||
x := feed.FeedXml() |
||||
// write default xml header, without the newline
|
||||
if _, err := w.Write([]byte(xml.Header[:len(xml.Header)-1])); err != nil { |
||||
return err |
||||
} |
||||
e := xml.NewEncoder(w) |
||||
e.Indent("", " ") |
||||
return e.Encode(x) |
||||
} |
||||
|
||||
// creates an Atom representation of this feed
|
||||
func (f *Feed) ToAtom() (string, error) { |
||||
a := &Atom{f} |
||||
return ToXML(a) |
||||
} |
||||
|
||||
// WriteAtom writes an Atom representation of this feed to the writer.
|
||||
func (f *Feed) WriteAtom(w io.Writer) error { |
||||
return WriteXML(&Atom{f}, w) |
||||
} |
||||
|
||||
// creates an Rss representation of this feed
|
||||
func (f *Feed) ToRss() (string, error) { |
||||
r := &Rss{f} |
||||
return ToXML(r) |
||||
} |
||||
|
||||
// WriteRss writes an RSS representation of this feed to the writer.
|
||||
func (f *Feed) WriteRss(w io.Writer) error { |
||||
return WriteXML(&Rss{f}, w) |
||||
} |
||||
|
||||
// ToJSON creates a JSON Feed representation of this feed
|
||||
func (f *Feed) ToJSON() (string, error) { |
||||
j := &JSON{f} |
||||
return j.ToJSON() |
||||
} |
||||
|
||||
// WriteJSON writes an JSON representation of this feed to the writer.
|
||||
func (f *Feed) WriteJSON(w io.Writer) error { |
||||
j := &JSON{f} |
||||
feed := j.JSONFeed() |
||||
|
||||
e := json.NewEncoder(w) |
||||
e.SetIndent("", " ") |
||||
return e.Encode(feed) |
||||
} |
||||
|
||||
// Sort sorts the Items in the feed with the given less function.
|
||||
func (f *Feed) Sort(less func(a, b *Item) bool) { |
||||
lessFunc := func(i, j int) bool { |
||||
return less(f.Items[i], f.Items[j]) |
||||
} |
||||
sort.SliceStable(f.Items, lessFunc) |
||||
} |
@ -0,0 +1,183 @@ |
||||
package feeds |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
const jsonFeedVersion = "https://jsonfeed.org/version/1" |
||||
|
||||
// JSONAuthor represents the author of the feed or of an individual item
|
||||
// in the feed
|
||||
type JSONAuthor struct { |
||||
Name string `json:"name,omitempty"` |
||||
Url string `json:"url,omitempty"` |
||||
Avatar string `json:"avatar,omitempty"` |
||||
} |
||||
|
||||
// JSONAttachment represents a related resource. Podcasts, for instance, would
|
||||
// include an attachment that’s an audio or video file.
|
||||
type JSONAttachment struct { |
||||
Url string `json:"url,omitempty"` |
||||
MIMEType string `json:"mime_type,omitempty"` |
||||
Title string `json:"title,omitempty"` |
||||
Size int32 `json:"size,omitempty"` |
||||
Duration time.Duration `json:"duration_in_seconds,omitempty"` |
||||
} |
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
// The Duration field is marshaled in seconds, all other fields are marshaled
|
||||
// based upon the definitions in struct tags.
|
||||
func (a *JSONAttachment) MarshalJSON() ([]byte, error) { |
||||
type EmbeddedJSONAttachment JSONAttachment |
||||
return json.Marshal(&struct { |
||||
Duration float64 `json:"duration_in_seconds,omitempty"` |
||||
*EmbeddedJSONAttachment |
||||
}{ |
||||
EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a), |
||||
Duration: a.Duration.Seconds(), |
||||
}) |
||||
} |
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// The Duration field is expected to be in seconds, all other field types
|
||||
// match the struct definition.
|
||||
func (a *JSONAttachment) UnmarshalJSON(data []byte) error { |
||||
type EmbeddedJSONAttachment JSONAttachment |
||||
var raw struct { |
||||
Duration float64 `json:"duration_in_seconds,omitempty"` |
||||
*EmbeddedJSONAttachment |
||||
} |
||||
raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a) |
||||
|
||||
err := json.Unmarshal(data, &raw) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if raw.Duration > 0 { |
||||
nsec := int64(raw.Duration * float64(time.Second)) |
||||
raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// JSONItem represents a single entry/post for the feed.
|
||||
type JSONItem struct { |
||||
Id string `json:"id"` |
||||
Url string `json:"url,omitempty"` |
||||
ExternalUrl string `json:"external_url,omitempty"` |
||||
Title string `json:"title,omitempty"` |
||||
ContentHTML string `json:"content_html,omitempty"` |
||||
ContentText string `json:"content_text,omitempty"` |
||||
Summary string `json:"summary,omitempty"` |
||||
Image string `json:"image,omitempty"` |
||||
BannerImage string `json:"banner_,omitempty"` |
||||
PublishedDate *time.Time `json:"date_published,omitempty"` |
||||
ModifiedDate *time.Time `json:"date_modified,omitempty"` |
||||
Author *JSONAuthor `json:"author,omitempty"` |
||||
Tags []string `json:"tags,omitempty"` |
||||
Attachments []JSONAttachment `json:"attachments,omitempty"` |
||||
} |
||||
|
||||
// JSONHub describes an endpoint that can be used to subscribe to real-time
|
||||
// notifications from the publisher of this feed.
|
||||
type JSONHub struct { |
||||
Type string `json:"type"` |
||||
Url string `json:"url"` |
||||
} |
||||
|
||||
// JSONFeed represents a syndication feed in the JSON Feed Version 1 format.
|
||||
// Matching the specification found here: https://jsonfeed.org/version/1.
|
||||
type JSONFeed struct { |
||||
Version string `json:"version"` |
||||
Title string `json:"title"` |
||||
HomePageUrl string `json:"home_page_url,omitempty"` |
||||
FeedUrl string `json:"feed_url,omitempty"` |
||||
Description string `json:"description,omitempty"` |
||||
UserComment string `json:"user_comment,omitempty"` |
||||
NextUrl string `json:"next_url,omitempty"` |
||||
Icon string `json:"icon,omitempty"` |
||||
Favicon string `json:"favicon,omitempty"` |
||||
Author *JSONAuthor `json:"author,omitempty"` |
||||
Expired *bool `json:"expired,omitempty"` |
||||
Hubs []*JSONItem `json:"hubs,omitempty"` |
||||
Items []*JSONItem `json:"items,omitempty"` |
||||
} |
||||
|
||||
// JSON is used to convert a generic Feed to a JSONFeed.
|
||||
type JSON struct { |
||||
*Feed |
||||
} |
||||
|
||||
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
||||
func (f *JSON) ToJSON() (string, error) { |
||||
return f.JSONFeed().ToJSON() |
||||
} |
||||
|
||||
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
||||
func (f *JSONFeed) ToJSON() (string, error) { |
||||
data, err := json.MarshalIndent(f, "", " ") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return string(data), nil |
||||
} |
||||
|
||||
// JSONFeed creates a new JSONFeed with a generic Feed struct's data.
|
||||
func (f *JSON) JSONFeed() *JSONFeed { |
||||
feed := &JSONFeed{ |
||||
Version: jsonFeedVersion, |
||||
Title: f.Title, |
||||
Description: f.Description, |
||||
} |
||||
|
||||
if f.Link != nil { |
||||
feed.HomePageUrl = f.Link.Href |
||||
} |
||||
if f.Author != nil { |
||||
feed.Author = &JSONAuthor{ |
||||
Name: f.Author.Name, |
||||
} |
||||
} |
||||
for _, e := range f.Items { |
||||
feed.Items = append(feed.Items, newJSONItem(e)) |
||||
} |
||||
return feed |
||||
} |
||||
|
||||
func newJSONItem(i *Item) *JSONItem { |
||||
item := &JSONItem{ |
||||
Id: i.Id, |
||||
Title: i.Title, |
||||
Summary: i.Description, |
||||
|
||||
ContentHTML: i.Content, |
||||
} |
||||
|
||||
if i.Link != nil { |
||||
item.Url = i.Link.Href |
||||
} |
||||
if i.Source != nil { |
||||
item.ExternalUrl = i.Source.Href |
||||
} |
||||
if i.Author != nil { |
||||
item.Author = &JSONAuthor{ |
||||
Name: i.Author.Name, |
||||
} |
||||
} |
||||
if !i.Created.IsZero() { |
||||
item.PublishedDate = &i.Created |
||||
} |
||||
if !i.Updated.IsZero() { |
||||
item.ModifiedDate = &i.Updated |
||||
} |
||||
if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "image/") { |
||||
item.Image = i.Enclosure.Url |
||||
} |
||||
|
||||
return item |
||||
} |
@ -0,0 +1,168 @@ |
||||
package feeds |
||||
|
||||
// rss support
|
||||
// validation done according to spec here:
|
||||
// http://cyber.law.harvard.edu/rss/rss.html
|
||||
|
||||
import ( |
||||
"encoding/xml" |
||||
"fmt" |
||||
"time" |
||||
) |
||||
|
||||
// private wrapper around the RssFeed which gives us the <rss>..</rss> xml
|
||||
type RssFeedXml struct { |
||||
XMLName xml.Name `xml:"rss"` |
||||
Version string `xml:"version,attr"` |
||||
ContentNamespace string `xml:"xmlns:content,attr"` |
||||
Channel *RssFeed |
||||
} |
||||
|
||||
type RssContent struct { |
||||
XMLName xml.Name `xml:"content:encoded"` |
||||
Content string `xml:",cdata"` |
||||
} |
||||
|
||||
type RssImage struct { |
||||
XMLName xml.Name `xml:"image"` |
||||
Url string `xml:"url"` |
||||
Title string `xml:"title"` |
||||
Link string `xml:"link"` |
||||
Width int `xml:"width,omitempty"` |
||||
Height int `xml:"height,omitempty"` |
||||
} |
||||
|
||||
type RssTextInput struct { |
||||
XMLName xml.Name `xml:"textInput"` |
||||
Title string `xml:"title"` |
||||
Description string `xml:"description"` |
||||
Name string `xml:"name"` |
||||
Link string `xml:"link"` |
||||
} |
||||
|
||||
type RssFeed struct { |
||||
XMLName xml.Name `xml:"channel"` |
||||
Title string `xml:"title"` // required
|
||||
Link string `xml:"link"` // required
|
||||
Description string `xml:"description"` // required
|
||||
Language string `xml:"language,omitempty"` |
||||
Copyright string `xml:"copyright,omitempty"` |
||||
ManagingEditor string `xml:"managingEditor,omitempty"` // Author used
|
||||
WebMaster string `xml:"webMaster,omitempty"` |
||||
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||
LastBuildDate string `xml:"lastBuildDate,omitempty"` // updated used
|
||||
Category string `xml:"category,omitempty"` |
||||
Generator string `xml:"generator,omitempty"` |
||||
Docs string `xml:"docs,omitempty"` |
||||
Cloud string `xml:"cloud,omitempty"` |
||||
Ttl int `xml:"ttl,omitempty"` |
||||
Rating string `xml:"rating,omitempty"` |
||||
SkipHours string `xml:"skipHours,omitempty"` |
||||
SkipDays string `xml:"skipDays,omitempty"` |
||||
Image *RssImage |
||||
TextInput *RssTextInput |
||||
Items []*RssItem `xml:"item"` |
||||
} |
||||
|
||||
type RssItem struct { |
||||
XMLName xml.Name `xml:"item"` |
||||
Title string `xml:"title"` // required
|
||||
Link string `xml:"link"` // required
|
||||
Description string `xml:"description"` // required
|
||||
Content *RssContent |
||||
Author string `xml:"author,omitempty"` |
||||
Category string `xml:"category,omitempty"` |
||||
Comments string `xml:"comments,omitempty"` |
||||
Enclosure *RssEnclosure |
||||
Guid string `xml:"guid,omitempty"` // Id used
|
||||
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||
Source string `xml:"source,omitempty"` |
||||
} |
||||
|
||||
type RssEnclosure struct { |
||||
//RSS 2.0 <enclosure url="http://example.com/file.mp3" length="123456789" type="audio/mpeg" />
|
||||
XMLName xml.Name `xml:"enclosure"` |
||||
Url string `xml:"url,attr"` |
||||
Length string `xml:"length,attr"` |
||||
Type string `xml:"type,attr"` |
||||
} |
||||
|
||||
type Rss struct { |
||||
*Feed |
||||
} |
||||
|
||||
// create a new RssItem with a generic Item struct's data
|
||||
func newRssItem(i *Item) *RssItem { |
||||
item := &RssItem{ |
||||
Title: i.Title, |
||||
Link: i.Link.Href, |
||||
Description: i.Description, |
||||
Guid: i.Id, |
||||
PubDate: anyTimeFormat(time.RFC1123Z, i.Created, i.Updated), |
||||
} |
||||
if len(i.Content) > 0 { |
||||
item.Content = &RssContent{Content: i.Content} |
||||
} |
||||
if i.Source != nil { |
||||
item.Source = i.Source.Href |
||||
} |
||||
|
||||
// Define a closure
|
||||
if i.Enclosure != nil && i.Enclosure.Type != "" && i.Enclosure.Length != "" { |
||||
item.Enclosure = &RssEnclosure{Url: i.Enclosure.Url, Type: i.Enclosure.Type, Length: i.Enclosure.Length} |
||||
} |
||||
|
||||
if i.Author != nil { |
||||
item.Author = i.Author.Name |
||||
} |
||||
return item |
||||
} |
||||
|
||||
// create a new RssFeed with a generic Feed struct's data
|
||||
func (r *Rss) RssFeed() *RssFeed { |
||||
pub := anyTimeFormat(time.RFC1123Z, r.Created, r.Updated) |
||||
build := anyTimeFormat(time.RFC1123Z, r.Updated) |
||||
author := "" |
||||
if r.Author != nil { |
||||
author = r.Author.Email |
||||
if len(r.Author.Name) > 0 { |
||||
author = fmt.Sprintf("%s (%s)", r.Author.Email, r.Author.Name) |
||||
} |
||||
} |
||||
|
||||
var image *RssImage |
||||
if r.Image != nil { |
||||
image = &RssImage{Url: r.Image.Url, Title: r.Image.Title, Link: r.Image.Link, Width: r.Image.Width, Height: r.Image.Height} |
||||
} |
||||
|
||||
channel := &RssFeed{ |
||||
Title: r.Title, |
||||
Link: r.Link.Href, |
||||
Description: r.Description, |
||||
ManagingEditor: author, |
||||
PubDate: pub, |
||||
LastBuildDate: build, |
||||
Copyright: r.Copyright, |
||||
Image: image, |
||||
} |
||||
for _, i := range r.Items { |
||||
channel.Items = append(channel.Items, newRssItem(i)) |
||||
} |
||||
return channel |
||||
} |
||||
|
||||
// FeedXml returns an XML-Ready object for an Rss object
|
||||
func (r *Rss) FeedXml() interface{} { |
||||
// only generate version 2.0 feeds for now
|
||||
return r.RssFeed().FeedXml() |
||||
|
||||
} |
||||
|
||||
// FeedXml returns an XML-ready object for an RssFeed object
|
||||
func (r *RssFeed) FeedXml() interface{} { |
||||
return &RssFeedXml{ |
||||
Version: "2.0", |
||||
Channel: r, |
||||
ContentNamespace: "http://purl.org/rss/1.0/modules/content/", |
||||
} |
||||
} |
@ -0,0 +1,92 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<feed xmlns:atom="http://www.w3.org/2005/Atom"> |
||||
<title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title> |
||||
<description><![CDATA[This is a constantly updating lorem ipsum feed]]></description> |
||||
<link>http://example.com/</link> |
||||
<generator>RSS for Node</generator> |
||||
<lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate> |
||||
<author><![CDATA[John Smith]]></author> |
||||
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate> |
||||
<copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright> |
||||
<ttl>60</ttl> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title> |
||||
<description><![CDATA[Exercitation ut Lorem sint proident.]]></description> |
||||
<link>http://example.com/test/1540941720</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941720</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title> |
||||
<description><![CDATA[Ea est do quis fugiat exercitation.]]></description> |
||||
<link>http://example.com/test/1540941660</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941660</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title> |
||||
<description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description> |
||||
<link>http://example.com/test/1540941600</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941600</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title> |
||||
<description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description> |
||||
<link>http://example.com/test/1540941540</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941540</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title> |
||||
<description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description> |
||||
<link>http://example.com/test/1540941480</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941480</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title> |
||||
<description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description> |
||||
<link>http://example.com/test/1540941420</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941420</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title> |
||||
<description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description> |
||||
<link>http://example.com/test/1540941360</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941360</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title> |
||||
<description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description> |
||||
<link>http://example.com/test/1540941300</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941300</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title> |
||||
<description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description> |
||||
<link>http://example.com/test/1540941240</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941240</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate> |
||||
</entry> |
||||
<entry> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title> |
||||
<description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description> |
||||
<link>http://example.com/test/1540941180</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941180</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate> |
||||
</entry> |
||||
</feed> |
@ -0,0 +1,96 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" |
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/" |
||||
xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> |
||||
<channel> |
||||
<title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title> |
||||
<description><![CDATA[This is a constantly updating lorem ipsum feed]]></description> |
||||
<link>http://example.com/</link> |
||||
<generator>RSS for Node</generator> |
||||
<lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate> |
||||
<author><![CDATA[John Smith]]></author> |
||||
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate> |
||||
<copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright> |
||||
<ttl>60</ttl> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title> |
||||
<description><![CDATA[Exercitation ut Lorem sint proident.]]></description> |
||||
<link>http://example.com/test/1540941720</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941720</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title> |
||||
<description><![CDATA[Ea est do quis fugiat exercitation.]]></description> |
||||
<link>http://example.com/test/1540941660</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941660</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title> |
||||
<description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description> |
||||
<link>http://example.com/test/1540941600</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941600</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title> |
||||
<description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description> |
||||
<link>http://example.com/test/1540941540</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941540</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title> |
||||
<description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description> |
||||
<link>http://example.com/test/1540941480</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941480</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title> |
||||
<description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description> |
||||
<link>http://example.com/test/1540941420</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941420</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title> |
||||
<description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description> |
||||
<link>http://example.com/test/1540941360</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941360</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title> |
||||
<description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description> |
||||
<link>http://example.com/test/1540941300</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941300</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title> |
||||
<description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description> |
||||
<link>http://example.com/test/1540941240</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941240</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate> |
||||
</item> |
||||
<item> |
||||
<title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title> |
||||
<description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description> |
||||
<link>http://example.com/test/1540941180</link> |
||||
<guid isPermaLink="true">http://example.com/test/1540941180</guid> |
||||
<dc:creator><![CDATA[John Smith]]></dc:creator> |
||||
<pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate> |
||||
</item> |
||||
</channel> |
||||
</rss> |
@ -0,0 +1,20 @@ |
||||
[Full iTunes list](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) |
||||
|
||||
[Example of ideal iTunes RSS feed](https://help.apple.com/itc/podcasts_connect/#/itcbaf351599) |
||||
|
||||
``` |
||||
<itunes:author> |
||||
<itunes:block> |
||||
<itunes:catergory> |
||||
<itunes:image> |
||||
<itunes:duration> |
||||
<itunes:explicit> |
||||
<itunes:isClosedCaptioned> |
||||
<itunes:order> |
||||
<itunes:complete> |
||||
<itunes:new-feed-url> |
||||
<itunes:owner> |
||||
<itunes:subtitle> |
||||
<itunes:summary> |
||||
<language> |
||||
``` |
@ -0,0 +1,27 @@ |
||||
package feeds |
||||
|
||||
// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go
|
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"fmt" |
||||
) |
||||
|
||||
type UUID [16]byte |
||||
|
||||
// create a new uuid v4
|
||||
func NewUUID() *UUID { |
||||
u := &UUID{} |
||||
_, err := rand.Read(u[:16]) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
u[8] = (u[8] | 0x80) & 0xBf |
||||
u[6] = (u[6] | 0x40) & 0x4f |
||||
return u |
||||
} |
||||
|
||||
func (u *UUID) String() string { |
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) |
||||
} |
Loading…
Reference in new issue