diff options
author | 6543 <6543@obermui.de> | 2021-10-16 16:21:16 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-16 16:21:16 +0200 |
commit | 3728f1daa08e4c228db212844612555e9e2904df (patch) | |
tree | a47302aa3106ee33643ebcdaa4b2c5fe52349596 /routers | |
parent | 8edda8b446200545b36432b57d00cd1972a5cb7e (diff) | |
download | gitea-3728f1daa08e4c228db212844612555e9e2904df.tar.gz gitea-3728f1daa08e4c228db212844612555e9e2904df.zip |
Add RSS/Atom feed support for user actions (#16002)
Return rss/atom feed for user based on rss url suffix or Content-Type header.
Diffstat (limited to 'routers')
-rw-r--r-- | routers/web/feed/convert.go | 154 | ||||
-rw-r--r-- | routers/web/feed/profile.go | 98 | ||||
-rw-r--r-- | routers/web/user/home.go | 40 | ||||
-rw-r--r-- | routers/web/user/profile.go | 32 |
4 files changed, 286 insertions, 38 deletions
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go new file mode 100644 index 0000000000..8fd8a6c6b7 --- /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("<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 +} diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go new file mode 100644 index 0000000000..8bd0cb7c29 --- /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 d2b67e6e59..959b1aa1e9 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 d64d5621de..d2a8d83faa 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, |