Commit 34c26a39 authored by johnnyjoy's avatar johnnyjoy

feat: implement user stats endpoint

parent 5ff8ab9a
......@@ -53,6 +53,12 @@ service UserService {
option (google.api.http) = {delete: "/api/v1/{name=users/*}"};
option (google.api.method_signature) = "name";
}
// ListUserStats returns the stats of a user.
// Use `users/-` to list all users.
rpc ListUserStats(ListUserStatsRequest) returns (ListUserStatsResponse) {
option (google.api.http) = {get: "/api/v1/{name=users/*}/stats"};
option (google.api.method_signature) = "name";
}
// GetUserSetting gets the setting of a user.
rpc GetUserSetting(GetUserSettingRequest) returns (UserSetting) {
option (google.api.http) = {get: "/api/v1/{name=users/*}/setting"};
......@@ -165,6 +171,39 @@ message DeleteUserRequest {
string name = 1;
}
message UserStats {
// The name of the user.
// Format: users/{user}
string name = 1;
// The timestamps when the memos were displayed.
// We should return raw data to the client, and let the client format the data with the user's timezone.
repeated google.protobuf.Timestamp memo_display_timestamps = 2;
// The stats of memo types.
MemoTypeStats memo_type_stats = 3;
// The count of tags.
// Format: "tag1": 1, "tag2": 2
map<string, int32> tag_count = 4;
message MemoTypeStats {
int32 link_count = 1;
int32 task_count = 2;
int32 code_count = 3;
}
}
message ListUserStatsRequest {
// The name of the user.
// Format: users/{user}. Use "-" to list all users.
string name = 1;
}
message ListUserStatsResponse {
repeated UserStats user_stats = 1;
}
message UserSetting {
// The name of the user.
// Format: users/{user}
......
This diff is collapsed.
......@@ -309,6 +309,42 @@ func local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runti
return msg, metadata, err
}
func request_UserService_ListUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserStatsRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.ListUserStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_ListUserStats_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserStatsRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.ListUserStats(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_GetUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetUserSettingRequest
......@@ -699,6 +735,26 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserStats", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_ListUserStats_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -958,6 +1014,23 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserStats", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_ListUserStats_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
......@@ -1054,6 +1127,7 @@ var (
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_ListUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "stats"}, ""))
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "setting"}, ""))
pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 4, 3, 5, 4}, []string{"api", "v1", "users", "setting", "setting.name"}, ""))
pattern_UserService_ListUserAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "access_tokens"}, ""))
......@@ -1069,6 +1143,7 @@ var (
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserAccessTokens_0 = runtime.ForwardResponseMessage
......
......@@ -28,6 +28,7 @@ const (
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
UserService_ListUserStats_FullMethodName = "/memos.api.v1.UserService/ListUserStats"
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens"
......@@ -53,6 +54,7 @@ type UserServiceClient interface {
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
// DeleteUser deletes a user.
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListUserStats(ctx context.Context, in *ListUserStatsRequest, opts ...grpc.CallOption) (*ListUserStatsResponse, error)
// GetUserSetting gets the setting of a user.
GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)
// UpdateUserSetting updates the setting of a user.
......@@ -143,6 +145,16 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques
return out, nil
}
func (c *userServiceClient) ListUserStats(ctx context.Context, in *ListUserStatsRequest, opts ...grpc.CallOption) (*ListUserStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserStatsResponse)
err := c.cc.Invoke(ctx, UserService_ListUserStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserSetting)
......@@ -211,6 +223,7 @@ type UserServiceServer interface {
UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
// DeleteUser deletes a user.
DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)
ListUserStats(context.Context, *ListUserStatsRequest) (*ListUserStatsResponse, error)
// GetUserSetting gets the setting of a user.
GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error)
// UpdateUserSetting updates the setting of a user.
......@@ -252,6 +265,9 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented")
}
func (UnimplementedUserServiceServer) ListUserStats(context.Context, *ListUserStatsRequest) (*ListUserStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserStats not implemented")
}
func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented")
}
......@@ -414,6 +430,24 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _UserService_ListUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).ListUserStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_ListUserStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListUserStats(ctx, req.(*ListUserStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetUserSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserSettingRequest)
if err := dec(in); err != nil {
......@@ -539,6 +573,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteUser",
Handler: _UserService_DeleteUser_Handler,
},
{
MethodName: "ListUserStats",
Handler: _UserService_ListUserStats_Handler,
},
{
MethodName: "GetUserSetting",
Handler: _UserService_GetUserSetting_Handler,
......
......@@ -1464,6 +1464,29 @@ paths:
pattern: users/[^/]+
tags:
- UserService
/api/v1/{name}/stats:
get:
operationId: UserService_ListUserStats
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1ListUserStatsResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: name
description: |-
The name of the user.
Format: users/{user}. Use "-" to list all users.
in: path
required: true
type: string
pattern: users/[^/]+
tags:
- UserService
/api/v1/{parent}/tags/{tag}:
delete:
summary: DeleteMemoTag deletes a tag for a memo.
......@@ -1808,6 +1831,18 @@ definitions:
expiresAt:
type: string
format: date-time
UserStatsMemoTypeStats:
type: object
properties:
linkCount:
type: integer
format: int32
taskCount:
type: integer
format: int32
codeCount:
type: integer
format: int32
WorkspaceStorageSettingS3Config:
type: object
properties:
......@@ -2635,6 +2670,14 @@ definitions:
items:
type: object
$ref: '#/definitions/v1UserAccessToken'
v1ListUserStatsResponse:
type: object
properties:
userStats:
type: array
items:
type: object
$ref: '#/definitions/v1UserStats'
v1ListUsersResponse:
type: object
properties:
......@@ -3074,6 +3117,33 @@ definitions:
expiresAt:
type: string
format: date-time
v1UserStats:
type: object
properties:
name:
type: string
title: |-
The name of the user.
Format: users/{user}
memoDisplayTimestamps:
type: array
items:
type: string
format: date-time
description: |-
The timestamps when the memos were displayed.
We should return raw data to the client, and let the client format the data with the user's timezone.
memoTypeStats:
$ref: '#/definitions/UserStatsMemoTypeStats'
description: The stats of memo types.
tagCount:
type: object
additionalProperties:
type: integer
format: int32
title: |-
The count of tags.
Format: "tag1": 1, "tag2": 2
v1Visibility:
type: string
enum:
......
......@@ -13,6 +13,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.AuthService/SignUp": true,
"/memos.api.v1.UserService/GetUser": true,
"/memos.api.v1.UserService/GetUserAvatarBinary": true,
"/memos.api.v1.UserService/ListUserStats": true,
"/memos.api.v1.UserService/SearchUsers": true,
"/memos.api.v1.MemoService/GetMemo": true,
"/memos.api.v1.MemoService/GetMemoByUid": true,
......
......@@ -275,6 +275,99 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) ListUserStats(ctx context.Context, request *v1pb.ListUserStatsRequest) (*v1pb.ListUserStatsResponse, error) {
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %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)
}
users := []*store.User{}
if request.Name == "users/-" {
users, err = s.Store.ListUsers(ctx, &store.FindUser{})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
}
} else {
userID, err := ExtractUserIDFromName(request.Name)
if err != nil {
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})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
users = append(users, user)
}
}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
userStatsList := []*v1pb.UserStats{}
for _, user := range users {
userStats := &v1pb.UserStats{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
MemoDisplayTimestamps: []*timestamppb.Timestamp{},
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{},
TagCount: map[string]int32{},
}
var visibilities []store.Visibility = defaultVisibilities
// 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.
ExcludeComments: true,
ExcludeContent: true,
CreatorID: &user.ID,
VisibilityList: visibilities,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
for _, memo := range memos {
displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
// Handle duplicated tags.
for _, tag := range memo.Payload.Tags {
userStats.TagCount[tag]++
}
if memo.Payload.Property.GetHasLink() {
userStats.MemoTypeStats.LinkCount++
}
if memo.Payload.Property.GetHasTaskList() {
userStats.MemoTypeStats.TaskCount++
}
if memo.Payload.Property.GetHasCode() {
userStats.MemoTypeStats.CodeCount++
}
}
userStatsList = append(userStatsList, userStats)
}
return &v1pb.ListUserStatsResponse{
UserStats: userStatsList,
}, nil
}
func getDefaultUserSetting(workspaceMemoRelatedSetting *storepb.WorkspaceMemoRelatedSetting) *v1pb.UserSetting {
defaultVisibility := "PRIVATE"
if workspaceMemoRelatedSetting.DefaultVisibility != "" {
......
......@@ -54,10 +54,8 @@ func TestMemoStore(t *testing.T) {
require.Equal(t, 0, len(memoList))
memoList, err = ts.ListMemos(ctx, &store.FindMemo{
CreatorID: &user.ID,
VisibilityList: []store.Visibility{
store.Public,
},
CreatorID: &user.ID,
VisibilityList: []store.Visibility{store.Public},
})
require.NoError(t, err)
require.Equal(t, 0, len(memoList))
......
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