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`
tokarchuk/v1.17
6543 3 years ago committed by GitHub
parent 509d811243
commit e2a3f3d259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      cmd/admin.go
  2. 3
      custom/conf/app.example.ini
  3. 1
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  4. 4
      integrations/api_nodeinfo_test.go
  5. 7
      models/issue_test.go
  6. 2
      models/statistic.go
  7. 23
      models/user/user.go
  8. 2
      modules/context/api.go
  9. 6
      modules/setting/federation.go
  10. 42
      routers/api/v1/misc/nodeinfo.go
  11. 2
      routers/web/auth/auth.go

@ -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()

Loading…
Cancel
Save