Add statistic information for total user count, active user count, issue count and comment count for `/nodeinfo`tags/v1.18.0-dev
@@ -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 | |||
} | |||
@@ -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 | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
@@ -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`) | |||
@@ -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) | |||
}) | |||
} |
@@ -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) | |||
} |
@@ -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) |
@@ -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 |
@@ -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{}, | |||
}, |
@@ -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, | |||
} | |||
) | |||
@@ -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) | |||
} |
@@ -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() |