From 3728f1daa08e4c228db212844612555e9e2904df Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 16 Oct 2021 16:21:16 +0200
Subject: [PATCH] Add RSS/Atom feed support for user actions (#16002)
Return rss/atom feed for user based on rss url suffix or Content-Type header.
---
go.mod | 1 +
go.sum | 2 +
modules/context/context.go | 2 +-
options/locale/locale_en-US.ini | 3 +
routers/web/feed/convert.go | 154 +++++++++++++++
routers/web/feed/profile.go | 98 ++++++++++
routers/web/user/home.go | 40 +---
routers/web/user/profile.go | 32 ++-
vendor/github.com/gorilla/feeds/.travis.yml | 16 ++
vendor/github.com/gorilla/feeds/AUTHORS | 29 +++
vendor/github.com/gorilla/feeds/LICENSE | 22 +++
vendor/github.com/gorilla/feeds/README.md | 185 ++++++++++++++++++
vendor/github.com/gorilla/feeds/atom.go | 169 ++++++++++++++++
vendor/github.com/gorilla/feeds/doc.go | 73 +++++++
vendor/github.com/gorilla/feeds/feed.go | 145 ++++++++++++++
vendor/github.com/gorilla/feeds/json.go | 183 +++++++++++++++++
vendor/github.com/gorilla/feeds/rss.go | 168 ++++++++++++++++
vendor/github.com/gorilla/feeds/test.atom | 92 +++++++++
vendor/github.com/gorilla/feeds/test.rss | 96 +++++++++
.../github.com/gorilla/feeds/to-implement.md | 20 ++
vendor/github.com/gorilla/feeds/uuid.go | 27 +++
vendor/modules.txt | 3 +
22 files changed, 1521 insertions(+), 39 deletions(-)
create mode 100644 routers/web/feed/convert.go
create mode 100644 routers/web/feed/profile.go
create mode 100644 vendor/github.com/gorilla/feeds/.travis.yml
create mode 100644 vendor/github.com/gorilla/feeds/AUTHORS
create mode 100644 vendor/github.com/gorilla/feeds/LICENSE
create mode 100644 vendor/github.com/gorilla/feeds/README.md
create mode 100644 vendor/github.com/gorilla/feeds/atom.go
create mode 100644 vendor/github.com/gorilla/feeds/doc.go
create mode 100644 vendor/github.com/gorilla/feeds/feed.go
create mode 100644 vendor/github.com/gorilla/feeds/json.go
create mode 100644 vendor/github.com/gorilla/feeds/rss.go
create mode 100644 vendor/github.com/gorilla/feeds/test.atom
create mode 100644 vendor/github.com/gorilla/feeds/test.rss
create mode 100644 vendor/github.com/gorilla/feeds/to-implement.md
create mode 100644 vendor/github.com/gorilla/feeds/uuid.go
diff --git a/go.mod b/go.mod
index e53d24091..7a4923e9e 100644
--- a/go.mod
+++ b/go.mod
@@ -57,6 +57,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.2.0
github.com/gorilla/context v1.1.1
+ github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
diff --git a/go.sum b/go.sum
index d86feed9e..d935b6b84 100644
--- a/go.sum
+++ b/go.sum
@@ -598,6 +598,8 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
+github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
diff --git a/modules/context/context.go b/modules/context/context.go
index 6bd934928..0a603cced 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -320,7 +320,7 @@ func (ctx *Context) PlainText(status int, bs []byte) {
ctx.Resp.WriteHeader(status)
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
if _, err := ctx.Resp.Write(bs); err != nil {
- ctx.ServerError("Render JSON failed", err)
+ ctx.ServerError("Write bytes failed", err)
}
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2cb1bcd1a..407ec9f84 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -228,6 +228,7 @@ view_home = View %s
search_repos = Find a repository…
filter = Other Filters
filter_by_team_repositories = Filter by team repositories
+feed_of = Feed of "%s"
show_archived = Archived
show_both_archived_unarchived = Showing both archived and unarchived
@@ -2777,6 +2778,8 @@ publish_release = `released "%[4]s" at %[4]s for %[3]s#%[2]s`
review_dismissed_reason = Reason:
create_branch = created branch %[3]s in %[4]s
+stared_repo = stared %[2]s
+watched_repo = started watching %[2]s
[tool]
ago = %s ago
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
new file mode 100644
index 000000000..8fd8a6c6b
--- /dev/null
+++ b/routers/web/feed/convert.go
@@ -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("%s\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
+}
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
new file mode 100644
index 000000000..8bd0cb7c2
--- /dev/null
+++ b/routers/web/feed/profile.go
@@ -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)
+ }
+ }
+}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d2b67e6e5..959b1aa1e 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/web/feed"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
@@ -60,42 +61,6 @@ func getDashboardContextUser(ctx *context.Context) *models.User {
return ctxUser
}
-// retrieveFeeds loads feeds for the specified user
-func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
- actions, err := models.GetFeeds(options)
- if err != nil {
- ctx.ServerError("GetFeeds", err)
- return
- }
-
- 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
- }
- userCache[repoOwner.ID] = repoOwner
- }
- act.Repo.Owner = repoOwner
- }
- ctx.Data["Feeds"] = actions
-}
-
// Dashboard render the dashboard page
func Dashboard(ctx *context.Context) {
ctxUser := getDashboardContextUser(ctx)
@@ -154,7 +119,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["MirrorCount"] = len(mirrors)
ctx.Data["Mirrors"] = mirrors
- retrieveFeeds(ctx, models.GetFeedsOptions{
+ ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team,
Actor: ctx.User,
@@ -167,6 +132,7 @@ func Dashboard(ctx *context.Context) {
if ctx.Written() {
return
}
+
ctx.HTML(http.StatusOK, tplDashboard)
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index d64d5621d..d2a8d83fa 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/routers/web/org"
)
@@ -71,12 +72,35 @@ func Profile(ctx *context.Context) {
uname = strings.TrimSuffix(uname, ".gpg")
}
+ showFeedType := ""
+ if strings.HasSuffix(uname, ".rss") {
+ showFeedType = "rss"
+ uname = strings.TrimSuffix(uname, ".rss")
+ } else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") {
+ showFeedType = "rss"
+ }
+ if strings.HasSuffix(uname, ".atom") {
+ showFeedType = "atom"
+ uname = strings.TrimSuffix(uname, ".atom")
+ } else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
+ showFeedType = "atom"
+ }
+
ctxUser := GetUserByName(ctx, uname)
if ctx.Written() {
return
}
if ctxUser.IsOrganization() {
+ /*
+ // TODO: enable after rss.RetrieveFeeds() do handle org correctly
+ // Show Org RSS feed
+ if len(showFeedType) != 0 {
+ rss.ShowUserFeed(ctx, ctxUser, showFeedType)
+ return
+ }
+ */
+
org.Home(ctx)
return
}
@@ -99,6 +123,12 @@ func Profile(ctx *context.Context) {
return
}
+ // Show User RSS feed
+ if len(showFeedType) != 0 {
+ feed.ShowUserFeed(ctx, ctxUser, showFeedType)
+ return
+ }
+
// Show OpenID URIs
openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
if err != nil {
@@ -217,7 +247,7 @@ func Profile(ctx *context.Context) {
total = ctxUser.NumFollowing
case "activity":
- retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
+ ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
Actor: ctx.User,
IncludePrivate: showPrivate,
OnlyPerformedBy: true,
diff --git a/vendor/github.com/gorilla/feeds/.travis.yml b/vendor/github.com/gorilla/feeds/.travis.yml
new file mode 100644
index 000000000..7939a2186
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/.travis.yml
@@ -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 ./...
diff --git a/vendor/github.com/gorilla/feeds/AUTHORS b/vendor/github.com/gorilla/feeds/AUTHORS
new file mode 100644
index 000000000..2c28cf943
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/AUTHORS
@@ -0,0 +1,29 @@
+# This is the official list of gorilla/feeds authors for copyright purposes.
+# Please keep the list sorted.
+
+Dmitry Chestnykh
+Eddie Scholtz
+Gabriel Simmer
+Google LLC (https://opensource.google.com/)
+honky
+James Gregory
+Jason Hall
+Jason Moiron
+Kamil Kisiel
+Kevin Stock
+Markus Zimmermann
+Matt Silverlock
+Matthew Dawson
+Milan Aleksic
+Milan Aleksić
+nlimpid
+Paul Petring
+Sean Enck
+Sue Spence
+Supermighty
+Toru Fukui
+Vabd
+Volker
+ZhiFeng Hu
+weberc2
+
diff --git a/vendor/github.com/gorilla/feeds/LICENSE b/vendor/github.com/gorilla/feeds/LICENSE
new file mode 100644
index 000000000..e24412d56
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/LICENSE
@@ -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.
diff --git a/vendor/github.com/gorilla/feeds/README.md b/vendor/github.com/gorilla/feeds/README.md
new file mode 100644
index 000000000..4d733cf53
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/README.md
@@ -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 effectively",
+ 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
+
+
+ jmoiron.net blog
+
+ http://jmoiron.net/blog
+ 2013-01-16T03:26:01-05:00
+ discussion about tech, footie, photos
+
+ Limiting Concurrency in Go
+
+ 2013-01-16T03:26:01-05:00
+ tag:jmoiron.net,2013-01-16:/blog/limiting-concurrency-in-go/
+ A discussion on controlled parallelism in golang
+
+ Jason Moiron
+ jmoiron@jmoiron.net
+
+
+
+ Logic-less Template Redux
+
+ 2013-01-16T03:26:01-05:00
+ tag:jmoiron.net,2013-01-16:/blog/logicless-template-redux/
+ More thoughts on logicless templates
+
+
+
+ Idiomatic Code Reuse in Go
+
+ 2013-01-16T03:26:01-05:00
+ tag:jmoiron.net,2013-01-16:/blog/idiomatic-code-reuse-in-go/
+ How to use interfaces <em>effectively</em>
+
+
+
+
+
+
+
+ jmoiron.net blog
+ http://jmoiron.net/blog
+ discussion about tech, footie, photos
+ jmoiron@jmoiron.net (Jason Moiron)
+ 2013-01-16T03:22:24-05:00
+ -
+ Limiting Concurrency in Go
+ http://jmoiron.net/blog/limiting-concurrency-in-go/
+ A discussion on controlled parallelism in golang
+ 2013-01-16T03:22:24-05:00
+
+ -
+ Logic-less Template Redux
+ http://jmoiron.net/blog/logicless-template-redux/
+ More thoughts on logicless templates
+ 2013-01-16T03:22:24-05:00
+
+ -
+ Idiomatic Code Reuse in Go
+ http://jmoiron.net/blog/idiomatic-code-reuse-in-go/
+ How to use interfaces <em>effectively</em>
+ 2013-01-16T03:22:24-05:00
+
+
+
+
+{
+ "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"
+ }
+ ]
+}
+```
+
diff --git a/vendor/github.com/gorilla/feeds/atom.go b/vendor/github.com/gorilla/feeds/atom.go
new file mode 100644
index 000000000..7196f4781
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/atom.go
@@ -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
+ 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
+}
diff --git a/vendor/github.com/gorilla/feeds/doc.go b/vendor/github.com/gorilla/feeds/doc.go
new file mode 100644
index 000000000..4e0759ccc
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/doc.go
@@ -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 effectively",
+ 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
diff --git a/vendor/github.com/gorilla/feeds/feed.go b/vendor/github.com/gorilla/feeds/feed.go
new file mode 100644
index 000000000..790a1b6ce
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/feed.go
@@ -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)
+}
diff --git a/vendor/github.com/gorilla/feeds/json.go b/vendor/github.com/gorilla/feeds/json.go
new file mode 100644
index 000000000..75a82fd62
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/json.go
@@ -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
+}
diff --git a/vendor/github.com/gorilla/feeds/rss.go b/vendor/github.com/gorilla/feeds/rss.go
new file mode 100644
index 000000000..09179dfb2
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/rss.go
@@ -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 .. 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
+ 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/",
+ }
+}
diff --git a/vendor/github.com/gorilla/feeds/test.atom b/vendor/github.com/gorilla/feeds/test.atom
new file mode 100644
index 000000000..aa1521481
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/test.atom
@@ -0,0 +1,92 @@
+
+
+
+
+ http://example.com/
+ RSS for Node
+ Tue, 30 Oct 2018 23:22:37 GMT
+
+ Tue, 30 Oct 2018 23:22:00 GMT
+
+ 60
+
+
+
+ http://example.com/test/1540941720
+ http://example.com/test/1540941720
+
+ Tue, 30 Oct 2018 23:22:00 GMT
+
+
+
+
+ http://example.com/test/1540941660
+ http://example.com/test/1540941660
+
+ Tue, 30 Oct 2018 23:21:00 GMT
+
+
+
+
+ http://example.com/test/1540941600
+ http://example.com/test/1540941600
+
+ Tue, 30 Oct 2018 23:20:00 GMT
+
+
+
+
+ http://example.com/test/1540941540
+ http://example.com/test/1540941540
+
+ Tue, 30 Oct 2018 23:19:00 GMT
+
+
+
+
+ http://example.com/test/1540941480
+ http://example.com/test/1540941480
+
+ Tue, 30 Oct 2018 23:18:00 GMT
+
+
+
+
+ http://example.com/test/1540941420
+ http://example.com/test/1540941420
+
+ Tue, 30 Oct 2018 23:17:00 GMT
+
+
+
+
+ http://example.com/test/1540941360
+ http://example.com/test/1540941360
+
+ Tue, 30 Oct 2018 23:16:00 GMT
+
+
+
+
+ http://example.com/test/1540941300
+ http://example.com/test/1540941300
+
+ Tue, 30 Oct 2018 23:15:00 GMT
+
+
+
+
+ http://example.com/test/1540941240
+ http://example.com/test/1540941240
+
+ Tue, 30 Oct 2018 23:14:00 GMT
+
+
+
+
+ http://example.com/test/1540941180
+ http://example.com/test/1540941180
+
+ Tue, 30 Oct 2018 23:13:00 GMT
+
+
\ No newline at end of file
diff --git a/vendor/github.com/gorilla/feeds/test.rss b/vendor/github.com/gorilla/feeds/test.rss
new file mode 100644
index 000000000..8d912aba5
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/test.rss
@@ -0,0 +1,96 @@
+
+
+
+
+
+ http://example.com/
+ RSS for Node
+ Tue, 30 Oct 2018 23:22:37 GMT
+
+ Tue, 30 Oct 2018 23:22:00 GMT
+
+ 60
+ -
+
+
+ http://example.com/test/1540941720
+ http://example.com/test/1540941720
+
+ Tue, 30 Oct 2018 23:22:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941660
+ http://example.com/test/1540941660
+
+ Tue, 30 Oct 2018 23:21:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941600
+ http://example.com/test/1540941600
+
+ Tue, 30 Oct 2018 23:20:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941540
+ http://example.com/test/1540941540
+
+ Tue, 30 Oct 2018 23:19:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941480
+ http://example.com/test/1540941480
+
+ Tue, 30 Oct 2018 23:18:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941420
+ http://example.com/test/1540941420
+
+ Tue, 30 Oct 2018 23:17:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941360
+ http://example.com/test/1540941360
+
+ Tue, 30 Oct 2018 23:16:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941300
+ http://example.com/test/1540941300
+
+ Tue, 30 Oct 2018 23:15:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941240
+ http://example.com/test/1540941240
+
+ Tue, 30 Oct 2018 23:14:00 GMT
+
+ -
+
+
+ http://example.com/test/1540941180
+ http://example.com/test/1540941180
+
+ Tue, 30 Oct 2018 23:13:00 GMT
+
+
+
\ No newline at end of file
diff --git a/vendor/github.com/gorilla/feeds/to-implement.md b/vendor/github.com/gorilla/feeds/to-implement.md
new file mode 100644
index 000000000..45fd1e75e
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/to-implement.md
@@ -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)
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/vendor/github.com/gorilla/feeds/uuid.go b/vendor/github.com/gorilla/feeds/uuid.go
new file mode 100644
index 000000000..51bbafe13
--- /dev/null
+++ b/vendor/github.com/gorilla/feeds/uuid.go
@@ -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:])
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 72704a7f4..82afbb673 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -469,6 +469,9 @@ github.com/google/uuid
github.com/gorilla/context
# github.com/gorilla/css v1.0.0
github.com/gorilla/css/scanner
+# github.com/gorilla/feeds v1.1.1
+## explicit
+github.com/gorilla/feeds
# github.com/gorilla/handlers v1.5.1
github.com/gorilla/handlers
# github.com/gorilla/mux v1.8.0