Add activity feeds API (#23494)

Close #5666

Add APIs for getting activity feeds.
This commit is contained in:
Zettat123 2023-04-04 21:35:31 +08:00 committed by GitHub
parent d149093ce3
commit 6b0df6d8da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 665 additions and 0 deletions

View file

@ -66,6 +66,67 @@ const (
ActionAutoMergePullRequest // 27 ActionAutoMergePullRequest // 27
) )
func (at ActionType) String() string {
switch at {
case ActionCreateRepo:
return "create_repo"
case ActionRenameRepo:
return "rename_repo"
case ActionStarRepo:
return "star_repo"
case ActionWatchRepo:
return "watch_repo"
case ActionCommitRepo:
return "commit_repo"
case ActionCreateIssue:
return "create_issue"
case ActionCreatePullRequest:
return "create_pull_request"
case ActionTransferRepo:
return "transfer_repo"
case ActionPushTag:
return "push_tag"
case ActionCommentIssue:
return "comment_issue"
case ActionMergePullRequest:
return "merge_pull_request"
case ActionCloseIssue:
return "close_issue"
case ActionReopenIssue:
return "reopen_issue"
case ActionClosePullRequest:
return "close_pull_request"
case ActionReopenPullRequest:
return "reopen_pull_request"
case ActionDeleteTag:
return "delete_tag"
case ActionDeleteBranch:
return "delete_branch"
case ActionMirrorSyncPush:
return "mirror_sync_push"
case ActionMirrorSyncCreate:
return "mirror_sync_create"
case ActionMirrorSyncDelete:
return "mirror_sync_delete"
case ActionApprovePullRequest:
return "approve_pull_request"
case ActionRejectPullRequest:
return "reject_pull_request"
case ActionCommentPull:
return "comment_pull"
case ActionPublishRelease:
return "publish_release"
case ActionPullReviewDismissed:
return "pull_review_dismissed"
case ActionPullRequestReadyForReview:
return "pull_request_ready_for_review"
case ActionAutoMergePullRequest:
return "auto_merge_pull_request"
default:
return "action-" + strconv.Itoa(int(at))
}
}
// Action represents user operation type and other information to // Action represents user operation type and other information to
// repository. It implemented interface base.Actioner so that can be // repository. It implemented interface base.Actioner so that can be
// used in template render. // used in template render.

View file

@ -0,0 +1,22 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
type Activity struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"` // Receiver user
OpType string `json:"op_type"`
ActUserID int64 `json:"act_user_id"`
ActUser *User `json:"act_user"`
RepoID int64 `json:"repo_id"`
Repo *Repository `json:"repo"`
CommentID int64 `json:"comment_id"`
Comment *Comment `json:"comment"`
RefName string `json:"ref_name"`
IsPrivate bool `json:"is_private"`
Content string `json:"content"`
Created time.Time `json:"created"`
}

View file

@ -754,6 +754,8 @@ func Routes(ctx gocontext.Context) *web.Route {
Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken) Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
m.Combo("/{id}").Delete(user.DeleteAccessToken) m.Combo("/{id}").Delete(user.DeleteAccessToken)
}, reqBasicAuth()) }, reqBasicAuth())
m.Get("/activities/feeds", user.ListUserActivityFeeds)
}, context_service.UserAssignmentAPI()) }, context_service.UserAssignmentAPI())
}) })
@ -1177,6 +1179,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
}, repoAssignment()) }, repoAssignment())
}) })
@ -1234,6 +1237,7 @@ func Routes(ctx gocontext.Context) *web.Route {
Patch(bind(api.EditHookOption{}), org.EditHook). Patch(bind(api.EditHookOption{}), org.EditHook).
Delete(org.DeleteHook) Delete(org.DeleteHook)
}, reqToken(auth_model.AccessTokenScopeAdminOrgHook), reqOrgOwnership(), reqWebhooksEnabled()) }, reqToken(auth_model.AccessTokenScopeAdminOrgHook), reqOrgOwnership(), reqWebhooksEnabled())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
}, orgAssignment(true)) }, orgAssignment(true))
m.Group("/teams/{teamid}", func() { m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeam). m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeam).
@ -1253,6 +1257,7 @@ func Routes(ctx gocontext.Context) *web.Route {
Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), org.RemoveTeamRepository). Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), org.RemoveTeamRepository).
Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepo) Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepo)
}) })
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, orgAssignment(false, true), reqToken(""), reqTeamMembership()) }, orgAssignment(false, true), reqToken(""), reqTeamMembership())
m.Group("/admin", func() { m.Group("/admin", func() {

View file

@ -7,6 +7,7 @@ package org
import ( import (
"net/http" "net/http"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -370,3 +371,69 @@ func Delete(ctx *context.APIContext) {
} }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
func ListOrgActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/activities/feeds organization orgListActivityFeeds
// ---
// summary: List an organization's activity feeds
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the org
// type: string
// required: true
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
includePrivate := false
if ctx.IsSigned {
if ctx.Doer.IsAdmin {
includePrivate = true
} else {
org := organization.OrgFromUser(ctx.ContextUser)
isMember, err := org.IsOrgMember(ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
return
}
includePrivate = isMember
}
}
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
IncludePrivate: includePrivate,
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
feeds, count, err := activities_model.GetFeeds(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetFeeds", err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -792,3 +793,55 @@ func SearchTeam(ctx *context.APIContext) {
"data": apiTeams, "data": apiTeams,
}) })
} }
func ListTeamActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/activities/feeds organization orgListTeamActivityFeeds
// ---
// summary: List a team's activity feeds
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer,
IncludePrivate: true,
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
feeds, count, err := activities_model.GetFeeds(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetFeeds", err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -1199,3 +1200,59 @@ func ValidateIssueConfig(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()}) ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()})
} }
} }
func ListRepoActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/activities/feeds repository repoListActivityFeeds
// ---
// summary: List a repository's activity feeds
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedRepo: ctx.Repo.Repository,
Actor: ctx.Doer,
IncludePrivate: true,
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
feeds, count, err := activities_model.GetFeeds(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetFeeds", err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}

View file

@ -0,0 +1,15 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
)
// ActivityFeedsList
// swagger:response ActivityFeedsList
type swaggerActivityFeedsList struct {
// in:body
Body []api.Activity `json:"body"`
}

View file

@ -145,3 +145,60 @@ func GetUserHeatmapData(ctx *context.APIContext) {
} }
ctx.JSON(http.StatusOK, heatmap) ctx.JSON(http.StatusOK, heatmap)
} }
func ListUserActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/activities/feeds user userListActivityFeeds
// ---
// summary: List a user's activity feeds
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of user
// type: string
// required: true
// - name: only-performed-by
// in: query
// description: if true, only show actions performed by the requested user
// type: boolean
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
IncludePrivate: includePrivate,
OnlyPerformedBy: ctx.FormBool("only-performed-by"),
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
feeds, count, err := activities_model.GetFeeds(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetFeeds", err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}

View file

@ -0,0 +1,52 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
activities_model "code.gitea.io/gitea/models/activities"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)
func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_model.User) *api.Activity {
p, err := access_model.GetUserRepoPermission(ctx, ac.Repo, doer)
if err != nil {
log.Error("GetUserRepoPermission[%d]: %v", ac.RepoID, err)
p.AccessMode = perm_model.AccessModeNone
}
result := &api.Activity{
ID: ac.ID,
UserID: ac.UserID,
OpType: ac.OpType.String(),
ActUserID: ac.ActUserID,
ActUser: ToUser(ctx, ac.ActUser, doer),
RepoID: ac.RepoID,
Repo: ToRepo(ctx, ac.Repo, p.AccessMode),
RefName: ac.RefName,
IsPrivate: ac.IsPrivate,
Content: ac.Content,
Created: ac.CreatedUnix.AsTime(),
}
if ac.Comment != nil {
result.CommentID = ac.CommentID
result.Comment = ToComment(ctx, ac.Comment)
}
return result
}
func ToActivities(ctx context.Context, al activities_model.ActionList, doer *user_model.User) []*api.Activity {
result := make([]*api.Activity, 0, len(al))
for _, ac := range al {
result = append(result, ToActivity(ctx, ac, doer))
}
return result
}

View file

@ -1411,6 +1411,54 @@
} }
} }
}, },
"/orgs/{org}/activities/feeds": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "List an organization's activity feeds",
"operationId": "orgListActivityFeeds",
"parameters": [
{
"type": "string",
"description": "name of the org",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"format": "date",
"description": "the date of the activities to be found",
"name": "date",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/ActivityFeedsList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/orgs/{org}/hooks": { "/orgs/{org}/hooks": {
"get": { "get": {
"produces": [ "produces": [
@ -2854,6 +2902,61 @@
} }
} }
}, },
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List a repository's activity feeds",
"operationId": "repoListActivityFeeds",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"format": "date",
"description": "the date of the activities to be found",
"name": "date",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/ActivityFeedsList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/archive/{archive}": { "/repos/{owner}/{repo}/archive/{archive}": {
"get": { "get": {
"produces": [ "produces": [
@ -12645,6 +12748,55 @@
} }
} }
}, },
"/teams/{id}/activities/feeds": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "List a team's activity feeds",
"operationId": "orgListTeamActivityFeeds",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "id of the team",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"format": "date",
"description": "the date of the activities to be found",
"name": "date",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/ActivityFeedsList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/teams/{id}/members": { "/teams/{id}/members": {
"get": { "get": {
"produces": [ "produces": [
@ -14304,6 +14456,60 @@
} }
} }
}, },
"/users/{username}/activities/feeds": {
"get": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "List a user's activity feeds",
"operationId": "userListActivityFeeds",
"parameters": [
{
"type": "string",
"description": "username of user",
"name": "username",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "if true, only show actions performed by the requested user",
"name": "only-performed-by",
"in": "query"
},
{
"type": "string",
"format": "date",
"description": "the date of the activities to be found",
"name": "date",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/ActivityFeedsList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/users/{username}/followers": { "/users/{username}/followers": {
"get": { "get": {
"produces": [ "produces": [
@ -14894,6 +15100,67 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"Activity": {
"type": "object",
"properties": {
"act_user": {
"$ref": "#/definitions/User"
},
"act_user_id": {
"type": "integer",
"format": "int64",
"x-go-name": "ActUserID"
},
"comment": {
"$ref": "#/definitions/Comment"
},
"comment_id": {
"type": "integer",
"format": "int64",
"x-go-name": "CommentID"
},
"content": {
"type": "string",
"x-go-name": "Content"
},
"created": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"is_private": {
"type": "boolean",
"x-go-name": "IsPrivate"
},
"op_type": {
"type": "string",
"x-go-name": "OpType"
},
"ref_name": {
"type": "string",
"x-go-name": "RefName"
},
"repo": {
"$ref": "#/definitions/Repository"
},
"repo_id": {
"type": "integer",
"format": "int64",
"x-go-name": "RepoID"
},
"user_id": {
"type": "integer",
"format": "int64",
"x-go-name": "UserID"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActivityPub": { "ActivityPub": {
"description": "ActivityPub type", "description": "ActivityPub type",
"type": "object", "type": "object",
@ -20942,6 +21209,15 @@
} }
} }
}, },
"ActivityFeedsList": {
"description": "ActivityFeedsList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Activity"
}
}
},
"ActivityPub": { "ActivityPub": {
"description": "ActivityPub", "description": "ActivityPub",
"schema": { "schema": {