mirror of https://github.com/usememos/memos.git
fix: batch user lookups and harden username resource handling
This commit is contained in:
parent
beb0232a25
commit
4b6f80596a
|
|
@ -52,7 +52,7 @@ service ShortcutService {
|
|||
message Shortcut {
|
||||
option (google.api.resource) = {
|
||||
type: "memos.api.v1/Shortcut"
|
||||
pattern: "users/{user}/shortcuts/{shortcut}"
|
||||
pattern: "users/{username}/shortcuts/{shortcut}"
|
||||
singular: "shortcut"
|
||||
plural: "shortcuts"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ message ListAllUserStatsResponse {
|
|||
message UserSetting {
|
||||
option (google.api.resource) = {
|
||||
type: "memos.api.v1/UserSetting"
|
||||
pattern: "users/{user}/settings/{setting}"
|
||||
pattern: "users/{username}/settings/{setting}"
|
||||
singular: "userSetting"
|
||||
plural: "userSettings"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -95,10 +95,8 @@ const (
|
|||
type UserServiceClient interface {
|
||||
// ListUsers returns a list of users.
|
||||
ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)
|
||||
// GetUser gets a user by ID or username.
|
||||
// Supports both numeric IDs and username strings:
|
||||
// - users/{id} (e.g., users/101)
|
||||
// - users/{username} (e.g., users/steven)
|
||||
// GetUser gets a user by username.
|
||||
// Format: users/{username} (e.g., users/steven)
|
||||
GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)
|
||||
// CreateUser creates a new user.
|
||||
CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error)
|
||||
|
|
@ -402,10 +400,8 @@ func (c *userServiceClient) DeleteUserNotification(ctx context.Context, req *con
|
|||
type UserServiceHandler interface {
|
||||
// ListUsers returns a list of users.
|
||||
ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)
|
||||
// GetUser gets a user by ID or username.
|
||||
// Supports both numeric IDs and username strings:
|
||||
// - users/{id} (e.g., users/101)
|
||||
// - users/{username} (e.g., users/steven)
|
||||
// GetUser gets a user by username.
|
||||
// Format: users/{username} (e.g., users/steven)
|
||||
GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)
|
||||
// CreateUser creates a new user.
|
||||
CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const (
|
|||
type Shortcut struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The resource name of the shortcut.
|
||||
// Format: users/{user}/shortcuts/{shortcut}
|
||||
// Format: users/{username}/shortcuts/{shortcut}
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// The title of the shortcut.
|
||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||
|
|
@ -91,7 +91,7 @@ func (x *Shortcut) GetFilter() string {
|
|||
type ListShortcutsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The parent resource where shortcuts are listed.
|
||||
// Format: users/{user}
|
||||
// Format: users/{username}
|
||||
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -182,7 +182,7 @@ func (x *ListShortcutsResponse) GetShortcuts() []*Shortcut {
|
|||
type GetShortcutRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the shortcut to retrieve.
|
||||
// Format: users/{user}/shortcuts/{shortcut}
|
||||
// Format: users/{username}/shortcuts/{shortcut}
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -228,7 +228,7 @@ func (x *GetShortcutRequest) GetName() string {
|
|||
type CreateShortcutRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The parent resource where this shortcut will be created.
|
||||
// Format: users/{user}
|
||||
// Format: users/{username}
|
||||
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
|
||||
// Required. The shortcut to create.
|
||||
Shortcut *Shortcut `protobuf:"bytes,2,opt,name=shortcut,proto3" json:"shortcut,omitempty"`
|
||||
|
|
@ -346,7 +346,7 @@ func (x *UpdateShortcutRequest) GetUpdateMask() *fieldmaskpb.FieldMask {
|
|||
type DeleteShortcutRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the shortcut to delete.
|
||||
// Format: users/{user}/shortcuts/{shortcut}
|
||||
// Format: users/{username}/shortcuts/{shortcut}
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -393,12 +393,12 @@ var File_api_v1_shortcut_service_proto protoreflect.FileDescriptor
|
|||
|
||||
const file_api_v1_shortcut_service_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1dapi/v1/shortcut_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\xaf\x01\n" +
|
||||
"\x1dapi/v1/shortcut_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\xb3\x01\n" +
|
||||
"\bShortcut\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x19\n" +
|
||||
"\x05title\x18\x02 \x01(\tB\x03\xe0A\x02R\x05title\x12\x1b\n" +
|
||||
"\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter:R\xeaAO\n" +
|
||||
"\x15memos.api.v1/Shortcut\x12!users/{user}/shortcuts/{shortcut}*\tshortcuts2\bshortcut\"M\n" +
|
||||
"\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter:V\xeaAS\n" +
|
||||
"\x15memos.api.v1/Shortcut\x12%users/{username}/shortcuts/{shortcut}*\tshortcuts2\bshortcut\"M\n" +
|
||||
"\x14ListShortcutsRequest\x125\n" +
|
||||
"\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/ShortcutR\x06parent\"M\n" +
|
||||
"\x15ListShortcutsResponse\x124\n" +
|
||||
|
|
|
|||
|
|
@ -506,11 +506,7 @@ func (x *ListUsersResponse) GetTotalSize() int32 {
|
|||
type GetUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the user.
|
||||
// Supports both numeric IDs and username strings:
|
||||
// - users/{id} (e.g., users/101)
|
||||
// - users/{username} (e.g., users/steven)
|
||||
//
|
||||
// Format: users/{id_or_username}
|
||||
// Format: users/{username}
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// Optional. The fields to return in the response.
|
||||
// If not specified, all fields are returned.
|
||||
|
|
@ -979,8 +975,8 @@ func (x *ListAllUserStatsResponse) GetStats() []*UserStats {
|
|||
type UserSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The name of the user setting.
|
||||
// Format: users/{user}/settings/{setting}, {setting} is the key for the setting.
|
||||
// For example, "users/123/settings/GENERAL" for general settings.
|
||||
// Format: users/{username}/settings/{setting}, {setting} is the key for the setting.
|
||||
// For example, "users/steven/settings/GENERAL" for general settings.
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// Types that are valid to be assigned to Value:
|
||||
//
|
||||
|
|
@ -2658,7 +2654,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
|||
"\x11memos.api.v1/UserR\x04name\"\x19\n" +
|
||||
"\x17ListAllUserStatsRequest\"I\n" +
|
||||
"\x18ListAllUserStatsResponse\x12-\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb0\x04\n" +
|
||||
"\x05stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\x05stats\"\xb4\x04\n" +
|
||||
"\vUserSetting\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12S\n" +
|
||||
"\x0fgeneral_setting\x18\x02 \x01(\v2(.memos.api.v1.UserSetting.GeneralSettingH\x00R\x0egeneralSetting\x12V\n" +
|
||||
|
|
@ -2672,8 +2668,8 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
|||
"\x03Key\x12\x13\n" +
|
||||
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
|
||||
"\aGENERAL\x10\x01\x12\f\n" +
|
||||
"\bWEBHOOKS\x10\x04:Y\xeaAV\n" +
|
||||
"\x18memos.api.v1/UserSetting\x12\x1fusers/{user}/settings/{setting}*\fuserSettings2\vuserSettingB\a\n" +
|
||||
"\bWEBHOOKS\x10\x04:]\xeaAZ\n" +
|
||||
"\x18memos.api.v1/UserSetting\x12#users/{username}/settings/{setting}*\fuserSettings2\vuserSettingB\a\n" +
|
||||
"\x05value\"M\n" +
|
||||
"\x15GetUserSettingRequest\x124\n" +
|
||||
"\x04name\x18\x01 \x01(\tB \xe0A\x02\xfaA\x1a\n" +
|
||||
|
|
|
|||
|
|
@ -48,10 +48,8 @@ const (
|
|||
type UserServiceClient interface {
|
||||
// ListUsers returns a list of users.
|
||||
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)
|
||||
// GetUser gets a user by ID or username.
|
||||
// Supports both numeric IDs and username strings:
|
||||
// - users/{id} (e.g., users/101)
|
||||
// - users/{username} (e.g., users/steven)
|
||||
// GetUser gets a user by username.
|
||||
// Format: users/{username} (e.g., users/steven)
|
||||
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
|
||||
// CreateUser creates a new user.
|
||||
CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
|
||||
|
|
@ -307,10 +305,8 @@ func (c *userServiceClient) DeleteUserNotification(ctx context.Context, in *Dele
|
|||
type UserServiceServer interface {
|
||||
// ListUsers returns a list of users.
|
||||
ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
|
||||
// GetUser gets a user by ID or username.
|
||||
// Supports both numeric IDs and username strings:
|
||||
// - users/{id} (e.g., users/101)
|
||||
// - users/{username} (e.g., users/steven)
|
||||
// GetUser gets a user by username.
|
||||
// Format: users/{username} (e.g., users/steven)
|
||||
GetUser(context.Context, *GetUserRequest) (*User, error)
|
||||
// CreateUser creates a new user.
|
||||
CreateUser(context.Context, *CreateUserRequest) (*User, error)
|
||||
|
|
|
|||
|
|
@ -1206,10 +1206,8 @@ paths:
|
|||
tags:
|
||||
- UserService
|
||||
description: |-
|
||||
GetUser gets a user by ID or username.
|
||||
Supports both numeric IDs and username strings:
|
||||
- users/{id} (e.g., users/101)
|
||||
- users/{username} (e.g., users/steven)
|
||||
GetUser gets a user by username.
|
||||
Format: users/{username} (e.g., users/steven)
|
||||
operationId: UserService_GetUser
|
||||
parameters:
|
||||
- name: user
|
||||
|
|
@ -2939,7 +2937,7 @@ components:
|
|||
type: string
|
||||
description: |-
|
||||
The resource name of the shortcut.
|
||||
Format: users/{user}/shortcuts/{shortcut}
|
||||
Format: users/{username}/shortcuts/{shortcut}
|
||||
title:
|
||||
type: string
|
||||
description: The title of the shortcut.
|
||||
|
|
@ -3178,8 +3176,8 @@ components:
|
|||
type: string
|
||||
description: |-
|
||||
The name of the user setting.
|
||||
Format: users/{user}/settings/{setting}, {setting} is the key for the setting.
|
||||
For example, "users/123/settings/GENERAL" for general settings.
|
||||
Format: users/{username}/settings/{setting}, {setting} is the key for the setting.
|
||||
For example, "users/steven/settings/GENERAL" for general settings.
|
||||
generalSetting:
|
||||
$ref: '#/components/schemas/UserSetting_GeneralSetting'
|
||||
webhooksSetting:
|
||||
|
|
|
|||
|
|
@ -278,6 +278,14 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
|
|||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to batch load memo relations")
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(memos))
|
||||
for _, memo := range memos {
|
||||
creatorIDs = append(creatorIDs, memo.CreatorID)
|
||||
}
|
||||
creatorMap, err := s.listUsersByID(ctx, creatorIDs)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list memo creators: %v", err)
|
||||
}
|
||||
|
||||
for _, memo := range memos {
|
||||
memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
|
||||
|
|
@ -285,7 +293,7 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
|
|||
attachments := attachmentMap[memo.ID]
|
||||
relations := relationMap[memo.ID]
|
||||
|
||||
memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations)
|
||||
memoMessage, err := s.convertMemoFromStoreWithCreators(ctx, memo, reactions, attachments, relations, creatorMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert memo")
|
||||
}
|
||||
|
|
@ -753,6 +761,14 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
|
|||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to batch load memo relations")
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(memos))
|
||||
for _, memo := range memos {
|
||||
creatorIDs = append(creatorIDs, memo.CreatorID)
|
||||
}
|
||||
creatorMap, err := s.listUsersByID(ctx, creatorIDs)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list memo creators: %v", err)
|
||||
}
|
||||
|
||||
var memosResponse []*v1pb.Memo
|
||||
for _, m := range memos {
|
||||
|
|
@ -761,7 +777,7 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
|
|||
attachments := attachmentMap[m.ID]
|
||||
relations := relationMap[m.ID]
|
||||
|
||||
memoMessage, err := s.convertMemoFromStore(ctx, m, reactions, attachments, relations)
|
||||
memoMessage, err := s.convertMemoFromStoreWithCreators(ctx, m, reactions, attachments, relations, creatorMap)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert memo")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ import (
|
|||
)
|
||||
|
||||
func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) {
|
||||
creatorMap, err := s.listUsersByID(ctx, []int32{memo.CreatorID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list memo creators")
|
||||
}
|
||||
return s.convertMemoFromStoreWithCreators(ctx, memo, reactions, attachments, relations, creatorMap)
|
||||
}
|
||||
|
||||
func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User) (*v1pb.Memo, error) {
|
||||
displayTs := memo.CreatedTs
|
||||
instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -24,10 +32,7 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
|
|||
}
|
||||
|
||||
name := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
|
||||
creator, err := s.Store.GetUser(ctx, &store.FindUser{ID: &memo.CreatorID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get memo creator")
|
||||
}
|
||||
creator := creatorMap[memo.CreatorID]
|
||||
if creator == nil {
|
||||
return nil, errors.New("memo creator not found")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,9 @@ func ResolveUserByName(ctx context.Context, stores *store.Store, name string) (*
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stores.GetUser(ctx, &store.FindUser{Username: &username})
|
||||
user, err := stores.GetUser(ctx, &store.FindUser{Username: &username})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "resolve user by name: GetUser failed")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
|
|||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
if user == nil {
|
||||
if request.AllowMissing {
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
userID := user.ID
|
||||
currentUser, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -179,15 +185,6 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
|
|||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
// Handle allow_missing field
|
||||
if request.AllowMissing {
|
||||
// Could create user if missing, but for now return not found
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
update := &store.UpdateUser{
|
||||
ID: user.ID,
|
||||
|
|
@ -271,6 +268,9 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
|
|||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
userID := user.ID
|
||||
currentUser, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -283,10 +283,6 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
|
|||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
}
|
||||
|
||||
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
|
||||
ID: user.ID,
|
||||
}); err != nil {
|
||||
|
|
@ -991,26 +987,6 @@ func extractImageInfo(dataURI string) (string, string, error) {
|
|||
return imageType, base64Data, nil
|
||||
}
|
||||
|
||||
// Helper functions for user settings
|
||||
|
||||
// ExtractUserIDAndSettingKeyFromName extracts user ID and setting key from a legacy numeric resource name.
|
||||
// e.g., "users/123/settings/general" -> 123, "general".
|
||||
func ExtractUserIDAndSettingKeyFromName(name string) (int32, string, error) {
|
||||
// Expected format: users/{user}/settings/{setting}
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) != 4 || parts[0] != "users" || parts[2] != "settings" {
|
||||
return 0, "", errors.Errorf("invalid resource name format: %s", name)
|
||||
}
|
||||
|
||||
userID, err := util.ConvertStringToInt32(parts[1])
|
||||
if err != nil {
|
||||
return 0, "", errors.Errorf("invalid user ID: %s", parts[1])
|
||||
}
|
||||
|
||||
settingKey := parts[3]
|
||||
return userID, settingKey, nil
|
||||
}
|
||||
|
||||
// convertSettingKeyToStore converts API setting key to store enum.
|
||||
func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {
|
||||
switch key {
|
||||
|
|
@ -1084,14 +1060,17 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, user *store.
|
|||
}
|
||||
case storepb.UserSetting_WEBHOOKS:
|
||||
webhooks := storeSetting.GetWebhooks()
|
||||
apiWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks))
|
||||
for _, webhook := range webhooks.Webhooks {
|
||||
apiWebhook := &v1pb.UserWebhook{
|
||||
Name: fmt.Sprintf("%s/webhooks/%s", BuildUserName(user.Username), webhook.Id),
|
||||
Url: webhook.Url,
|
||||
DisplayName: webhook.Title,
|
||||
apiWebhooks := make([]*v1pb.UserWebhook, 0)
|
||||
if webhooks != nil {
|
||||
apiWebhooks = make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks))
|
||||
for _, webhook := range webhooks.Webhooks {
|
||||
apiWebhook := &v1pb.UserWebhook{
|
||||
Name: fmt.Sprintf("%s/webhooks/%s", BuildUserName(user.Username), webhook.Id),
|
||||
Url: webhook.Url,
|
||||
DisplayName: webhook.Title,
|
||||
}
|
||||
apiWebhooks = append(apiWebhooks, apiWebhook)
|
||||
}
|
||||
apiWebhooks = append(apiWebhooks, apiWebhook)
|
||||
}
|
||||
setting.Value = &v1pb.UserSetting_WebhooksSetting_{
|
||||
WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{
|
||||
|
|
@ -1290,10 +1269,19 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
|
|||
return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err)
|
||||
}
|
||||
|
||||
// Convert storage layer inboxes to API notifications
|
||||
// Convert storage layer inboxes to API notifications.
|
||||
userIDs := make([]int32, 0, len(inboxes)*2)
|
||||
for _, inbox := range inboxes {
|
||||
userIDs = append(userIDs, inbox.ReceiverID, inbox.SenderID)
|
||||
}
|
||||
usersByID, err := s.listUsersByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
|
||||
}
|
||||
|
||||
notifications := []*v1pb.UserNotification{}
|
||||
for _, inbox := range inboxes {
|
||||
notification, err := s.convertInboxToUserNotification(ctx, inbox)
|
||||
notification, err := s.convertInboxToUserNotificationWithUsers(ctx, inbox, usersByID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err)
|
||||
}
|
||||
|
|
@ -1426,17 +1414,19 @@ func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb
|
|||
// convertInboxToUserNotification converts a storage-layer inbox to an API notification.
|
||||
// This handles the mapping between the internal inbox representation and the public API.
|
||||
func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) {
|
||||
receiver, err := s.Store.GetUser(ctx, &store.FindUser{ID: &inbox.ReceiverID})
|
||||
usersByID, err := s.listUsersByID(ctx, []int32{inbox.ReceiverID, inbox.SenderID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get notification receiver: %v", err)
|
||||
return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
|
||||
}
|
||||
return s.convertInboxToUserNotificationWithUsers(ctx, inbox, usersByID)
|
||||
}
|
||||
|
||||
func (s *APIV1Service) convertInboxToUserNotificationWithUsers(ctx context.Context, inbox *store.Inbox, usersByID map[int32]*store.User) (*v1pb.UserNotification, error) {
|
||||
receiver := usersByID[inbox.ReceiverID]
|
||||
if receiver == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "notification receiver not found")
|
||||
}
|
||||
sender, err := s.Store.GetUser(ctx, &store.FindUser{ID: &inbox.SenderID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get notification sender: %v", err)
|
||||
}
|
||||
sender := usersByID[inbox.SenderID]
|
||||
if sender == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "notification sender not found")
|
||||
}
|
||||
|
|
@ -1513,20 +1503,3 @@ func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, messa
|
|||
RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExtractNotificationIDFromName extracts the notification ID from a resource name.
|
||||
// Expected format: users/{user_id}/notifications/{notification_id}.
|
||||
func ExtractNotificationIDFromName(name string) (int32, error) {
|
||||
pattern := regexp.MustCompile(`^users/(\d+)/notifications/(\d+)$`)
|
||||
matches := pattern.FindStringSubmatch(name)
|
||||
if len(matches) != 3 {
|
||||
return 0, errors.Errorf("invalid notification name: %s", name)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
return 0, errors.Errorf("invalid notification id: %s", matches[2])
|
||||
}
|
||||
|
||||
return int32(id), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package v1
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -15,18 +14,41 @@ import (
|
|||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (s *APIV1Service) listUsernamesByID(ctx context.Context, userIDs []int32) (map[int32]string, error) {
|
||||
func (s *APIV1Service) listUsersByID(ctx context.Context, userIDs []int32) (map[int32]*store.User, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return map[int32]string{}, nil
|
||||
return map[int32]*store.User{}, nil
|
||||
}
|
||||
|
||||
users, err := s.Store.ListUsers(ctx, &store.FindUser{IDList: userIDs})
|
||||
uniqueUserIDs := make([]int32, 0, len(userIDs))
|
||||
seenUserIDs := make(map[int32]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if _, seen := seenUserIDs[userID]; seen {
|
||||
continue
|
||||
}
|
||||
seenUserIDs[userID] = struct{}{}
|
||||
uniqueUserIDs = append(uniqueUserIDs, userID)
|
||||
}
|
||||
|
||||
users, err := s.Store.ListUsers(ctx, &store.FindUser{IDList: uniqueUserIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usernamesByID := make(map[int32]string, len(users))
|
||||
usersByID := make(map[int32]*store.User, len(users))
|
||||
for _, user := range users {
|
||||
usersByID[user.ID] = user
|
||||
}
|
||||
return usersByID, nil
|
||||
}
|
||||
|
||||
func (s *APIV1Service) listUsernamesByID(ctx context.Context, userIDs []int32) (map[int32]string, error) {
|
||||
usersByID, err := s.listUsersByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usernamesByID := make(map[int32]string, len(usersByID))
|
||||
for _, user := range usersByID {
|
||||
usernamesByID[user.ID] = user.Username
|
||||
}
|
||||
return usernamesByID, nil
|
||||
|
|
@ -62,6 +84,7 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
|
|||
}
|
||||
|
||||
userMemoStatMap := make(map[int32]*v1pb.UserStats)
|
||||
pinnedMemoIDsByUserID := make(map[int32][]int32)
|
||||
limit := 1000
|
||||
offset := 0
|
||||
memoFind.Limit = &limit
|
||||
|
|
@ -128,7 +151,7 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
|
|||
|
||||
// Track pinned memos
|
||||
if memo.Pinned {
|
||||
stats.PinnedMemos = append(stats.PinnedMemos, fmt.Sprintf("users/%d/memos/%d", memo.CreatorID, memo.ID))
|
||||
pinnedMemoIDsByUserID[memo.CreatorID] = append(pinnedMemoIDsByUserID[memo.CreatorID], memo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,12 +173,8 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
|
|||
return nil, status.Errorf(codes.Internal, "failed to resolve user stats name")
|
||||
}
|
||||
userMemoStat.Name = fmt.Sprintf("%s/stats", BuildUserName(username))
|
||||
for i, pinnedMemo := range userMemoStat.PinnedMemos {
|
||||
parts := strings.Split(pinnedMemo, "/")
|
||||
if len(parts) != 4 {
|
||||
return nil, status.Errorf(codes.Internal, "failed to resolve pinned memo name")
|
||||
}
|
||||
userMemoStat.PinnedMemos[i] = fmt.Sprintf("%s/memos/%s", BuildUserName(username), parts[3])
|
||||
for _, memoID := range pinnedMemoIDsByUserID[userID] {
|
||||
userMemoStat.PinnedMemos = append(userMemoStat.PinnedMemos, fmt.Sprintf("%s/memos/%d", BuildUserName(username), memoID))
|
||||
}
|
||||
userMemoStats = append(userMemoStats, userMemoStat)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,37 @@ func storeAttachmentToJSON(ctx context.Context, stores *store.Store, a *store.At
|
|||
return j, nil
|
||||
}
|
||||
|
||||
func storeAttachmentToJSONWithUsernames(a *store.Attachment, usernamesByID map[int32]string) (attachmentJSON, error) {
|
||||
creator, err := lookupUsernameFromCache(usernamesByID, a.CreatorID)
|
||||
if err != nil {
|
||||
return attachmentJSON{}, err
|
||||
}
|
||||
j := attachmentJSON{
|
||||
Name: "attachments/" + a.UID,
|
||||
Creator: creator,
|
||||
CreateTime: a.CreatedTs,
|
||||
Filename: a.Filename,
|
||||
Type: a.Type,
|
||||
Size: a.Size,
|
||||
}
|
||||
switch a.StorageType {
|
||||
case storepb.AttachmentStorageType_LOCAL:
|
||||
j.StorageType = "LOCAL"
|
||||
case storepb.AttachmentStorageType_S3:
|
||||
j.StorageType = "S3"
|
||||
j.ExternalLink = a.Reference
|
||||
case storepb.AttachmentStorageType_EXTERNAL:
|
||||
j.StorageType = "EXTERNAL"
|
||||
j.ExternalLink = a.Reference
|
||||
default:
|
||||
j.StorageType = "DATABASE"
|
||||
}
|
||||
if a.MemoUID != nil && *a.MemoUID != "" {
|
||||
j.Memo = "memos/" + *a.MemoUID
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
func parseAttachmentUID(name string) (string, error) {
|
||||
uid, ok := strings.CutPrefix(name, "attachments/")
|
||||
if !ok || uid == "" {
|
||||
|
|
@ -140,10 +171,18 @@ func (s *MCPService) handleListAttachments(ctx context.Context, req mcp.CallTool
|
|||
if hasMore {
|
||||
attachments = attachments[:pageSize]
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(attachments))
|
||||
for _, attachment := range attachments {
|
||||
creatorIDs = append(creatorIDs, attachment.CreatorID)
|
||||
}
|
||||
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to preload attachment creators: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]attachmentJSON, len(attachments))
|
||||
for i, a := range attachments {
|
||||
result, err := storeAttachmentToJSON(ctx, s.store, a)
|
||||
result, err := storeAttachmentToJSONWithUsernames(a, usernamesByID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve attachment creator: %v", err)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,41 @@ func lookupUsername(ctx context.Context, stores *store.Store, userID int32) (str
|
|||
return "users/" + user.Username, nil
|
||||
}
|
||||
|
||||
func preloadUsernames(ctx context.Context, stores *store.Store, userIDs []int32) (map[int32]string, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return map[int32]string{}, nil
|
||||
}
|
||||
|
||||
uniqueUserIDs := make([]int32, 0, len(userIDs))
|
||||
seenUserIDs := make(map[int32]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if _, seen := seenUserIDs[userID]; seen {
|
||||
continue
|
||||
}
|
||||
seenUserIDs[userID] = struct{}{}
|
||||
uniqueUserIDs = append(uniqueUserIDs, userID)
|
||||
}
|
||||
|
||||
users, err := stores.ListUsers(ctx, &store.FindUser{IDList: uniqueUserIDs})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list creator users")
|
||||
}
|
||||
|
||||
usernamesByID := make(map[int32]string, len(users))
|
||||
for _, user := range users {
|
||||
usernamesByID[user.ID] = "users/" + user.Username
|
||||
}
|
||||
return usernamesByID, nil
|
||||
}
|
||||
|
||||
func lookupUsernameFromCache(usernamesByID map[int32]string, userID int32) (string, error) {
|
||||
username, ok := usernamesByID[userID]
|
||||
if !ok {
|
||||
return "", errors.Errorf("creator user %d not found", userID)
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func storeMemoToJSONWithStore(ctx context.Context, stores *store.Store, m *store.Memo) (memoJSON, error) {
|
||||
j := storeMemoToJSON(m)
|
||||
creator, err := lookupUsername(ctx, stores, m.CreatorID)
|
||||
|
|
@ -123,6 +158,16 @@ func storeMemoToJSONWithStore(ctx context.Context, stores *store.Store, m *store
|
|||
return j, nil
|
||||
}
|
||||
|
||||
func storeMemoToJSONWithUsernames(m *store.Memo, usernamesByID map[int32]string) (memoJSON, error) {
|
||||
j := storeMemoToJSON(m)
|
||||
creator, err := lookupUsernameFromCache(usernamesByID, m.CreatorID)
|
||||
if err != nil {
|
||||
return memoJSON{}, err
|
||||
}
|
||||
j.Creator = creator
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// checkMemoAccess returns an error if the caller cannot read memo.
|
||||
// userID == 0 means anonymous.
|
||||
func checkMemoAccess(memo *store.Memo, userID int32) error {
|
||||
|
|
@ -306,10 +351,18 @@ func (s *MCPService) handleListMemos(ctx context.Context, req mcp.CallToolReques
|
|||
if hasMore {
|
||||
memos = memos[:pageSize]
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(memos))
|
||||
for _, memo := range memos {
|
||||
creatorIDs = append(creatorIDs, memo.CreatorID)
|
||||
}
|
||||
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to preload memo creators: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]memoJSON, len(memos))
|
||||
for i, m := range memos {
|
||||
result, err := storeMemoToJSONWithStore(ctx, s.store, m)
|
||||
result, err := storeMemoToJSONWithUsernames(m, usernamesByID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve memo creator: %v", err)), nil
|
||||
}
|
||||
|
|
@ -514,10 +567,18 @@ func (s *MCPService) handleSearchMemos(ctx context.Context, req mcp.CallToolRequ
|
|||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to search memos: %v", err)), nil
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(memos))
|
||||
for _, memo := range memos {
|
||||
creatorIDs = append(creatorIDs, memo.CreatorID)
|
||||
}
|
||||
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to preload memo creators: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]memoJSON, len(memos))
|
||||
for i, m := range memos {
|
||||
result, err := storeMemoToJSONWithStore(ctx, s.store, m)
|
||||
result, err := storeMemoToJSONWithUsernames(m, usernamesByID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve memo creator: %v", err)), nil
|
||||
}
|
||||
|
|
@ -571,11 +632,21 @@ func (s *MCPService) handleListMemoComments(ctx context.Context, req mcp.CallToo
|
|||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to list comments: %v", err)), nil
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(memos))
|
||||
for _, memo := range memos {
|
||||
if checkMemoAccess(memo, userID) == nil {
|
||||
creatorIDs = append(creatorIDs, memo.CreatorID)
|
||||
}
|
||||
}
|
||||
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to preload memo creators: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]memoJSON, 0, len(memos))
|
||||
for _, m := range memos {
|
||||
if checkMemoAccess(m, userID) == nil {
|
||||
result, err := storeMemoToJSONWithStore(ctx, s.store, m)
|
||||
result, err := storeMemoToJSONWithUsernames(m, usernamesByID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve memo creator: %v", err)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,10 +60,18 @@ func (s *MCPService) handleListReactions(ctx context.Context, req mcp.CallToolRe
|
|||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to list reactions: %v", err)), nil
|
||||
}
|
||||
creatorIDs := make([]int32, 0, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
creatorIDs = append(creatorIDs, reaction.CreatorID)
|
||||
}
|
||||
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to preload reaction creators: %v", err)), nil
|
||||
}
|
||||
|
||||
results := make([]reactionJSON, len(reactions))
|
||||
for i, r := range reactions {
|
||||
creator, err := lookupUsername(ctx, s.store, r.CreatorID)
|
||||
creator, err := lookupUsernameFromCache(usernamesByID, r.CreatorID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve reaction creator: %v", err)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const getShortcutId = (name: string): string => {
|
|||
return parts.length === 4 ? parts[3] : "";
|
||||
};
|
||||
|
||||
const escapeFilterValue = (value: string): string => JSON.stringify(value);
|
||||
|
||||
export interface UseMemoFiltersOptions {
|
||||
creatorName?: string;
|
||||
includeShortcuts?: boolean;
|
||||
|
|
@ -63,9 +65,9 @@ export const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | un
|
|||
// Add active filters from context
|
||||
for (const filter of filters) {
|
||||
if (filter.factor === "contentSearch") {
|
||||
conditions.push(`content.contains("${filter.value}")`);
|
||||
conditions.push(`content.contains(${escapeFilterValue(filter.value)})`);
|
||||
} else if (filter.factor === "tagSearch") {
|
||||
conditions.push(`tag in ["${filter.value}"]`);
|
||||
conditions.push(`tag in [${escapeFilterValue(filter.value)}]`);
|
||||
} else if (filter.factor === "pinned") {
|
||||
if (includePinned) {
|
||||
conditions.push(`pinned`);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf";
|
|||
* Describes the file api/v1/shortcut_service.proto.
|
||||
*/
|
||||
export const file_api_v1_shortcut_service: GenFile = /*@__PURE__*/
|
||||
fileDesc("Ch1hcGkvdjEvc2hvcnRjdXRfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxIpoBCghTaG9ydGN1dBIRCgRuYW1lGAEgASgJQgPgQQgSEgoFdGl0bGUYAiABKAlCA+BBAhITCgZmaWx0ZXIYAyABKAlCA+BBATpS6kFPChVtZW1vcy5hcGkudjEvU2hvcnRjdXQSIXVzZXJzL3t1c2VyfS9zaG9ydGN1dHMve3Nob3J0Y3V0fSoJc2hvcnRjdXRzMghzaG9ydGN1dCJFChRMaXN0U2hvcnRjdXRzUmVxdWVzdBItCgZwYXJlbnQYASABKAlCHeBBAvpBFxIVbWVtb3MuYXBpLnYxL1Nob3J0Y3V0IkIKFUxpc3RTaG9ydGN1dHNSZXNwb25zZRIpCglzaG9ydGN1dHMYASADKAsyFi5tZW1vcy5hcGkudjEuU2hvcnRjdXQiQQoSR2V0U2hvcnRjdXRSZXF1ZXN0EisKBG5hbWUYASABKAlCHeBBAvpBFwoVbWVtb3MuYXBpLnYxL1Nob3J0Y3V0IpEBChVDcmVhdGVTaG9ydGN1dFJlcXVlc3QSLQoGcGFyZW50GAEgASgJQh3gQQL6QRcSFW1lbW9zLmFwaS52MS9TaG9ydGN1dBItCghzaG9ydGN1dBgCIAEoCzIWLm1lbW9zLmFwaS52MS5TaG9ydGN1dEID4EECEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBASJ8ChVVcGRhdGVTaG9ydGN1dFJlcXVlc3QSLQoIc2hvcnRjdXQYASABKAsyFi5tZW1vcy5hcGkudjEuU2hvcnRjdXRCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASJEChVEZWxldGVTaG9ydGN1dFJlcXVlc3QSKwoEbmFtZRgBIAEoCUId4EEC+kEXChVtZW1vcy5hcGkudjEvU2hvcnRjdXQy3gUKD1Nob3J0Y3V0U2VydmljZRKNAQoNTGlzdFNob3J0Y3V0cxIiLm1lbW9zLmFwaS52MS5MaXN0U2hvcnRjdXRzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5MaXN0U2hvcnRjdXRzUmVzcG9uc2UiM9pBBnBhcmVudILT5JMCJBIiL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3Nob3J0Y3V0cxJ6CgtHZXRTaG9ydGN1dBIgLm1lbW9zLmFwaS52MS5HZXRTaG9ydGN1dFJlcXVlc3QaFi5tZW1vcy5hcGkudjEuU2hvcnRjdXQiMdpBBG5hbWWC0+STAiQSIi9hcGkvdjEve25hbWU9dXNlcnMvKi9zaG9ydGN1dHMvKn0SlQEKDkNyZWF0ZVNob3J0Y3V0EiMubWVtb3MuYXBpLnYxLkNyZWF0ZVNob3J0Y3V0UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5TaG9ydGN1dCJG2kEPcGFyZW50LHNob3J0Y3V0gtPkkwIuOghzaG9ydGN1dCIiL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3Nob3J0Y3V0cxKjAQoOVXBkYXRlU2hvcnRjdXQSIy5tZW1vcy5hcGkudjEuVXBkYXRlU2hvcnRjdXRSZXF1ZXN0GhYubWVtb3MuYXBpLnYxLlNob3J0Y3V0IlTaQRRzaG9ydGN1dCx1cGRhdGVfbWFza4LT5JMCNzoIc2hvcnRjdXQyKy9hcGkvdjEve3Nob3J0Y3V0Lm5hbWU9dXNlcnMvKi9zaG9ydGN1dHMvKn0SgAEKDkRlbGV0ZVNob3J0Y3V0EiMubWVtb3MuYXBpLnYxLkRlbGV0ZVNob3J0Y3V0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIx2kEEbmFtZYLT5JMCJCoiL2FwaS92MS97bmFtZT11c2Vycy8qL3Nob3J0Y3V0cy8qfUKsAQoQY29tLm1lbW9zLmFwaS52MUIUU2hvcnRjdXRTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask]);
|
||||
fileDesc("Ch1hcGkvdjEvc2hvcnRjdXRfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxIp4BCghTaG9ydGN1dBIRCgRuYW1lGAEgASgJQgPgQQgSEgoFdGl0bGUYAiABKAlCA+BBAhITCgZmaWx0ZXIYAyABKAlCA+BBATpW6kFTChVtZW1vcy5hcGkudjEvU2hvcnRjdXQSJXVzZXJzL3t1c2VybmFtZX0vc2hvcnRjdXRzL3tzaG9ydGN1dH0qCXNob3J0Y3V0czIIc2hvcnRjdXQiRQoUTGlzdFNob3J0Y3V0c1JlcXVlc3QSLQoGcGFyZW50GAEgASgJQh3gQQL6QRcSFW1lbW9zLmFwaS52MS9TaG9ydGN1dCJCChVMaXN0U2hvcnRjdXRzUmVzcG9uc2USKQoJc2hvcnRjdXRzGAEgAygLMhYubWVtb3MuYXBpLnYxLlNob3J0Y3V0IkEKEkdldFNob3J0Y3V0UmVxdWVzdBIrCgRuYW1lGAEgASgJQh3gQQL6QRcKFW1lbW9zLmFwaS52MS9TaG9ydGN1dCKRAQoVQ3JlYXRlU2hvcnRjdXRSZXF1ZXN0Ei0KBnBhcmVudBgBIAEoCUId4EEC+kEXEhVtZW1vcy5hcGkudjEvU2hvcnRjdXQSLQoIc2hvcnRjdXQYAiABKAsyFi5tZW1vcy5hcGkudjEuU2hvcnRjdXRCA+BBAhIaCg12YWxpZGF0ZV9vbmx5GAMgASgIQgPgQQEifAoVVXBkYXRlU2hvcnRjdXRSZXF1ZXN0Ei0KCHNob3J0Y3V0GAEgASgLMhYubWVtb3MuYXBpLnYxLlNob3J0Y3V0QgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQEiRAoVRGVsZXRlU2hvcnRjdXRSZXF1ZXN0EisKBG5hbWUYASABKAlCHeBBAvpBFwoVbWVtb3MuYXBpLnYxL1Nob3J0Y3V0Mt4FCg9TaG9ydGN1dFNlcnZpY2USjQEKDUxpc3RTaG9ydGN1dHMSIi5tZW1vcy5hcGkudjEuTGlzdFNob3J0Y3V0c1JlcXVlc3QaIy5tZW1vcy5hcGkudjEuTGlzdFNob3J0Y3V0c1Jlc3BvbnNlIjPaQQZwYXJlbnSC0+STAiQSIi9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zaG9ydGN1dHMSegoLR2V0U2hvcnRjdXQSIC5tZW1vcy5hcGkudjEuR2V0U2hvcnRjdXRSZXF1ZXN0GhYubWVtb3MuYXBpLnYxLlNob3J0Y3V0IjHaQQRuYW1lgtPkkwIkEiIvYXBpL3YxL3tuYW1lPXVzZXJzLyovc2hvcnRjdXRzLyp9EpUBCg5DcmVhdGVTaG9ydGN1dBIjLm1lbW9zLmFwaS52MS5DcmVhdGVTaG9ydGN1dFJlcXVlc3QaFi5tZW1vcy5hcGkudjEuU2hvcnRjdXQiRtpBD3BhcmVudCxzaG9ydGN1dILT5JMCLjoIc2hvcnRjdXQiIi9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zaG9ydGN1dHMSowEKDlVwZGF0ZVNob3J0Y3V0EiMubWVtb3MuYXBpLnYxLlVwZGF0ZVNob3J0Y3V0UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5TaG9ydGN1dCJU2kEUc2hvcnRjdXQsdXBkYXRlX21hc2uC0+STAjc6CHNob3J0Y3V0MisvYXBpL3YxL3tzaG9ydGN1dC5uYW1lPXVzZXJzLyovc2hvcnRjdXRzLyp9EoABCg5EZWxldGVTaG9ydGN1dBIjLm1lbW9zLmFwaS52MS5EZWxldGVTaG9ydGN1dFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMdpBBG5hbWWC0+STAiQqIi9hcGkvdjEve25hbWU9dXNlcnMvKi9zaG9ydGN1dHMvKn1CrAEKEGNvbS5tZW1vcy5hcGkudjFCFFNob3J0Y3V0U2VydmljZVByb3RvUAFaMGdpdGh1Yi5jb20vdXNlbWVtb3MvbWVtb3MvcHJvdG8vZ2VuL2FwaS92MTthcGl2MaICA01BWKoCDE1lbW9zLkFwaS5WMcoCDE1lbW9zXEFwaVxWMeICGE1lbW9zXEFwaVxWMVxHUEJNZXRhZGF0YeoCDk1lbW9zOjpBcGk6OlYxYgZwcm90bzM", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask]);
|
||||
|
||||
/**
|
||||
* @generated from message memos.api.v1.Shortcut
|
||||
|
|
@ -24,7 +24,7 @@ export const file_api_v1_shortcut_service: GenFile = /*@__PURE__*/
|
|||
export type Shortcut = Message<"memos.api.v1.Shortcut"> & {
|
||||
/**
|
||||
* The resource name of the shortcut.
|
||||
* Format: users/{user}/shortcuts/{shortcut}
|
||||
* Format: users/{username}/shortcuts/{shortcut}
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
|
|
@ -58,7 +58,7 @@ export const ShortcutSchema: GenMessage<Shortcut> = /*@__PURE__*/
|
|||
export type ListShortcutsRequest = Message<"memos.api.v1.ListShortcutsRequest"> & {
|
||||
/**
|
||||
* Required. The parent resource where shortcuts are listed.
|
||||
* Format: users/{user}
|
||||
* Format: users/{username}
|
||||
*
|
||||
* @generated from field: string parent = 1;
|
||||
*/
|
||||
|
|
@ -97,7 +97,7 @@ export const ListShortcutsResponseSchema: GenMessage<ListShortcutsResponse> = /*
|
|||
export type GetShortcutRequest = Message<"memos.api.v1.GetShortcutRequest"> & {
|
||||
/**
|
||||
* Required. The resource name of the shortcut to retrieve.
|
||||
* Format: users/{user}/shortcuts/{shortcut}
|
||||
* Format: users/{username}/shortcuts/{shortcut}
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
|
|
@ -117,7 +117,7 @@ export const GetShortcutRequestSchema: GenMessage<GetShortcutRequest> = /*@__PUR
|
|||
export type CreateShortcutRequest = Message<"memos.api.v1.CreateShortcutRequest"> & {
|
||||
/**
|
||||
* Required. The parent resource where this shortcut will be created.
|
||||
* Format: users/{user}
|
||||
* Format: users/{username}
|
||||
*
|
||||
* @generated from field: string parent = 1;
|
||||
*/
|
||||
|
|
@ -177,7 +177,7 @@ export const UpdateShortcutRequestSchema: GenMessage<UpdateShortcutRequest> = /*
|
|||
export type DeleteShortcutRequest = Message<"memos.api.v1.DeleteShortcutRequest"> & {
|
||||
/**
|
||||
* Required. The resource name of the shortcut to delete.
|
||||
* Format: users/{user}/shortcuts/{shortcut}
|
||||
* Format: users/{username}/shortcuts/{shortcut}
|
||||
*
|
||||
* @generated from field: string name = 1;
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue