mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Federation: return useful statistic information for nodeinfo (#19561)
Add statistic information for total user count, active user count, issue count and comment count for `/nodeinfo`
This commit is contained in:
		| @@ -556,7 +556,7 @@ func runCreateUser(c *cli.Context) error { | |||||||
|  |  | ||||||
| 	// If this is the first user being created. | 	// If this is the first user being created. | ||||||
| 	// Take it as the admin and don't force a password update. | 	// Take it as the admin and don't force a password update. | ||||||
| 	if n := user_model.CountUsers(); n == 0 { | 	if n := user_model.CountUsers(nil); n == 0 { | ||||||
| 		changePassword = false | 		changePassword = false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2240,6 +2240,9 @@ PATH = | |||||||
| ;; | ;; | ||||||
| ;; Enable/Disable federation capabilities | ;; Enable/Disable federation capabilities | ||||||
| ; ENABLED = true | ; ENABLED = true | ||||||
|  | ;; | ||||||
|  | ;; Enable/Disable user statistics for nodeinfo if federation is enabled | ||||||
|  | ; SHARE_USER_STATISTICS = true | ||||||
|  |  | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|   | |||||||
| @@ -1085,6 +1085,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf | |||||||
| ## Federation (`federation`) | ## Federation (`federation`) | ||||||
|  |  | ||||||
| - `ENABLED`: **true**: Enable/Disable federation capabilities | - `ENABLED`: **true**: Enable/Disable federation capabilities | ||||||
|  | - `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled | ||||||
|  |  | ||||||
| ## Packages (`packages`) | ## Packages (`packages`) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ func TestNodeinfo(t *testing.T) { | |||||||
| 		resp := MakeRequest(t, req, http.StatusOK) | 		resp := MakeRequest(t, req, http.StatusOK) | ||||||
| 		var nodeinfo api.NodeInfo | 		var nodeinfo api.NodeInfo | ||||||
| 		DecodeJSON(t, resp, &nodeinfo) | 		DecodeJSON(t, resp, &nodeinfo) | ||||||
|  | 		assert.True(t, nodeinfo.OpenRegistrations) | ||||||
| 		assert.Equal(t, "gitea", nodeinfo.Software.Name) | 		assert.Equal(t, "gitea", nodeinfo.Software.Name) | ||||||
|  | 		assert.Equal(t, 23, nodeinfo.Usage.Users.Total) | ||||||
|  | 		assert.Equal(t, 15, nodeinfo.Usage.LocalPosts) | ||||||
|  | 		assert.Equal(t, 2, nodeinfo.Usage.LocalComments) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -590,3 +590,10 @@ func TestLoadTotalTrackedTime(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, int64(3682), milestone.TotalTrackedTime) | 	assert.Equal(t, int64(3682), milestone.TotalTrackedTime) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestCountIssues(t *testing.T) { | ||||||
|  | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  | 	count, err := CountIssues(&IssuesOptions{}) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.EqualValues(t, 15, count) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ type IssueByRepositoryCount struct { | |||||||
| // GetStatistic returns the database statistics | // GetStatistic returns the database statistics | ||||||
| func GetStatistic() (stats Statistic) { | func GetStatistic() (stats Statistic) { | ||||||
| 	e := db.GetEngine(db.DefaultContext) | 	e := db.GetEngine(db.DefaultContext) | ||||||
| 	stats.Counter.User = user_model.CountUsers() | 	stats.Counter.User = user_model.CountUsers(nil) | ||||||
| 	stats.Counter.Org = organization.CountOrganizations() | 	stats.Counter.Org = organization.CountOrganizations() | ||||||
| 	stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) | 	stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) | ||||||
| 	stats.Counter.Repo = repo_model.CountRepositories(true) | 	stats.Counter.Repo = repo_model.CountRepositories(true) | ||||||
|   | |||||||
| @@ -744,16 +744,25 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e | |||||||
| 	return committer.Commit() | 	return committer.Commit() | ||||||
| } | } | ||||||
|  |  | ||||||
| func countUsers(e db.Engine) int64 { | // CountUserFilter represent optional filters for CountUsers | ||||||
| 	count, _ := e. | type CountUserFilter struct { | ||||||
| 		Where("type=0"). | 	LastLoginSince *int64 | ||||||
| 		Count(new(User)) |  | ||||||
| 	return count |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // CountUsers returns number of users. | // CountUsers returns number of users. | ||||||
| func CountUsers() int64 { | func CountUsers(opts *CountUserFilter) int64 { | ||||||
| 	return countUsers(db.GetEngine(db.DefaultContext)) | 	return countUsers(db.DefaultContext, opts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func countUsers(ctx context.Context, opts *CountUserFilter) int64 { | ||||||
|  | 	sess := db.GetEngine(ctx).Where(builder.Eq{"type": "0"}) | ||||||
|  |  | ||||||
|  | 	if opts != nil && opts.LastLoginSince != nil { | ||||||
|  | 		sess = sess.Where(builder.Gte{"last_login_unix": *opts.LastLoginSince}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	count, _ := sess.Count(new(User)) | ||||||
|  | 	return count | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetVerifyUser get user by verify code | // GetVerifyUser get user by verify code | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/auth" | 	"code.gitea.io/gitea/models/auth" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
|  | 	"code.gitea.io/gitea/modules/cache" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| @@ -247,6 +248,7 @@ func APIContexter() func(http.Handler) http.Handler { | |||||||
| 					Resp:   NewResponse(w), | 					Resp:   NewResponse(w), | ||||||
| 					Data:   map[string]interface{}{}, | 					Data:   map[string]interface{}{}, | ||||||
| 					Locale: locale, | 					Locale: locale, | ||||||
|  | 					Cache:  cache.GetCache(), | ||||||
| 					Repo: &Repository{ | 					Repo: &Repository{ | ||||||
| 						PullRequest: &PullRequest{}, | 						PullRequest: &PullRequest{}, | ||||||
| 					}, | 					}, | ||||||
|   | |||||||
| @@ -9,9 +9,11 @@ import "code.gitea.io/gitea/modules/log" | |||||||
| // Federation settings | // Federation settings | ||||||
| var ( | var ( | ||||||
| 	Federation = struct { | 	Federation = struct { | ||||||
| 		Enabled bool | 		Enabled             bool | ||||||
|  | 		ShareUserStatistics bool | ||||||
| 	}{ | 	}{ | ||||||
| 		Enabled: true, | 		Enabled:             true, | ||||||
|  | 		ShareUserStatistics: true, | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,12 +6,17 @@ package misc | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const cacheKeyNodeInfoUsage = "API_NodeInfoUsage" | ||||||
|  |  | ||||||
| // NodeInfo returns the NodeInfo for the Gitea instance to allow for federation | // NodeInfo returns the NodeInfo for the Gitea instance to allow for federation | ||||||
| func NodeInfo(ctx *context.APIContext) { | func NodeInfo(ctx *context.APIContext) { | ||||||
| 	// swagger:operation GET /nodeinfo miscellaneous getNodeInfo | 	// swagger:operation GET /nodeinfo miscellaneous getNodeInfo | ||||||
| @@ -23,6 +28,37 @@ func NodeInfo(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/NodeInfo" | 	//     "$ref": "#/responses/NodeInfo" | ||||||
|  |  | ||||||
|  | 	nodeInfoUsage := structs.NodeInfoUsage{} | ||||||
|  | 	if setting.Federation.ShareUserStatistics { | ||||||
|  | 		info, ok := ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage) | ||||||
|  | 		if !ok { | ||||||
|  | 			usersTotal := int(user_model.CountUsers(nil)) | ||||||
|  | 			now := time.Now() | ||||||
|  | 			timeOneMonthAgo := now.AddDate(0, -1, 0).Unix() | ||||||
|  | 			timeHaveYearAgo := now.AddDate(0, -6, 0).Unix() | ||||||
|  | 			usersActiveMonth := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo})) | ||||||
|  | 			usersActiveHalfyear := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo})) | ||||||
|  |  | ||||||
|  | 			allIssues, _ := models.CountIssues(&models.IssuesOptions{}) | ||||||
|  | 			allComments, _ := models.CountComments(&models.FindCommentsOptions{}) | ||||||
|  |  | ||||||
|  | 			info = structs.NodeInfoUsage{ | ||||||
|  | 				Users: structs.NodeInfoUsageUsers{ | ||||||
|  | 					Total:          usersTotal, | ||||||
|  | 					ActiveMonth:    usersActiveMonth, | ||||||
|  | 					ActiveHalfyear: usersActiveHalfyear, | ||||||
|  | 				}, | ||||||
|  | 				LocalPosts:    int(allIssues), | ||||||
|  | 				LocalComments: int(allComments), | ||||||
|  | 			} | ||||||
|  | 			if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { | ||||||
|  | 				ctx.InternalServerError(err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		nodeInfoUsage = info | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	nodeInfo := &structs.NodeInfo{ | 	nodeInfo := &structs.NodeInfo{ | ||||||
| 		Version: "2.1", | 		Version: "2.1", | ||||||
| 		Software: structs.NodeInfoSoftware{ | 		Software: structs.NodeInfoSoftware{ | ||||||
| @@ -34,12 +70,10 @@ func NodeInfo(ctx *context.APIContext) { | |||||||
| 		Protocols: []string{"activitypub"}, | 		Protocols: []string{"activitypub"}, | ||||||
| 		Services: structs.NodeInfoServices{ | 		Services: structs.NodeInfoServices{ | ||||||
| 			Inbound:  []string{}, | 			Inbound:  []string{}, | ||||||
| 			Outbound: []string{}, | 			Outbound: []string{"rss2.0"}, | ||||||
| 		}, | 		}, | ||||||
| 		OpenRegistrations: setting.Service.ShowRegistrationButton, | 		OpenRegistrations: setting.Service.ShowRegistrationButton, | ||||||
| 		Usage: structs.NodeInfoUsage{ | 		Usage:             nodeInfoUsage, | ||||||
| 			Users: structs.NodeInfoUsageUsers{}, |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusOK, nodeInfo) | 	ctx.JSON(http.StatusOK, nodeInfo) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -600,7 +600,7 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{ | |||||||
| // sends a confirmation email if required. | // sends a confirmation email if required. | ||||||
| func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { | func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { | ||||||
| 	// Auto-set admin for the only user. | 	// Auto-set admin for the only user. | ||||||
| 	if user_model.CountUsers() == 1 { | 	if user_model.CountUsers(nil) == 1 { | ||||||
| 		u.IsAdmin = true | 		u.IsAdmin = true | ||||||
| 		u.IsActive = true | 		u.IsActive = true | ||||||
| 		u.SetLastLogin() | 		u.SetLastLogin() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user