Commit d050a6fd authored by johnnyjoy's avatar johnnyjoy

chore: update user stats

parent 34c26a39
...@@ -53,9 +53,12 @@ service UserService { ...@@ -53,9 +53,12 @@ service UserService {
option (google.api.http) = {delete: "/api/v1/{name=users/*}"}; option (google.api.http) = {delete: "/api/v1/{name=users/*}"};
option (google.api.method_signature) = "name"; option (google.api.method_signature) = "name";
} }
// ListUserStats returns the stats of a user. // ListAllUserStats returns all user stats.
// Use `users/-` to list all users. rpc ListAllUserStats(ListAllUserStatsRequest) returns (ListAllUserStatsResponse) {
rpc ListUserStats(ListUserStatsRequest) returns (ListUserStatsResponse) { option (google.api.http) = {post: "/api/v1/users/-/stats"};
}
// GetUserStats returns the stats of a user.
rpc GetUserStats(GetUserStatsRequest) returns (UserStats) {
option (google.api.http) = {get: "/api/v1/{name=users/*}/stats"}; option (google.api.http) = {get: "/api/v1/{name=users/*}/stats"};
option (google.api.method_signature) = "name"; option (google.api.method_signature) = "name";
} }
...@@ -194,16 +197,26 @@ message UserStats { ...@@ -194,16 +197,26 @@ message UserStats {
} }
} }
message ListUserStatsRequest { message ListAllUserStatsRequest {
// The name of the user. // Filter is used to filter memos to calculate stats.
// Format: users/{user}. Use "-" to list all users. // Same as `ListMemosRequest.filter`.
string name = 1; string filter = 1;
} }
message ListUserStatsResponse { message ListAllUserStatsResponse {
repeated UserStats user_stats = 1; repeated UserStats user_stats = 1;
} }
message GetUserStatsRequest {
// The name of the user.
// Format: users/{user}.
string name = 1;
// Filter is used to filter memos to calculate stats.
// Same as `ListMemosRequest.filter`.
string filter = 2;
}
message UserSetting { message UserSetting {
// The name of the user. // The name of the user.
// Format: users/{user} // Format: users/{user}
......
This diff is collapsed.
This diff is collapsed.
...@@ -28,7 +28,8 @@ const ( ...@@ -28,7 +28,8 @@ const (
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser" UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser" UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser" UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
UserService_ListUserStats_FullMethodName = "/memos.api.v1.UserService/ListUserStats" UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting" UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting" UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens" UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens"
...@@ -54,7 +55,10 @@ type UserServiceClient interface { ...@@ -54,7 +55,10 @@ type UserServiceClient interface {
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
// DeleteUser deletes a user. // DeleteUser deletes a user.
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListUserStats(ctx context.Context, in *ListUserStatsRequest, opts ...grpc.CallOption) (*ListUserStatsResponse, error) // ListAllUserStats returns all user stats.
ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error)
// GetUserStats returns the stats of a user.
GetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error)
// GetUserSetting gets the setting of a user. // GetUserSetting gets the setting of a user.
GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)
// UpdateUserSetting updates the setting of a user. // UpdateUserSetting updates the setting of a user.
...@@ -145,10 +149,20 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques ...@@ -145,10 +149,20 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques
return out, nil return out, nil
} }
func (c *userServiceClient) ListUserStats(ctx context.Context, in *ListUserStatsRequest, opts ...grpc.CallOption) (*ListUserStatsResponse, error) { func (c *userServiceClient) ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserStatsResponse) out := new(ListAllUserStatsResponse)
err := c.cc.Invoke(ctx, UserService_ListUserStats_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, UserService_ListAllUserStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserStats)
err := c.cc.Invoke(ctx, UserService_GetUserStats_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -223,7 +237,10 @@ type UserServiceServer interface { ...@@ -223,7 +237,10 @@ type UserServiceServer interface {
UpdateUser(context.Context, *UpdateUserRequest) (*User, error) UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
// DeleteUser deletes a user. // DeleteUser deletes a user.
DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)
ListUserStats(context.Context, *ListUserStatsRequest) (*ListUserStatsResponse, error) // ListAllUserStats returns all user stats.
ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error)
// GetUserStats returns the stats of a user.
GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error)
// GetUserSetting gets the setting of a user. // GetUserSetting gets the setting of a user.
GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error)
// UpdateUserSetting updates the setting of a user. // UpdateUserSetting updates the setting of a user.
...@@ -265,8 +282,11 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq ...@@ -265,8 +282,11 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented")
} }
func (UnimplementedUserServiceServer) ListUserStats(context.Context, *ListUserStatsRequest) (*ListUserStatsResponse, error) { func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserStats not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListAllUserStats not implemented")
}
func (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserStats not implemented")
} }
func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) { func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented")
...@@ -430,20 +450,38 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f ...@@ -430,20 +450,38 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserService_ListUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserService_ListAllUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserStatsRequest) in := new(ListAllUserStatsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(UserServiceServer).ListUserStats(ctx, in) return srv.(UserServiceServer).ListAllUserStats(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: UserService_ListUserStats_FullMethodName, FullMethod: UserService_ListAllUserStats_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListUserStats(ctx, req.(*ListUserStatsRequest)) return srv.(UserServiceServer).ListAllUserStats(ctx, req.(*ListAllUserStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetUserStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_GetUserStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetUserStats(ctx, req.(*GetUserStatsRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
...@@ -574,8 +612,12 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ ...@@ -574,8 +612,12 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
Handler: _UserService_DeleteUser_Handler, Handler: _UserService_DeleteUser_Handler,
}, },
{ {
MethodName: "ListUserStats", MethodName: "ListAllUserStats",
Handler: _UserService_ListUserStats_Handler, Handler: _UserService_ListAllUserStats_Handler,
},
{
MethodName: "GetUserStats",
Handler: _UserService_GetUserStats_Handler,
}, },
{ {
MethodName: "GetUserSetting", MethodName: "GetUserSetting",
......
...@@ -494,6 +494,29 @@ paths: ...@@ -494,6 +494,29 @@ paths:
$ref: '#/definitions/v1User' $ref: '#/definitions/v1User'
tags: tags:
- UserService - UserService
/api/v1/users/-/stats:
post:
summary: ListAllUserStats returns all user stats.
operationId: UserService_ListAllUserStats
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1ListAllUserStatsResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: filter
description: |-
Filter is used to filter memos to calculate stats.
Same as `ListMemosRequest.filter`.
in: query
required: false
type: string
tags:
- UserService
/api/v1/users:search: /api/v1/users:search:
get: get:
summary: SearchUsers searches users by filter. summary: SearchUsers searches users by filter.
...@@ -1466,12 +1489,13 @@ paths: ...@@ -1466,12 +1489,13 @@ paths:
- UserService - UserService
/api/v1/{name}/stats: /api/v1/{name}/stats:
get: get:
operationId: UserService_ListUserStats summary: GetUserStats returns the stats of a user.
operationId: UserService_GetUserStats
responses: responses:
"200": "200":
description: A successful response. description: A successful response.
schema: schema:
$ref: '#/definitions/v1ListUserStatsResponse' $ref: '#/definitions/v1UserStats'
default: default:
description: An unexpected error response. description: An unexpected error response.
schema: schema:
...@@ -1480,11 +1504,18 @@ paths: ...@@ -1480,11 +1504,18 @@ paths:
- name: name - name: name
description: |- description: |-
The name of the user. The name of the user.
Format: users/{user}. Use "-" to list all users. Format: users/{user}.
in: path in: path
required: true required: true
type: string type: string
pattern: users/[^/]+ pattern: users/[^/]+
- name: filter
description: |-
Filter is used to filter memos to calculate stats.
Same as `ListMemosRequest.filter`.
in: query
required: false
type: string
tags: tags:
- UserService - UserService
/api/v1/{parent}/tags/{tag}: /api/v1/{parent}/tags/{tag}:
...@@ -2575,6 +2606,14 @@ definitions: ...@@ -2575,6 +2606,14 @@ definitions:
type: string type: string
url: url:
type: string type: string
v1ListAllUserStatsResponse:
type: object
properties:
userStats:
type: array
items:
type: object
$ref: '#/definitions/v1UserStats'
v1ListIdentityProvidersResponse: v1ListIdentityProvidersResponse:
type: object type: object
properties: properties:
...@@ -2670,14 +2709,6 @@ definitions: ...@@ -2670,14 +2709,6 @@ definitions:
items: items:
type: object type: object
$ref: '#/definitions/v1UserAccessToken' $ref: '#/definitions/v1UserAccessToken'
v1ListUserStatsResponse:
type: object
properties:
userStats:
type: array
items:
type: object
$ref: '#/definitions/v1UserStats'
v1ListUsersResponse: v1ListUsersResponse:
type: object type: object
properties: properties:
......
...@@ -275,41 +275,49 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR ...@@ -275,41 +275,49 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUserStatsRequest) (*v1pb.ListUserStatsResponse, error) { func (s *APIV1Service) ListAllUserStats(ctx context.Context, request *v1pb.ListAllUserStatsRequest) (*v1pb.ListAllUserStatsResponse, error) {
currentUser, err := s.GetCurrentUser(ctx) users, err := s.Store.ListUsers(ctx, &store.FindUser{})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
}
// For unauthenticated users, only public memos are visible.
defaultVisibilities := []store.Visibility{store.Public}
if currentUser != nil {
// For authenticated users, protected memos are also visible.
defaultVisibilities = append(defaultVisibilities, store.Protected)
} }
userStatsList := []*v1pb.UserStats{}
users := []*store.User{} for _, user := range users {
if request.Name == "users/-" { userStats, err := s.GetUserStats(ctx, &v1pb.GetUserStatsRequest{
users, err = s.Store.ListUsers(ctx, &store.FindUser{}) Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
Filter: request.Filter,
})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user stats: %v", err)
} }
} else { userStatsList = append(userStatsList, userStats)
}
return &v1pb.ListAllUserStatsResponse{
UserStats: userStatsList,
}, nil
}
func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) {
userID, err := ExtractUserIDFromName(request.Name) userID, err := ExtractUserIDFromName(request.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
} }
if userID == currentUser.ID {
users = append(users, currentUser)
} else {
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found") currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
users = append(users, user) // For unauthenticated users, only public memos are visible.
visibilities := []store.Visibility{store.Public}
if currentUser != nil {
// For authenticated users, protected memos are also visible.
visibilities = append(visibilities, store.Protected)
if currentUser.ID == user.ID {
// For the current user, show all memos including private ones.
visibilities = []store.Visibility{store.Public, store.Protected, store.Private}
} }
} }
...@@ -317,26 +325,24 @@ func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUser ...@@ -317,26 +325,24 @@ func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUser
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting") return nil, errors.Wrap(err, "failed to get workspace memo related setting")
} }
userStatsList := []*v1pb.UserStats{}
for _, user := range users {
userStats := &v1pb.UserStats{ userStats := &v1pb.UserStats{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID), Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
MemoDisplayTimestamps: []*timestamppb.Timestamp{}, MemoDisplayTimestamps: []*timestamppb.Timestamp{},
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{}, MemoTypeStats: &v1pb.UserStats_MemoTypeStats{},
TagCount: map[string]int32{}, TagCount: map[string]int32{},
} }
var visibilities []store.Visibility = defaultVisibilities memoFind := &store.FindMemo{
// For the current user, show all memos including private ones.
if user.ID == currentUser.ID {
visibilities = []store.Visibility{store.Public, store.Protected, store.Private}
}
memos, err := s.Store.ListMemos(ctx, &store.FindMemo{
// Exclude comments by default. // Exclude comments by default.
ExcludeComments: true, ExcludeComments: true,
ExcludeContent: true, ExcludeContent: true,
CreatorID: &user.ID, }
VisibilityList: visibilities, if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
}) return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err)
}
// Override the creator ID and visibility list.
memoFind.CreatorID = &user.ID
memoFind.VisibilityList = visibilities
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
} }
...@@ -360,12 +366,7 @@ func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUser ...@@ -360,12 +366,7 @@ func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUser
userStats.MemoTypeStats.CodeCount++ userStats.MemoTypeStats.CodeCount++
} }
} }
userStatsList = append(userStatsList, userStats) return userStats, nil
}
return &v1pb.ListUserStatsResponse{
UserStats: userStatsList,
}, nil
} }
func getDefaultUserSetting(workspaceMemoRelatedSetting *storepb.WorkspaceMemoRelatedSetting) *v1pb.UserSetting { func getDefaultUserSetting(workspaceMemoRelatedSetting *storepb.WorkspaceMemoRelatedSetting) *v1pb.UserSetting {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment