From e2a3f3d259d230b7d1f62138e789ccdfd8f43b73 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Mon, 2 May 2022 15:35:45 +0200 Subject: [PATCH] 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` --- cmd/admin.go | 2 +- custom/conf/app.example.ini | 3 ++ .../doc/advanced/config-cheat-sheet.en-us.md | 1 + integrations/api_nodeinfo_test.go | 4 ++ models/issue_test.go | 7 ++++ models/statistic.go | 2 +- models/user/user.go | 23 ++++++---- modules/context/api.go | 2 + modules/setting/federation.go | 6 ++- routers/api/v1/misc/nodeinfo.go | 42 +++++++++++++++++-- routers/web/auth/auth.go | 2 +- 11 files changed, 78 insertions(+), 16 deletions(-) diff --git a/cmd/admin.go b/cmd/admin.go index 36cac50a4..fcf331751 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -556,7 +556,7 @@ func runCreateUser(c *cli.Context) error { // If this is the first user being created. // 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 } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index cbfde9c8d..0d0d80975 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2240,6 +2240,9 @@ PATH = ;; ;; Enable/Disable federation capabilities ; ENABLED = true +;; +;; Enable/Disable user statistics for nodeinfo if federation is enabled +; SHARE_USER_STATISTICS = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 84703aa56..5400f3208 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -1085,6 +1085,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf ## Federation (`federation`) - `ENABLED`: **true**: Enable/Disable federation capabilities +- `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled ## Packages (`packages`) diff --git a/integrations/api_nodeinfo_test.go b/integrations/api_nodeinfo_test.go index 1d25dc026..822dbf3f0 100644 --- a/integrations/api_nodeinfo_test.go +++ b/integrations/api_nodeinfo_test.go @@ -26,6 +26,10 @@ func TestNodeinfo(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) var nodeinfo api.NodeInfo DecodeJSON(t, resp, &nodeinfo) + assert.True(t, nodeinfo.OpenRegistrations) 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) }) } diff --git a/models/issue_test.go b/models/issue_test.go index 19e129526..5382c04f4 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -590,3 +590,10 @@ func TestLoadTotalTrackedTime(t *testing.T) { 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) +} diff --git a/models/statistic.go b/models/statistic.go index 87c1bd6d7..d858102be 100644 --- a/models/statistic.go +++ b/models/statistic.go @@ -49,7 +49,7 @@ type IssueByRepositoryCount struct { // GetStatistic returns the database statistics func GetStatistic() (stats Statistic) { 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.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) stats.Counter.Repo = repo_model.CountRepositories(true) diff --git a/models/user/user.go b/models/user/user.go index 5b556c988..6aa63a0a5 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -744,16 +744,25 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e return committer.Commit() } -func countUsers(e db.Engine) int64 { - count, _ := e. - Where("type=0"). - Count(new(User)) - return count +// CountUserFilter represent optional filters for CountUsers +type CountUserFilter struct { + LastLoginSince *int64 } // CountUsers returns number of users. -func CountUsers() int64 { - return countUsers(db.GetEngine(db.DefaultContext)) +func CountUsers(opts *CountUserFilter) int64 { + 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 diff --git a/modules/context/api.go b/modules/context/api.go index 7d281b998..20a3e0517 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/auth" 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/log" "code.gitea.io/gitea/modules/setting" @@ -247,6 +248,7 @@ func APIContexter() func(http.Handler) http.Handler { Resp: NewResponse(w), Data: map[string]interface{}{}, Locale: locale, + Cache: cache.GetCache(), Repo: &Repository{ PullRequest: &PullRequest{}, }, diff --git a/modules/setting/federation.go b/modules/setting/federation.go index c30006078..fd39e5c7c 100644 --- a/modules/setting/federation.go +++ b/modules/setting/federation.go @@ -9,9 +9,11 @@ import "code.gitea.io/gitea/modules/log" // Federation settings var ( Federation = struct { - Enabled bool + Enabled bool + ShareUserStatistics bool }{ - Enabled: true, + Enabled: true, + ShareUserStatistics: true, } ) diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go index bc36fa1be..ce1f9ec0f 100644 --- a/routers/api/v1/misc/nodeinfo.go +++ b/routers/api/v1/misc/nodeinfo.go @@ -6,12 +6,17 @@ package misc import ( "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/setting" "code.gitea.io/gitea/modules/structs" ) +const cacheKeyNodeInfoUsage = "API_NodeInfoUsage" + // NodeInfo returns the NodeInfo for the Gitea instance to allow for federation func NodeInfo(ctx *context.APIContext) { // swagger:operation GET /nodeinfo miscellaneous getNodeInfo @@ -23,6 +28,37 @@ func NodeInfo(ctx *context.APIContext) { // "200": // "$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{ Version: "2.1", Software: structs.NodeInfoSoftware{ @@ -34,12 +70,10 @@ func NodeInfo(ctx *context.APIContext) { Protocols: []string{"activitypub"}, Services: structs.NodeInfoServices{ Inbound: []string{}, - Outbound: []string{}, + Outbound: []string{"rss2.0"}, }, OpenRegistrations: setting.Service.ShowRegistrationButton, - Usage: structs.NodeInfoUsage{ - Users: structs.NodeInfoUsageUsers{}, - }, + Usage: nodeInfoUsage, } ctx.JSON(http.StatusOK, nodeInfo) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index e8250616a..be936d223 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -600,7 +600,7 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{ // sends a confirmation email if required. func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { // Auto-set admin for the only user. - if user_model.CountUsers() == 1 { + if user_model.CountUsers(nil) == 1 { u.IsAdmin = true u.IsActive = true u.SetLastLogin()