mirror of https://github.com/usememos/memos.git
feat(memo): add share links for private memos (#5742)
Co-authored-by: memoclaw <265580040+memoclaw@users.noreply.github.com>
This commit is contained in:
parent
e164517773
commit
3f3133d6e2
|
|
@ -103,6 +103,29 @@ service MemoService {
|
|||
option (google.api.http) = {delete: "/api/v1/{name=memos/*/reactions/*}"};
|
||||
option (google.api.method_signature) = "name";
|
||||
}
|
||||
// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
rpc CreateMemoShare(CreateMemoShareRequest) returns (MemoShare) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/{parent=memos/*}/shares"
|
||||
body: "memo_share"
|
||||
};
|
||||
option (google.api.method_signature) = "parent,memo_share";
|
||||
}
|
||||
// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
rpc ListMemoShares(ListMemoSharesRequest) returns (ListMemoSharesResponse) {
|
||||
option (google.api.http) = {get: "/api/v1/{parent=memos/*}/shares"};
|
||||
option (google.api.method_signature) = "parent";
|
||||
}
|
||||
// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
rpc DeleteMemoShare(DeleteMemoShareRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = {delete: "/api/v1/{name=memos/*/shares/*}"};
|
||||
option (google.api.method_signature) = "name";
|
||||
}
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND if the token is invalid or expired.
|
||||
rpc GetMemoByShare(GetMemoByShareRequest) returns (Memo) {
|
||||
option (google.api.http) = {get: "/api/v1/shares/{share_id}"};
|
||||
}
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
|
|
@ -511,3 +534,64 @@ message DeleteMemoReactionRequest {
|
|||
(google.api.resource_reference) = {type: "memos.api.v1/Reaction"}
|
||||
];
|
||||
}
|
||||
|
||||
// MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.
|
||||
message MemoShare {
|
||||
option (google.api.resource) = {
|
||||
type: "memos.api.v1/MemoShare"
|
||||
pattern: "memos/{memo}/shares/{share}"
|
||||
singular: "share"
|
||||
plural: "shares"
|
||||
};
|
||||
|
||||
// The resource name of the share. Format: memos/{memo}/shares/{share}
|
||||
// The {share} segment is the opaque token used in the share URL.
|
||||
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
|
||||
|
||||
// Output only. When this share link was created.
|
||||
google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
|
||||
|
||||
// Optional. When set, the share link stops working after this time.
|
||||
// If unset, the link never expires.
|
||||
optional google.protobuf.Timestamp expire_time = 3 [(google.api.field_behavior) = OPTIONAL];
|
||||
}
|
||||
|
||||
message CreateMemoShareRequest {
|
||||
// Required. The resource name of the memo to share.
|
||||
// Format: memos/{memo}
|
||||
string parent = 1 [
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(google.api.resource_reference) = {type: "memos.api.v1/Memo"}
|
||||
];
|
||||
|
||||
// Required. The share to create.
|
||||
MemoShare memo_share = 2 [(google.api.field_behavior) = REQUIRED];
|
||||
}
|
||||
|
||||
message ListMemoSharesRequest {
|
||||
// Required. The resource name of the memo.
|
||||
// Format: memos/{memo}
|
||||
string parent = 1 [
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(google.api.resource_reference) = {type: "memos.api.v1/Memo"}
|
||||
];
|
||||
}
|
||||
|
||||
message ListMemoSharesResponse {
|
||||
// The list of share links.
|
||||
repeated MemoShare memo_shares = 1;
|
||||
}
|
||||
|
||||
message DeleteMemoShareRequest {
|
||||
// Required. The resource name of the share to delete.
|
||||
// Format: memos/{memo}/shares/{share}
|
||||
string name = 1 [
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(google.api.resource_reference) = {type: "memos.api.v1/MemoShare"}
|
||||
];
|
||||
}
|
||||
|
||||
message GetMemoByShareRequest {
|
||||
// Required. The share token extracted from the share URL (/s/{share_id}).
|
||||
string share_id = 1 [(google.api.field_behavior) = REQUIRED];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,18 @@ const (
|
|||
// MemoServiceDeleteMemoReactionProcedure is the fully-qualified name of the MemoService's
|
||||
// DeleteMemoReaction RPC.
|
||||
MemoServiceDeleteMemoReactionProcedure = "/memos.api.v1.MemoService/DeleteMemoReaction"
|
||||
// MemoServiceCreateMemoShareProcedure is the fully-qualified name of the MemoService's
|
||||
// CreateMemoShare RPC.
|
||||
MemoServiceCreateMemoShareProcedure = "/memos.api.v1.MemoService/CreateMemoShare"
|
||||
// MemoServiceListMemoSharesProcedure is the fully-qualified name of the MemoService's
|
||||
// ListMemoShares RPC.
|
||||
MemoServiceListMemoSharesProcedure = "/memos.api.v1.MemoService/ListMemoShares"
|
||||
// MemoServiceDeleteMemoShareProcedure is the fully-qualified name of the MemoService's
|
||||
// DeleteMemoShare RPC.
|
||||
MemoServiceDeleteMemoShareProcedure = "/memos.api.v1.MemoService/DeleteMemoShare"
|
||||
// MemoServiceGetMemoByShareProcedure is the fully-qualified name of the MemoService's
|
||||
// GetMemoByShare RPC.
|
||||
MemoServiceGetMemoByShareProcedure = "/memos.api.v1.MemoService/GetMemoByShare"
|
||||
)
|
||||
|
||||
// MemoServiceClient is a client for the memos.api.v1.MemoService service.
|
||||
|
|
@ -103,6 +115,15 @@ type MemoServiceClient interface {
|
|||
UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error)
|
||||
// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error)
|
||||
// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND if the token is invalid or expired.
|
||||
GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error)
|
||||
}
|
||||
|
||||
// NewMemoServiceClient constructs a client for the memos.api.v1.MemoService service. By default, it
|
||||
|
|
@ -200,6 +221,30 @@ func NewMemoServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
|
|||
connect.WithSchema(memoServiceMethods.ByName("DeleteMemoReaction")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
createMemoShare: connect.NewClient[v1.CreateMemoShareRequest, v1.MemoShare](
|
||||
httpClient,
|
||||
baseURL+MemoServiceCreateMemoShareProcedure,
|
||||
connect.WithSchema(memoServiceMethods.ByName("CreateMemoShare")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
listMemoShares: connect.NewClient[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse](
|
||||
httpClient,
|
||||
baseURL+MemoServiceListMemoSharesProcedure,
|
||||
connect.WithSchema(memoServiceMethods.ByName("ListMemoShares")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
deleteMemoShare: connect.NewClient[v1.DeleteMemoShareRequest, emptypb.Empty](
|
||||
httpClient,
|
||||
baseURL+MemoServiceDeleteMemoShareProcedure,
|
||||
connect.WithSchema(memoServiceMethods.ByName("DeleteMemoShare")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
getMemoByShare: connect.NewClient[v1.GetMemoByShareRequest, v1.Memo](
|
||||
httpClient,
|
||||
baseURL+MemoServiceGetMemoByShareProcedure,
|
||||
connect.WithSchema(memoServiceMethods.ByName("GetMemoByShare")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +264,10 @@ type memoServiceClient struct {
|
|||
listMemoReactions *connect.Client[v1.ListMemoReactionsRequest, v1.ListMemoReactionsResponse]
|
||||
upsertMemoReaction *connect.Client[v1.UpsertMemoReactionRequest, v1.Reaction]
|
||||
deleteMemoReaction *connect.Client[v1.DeleteMemoReactionRequest, emptypb.Empty]
|
||||
createMemoShare *connect.Client[v1.CreateMemoShareRequest, v1.MemoShare]
|
||||
listMemoShares *connect.Client[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse]
|
||||
deleteMemoShare *connect.Client[v1.DeleteMemoShareRequest, emptypb.Empty]
|
||||
getMemoByShare *connect.Client[v1.GetMemoByShareRequest, v1.Memo]
|
||||
}
|
||||
|
||||
// CreateMemo calls memos.api.v1.MemoService.CreateMemo.
|
||||
|
|
@ -291,6 +340,26 @@ func (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, req *connect
|
|||
return c.deleteMemoReaction.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// CreateMemoShare calls memos.api.v1.MemoService.CreateMemoShare.
|
||||
func (c *memoServiceClient) CreateMemoShare(ctx context.Context, req *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) {
|
||||
return c.createMemoShare.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ListMemoShares calls memos.api.v1.MemoService.ListMemoShares.
|
||||
func (c *memoServiceClient) ListMemoShares(ctx context.Context, req *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) {
|
||||
return c.listMemoShares.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// DeleteMemoShare calls memos.api.v1.MemoService.DeleteMemoShare.
|
||||
func (c *memoServiceClient) DeleteMemoShare(ctx context.Context, req *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {
|
||||
return c.deleteMemoShare.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// GetMemoByShare calls memos.api.v1.MemoService.GetMemoByShare.
|
||||
func (c *memoServiceClient) GetMemoByShare(ctx context.Context, req *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) {
|
||||
return c.getMemoByShare.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// MemoServiceHandler is an implementation of the memos.api.v1.MemoService service.
|
||||
type MemoServiceHandler interface {
|
||||
// CreateMemo creates a memo.
|
||||
|
|
@ -321,6 +390,15 @@ type MemoServiceHandler interface {
|
|||
UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error)
|
||||
// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error)
|
||||
// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error)
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND if the token is invalid or expired.
|
||||
GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error)
|
||||
}
|
||||
|
||||
// NewMemoServiceHandler builds an HTTP handler from the service implementation. It returns the path
|
||||
|
|
@ -414,6 +492,30 @@ func NewMemoServiceHandler(svc MemoServiceHandler, opts ...connect.HandlerOption
|
|||
connect.WithSchema(memoServiceMethods.ByName("DeleteMemoReaction")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
memoServiceCreateMemoShareHandler := connect.NewUnaryHandler(
|
||||
MemoServiceCreateMemoShareProcedure,
|
||||
svc.CreateMemoShare,
|
||||
connect.WithSchema(memoServiceMethods.ByName("CreateMemoShare")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
memoServiceListMemoSharesHandler := connect.NewUnaryHandler(
|
||||
MemoServiceListMemoSharesProcedure,
|
||||
svc.ListMemoShares,
|
||||
connect.WithSchema(memoServiceMethods.ByName("ListMemoShares")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
memoServiceDeleteMemoShareHandler := connect.NewUnaryHandler(
|
||||
MemoServiceDeleteMemoShareProcedure,
|
||||
svc.DeleteMemoShare,
|
||||
connect.WithSchema(memoServiceMethods.ByName("DeleteMemoShare")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
memoServiceGetMemoByShareHandler := connect.NewUnaryHandler(
|
||||
MemoServiceGetMemoByShareProcedure,
|
||||
svc.GetMemoByShare,
|
||||
connect.WithSchema(memoServiceMethods.ByName("GetMemoByShare")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/memos.api.v1.MemoService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case MemoServiceCreateMemoProcedure:
|
||||
|
|
@ -444,6 +546,14 @@ func NewMemoServiceHandler(svc MemoServiceHandler, opts ...connect.HandlerOption
|
|||
memoServiceUpsertMemoReactionHandler.ServeHTTP(w, r)
|
||||
case MemoServiceDeleteMemoReactionProcedure:
|
||||
memoServiceDeleteMemoReactionHandler.ServeHTTP(w, r)
|
||||
case MemoServiceCreateMemoShareProcedure:
|
||||
memoServiceCreateMemoShareHandler.ServeHTTP(w, r)
|
||||
case MemoServiceListMemoSharesProcedure:
|
||||
memoServiceListMemoSharesHandler.ServeHTTP(w, r)
|
||||
case MemoServiceDeleteMemoShareProcedure:
|
||||
memoServiceDeleteMemoShareHandler.ServeHTTP(w, r)
|
||||
case MemoServiceGetMemoByShareProcedure:
|
||||
memoServiceGetMemoByShareHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
|
@ -508,3 +618,19 @@ func (UnimplementedMemoServiceHandler) UpsertMemoReaction(context.Context, *conn
|
|||
func (UnimplementedMemoServiceHandler) DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.DeleteMemoReaction is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedMemoServiceHandler) CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.CreateMemoShare is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedMemoServiceHandler) ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.ListMemoShares is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedMemoServiceHandler) DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.DeleteMemoShare is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedMemoServiceHandler) GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.MemoService.GetMemoByShare is not implemented"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1672,6 +1672,309 @@ func (x *DeleteMemoReactionRequest) GetName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.
|
||||
type MemoShare struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The resource name of the share. Format: memos/{memo}/shares/{share}
|
||||
// The {share} segment is the opaque token used in the share URL.
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// Output only. When this share link was created.
|
||||
CreateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"`
|
||||
// Optional. When set, the share link stops working after this time.
|
||||
// If unset, the link never expires.
|
||||
ExpireTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expire_time,json=expireTime,proto3,oneof" json:"expire_time,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MemoShare) Reset() {
|
||||
*x = MemoShare{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MemoShare) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MemoShare) ProtoMessage() {}
|
||||
|
||||
func (x *MemoShare) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[23]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MemoShare.ProtoReflect.Descriptor instead.
|
||||
func (*MemoShare) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *MemoShare) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MemoShare) GetCreateTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MemoShare) GetExpireTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.ExpireTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateMemoShareRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the memo to share.
|
||||
// Format: memos/{memo}
|
||||
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
|
||||
// Required. The share to create.
|
||||
MemoShare *MemoShare `protobuf:"bytes,2,opt,name=memo_share,json=memoShare,proto3" json:"memo_share,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateMemoShareRequest) Reset() {
|
||||
*x = CreateMemoShareRequest{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateMemoShareRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateMemoShareRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateMemoShareRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[24]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateMemoShareRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateMemoShareRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{24}
|
||||
}
|
||||
|
||||
func (x *CreateMemoShareRequest) GetParent() string {
|
||||
if x != nil {
|
||||
return x.Parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateMemoShareRequest) GetMemoShare() *MemoShare {
|
||||
if x != nil {
|
||||
return x.MemoShare
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListMemoSharesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the memo.
|
||||
// Format: memos/{memo}
|
||||
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesRequest) Reset() {
|
||||
*x = ListMemoSharesRequest{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListMemoSharesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListMemoSharesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[25]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListMemoSharesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListMemoSharesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{25}
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesRequest) GetParent() string {
|
||||
if x != nil {
|
||||
return x.Parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListMemoSharesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The list of share links.
|
||||
MemoShares []*MemoShare `protobuf:"bytes,1,rep,name=memo_shares,json=memoShares,proto3" json:"memo_shares,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesResponse) Reset() {
|
||||
*x = ListMemoSharesResponse{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[26]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListMemoSharesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListMemoSharesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[26]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListMemoSharesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListMemoSharesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{26}
|
||||
}
|
||||
|
||||
func (x *ListMemoSharesResponse) GetMemoShares() []*MemoShare {
|
||||
if x != nil {
|
||||
return x.MemoShares
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteMemoShareRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The resource name of the share to delete.
|
||||
// Format: memos/{memo}/shares/{share}
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteMemoShareRequest) Reset() {
|
||||
*x = DeleteMemoShareRequest{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[27]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteMemoShareRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteMemoShareRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteMemoShareRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[27]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteMemoShareRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteMemoShareRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{27}
|
||||
}
|
||||
|
||||
func (x *DeleteMemoShareRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetMemoByShareRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Required. The share token extracted from the share URL (/s/{share_id}).
|
||||
ShareId string `protobuf:"bytes,1,opt,name=share_id,json=shareId,proto3" json:"share_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetMemoByShareRequest) Reset() {
|
||||
*x = GetMemoByShareRequest{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[28]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetMemoByShareRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetMemoByShareRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetMemoByShareRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[28]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetMemoByShareRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetMemoByShareRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_v1_memo_service_proto_rawDescGZIP(), []int{28}
|
||||
}
|
||||
|
||||
func (x *GetMemoByShareRequest) GetShareId() string {
|
||||
if x != nil {
|
||||
return x.ShareId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Computed properties of a memo.
|
||||
type Memo_Property struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
|
|
@ -1687,7 +1990,7 @@ type Memo_Property struct {
|
|||
|
||||
func (x *Memo_Property) Reset() {
|
||||
*x = Memo_Property{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[23]
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[29]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -1699,7 +2002,7 @@ func (x *Memo_Property) String() string {
|
|||
func (*Memo_Property) ProtoMessage() {}
|
||||
|
||||
func (x *Memo_Property) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[23]
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[29]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1764,7 +2067,7 @@ type MemoRelation_Memo struct {
|
|||
|
||||
func (x *MemoRelation_Memo) Reset() {
|
||||
*x = MemoRelation_Memo{}
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[24]
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[30]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -1776,7 +2079,7 @@ func (x *MemoRelation_Memo) String() string {
|
|||
func (*MemoRelation_Memo) ProtoMessage() {}
|
||||
|
||||
func (x *MemoRelation_Memo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[24]
|
||||
mi := &file_api_v1_memo_service_proto_msgTypes[30]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1958,14 +2261,38 @@ const file_api_v1_memo_service_proto_rawDesc = "" +
|
|||
"\breaction\x18\x02 \x01(\v2\x16.memos.api.v1.ReactionB\x03\xe0A\x02R\breaction\"N\n" +
|
||||
"\x19DeleteMemoReactionRequest\x121\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\n" +
|
||||
"\x15memos.api.v1/ReactionR\x04name*P\n" +
|
||||
"\x15memos.api.v1/ReactionR\x04name\"\x86\x02\n" +
|
||||
"\tMemoShare\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" +
|
||||
"\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
|
||||
"createTime\x12E\n" +
|
||||
"\vexpire_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01H\x00R\n" +
|
||||
"expireTime\x88\x01\x01:G\xeaAD\n" +
|
||||
"\x16memos.api.v1/MemoShare\x12\x1bmemos/{memo}/shares/{share}*\x06shares2\x05shareB\x0e\n" +
|
||||
"\f_expire_time\"\x88\x01\n" +
|
||||
"\x16CreateMemoShareRequest\x121\n" +
|
||||
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
||||
"\x11memos.api.v1/MemoR\x06parent\x12;\n" +
|
||||
"\n" +
|
||||
"memo_share\x18\x02 \x01(\v2\x17.memos.api.v1.MemoShareB\x03\xe0A\x02R\tmemoShare\"J\n" +
|
||||
"\x15ListMemoSharesRequest\x121\n" +
|
||||
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
||||
"\x11memos.api.v1/MemoR\x06parent\"R\n" +
|
||||
"\x16ListMemoSharesResponse\x128\n" +
|
||||
"\vmemo_shares\x18\x01 \x03(\v2\x17.memos.api.v1.MemoShareR\n" +
|
||||
"memoShares\"L\n" +
|
||||
"\x16DeleteMemoShareRequest\x122\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x1e\xe0A\x02\xfaA\x18\n" +
|
||||
"\x16memos.api.v1/MemoShareR\x04name\"7\n" +
|
||||
"\x15GetMemoByShareRequest\x12\x1e\n" +
|
||||
"\bshare_id\x18\x01 \x01(\tB\x03\xe0A\x02R\ashareId*P\n" +
|
||||
"\n" +
|
||||
"Visibility\x12\x1a\n" +
|
||||
"\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\v\n" +
|
||||
"\aPRIVATE\x10\x01\x12\r\n" +
|
||||
"\tPROTECTED\x10\x02\x12\n" +
|
||||
"\n" +
|
||||
"\x06PUBLIC\x10\x032\xd3\x0e\n" +
|
||||
"\x06PUBLIC\x10\x032\xee\x12\n" +
|
||||
"\vMemoService\x12e\n" +
|
||||
"\n" +
|
||||
"CreateMemo\x12\x1f.memos.api.v1.CreateMemoRequest\x1a\x12.memos.api.v1.Memo\"\"\xdaA\x04memo\x82\xd3\xe4\x93\x02\x15:\x04memo\"\r/api/v1/memos\x12f\n" +
|
||||
|
|
@ -1983,7 +2310,12 @@ const file_api_v1_memo_service_proto_rawDesc = "" +
|
|||
"\x10ListMemoComments\x12%.memos.api.v1.ListMemoCommentsRequest\x1a&.memos.api.v1.ListMemoCommentsResponse\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=memos/*}/comments\x12\x95\x01\n" +
|
||||
"\x11ListMemoReactions\x12&.memos.api.v1.ListMemoReactionsRequest\x1a'.memos.api.v1.ListMemoReactionsResponse\"/\xdaA\x04name\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{name=memos/*}/reactions\x12\x89\x01\n" +
|
||||
"\x12UpsertMemoReaction\x12'.memos.api.v1.UpsertMemoReactionRequest\x1a\x16.memos.api.v1.Reaction\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/{name=memos/*}/reactions\x12\x88\x01\n" +
|
||||
"\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=memos/*/reactions/*}B\xa8\x01\n" +
|
||||
"\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=memos/*/reactions/*}\x12\x99\x01\n" +
|
||||
"\x0fCreateMemoShare\x12$.memos.api.v1.CreateMemoShareRequest\x1a\x17.memos.api.v1.MemoShare\"G\xdaA\x11parent,memo_share\x82\xd3\xe4\x93\x02-:\n" +
|
||||
"memo_share\"\x1f/api/v1/{parent=memos/*}/shares\x12\x8d\x01\n" +
|
||||
"\x0eListMemoShares\x12#.memos.api.v1.ListMemoSharesRequest\x1a$.memos.api.v1.ListMemoSharesResponse\"0\xdaA\x06parent\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{parent=memos/*}/shares\x12\x7f\n" +
|
||||
"\x0fDeleteMemoShare\x12$.memos.api.v1.DeleteMemoShareRequest\x1a\x16.google.protobuf.Empty\".\xdaA\x04name\x82\xd3\xe4\x93\x02!*\x1f/api/v1/{name=memos/*/shares/*}\x12l\n" +
|
||||
"\x0eGetMemoByShare\x12#.memos.api.v1.GetMemoByShareRequest\x1a\x12.memos.api.v1.Memo\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/shares/{share_id}B\xa8\x01\n" +
|
||||
"\x10com.memos.api.v1B\x10MemoServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3"
|
||||
|
||||
var (
|
||||
|
|
@ -1999,7 +2331,7 @@ func file_api_v1_memo_service_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_api_v1_memo_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||
var file_api_v1_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
|
||||
var file_api_v1_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
|
||||
var file_api_v1_memo_service_proto_goTypes = []any{
|
||||
(Visibility)(0), // 0: memos.api.v1.Visibility
|
||||
(MemoRelation_Type)(0), // 1: memos.api.v1.MemoRelation.Type
|
||||
|
|
@ -2026,35 +2358,41 @@ var file_api_v1_memo_service_proto_goTypes = []any{
|
|||
(*ListMemoReactionsResponse)(nil), // 22: memos.api.v1.ListMemoReactionsResponse
|
||||
(*UpsertMemoReactionRequest)(nil), // 23: memos.api.v1.UpsertMemoReactionRequest
|
||||
(*DeleteMemoReactionRequest)(nil), // 24: memos.api.v1.DeleteMemoReactionRequest
|
||||
(*Memo_Property)(nil), // 25: memos.api.v1.Memo.Property
|
||||
(*MemoRelation_Memo)(nil), // 26: memos.api.v1.MemoRelation.Memo
|
||||
(*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
|
||||
(State)(0), // 28: memos.api.v1.State
|
||||
(*Attachment)(nil), // 29: memos.api.v1.Attachment
|
||||
(*fieldmaskpb.FieldMask)(nil), // 30: google.protobuf.FieldMask
|
||||
(*emptypb.Empty)(nil), // 31: google.protobuf.Empty
|
||||
(*MemoShare)(nil), // 25: memos.api.v1.MemoShare
|
||||
(*CreateMemoShareRequest)(nil), // 26: memos.api.v1.CreateMemoShareRequest
|
||||
(*ListMemoSharesRequest)(nil), // 27: memos.api.v1.ListMemoSharesRequest
|
||||
(*ListMemoSharesResponse)(nil), // 28: memos.api.v1.ListMemoSharesResponse
|
||||
(*DeleteMemoShareRequest)(nil), // 29: memos.api.v1.DeleteMemoShareRequest
|
||||
(*GetMemoByShareRequest)(nil), // 30: memos.api.v1.GetMemoByShareRequest
|
||||
(*Memo_Property)(nil), // 31: memos.api.v1.Memo.Property
|
||||
(*MemoRelation_Memo)(nil), // 32: memos.api.v1.MemoRelation.Memo
|
||||
(*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp
|
||||
(State)(0), // 34: memos.api.v1.State
|
||||
(*Attachment)(nil), // 35: memos.api.v1.Attachment
|
||||
(*fieldmaskpb.FieldMask)(nil), // 36: google.protobuf.FieldMask
|
||||
(*emptypb.Empty)(nil), // 37: google.protobuf.Empty
|
||||
}
|
||||
var file_api_v1_memo_service_proto_depIdxs = []int32{
|
||||
27, // 0: memos.api.v1.Reaction.create_time:type_name -> google.protobuf.Timestamp
|
||||
28, // 1: memos.api.v1.Memo.state:type_name -> memos.api.v1.State
|
||||
27, // 2: memos.api.v1.Memo.create_time:type_name -> google.protobuf.Timestamp
|
||||
27, // 3: memos.api.v1.Memo.update_time:type_name -> google.protobuf.Timestamp
|
||||
27, // 4: memos.api.v1.Memo.display_time:type_name -> google.protobuf.Timestamp
|
||||
33, // 0: memos.api.v1.Reaction.create_time:type_name -> google.protobuf.Timestamp
|
||||
34, // 1: memos.api.v1.Memo.state:type_name -> memos.api.v1.State
|
||||
33, // 2: memos.api.v1.Memo.create_time:type_name -> google.protobuf.Timestamp
|
||||
33, // 3: memos.api.v1.Memo.update_time:type_name -> google.protobuf.Timestamp
|
||||
33, // 4: memos.api.v1.Memo.display_time:type_name -> google.protobuf.Timestamp
|
||||
0, // 5: memos.api.v1.Memo.visibility:type_name -> memos.api.v1.Visibility
|
||||
29, // 6: memos.api.v1.Memo.attachments:type_name -> memos.api.v1.Attachment
|
||||
35, // 6: memos.api.v1.Memo.attachments:type_name -> memos.api.v1.Attachment
|
||||
14, // 7: memos.api.v1.Memo.relations:type_name -> memos.api.v1.MemoRelation
|
||||
2, // 8: memos.api.v1.Memo.reactions:type_name -> memos.api.v1.Reaction
|
||||
25, // 9: memos.api.v1.Memo.property:type_name -> memos.api.v1.Memo.Property
|
||||
31, // 9: memos.api.v1.Memo.property:type_name -> memos.api.v1.Memo.Property
|
||||
4, // 10: memos.api.v1.Memo.location:type_name -> memos.api.v1.Location
|
||||
3, // 11: memos.api.v1.CreateMemoRequest.memo:type_name -> memos.api.v1.Memo
|
||||
28, // 12: memos.api.v1.ListMemosRequest.state:type_name -> memos.api.v1.State
|
||||
34, // 12: memos.api.v1.ListMemosRequest.state:type_name -> memos.api.v1.State
|
||||
3, // 13: memos.api.v1.ListMemosResponse.memos:type_name -> memos.api.v1.Memo
|
||||
3, // 14: memos.api.v1.UpdateMemoRequest.memo:type_name -> memos.api.v1.Memo
|
||||
30, // 15: memos.api.v1.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
29, // 16: memos.api.v1.SetMemoAttachmentsRequest.attachments:type_name -> memos.api.v1.Attachment
|
||||
29, // 17: memos.api.v1.ListMemoAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
|
||||
26, // 18: memos.api.v1.MemoRelation.memo:type_name -> memos.api.v1.MemoRelation.Memo
|
||||
26, // 19: memos.api.v1.MemoRelation.related_memo:type_name -> memos.api.v1.MemoRelation.Memo
|
||||
36, // 15: memos.api.v1.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||
35, // 16: memos.api.v1.SetMemoAttachmentsRequest.attachments:type_name -> memos.api.v1.Attachment
|
||||
35, // 17: memos.api.v1.ListMemoAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
|
||||
32, // 18: memos.api.v1.MemoRelation.memo:type_name -> memos.api.v1.MemoRelation.Memo
|
||||
32, // 19: memos.api.v1.MemoRelation.related_memo:type_name -> memos.api.v1.MemoRelation.Memo
|
||||
1, // 20: memos.api.v1.MemoRelation.type:type_name -> memos.api.v1.MemoRelation.Type
|
||||
14, // 21: memos.api.v1.SetMemoRelationsRequest.relations:type_name -> memos.api.v1.MemoRelation
|
||||
14, // 22: memos.api.v1.ListMemoRelationsResponse.relations:type_name -> memos.api.v1.MemoRelation
|
||||
|
|
@ -2062,39 +2400,51 @@ var file_api_v1_memo_service_proto_depIdxs = []int32{
|
|||
3, // 24: memos.api.v1.ListMemoCommentsResponse.memos:type_name -> memos.api.v1.Memo
|
||||
2, // 25: memos.api.v1.ListMemoReactionsResponse.reactions:type_name -> memos.api.v1.Reaction
|
||||
2, // 26: memos.api.v1.UpsertMemoReactionRequest.reaction:type_name -> memos.api.v1.Reaction
|
||||
5, // 27: memos.api.v1.MemoService.CreateMemo:input_type -> memos.api.v1.CreateMemoRequest
|
||||
6, // 28: memos.api.v1.MemoService.ListMemos:input_type -> memos.api.v1.ListMemosRequest
|
||||
8, // 29: memos.api.v1.MemoService.GetMemo:input_type -> memos.api.v1.GetMemoRequest
|
||||
9, // 30: memos.api.v1.MemoService.UpdateMemo:input_type -> memos.api.v1.UpdateMemoRequest
|
||||
10, // 31: memos.api.v1.MemoService.DeleteMemo:input_type -> memos.api.v1.DeleteMemoRequest
|
||||
11, // 32: memos.api.v1.MemoService.SetMemoAttachments:input_type -> memos.api.v1.SetMemoAttachmentsRequest
|
||||
12, // 33: memos.api.v1.MemoService.ListMemoAttachments:input_type -> memos.api.v1.ListMemoAttachmentsRequest
|
||||
15, // 34: memos.api.v1.MemoService.SetMemoRelations:input_type -> memos.api.v1.SetMemoRelationsRequest
|
||||
16, // 35: memos.api.v1.MemoService.ListMemoRelations:input_type -> memos.api.v1.ListMemoRelationsRequest
|
||||
18, // 36: memos.api.v1.MemoService.CreateMemoComment:input_type -> memos.api.v1.CreateMemoCommentRequest
|
||||
19, // 37: memos.api.v1.MemoService.ListMemoComments:input_type -> memos.api.v1.ListMemoCommentsRequest
|
||||
21, // 38: memos.api.v1.MemoService.ListMemoReactions:input_type -> memos.api.v1.ListMemoReactionsRequest
|
||||
23, // 39: memos.api.v1.MemoService.UpsertMemoReaction:input_type -> memos.api.v1.UpsertMemoReactionRequest
|
||||
24, // 40: memos.api.v1.MemoService.DeleteMemoReaction:input_type -> memos.api.v1.DeleteMemoReactionRequest
|
||||
3, // 41: memos.api.v1.MemoService.CreateMemo:output_type -> memos.api.v1.Memo
|
||||
7, // 42: memos.api.v1.MemoService.ListMemos:output_type -> memos.api.v1.ListMemosResponse
|
||||
3, // 43: memos.api.v1.MemoService.GetMemo:output_type -> memos.api.v1.Memo
|
||||
3, // 44: memos.api.v1.MemoService.UpdateMemo:output_type -> memos.api.v1.Memo
|
||||
31, // 45: memos.api.v1.MemoService.DeleteMemo:output_type -> google.protobuf.Empty
|
||||
31, // 46: memos.api.v1.MemoService.SetMemoAttachments:output_type -> google.protobuf.Empty
|
||||
13, // 47: memos.api.v1.MemoService.ListMemoAttachments:output_type -> memos.api.v1.ListMemoAttachmentsResponse
|
||||
31, // 48: memos.api.v1.MemoService.SetMemoRelations:output_type -> google.protobuf.Empty
|
||||
17, // 49: memos.api.v1.MemoService.ListMemoRelations:output_type -> memos.api.v1.ListMemoRelationsResponse
|
||||
3, // 50: memos.api.v1.MemoService.CreateMemoComment:output_type -> memos.api.v1.Memo
|
||||
20, // 51: memos.api.v1.MemoService.ListMemoComments:output_type -> memos.api.v1.ListMemoCommentsResponse
|
||||
22, // 52: memos.api.v1.MemoService.ListMemoReactions:output_type -> memos.api.v1.ListMemoReactionsResponse
|
||||
2, // 53: memos.api.v1.MemoService.UpsertMemoReaction:output_type -> memos.api.v1.Reaction
|
||||
31, // 54: memos.api.v1.MemoService.DeleteMemoReaction:output_type -> google.protobuf.Empty
|
||||
41, // [41:55] is the sub-list for method output_type
|
||||
27, // [27:41] is the sub-list for method input_type
|
||||
27, // [27:27] is the sub-list for extension type_name
|
||||
27, // [27:27] is the sub-list for extension extendee
|
||||
0, // [0:27] is the sub-list for field type_name
|
||||
33, // 27: memos.api.v1.MemoShare.create_time:type_name -> google.protobuf.Timestamp
|
||||
33, // 28: memos.api.v1.MemoShare.expire_time:type_name -> google.protobuf.Timestamp
|
||||
25, // 29: memos.api.v1.CreateMemoShareRequest.memo_share:type_name -> memos.api.v1.MemoShare
|
||||
25, // 30: memos.api.v1.ListMemoSharesResponse.memo_shares:type_name -> memos.api.v1.MemoShare
|
||||
5, // 31: memos.api.v1.MemoService.CreateMemo:input_type -> memos.api.v1.CreateMemoRequest
|
||||
6, // 32: memos.api.v1.MemoService.ListMemos:input_type -> memos.api.v1.ListMemosRequest
|
||||
8, // 33: memos.api.v1.MemoService.GetMemo:input_type -> memos.api.v1.GetMemoRequest
|
||||
9, // 34: memos.api.v1.MemoService.UpdateMemo:input_type -> memos.api.v1.UpdateMemoRequest
|
||||
10, // 35: memos.api.v1.MemoService.DeleteMemo:input_type -> memos.api.v1.DeleteMemoRequest
|
||||
11, // 36: memos.api.v1.MemoService.SetMemoAttachments:input_type -> memos.api.v1.SetMemoAttachmentsRequest
|
||||
12, // 37: memos.api.v1.MemoService.ListMemoAttachments:input_type -> memos.api.v1.ListMemoAttachmentsRequest
|
||||
15, // 38: memos.api.v1.MemoService.SetMemoRelations:input_type -> memos.api.v1.SetMemoRelationsRequest
|
||||
16, // 39: memos.api.v1.MemoService.ListMemoRelations:input_type -> memos.api.v1.ListMemoRelationsRequest
|
||||
18, // 40: memos.api.v1.MemoService.CreateMemoComment:input_type -> memos.api.v1.CreateMemoCommentRequest
|
||||
19, // 41: memos.api.v1.MemoService.ListMemoComments:input_type -> memos.api.v1.ListMemoCommentsRequest
|
||||
21, // 42: memos.api.v1.MemoService.ListMemoReactions:input_type -> memos.api.v1.ListMemoReactionsRequest
|
||||
23, // 43: memos.api.v1.MemoService.UpsertMemoReaction:input_type -> memos.api.v1.UpsertMemoReactionRequest
|
||||
24, // 44: memos.api.v1.MemoService.DeleteMemoReaction:input_type -> memos.api.v1.DeleteMemoReactionRequest
|
||||
26, // 45: memos.api.v1.MemoService.CreateMemoShare:input_type -> memos.api.v1.CreateMemoShareRequest
|
||||
27, // 46: memos.api.v1.MemoService.ListMemoShares:input_type -> memos.api.v1.ListMemoSharesRequest
|
||||
29, // 47: memos.api.v1.MemoService.DeleteMemoShare:input_type -> memos.api.v1.DeleteMemoShareRequest
|
||||
30, // 48: memos.api.v1.MemoService.GetMemoByShare:input_type -> memos.api.v1.GetMemoByShareRequest
|
||||
3, // 49: memos.api.v1.MemoService.CreateMemo:output_type -> memos.api.v1.Memo
|
||||
7, // 50: memos.api.v1.MemoService.ListMemos:output_type -> memos.api.v1.ListMemosResponse
|
||||
3, // 51: memos.api.v1.MemoService.GetMemo:output_type -> memos.api.v1.Memo
|
||||
3, // 52: memos.api.v1.MemoService.UpdateMemo:output_type -> memos.api.v1.Memo
|
||||
37, // 53: memos.api.v1.MemoService.DeleteMemo:output_type -> google.protobuf.Empty
|
||||
37, // 54: memos.api.v1.MemoService.SetMemoAttachments:output_type -> google.protobuf.Empty
|
||||
13, // 55: memos.api.v1.MemoService.ListMemoAttachments:output_type -> memos.api.v1.ListMemoAttachmentsResponse
|
||||
37, // 56: memos.api.v1.MemoService.SetMemoRelations:output_type -> google.protobuf.Empty
|
||||
17, // 57: memos.api.v1.MemoService.ListMemoRelations:output_type -> memos.api.v1.ListMemoRelationsResponse
|
||||
3, // 58: memos.api.v1.MemoService.CreateMemoComment:output_type -> memos.api.v1.Memo
|
||||
20, // 59: memos.api.v1.MemoService.ListMemoComments:output_type -> memos.api.v1.ListMemoCommentsResponse
|
||||
22, // 60: memos.api.v1.MemoService.ListMemoReactions:output_type -> memos.api.v1.ListMemoReactionsResponse
|
||||
2, // 61: memos.api.v1.MemoService.UpsertMemoReaction:output_type -> memos.api.v1.Reaction
|
||||
37, // 62: memos.api.v1.MemoService.DeleteMemoReaction:output_type -> google.protobuf.Empty
|
||||
25, // 63: memos.api.v1.MemoService.CreateMemoShare:output_type -> memos.api.v1.MemoShare
|
||||
28, // 64: memos.api.v1.MemoService.ListMemoShares:output_type -> memos.api.v1.ListMemoSharesResponse
|
||||
37, // 65: memos.api.v1.MemoService.DeleteMemoShare:output_type -> google.protobuf.Empty
|
||||
3, // 66: memos.api.v1.MemoService.GetMemoByShare:output_type -> memos.api.v1.Memo
|
||||
49, // [49:67] is the sub-list for method output_type
|
||||
31, // [31:49] is the sub-list for method input_type
|
||||
31, // [31:31] is the sub-list for extension type_name
|
||||
31, // [31:31] is the sub-list for extension extendee
|
||||
0, // [0:31] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_api_v1_memo_service_proto_init() }
|
||||
|
|
@ -2105,13 +2455,14 @@ func file_api_v1_memo_service_proto_init() {
|
|||
file_api_v1_attachment_service_proto_init()
|
||||
file_api_v1_common_proto_init()
|
||||
file_api_v1_memo_service_proto_msgTypes[1].OneofWrappers = []any{}
|
||||
file_api_v1_memo_service_proto_msgTypes[23].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_memo_service_proto_rawDesc), len(file_api_v1_memo_service_proto_rawDesc)),
|
||||
NumEnums: 2,
|
||||
NumMessages: 25,
|
||||
NumMessages: 31,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -729,6 +729,168 @@ func local_request_MemoService_DeleteMemoReaction_0(ctx context.Context, marshal
|
|||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_MemoService_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CreateMemoShareRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
val, ok := pathParams["parent"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
|
||||
}
|
||||
protoReq.Parent, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
|
||||
}
|
||||
msg, err := client.CreateMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_MemoService_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CreateMemoShareRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["parent"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
|
||||
}
|
||||
protoReq.Parent, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
|
||||
}
|
||||
msg, err := server.CreateMemoShare(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ListMemoSharesRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
val, ok := pathParams["parent"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
|
||||
}
|
||||
protoReq.Parent, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
|
||||
}
|
||||
msg, err := client.ListMemoShares(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ListMemoSharesRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
val, ok := pathParams["parent"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
|
||||
}
|
||||
protoReq.Parent, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
|
||||
}
|
||||
msg, err := server.ListMemoShares(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeleteMemoShareRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
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.DeleteMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeleteMemoShareRequest
|
||||
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.DeleteMemoShare(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetMemoByShareRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
val, ok := pathParams["share_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "share_id")
|
||||
}
|
||||
protoReq.ShareId, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "share_id", err)
|
||||
}
|
||||
msg, err := client.GetMemoByShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetMemoByShareRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
val, ok := pathParams["share_id"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "share_id")
|
||||
}
|
||||
protoReq.ShareId, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "share_id", err)
|
||||
}
|
||||
msg, err := server.GetMemoByShare(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
// RegisterMemoServiceHandlerServer registers the http handlers for service MemoService to "mux".
|
||||
// UnaryRPC :call MemoServiceServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
|
|
@ -1015,6 +1177,86 @@ func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
|
|||
}
|
||||
forward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_MemoService_CreateMemoShare_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.MemoService/CreateMemoShare", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_MemoService_CreateMemoShare_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_MemoService_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_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.MemoService/ListMemoShares", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_MemoService_ListMemoShares_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_MemoService_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_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.MemoService/DeleteMemoShare", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/shares/*}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_MemoService_DeleteMemoShare_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_MemoService_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_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.MemoService/GetMemoByShare", runtime.WithHTTPPathPattern("/api/v1/shares/{share_id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_MemoService_GetMemoByShare_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_MemoService_GetMemoByShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1293,6 +1535,74 @@ func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
|
|||
}
|
||||
forward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_MemoService_CreateMemoShare_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.MemoService/CreateMemoShare", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_MemoService_CreateMemoShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_MemoService_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_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.MemoService/ListMemoShares", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/shares"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_MemoService_ListMemoShares_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_MemoService_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_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.MemoService/DeleteMemoShare", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/shares/*}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_MemoService_DeleteMemoShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_MemoService_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_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.MemoService/GetMemoByShare", runtime.WithHTTPPathPattern("/api/v1/shares/{share_id}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_MemoService_GetMemoByShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_MemoService_GetMemoByShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1311,6 +1621,10 @@ var (
|
|||
pattern_MemoService_ListMemoReactions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
|
||||
pattern_MemoService_UpsertMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
|
||||
pattern_MemoService_DeleteMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "memos", "reactions", "name"}, ""))
|
||||
pattern_MemoService_CreateMemoShare_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "parent", "shares"}, ""))
|
||||
pattern_MemoService_ListMemoShares_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "parent", "shares"}, ""))
|
||||
pattern_MemoService_DeleteMemoShare_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "memos", "shares", "name"}, ""))
|
||||
pattern_MemoService_GetMemoByShare_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "shares", "share_id"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -1328,4 +1642,8 @@ var (
|
|||
forward_MemoService_ListMemoReactions_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_UpsertMemoReaction_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_DeleteMemoReaction_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_CreateMemoShare_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_ListMemoShares_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_DeleteMemoShare_0 = runtime.ForwardResponseMessage
|
||||
forward_MemoService_GetMemoByShare_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ const (
|
|||
MemoService_ListMemoReactions_FullMethodName = "/memos.api.v1.MemoService/ListMemoReactions"
|
||||
MemoService_UpsertMemoReaction_FullMethodName = "/memos.api.v1.MemoService/UpsertMemoReaction"
|
||||
MemoService_DeleteMemoReaction_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoReaction"
|
||||
MemoService_CreateMemoShare_FullMethodName = "/memos.api.v1.MemoService/CreateMemoShare"
|
||||
MemoService_ListMemoShares_FullMethodName = "/memos.api.v1.MemoService/ListMemoShares"
|
||||
MemoService_DeleteMemoShare_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoShare"
|
||||
MemoService_GetMemoByShare_FullMethodName = "/memos.api.v1.MemoService/GetMemoByShare"
|
||||
)
|
||||
|
||||
// MemoServiceClient is the client API for MemoService service.
|
||||
|
|
@ -68,6 +72,15 @@ type MemoServiceClient interface {
|
|||
UpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
CreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error)
|
||||
// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
ListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error)
|
||||
// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
DeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND if the token is invalid or expired.
|
||||
GetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, error)
|
||||
}
|
||||
|
||||
type memoServiceClient struct {
|
||||
|
|
@ -218,6 +231,46 @@ func (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, in *DeleteMe
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *memoServiceClient) CreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MemoShare)
|
||||
err := c.cc.Invoke(ctx, MemoService_CreateMemoShare_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *memoServiceClient) ListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListMemoSharesResponse)
|
||||
err := c.cc.Invoke(ctx, MemoService_ListMemoShares_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *memoServiceClient) DeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, MemoService_DeleteMemoShare_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *memoServiceClient) GetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Memo)
|
||||
err := c.cc.Invoke(ctx, MemoService_GetMemoByShare_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MemoServiceServer is the server API for MemoService service.
|
||||
// All implementations must embed UnimplementedMemoServiceServer
|
||||
// for forward compatibility.
|
||||
|
|
@ -250,6 +303,15 @@ type MemoServiceServer interface {
|
|||
UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error)
|
||||
// DeleteMemoReaction deletes a reaction for a memo.
|
||||
DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error)
|
||||
// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
CreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error)
|
||||
// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
ListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error)
|
||||
// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
DeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error)
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND if the token is invalid or expired.
|
||||
GetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, error)
|
||||
mustEmbedUnimplementedMemoServiceServer()
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +364,18 @@ func (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *Upser
|
|||
func (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteMemoReaction not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) CreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateMemoShare not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemoShares not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) DeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteMemoShare not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) GetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMemoByShare not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {}
|
||||
func (UnimplementedMemoServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
|
|
@ -575,6 +649,78 @@ func _MemoService_DeleteMemoReaction_Handler(srv interface{}, ctx context.Contex
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _MemoService_CreateMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateMemoShareRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MemoServiceServer).CreateMemoShare(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: MemoService_CreateMemoShare_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MemoServiceServer).CreateMemoShare(ctx, req.(*CreateMemoShareRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _MemoService_ListMemoShares_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListMemoSharesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MemoServiceServer).ListMemoShares(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: MemoService_ListMemoShares_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MemoServiceServer).ListMemoShares(ctx, req.(*ListMemoSharesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _MemoService_DeleteMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeleteMemoShareRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MemoServiceServer).DeleteMemoShare(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: MemoService_DeleteMemoShare_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MemoServiceServer).DeleteMemoShare(ctx, req.(*DeleteMemoShareRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _MemoService_GetMemoByShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetMemoByShareRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MemoServiceServer).GetMemoByShare(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: MemoService_GetMemoByShare_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MemoServiceServer).GetMemoByShare(ctx, req.(*GetMemoByShareRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// MemoService_ServiceDesc is the grpc.ServiceDesc for MemoService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
|
|
@ -638,6 +784,22 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "DeleteMemoReaction",
|
||||
Handler: _MemoService_DeleteMemoReaction_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateMemoShare",
|
||||
Handler: _MemoService_CreateMemoShare_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListMemoShares",
|
||||
Handler: _MemoService_ListMemoShares_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeleteMemoShare",
|
||||
Handler: _MemoService_DeleteMemoShare_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMemoByShare",
|
||||
Handler: _MemoService_GetMemoByShare_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "api/v1/memo_service.proto",
|
||||
|
|
|
|||
|
|
@ -991,6 +991,120 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Status'
|
||||
/api/v1/memos/{memo}/shares:
|
||||
get:
|
||||
tags:
|
||||
- MemoService
|
||||
description: ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.
|
||||
operationId: MemoService_ListMemoShares
|
||||
parameters:
|
||||
- name: memo
|
||||
in: path
|
||||
description: The memo id.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ListMemoSharesResponse'
|
||||
default:
|
||||
description: Default error response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Status'
|
||||
post:
|
||||
tags:
|
||||
- MemoService
|
||||
description: CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.
|
||||
operationId: MemoService_CreateMemoShare
|
||||
parameters:
|
||||
- name: memo
|
||||
in: path
|
||||
description: The memo id.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MemoShare'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MemoShare'
|
||||
default:
|
||||
description: Default error response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Status'
|
||||
/api/v1/memos/{memo}/shares/{share}:
|
||||
delete:
|
||||
tags:
|
||||
- MemoService
|
||||
description: DeleteMemoShare revokes a share link. Requires authentication as the memo creator.
|
||||
operationId: MemoService_DeleteMemoShare
|
||||
parameters:
|
||||
- name: memo
|
||||
in: path
|
||||
description: The memo id.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: share
|
||||
in: path
|
||||
description: The share id.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content: {}
|
||||
default:
|
||||
description: Default error response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Status'
|
||||
/api/v1/shares/{shareId}:
|
||||
get:
|
||||
tags:
|
||||
- MemoService
|
||||
description: |-
|
||||
GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
Returns NOT_FOUND if the token is invalid or expired.
|
||||
operationId: MemoService_GetMemoByShare
|
||||
parameters:
|
||||
- name: shareId
|
||||
in: path
|
||||
description: Required. The share token extracted from the share URL (/s/{share_id}).
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Memo'
|
||||
default:
|
||||
description: Default error response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Status'
|
||||
/api/v1/users:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -2382,6 +2496,14 @@ components:
|
|||
nextPageToken:
|
||||
type: string
|
||||
description: A token for the next page of results.
|
||||
ListMemoSharesResponse:
|
||||
type: object
|
||||
properties:
|
||||
memoShares:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MemoShare'
|
||||
description: The list of share links.
|
||||
ListMemosResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -2619,6 +2741,26 @@ components:
|
|||
type: string
|
||||
description: Output only. The snippet of the memo content. Plain text only.
|
||||
description: Memo reference in relations.
|
||||
MemoShare:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |-
|
||||
The resource name of the share. Format: memos/{memo}/shares/{share}
|
||||
The {share} segment is the opaque token used in the share URL.
|
||||
createTime:
|
||||
readOnly: true
|
||||
type: string
|
||||
description: Output only. When this share link was created.
|
||||
format: date-time
|
||||
expireTime:
|
||||
type: string
|
||||
description: |-
|
||||
Optional. When set, the share link stops working after this time.
|
||||
If unset, the link never expires.
|
||||
format: date-time
|
||||
description: MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.
|
||||
Memo_Property:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ var PublicMethods = map[string]struct{}{
|
|||
"/memos.api.v1.MemoService/GetMemo": {},
|
||||
"/memos.api.v1.MemoService/ListMemos": {},
|
||||
"/memos.api.v1.MemoService/ListMemoComments": {},
|
||||
|
||||
// Memo sharing - share-token endpoints require no authentication
|
||||
"/memos.api.v1.MemoService/GetMemoByShare": {},
|
||||
}
|
||||
|
||||
// IsPublicMethod checks if a procedure path is public (no authentication required).
|
||||
|
|
|
|||
|
|
@ -345,6 +345,38 @@ func (s *ConnectServiceHandler) DeleteMemoReaction(ctx context.Context, req *con
|
|||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *ConnectServiceHandler) CreateMemoShare(ctx context.Context, req *connect.Request[v1pb.CreateMemoShareRequest]) (*connect.Response[v1pb.MemoShare], error) {
|
||||
resp, err := s.APIV1Service.CreateMemoShare(ctx, req.Msg)
|
||||
if err != nil {
|
||||
return nil, convertGRPCError(err)
|
||||
}
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *ConnectServiceHandler) ListMemoShares(ctx context.Context, req *connect.Request[v1pb.ListMemoSharesRequest]) (*connect.Response[v1pb.ListMemoSharesResponse], error) {
|
||||
resp, err := s.APIV1Service.ListMemoShares(ctx, req.Msg)
|
||||
if err != nil {
|
||||
return nil, convertGRPCError(err)
|
||||
}
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *ConnectServiceHandler) DeleteMemoShare(ctx context.Context, req *connect.Request[v1pb.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {
|
||||
resp, err := s.APIV1Service.DeleteMemoShare(ctx, req.Msg)
|
||||
if err != nil {
|
||||
return nil, convertGRPCError(err)
|
||||
}
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *ConnectServiceHandler) GetMemoByShare(ctx context.Context, req *connect.Request[v1pb.GetMemoByShareRequest]) (*connect.Response[v1pb.Memo], error) {
|
||||
resp, err := s.APIV1Service.GetMemoByShare(ctx, req.Msg)
|
||||
if err != nil {
|
||||
return nil, convertGRPCError(err)
|
||||
}
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
// AttachmentService
|
||||
|
||||
func (s *ConnectServiceHandler) CreateAttachment(ctx context.Context, req *connect.Request[v1pb.CreateAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
// CreateMemoShare creates an opaque share link for a memo.
|
||||
// Only the memo's creator or an admin may call this.
|
||||
func (s *APIV1Service) CreateMemoShare(ctx context.Context, request *v1pb.CreateMemoShareRequest) (*v1pb.MemoShare, error) {
|
||||
user, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
||||
}
|
||||
|
||||
memoUID, err := ExtractMemoUIDFromName(request.Parent)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
|
||||
}
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
||||
}
|
||||
if memo == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "memo not found")
|
||||
}
|
||||
if memo.CreatorID != user.ID && !isSuperUser(user) {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
var expiresTs *int64
|
||||
if request.MemoShare != nil && request.MemoShare.ExpireTime != nil {
|
||||
ts := request.MemoShare.ExpireTime.AsTime().Unix()
|
||||
if ts <= time.Now().Unix() {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "expire_time must be in the future")
|
||||
}
|
||||
expiresTs = &ts
|
||||
}
|
||||
|
||||
// Generate a URL-safe token using shortuuid (base57-encoded UUID v4, 22 chars, 122-bit entropy).
|
||||
ms, err := s.Store.CreateMemoShare(ctx, &store.MemoShare{
|
||||
UID: shortuuid.New(),
|
||||
MemoID: memo.ID,
|
||||
CreatorID: user.ID,
|
||||
ExpiresTs: expiresTs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create memo share")
|
||||
}
|
||||
|
||||
return convertMemoShareFromStore(ms, memo.UID), nil
|
||||
}
|
||||
|
||||
// ListMemoShares lists all share links for a memo.
|
||||
// Only the memo's creator or an admin may call this.
|
||||
func (s *APIV1Service) ListMemoShares(ctx context.Context, request *v1pb.ListMemoSharesRequest) (*v1pb.ListMemoSharesResponse, error) {
|
||||
user, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
||||
}
|
||||
|
||||
memoUID, err := ExtractMemoUIDFromName(request.Parent)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
|
||||
}
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
||||
}
|
||||
if memo == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "memo not found")
|
||||
}
|
||||
if memo.CreatorID != user.ID && !isSuperUser(user) {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
shares, err := s.Store.ListMemoShares(ctx, &store.FindMemoShare{MemoID: &memo.ID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list memo shares")
|
||||
}
|
||||
|
||||
response := &v1pb.ListMemoSharesResponse{}
|
||||
for _, ms := range shares {
|
||||
response.MemoShares = append(response.MemoShares, convertMemoShareFromStore(ms, memo.UID))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// DeleteMemoShare revokes a share link.
|
||||
// Only the memo's creator or an admin may call this.
|
||||
func (s *APIV1Service) DeleteMemoShare(ctx context.Context, request *v1pb.DeleteMemoShareRequest) (*emptypb.Empty, error) {
|
||||
user, err := s.fetchCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
||||
}
|
||||
|
||||
// name format: memos/{memoUID}/shares/{shareToken}
|
||||
tokens, err := GetNameParentTokens(request.Name, MemoNamePrefix, MemoShareNamePrefix)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid share name: %v", err)
|
||||
}
|
||||
memoUID, shareToken := tokens[0], tokens[1]
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
||||
}
|
||||
if memo == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "memo not found")
|
||||
}
|
||||
if memo.CreatorID != user.ID && !isSuperUser(user) {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
|
||||
ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo share")
|
||||
}
|
||||
if ms == nil || ms.MemoID != memo.ID {
|
||||
return nil, status.Errorf(codes.NotFound, "memo share not found")
|
||||
}
|
||||
|
||||
if err := s.Store.DeleteMemoShare(ctx, &store.DeleteMemoShare{UID: &shareToken}); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete memo share")
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// GetMemoByShare resolves a share token to its memo. No authentication required.
|
||||
// Returns NOT_FOUND for invalid or expired tokens (no information leakage).
|
||||
func (s *APIV1Service) GetMemoByShare(ctx context.Context, request *v1pb.GetMemoByShareRequest) (*v1pb.Memo, error) {
|
||||
ms, err := s.getActiveMemoShare(ctx, request.ShareId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &ms.MemoID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
||||
}
|
||||
// Treat archived or missing memos the same as an invalid token — no information leakage.
|
||||
if memo == nil || memo.RowStatus == store.Archived {
|
||||
return nil, status.Errorf(codes.NotFound, "not found")
|
||||
}
|
||||
|
||||
reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{
|
||||
ContentID: stringPointer(fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list reactions")
|
||||
}
|
||||
|
||||
attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list attachments")
|
||||
}
|
||||
relations, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to load memo relations")
|
||||
}
|
||||
|
||||
memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations[memo.ID])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert memo")
|
||||
}
|
||||
return memoMessage, nil
|
||||
}
|
||||
|
||||
// isMemoShareExpired returns true if the share has a defined expiry that has already passed.
|
||||
func isMemoShareExpired(ms *store.MemoShare) bool {
|
||||
return ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs
|
||||
}
|
||||
|
||||
func (s *APIV1Service) getActiveMemoShare(ctx context.Context, shareID string) (*store.MemoShare, error) {
|
||||
ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareID})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get memo share")
|
||||
}
|
||||
if ms == nil || isMemoShareExpired(ms) {
|
||||
return nil, status.Errorf(codes.NotFound, "not found")
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
func stringPointer(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// convertMemoShareFromStore converts a store MemoShare to the proto MemoShare message.
|
||||
// name format: memos/{memoUID}/shares/{shareToken}.
|
||||
func convertMemoShareFromStore(ms *store.MemoShare, memoUID string) *v1pb.MemoShare {
|
||||
name := fmt.Sprintf("%s%s/%s%s", MemoNamePrefix, memoUID, MemoShareNamePrefix, ms.UID)
|
||||
pb := &v1pb.MemoShare{
|
||||
Name: name,
|
||||
CreateTime: timestamppb.New(time.Unix(ms.CreatedTs, 0)),
|
||||
}
|
||||
if ms.ExpiresTs != nil {
|
||||
pb.ExpireTime = timestamppb.New(time.Unix(*ms.ExpiresTs, 0))
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ const (
|
|||
InstanceSettingNamePrefix = "instance/settings/"
|
||||
UserNamePrefix = "users/"
|
||||
MemoNamePrefix = "memos/"
|
||||
MemoShareNamePrefix = "shares/"
|
||||
AttachmentNamePrefix = "attachments/"
|
||||
ReactionNamePrefix = "reactions/"
|
||||
InboxNamePrefix = "inboxes/"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
|
||||
)
|
||||
|
||||
func TestDeleteMemoShare_VerifiesShareBelongsToMemo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
userOne, err := ts.CreateRegularUser(ctx, "share-owner-one")
|
||||
require.NoError(t, err)
|
||||
userTwo, err := ts.CreateRegularUser(ctx, "share-owner-two")
|
||||
require.NoError(t, err)
|
||||
|
||||
userOneCtx := ts.CreateUserContext(ctx, userOne.ID)
|
||||
userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)
|
||||
|
||||
memoOne, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
|
||||
Memo: &apiv1.Memo{
|
||||
Content: "memo one",
|
||||
Visibility: apiv1.Visibility_PRIVATE,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
memoTwo, err := ts.Service.CreateMemo(userTwoCtx, &apiv1.CreateMemoRequest{
|
||||
Memo: &apiv1.Memo{
|
||||
Content: "memo two",
|
||||
Visibility: apiv1.Visibility_PRIVATE,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
share, err := ts.Service.CreateMemoShare(userTwoCtx, &apiv1.CreateMemoShareRequest{
|
||||
Parent: memoTwo.Name,
|
||||
MemoShare: &apiv1.MemoShare{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:]
|
||||
forgedName := memoOne.Name + "/shares/" + shareToken
|
||||
|
||||
_, err = ts.Service.DeleteMemoShare(userOneCtx, &apiv1.DeleteMemoShareRequest{
|
||||
Name: forgedName,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, codes.NotFound, status.Code(err))
|
||||
|
||||
sharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{
|
||||
ShareId: shareToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, memoTwo.Name, sharedMemo.Name)
|
||||
}
|
||||
|
||||
func TestGetMemoByShare_IncludesReactions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
user, err := ts.CreateRegularUser(ctx, "share-reactions")
|
||||
require.NoError(t, err)
|
||||
userCtx := ts.CreateUserContext(ctx, user.ID)
|
||||
|
||||
memo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
|
||||
Memo: &apiv1.Memo{
|
||||
Content: "memo with reactions",
|
||||
Visibility: apiv1.Visibility_PRIVATE,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
reaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{
|
||||
Name: memo.Name,
|
||||
Reaction: &apiv1.Reaction{
|
||||
ContentId: memo.Name,
|
||||
ReactionType: "👍",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reaction)
|
||||
|
||||
share, err := ts.Service.CreateMemoShare(userCtx, &apiv1.CreateMemoShareRequest{
|
||||
Parent: memo.Name,
|
||||
MemoShare: &apiv1.MemoShare{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:]
|
||||
sharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{
|
||||
ShareId: shareToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sharedMemo.Reactions, 1)
|
||||
require.Equal(t, "👍", sharedMemo.Reactions[0].ReactionType)
|
||||
require.Equal(t, memo.Name, sharedMemo.Reactions[0].ContentId)
|
||||
}
|
||||
|
|
@ -497,6 +497,16 @@ func (s *FileServerService) checkAttachmentPermission(ctx context.Context, c *ec
|
|||
return nil
|
||||
}
|
||||
|
||||
// Check share token fallback: allow access if request carries a valid, non-expired share token
|
||||
// that was issued for this specific memo. This covers attachment requests made from the shared
|
||||
// memo page for private or protected memos.
|
||||
if shareToken := (*c).QueryParam("share_token"); shareToken != "" {
|
||||
ms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken})
|
||||
if err == nil && ms != nil && !isMemoShareExpired(ms) && ms.MemoID == memo.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
user, err := s.getCurrentUser(ctx, c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get current user").Wrap(err)
|
||||
|
|
@ -585,3 +595,8 @@ func setMediaHeaders(c *echo.Context, contentType, originalType string) {
|
|||
h.Set("Color-Gamut", "srgb, p3, rec2020")
|
||||
}
|
||||
}
|
||||
|
||||
// isMemoShareExpired returns true if the share has a defined expiry that has already passed.
|
||||
func isMemoShareExpired(ms *store.MemoShare) bool {
|
||||
return ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
package fileserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/usememos/memos/internal/profile"
|
||||
"github.com/usememos/memos/plugin/markdown"
|
||||
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
|
||||
"github.com/usememos/memos/server/auth"
|
||||
apiv1service "github.com/usememos/memos/server/router/api/v1"
|
||||
"github.com/usememos/memos/store"
|
||||
teststore "github.com/usememos/memos/store/test"
|
||||
)
|
||||
|
||||
func TestServeAttachmentFile_ShareTokenAllowsDirectMemoAttachment(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t)
|
||||
defer cleanup()
|
||||
|
||||
creator, err := svc.Store.CreateUser(ctx, &store.User{
|
||||
Username: "share-parent-owner",
|
||||
Role: store.RoleUser,
|
||||
Email: "share-parent-owner@example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
creatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID)
|
||||
|
||||
attachment, err := svc.CreateAttachment(creatorCtx, &apiv1.CreateAttachmentRequest{
|
||||
Attachment: &apiv1.Attachment{
|
||||
Filename: "memo.txt",
|
||||
Type: "text/plain",
|
||||
Content: []byte("memo attachment"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
parentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{
|
||||
Memo: &apiv1.Memo{
|
||||
Content: "shared parent",
|
||||
Visibility: apiv1.Visibility_PROTECTED,
|
||||
Attachments: []*apiv1.Attachment{
|
||||
{Name: attachment.Name},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
share, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{
|
||||
Parent: parentMemo.Name,
|
||||
MemoShare: &apiv1.MemoShare{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:]
|
||||
|
||||
e := echo.New()
|
||||
fs.RegisterRoutes(e)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/file/%s/%s?share_token=%s", attachment.Name, attachment.Filename, shareToken), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "memo attachment", rec.Body.String())
|
||||
}
|
||||
|
||||
func TestServeAttachmentFile_ShareTokenRejectsCommentAttachment(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t)
|
||||
defer cleanup()
|
||||
|
||||
creator, err := svc.Store.CreateUser(ctx, &store.User{
|
||||
Username: "private-parent-owner",
|
||||
Role: store.RoleUser,
|
||||
Email: "private-parent-owner@example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
creatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID)
|
||||
commenter, err := svc.Store.CreateUser(ctx, &store.User{
|
||||
Username: "share-commenter",
|
||||
Role: store.RoleUser,
|
||||
Email: "share-commenter@example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
commenterCtx := context.WithValue(ctx, auth.UserIDContextKey, commenter.ID)
|
||||
|
||||
parentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{
|
||||
Memo: &apiv1.Memo{
|
||||
Content: "shared parent",
|
||||
Visibility: apiv1.Visibility_PROTECTED,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
commentAttachment, err := svc.CreateAttachment(commenterCtx, &apiv1.CreateAttachmentRequest{
|
||||
Attachment: &apiv1.Attachment{
|
||||
Filename: "comment.txt",
|
||||
Type: "text/plain",
|
||||
Content: []byte("comment attachment"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{
|
||||
Name: parentMemo.Name,
|
||||
Comment: &apiv1.Memo{
|
||||
Content: "comment with attachment",
|
||||
Visibility: apiv1.Visibility_PROTECTED,
|
||||
Attachments: []*apiv1.Attachment{
|
||||
{Name: commentAttachment.Name},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
share, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{
|
||||
Parent: parentMemo.Name,
|
||||
MemoShare: &apiv1.MemoShare{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
shareToken := share.Name[strings.LastIndex(share.Name, "/")+1:]
|
||||
|
||||
e := echo.New()
|
||||
fs.RegisterRoutes(e)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/file/%s/%s?share_token=%s", commentAttachment.Name, commentAttachment.Filename, shareToken), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
|
||||
func newShareAttachmentTestServices(ctx context.Context, t *testing.T) (*apiv1service.APIV1Service, *FileServerService, *store.Store, func()) {
|
||||
t.Helper()
|
||||
|
||||
testStore := teststore.NewTestingStore(ctx, t)
|
||||
testProfile := &profile.Profile{
|
||||
Demo: true,
|
||||
Version: "test-1.0.0",
|
||||
InstanceURL: "http://localhost:8080",
|
||||
Driver: "sqlite",
|
||||
DSN: ":memory:",
|
||||
Data: t.TempDir(),
|
||||
}
|
||||
secret := "test-secret"
|
||||
markdownService := markdown.NewService(markdown.WithTagExtension())
|
||||
apiService := &apiv1service.APIV1Service{
|
||||
Secret: secret,
|
||||
Profile: testProfile,
|
||||
Store: testStore,
|
||||
MarkdownService: markdownService,
|
||||
SSEHub: apiv1service.NewSSEHub(),
|
||||
}
|
||||
fileService := NewFileServerService(testProfile, testStore, secret)
|
||||
|
||||
return apiService, fileService, testStore, func() {
|
||||
testStore.Close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {
|
||||
fields := []string{"`uid`", "`memo_id`", "`creator_id`"}
|
||||
placeholders := []string{"?", "?", "?"}
|
||||
args := []any{create.UID, create.MemoID, create.CreatorID}
|
||||
|
||||
if create.ExpiresTs != nil {
|
||||
fields = append(fields, "`expires_ts`")
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, *create.ExpiresTs)
|
||||
}
|
||||
|
||||
stmt := "INSERT INTO `memo_share` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ")"
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := int32(rawID)
|
||||
ms, err := d.GetMemoShare(ctx, &store.FindMemoShare{ID: &id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ms == nil {
|
||||
return nil, errors.Errorf("failed to create memo share")
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.UID != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *find.UID)
|
||||
}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
uid,
|
||||
memo_id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
expires_ts
|
||||
FROM memo_share
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY id ASC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.MemoShare{}
|
||||
for rows.Next() {
|
||||
ms := &store.MemoShare{}
|
||||
if err := rows.Scan(
|
||||
&ms.ID,
|
||||
&ms.UID,
|
||||
&ms.MemoID,
|
||||
&ms.CreatorID,
|
||||
&ms.CreatedTs,
|
||||
&ms.ExpiresTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, ms)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {
|
||||
list, err := d.ListMemoShares(ctx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return list[0], nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if delete.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
|
||||
}
|
||||
if delete.UID != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
||||
}
|
||||
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {
|
||||
fields := []string{"uid", "memo_id", "creator_id"}
|
||||
args := []any{create.UID, create.MemoID, create.CreatorID}
|
||||
|
||||
if create.ExpiresTs != nil {
|
||||
fields = append(fields, "expires_ts")
|
||||
args = append(args, *create.ExpiresTs)
|
||||
}
|
||||
|
||||
stmt := "INSERT INTO memo_share (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID)
|
||||
}
|
||||
if find.UID != nil {
|
||||
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *find.UID)
|
||||
}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *find.MemoID)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
uid,
|
||||
memo_id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
expires_ts
|
||||
FROM memo_share
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY id ASC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.MemoShare{}
|
||||
for rows.Next() {
|
||||
ms := &store.MemoShare{}
|
||||
if err := rows.Scan(
|
||||
&ms.ID,
|
||||
&ms.UID,
|
||||
&ms.MemoID,
|
||||
&ms.CreatorID,
|
||||
&ms.CreatedTs,
|
||||
&ms.ExpiresTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, ms)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID)
|
||||
}
|
||||
if find.UID != nil {
|
||||
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *find.UID)
|
||||
}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *find.MemoID)
|
||||
}
|
||||
|
||||
ms := &store.MemoShare{}
|
||||
if err := d.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
uid,
|
||||
memo_id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
expires_ts
|
||||
FROM memo_share
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
LIMIT 1`,
|
||||
args...,
|
||||
).Scan(
|
||||
&ms.ID,
|
||||
&ms.UID,
|
||||
&ms.MemoID,
|
||||
&ms.CreatorID,
|
||||
&ms.CreatedTs,
|
||||
&ms.ExpiresTs,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if delete.ID != nil {
|
||||
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *delete.ID)
|
||||
}
|
||||
if delete.UID != nil {
|
||||
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *delete.UID)
|
||||
}
|
||||
_, err := d.db.ExecContext(ctx, "DELETE FROM memo_share WHERE "+strings.Join(where, " AND "), args...)
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {
|
||||
fields := []string{"`uid`", "`memo_id`", "`creator_id`"}
|
||||
placeholders := []string{"?", "?", "?"}
|
||||
args := []any{create.UID, create.MemoID, create.CreatorID}
|
||||
|
||||
if create.ExpiresTs != nil {
|
||||
fields = append(fields, "`expires_ts`")
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, *create.ExpiresTs)
|
||||
}
|
||||
|
||||
stmt := "INSERT INTO `memo_share` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") RETURNING `id`, `created_ts`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.UID != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *find.UID)
|
||||
}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
uid,
|
||||
memo_id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
expires_ts
|
||||
FROM memo_share
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY id ASC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.MemoShare{}
|
||||
for rows.Next() {
|
||||
ms := &store.MemoShare{}
|
||||
if err := rows.Scan(
|
||||
&ms.ID,
|
||||
&ms.UID,
|
||||
&ms.MemoID,
|
||||
&ms.CreatorID,
|
||||
&ms.CreatedTs,
|
||||
&ms.ExpiresTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, ms)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.UID != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *find.UID)
|
||||
}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID)
|
||||
}
|
||||
|
||||
ms := &store.MemoShare{}
|
||||
if err := d.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
uid,
|
||||
memo_id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
expires_ts
|
||||
FROM memo_share
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
LIMIT 1`,
|
||||
args...,
|
||||
).Scan(
|
||||
&ms.ID,
|
||||
&ms.UID,
|
||||
&ms.MemoID,
|
||||
&ms.CreatorID,
|
||||
&ms.CreatedTs,
|
||||
&ms.ExpiresTs,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if delete.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
|
||||
}
|
||||
if delete.UID != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
|
||||
}
|
||||
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
|
||||
return err
|
||||
}
|
||||
|
|
@ -63,4 +63,10 @@ type Driver interface {
|
|||
ListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error)
|
||||
GetReaction(ctx context.Context, find *FindReaction) (*Reaction, error)
|
||||
DeleteReaction(ctx context.Context, delete *DeleteReaction) error
|
||||
|
||||
// MemoShare model related methods.
|
||||
CreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error)
|
||||
ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error)
|
||||
GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error)
|
||||
DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package store
|
||||
|
||||
import "context"
|
||||
|
||||
// MemoShare is an access grant that permits read-only access to a memo via a bearer token.
|
||||
type MemoShare struct {
|
||||
ID int32
|
||||
UID string
|
||||
MemoID int32
|
||||
CreatorID int32
|
||||
CreatedTs int64
|
||||
ExpiresTs *int64 // nil means the share never expires
|
||||
}
|
||||
|
||||
// FindMemoShare is used to filter memo shares in list/get queries.
|
||||
type FindMemoShare struct {
|
||||
ID *int32
|
||||
UID *string
|
||||
MemoID *int32
|
||||
}
|
||||
|
||||
// DeleteMemoShare identifies a share grant to remove.
|
||||
type DeleteMemoShare struct {
|
||||
ID *int32
|
||||
UID *string
|
||||
}
|
||||
|
||||
// CreateMemoShare creates a new share grant.
|
||||
func (s *Store) CreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error) {
|
||||
return s.driver.CreateMemoShare(ctx, create)
|
||||
}
|
||||
|
||||
// ListMemoShares returns all share grants matching the filter.
|
||||
func (s *Store) ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error) {
|
||||
return s.driver.ListMemoShares(ctx, find)
|
||||
}
|
||||
|
||||
// GetMemoShare returns the first share grant matching the filter, or nil if none found.
|
||||
func (s *Store) GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error) {
|
||||
return s.driver.GetMemoShare(ctx, find)
|
||||
}
|
||||
|
||||
// DeleteMemoShare removes a share grant.
|
||||
func (s *Store) DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error {
|
||||
return s.driver.DeleteMemoShare(ctx, delete)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-- memo_share stores per-memo share grants (one row per share link).
|
||||
-- uid is the opaque bearer token included in the share URL.
|
||||
-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.
|
||||
CREATE TABLE memo_share (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uid VARCHAR(255) NOT NULL UNIQUE,
|
||||
memo_id INT NOT NULL,
|
||||
creator_id INT NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
|
||||
expires_ts BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
|
||||
|
|
@ -96,3 +96,16 @@ CREATE TABLE `reaction` (
|
|||
`reaction_type` VARCHAR(256) NOT NULL,
|
||||
UNIQUE(`creator_id`,`content_id`,`reaction_type`)
|
||||
);
|
||||
|
||||
-- memo_share
|
||||
CREATE TABLE `memo_share` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`uid` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`memo_id` INT NOT NULL,
|
||||
`creator_id` INT NOT NULL,
|
||||
`created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
|
||||
`expires_ts` BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (`memo_id`) REFERENCES `memo`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
-- memo_share stores per-memo share grants (one row per share link).
|
||||
-- uid is the opaque bearer token included in the share URL.
|
||||
-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.
|
||||
CREATE TABLE memo_share (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uid TEXT NOT NULL UNIQUE,
|
||||
memo_id INTEGER NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||
expires_ts BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
|
||||
|
|
@ -96,3 +96,16 @@ CREATE TABLE reaction (
|
|||
reaction_type TEXT NOT NULL,
|
||||
UNIQUE(creator_id, content_id, reaction_type)
|
||||
);
|
||||
|
||||
-- memo_share
|
||||
CREATE TABLE memo_share (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uid TEXT NOT NULL UNIQUE,
|
||||
memo_id INTEGER NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||
expires_ts BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
-- memo_share stores per-memo share grants (one row per share link).
|
||||
-- uid is the opaque bearer token included in the share URL.
|
||||
-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.
|
||||
CREATE TABLE memo_share (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uid TEXT NOT NULL UNIQUE,
|
||||
memo_id INTEGER NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
expires_ts BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
|
||||
|
|
@ -97,3 +97,16 @@ CREATE TABLE reaction (
|
|||
reaction_type TEXT NOT NULL,
|
||||
UNIQUE(creator_id, content_id, reaction_type)
|
||||
);
|
||||
|
||||
-- memo_share
|
||||
CREATE TABLE memo_share (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uid TEXT NOT NULL UNIQUE,
|
||||
memo_id INTEGER NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
expires_ts BIGINT DEFAULT NULL,
|
||||
FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon } from "lucide-react";
|
||||
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, Share2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import MemoSharePanel from "@/components/MemoSharePanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Memo, Memo_PropertySchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { isSuperUser } from "@/utils/user";
|
||||
import MemoRelationForceGraph from "../MemoRelationForceGraph";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -19,12 +24,25 @@ const SectionLabel = ({ children }: { children: React.ReactNode }) => (
|
|||
|
||||
const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
|
||||
const t = useTranslate();
|
||||
const currentUser = useCurrentUser();
|
||||
const [sharePanelOpen, setSharePanelOpen] = useState(false);
|
||||
const property = create(Memo_PropertySchema, memo.property || {});
|
||||
const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode;
|
||||
const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE);
|
||||
const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser));
|
||||
|
||||
return (
|
||||
<aside className={cn("relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5", className)}>
|
||||
{canManageShares && (
|
||||
<div className="w-full space-y-2">
|
||||
<SectionLabel>{t("memo-share.section-label")}</SectionLabel>
|
||||
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
|
||||
<Share2Icon className="w-4 h-4" />
|
||||
{t("memo-share.open-panel")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasReferenceRelations && (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
|
@ -94,6 +112,8 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
import { ConnectError } from "@connectrpc/connect";
|
||||
import { CheckIcon, CopyIcon, LinkIcon, Loader2Icon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { getShareUrl, useCreateMemoShare, useDeleteMemoShare, useMemoShares } from "@/hooks/useMemoShareQueries";
|
||||
import type { MemoShare } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
type ExpiryOption = "never" | "1d" | "7d" | "30d";
|
||||
|
||||
function getExpireDate(option: ExpiryOption): Date | undefined {
|
||||
if (option === "never") return undefined;
|
||||
const d = new Date();
|
||||
if (option === "1d") d.setDate(d.getDate() + 1);
|
||||
else if (option === "7d") d.setDate(d.getDate() + 7);
|
||||
else if (option === "30d") d.setDate(d.getDate() + 30);
|
||||
return d;
|
||||
}
|
||||
|
||||
function formatExpiry(share: MemoShare, t: ReturnType<typeof useTranslate>): string {
|
||||
if (!share.expireTime) return t("memo-share.never-expires");
|
||||
const d = timestampDate(share.expireTime);
|
||||
return t("memo-share.expires-on", { date: d.toLocaleDateString() });
|
||||
}
|
||||
|
||||
interface ShareLinkRowProps {
|
||||
share: MemoShare;
|
||||
memoName: string;
|
||||
}
|
||||
|
||||
function ShareLinkRow({ share, memoName }: ShareLinkRowProps) {
|
||||
const t = useTranslate();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const deleteShare = useDeleteMemoShare();
|
||||
const url = getShareUrl(share);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(url);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleRevoke = async () => {
|
||||
try {
|
||||
await deleteShare.mutateAsync({ name: share.name, memoName });
|
||||
toast.success(t("memo-share.revoked"));
|
||||
} catch (e) {
|
||||
toast.error((e as ConnectError).message || t("memo-share.revoke-failed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 rounded-md border border-border p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate font-mono text-xs text-muted-foreground">{url}</span>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy} title={t("memo-share.copy")}>
|
||||
{copied ? <CheckIcon className="h-3.5 w-3.5 text-green-500" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||
onClick={handleRevoke}
|
||||
disabled={deleteShare.isPending}
|
||||
title={t("memo-share.revoke")}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{formatExpiry(share, t)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MemoSharePanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
memoName: string;
|
||||
}
|
||||
|
||||
const MemoSharePanel = ({ open, onClose, memoName }: MemoSharePanelProps) => {
|
||||
const t = useTranslate();
|
||||
const [expiry, setExpiry] = useState<ExpiryOption>("never");
|
||||
const { data: shares = [], isLoading } = useMemoShares(memoName, { enabled: open });
|
||||
const createShare = useCreateMemoShare();
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
await createShare.mutateAsync({ memoName, expireTime: getExpireDate(expiry) });
|
||||
} catch (e) {
|
||||
toast.error((e as ConnectError).message || t("memo-share.create-failed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
{t("memo-share.title")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{/* Active links */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t("memo-share.active-links")}</p>
|
||||
{isLoading ? (
|
||||
<Loader2Icon className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : shares.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t("memo-share.no-links")}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{shares.map((share) => (
|
||||
<ShareLinkRow key={share.name} share={share} memoName={memoName} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create new link */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={expiry} onValueChange={(v) => setExpiry(v as ExpiryOption)}>
|
||||
<SelectTrigger className="w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="never">{t("memo-share.expiry-never")}</SelectItem>
|
||||
<SelectItem value="1d">{t("memo-share.expiry-1-day")}</SelectItem>
|
||||
<SelectItem value="7d">{t("memo-share.expiry-7-days")}</SelectItem>
|
||||
<SelectItem value="30d">{t("memo-share.expiry-30-days")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleCreate} disabled={createShare.isPending} className="flex-1">
|
||||
{createShare.isPending ? (
|
||||
<>
|
||||
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("memo-share.creating")}
|
||||
</>
|
||||
) : (
|
||||
t("memo-share.create-link")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoSharePanel;
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { memoServiceClient } from "@/connect";
|
||||
import type { MemoShare } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import {
|
||||
CreateMemoShareRequestSchema,
|
||||
DeleteMemoShareRequestSchema,
|
||||
GetMemoByShareRequestSchema,
|
||||
ListMemoSharesRequestSchema,
|
||||
MemoShareSchema,
|
||||
} from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
// Query keys factory for share-related cache management.
|
||||
export const memoShareKeys = {
|
||||
all: ["memo-shares"] as const,
|
||||
list: (memoName: string) => [...memoShareKeys.all, "list", memoName] as const,
|
||||
byShare: (shareId: string) => [...memoShareKeys.all, "by-share", shareId] as const,
|
||||
};
|
||||
|
||||
/** Lists all active share links for a memo (creator-only). */
|
||||
export function useMemoShares(memoName: string, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: memoShareKeys.list(memoName),
|
||||
queryFn: async () => {
|
||||
const response = await memoServiceClient.listMemoShares(create(ListMemoSharesRequestSchema, { parent: memoName }));
|
||||
return response.memoShares;
|
||||
},
|
||||
enabled: options?.enabled ?? !!memoName,
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates a new share link for a memo. */
|
||||
export function useCreateMemoShare() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ memoName, expireTime }: { memoName: string; expireTime?: Date }) => {
|
||||
const memoShare = create(MemoShareSchema, {
|
||||
expireTime: expireTime ? timestampFromDate(expireTime) : undefined,
|
||||
});
|
||||
const response = await memoServiceClient.createMemoShare(create(CreateMemoShareRequestSchema, { parent: memoName, memoShare }));
|
||||
return response;
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: memoShareKeys.list(variables.memoName) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Revokes (deletes) a share link. */
|
||||
export function useDeleteMemoShare() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ name, memoName }: { name: string; memoName: string }) => {
|
||||
await memoServiceClient.deleteMemoShare(create(DeleteMemoShareRequestSchema, { name }));
|
||||
return memoName;
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: memoShareKeys.list(variables.memoName) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves a share token to its memo. Used by the public SharedMemo page. */
|
||||
export function useSharedMemo(shareId: string, options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: memoShareKeys.byShare(shareId),
|
||||
queryFn: async () => {
|
||||
const memo = await memoServiceClient.getMemoByShare(create(GetMemoByShareRequestSchema, { shareId }));
|
||||
return memo;
|
||||
},
|
||||
enabled: options?.enabled ?? !!shareId,
|
||||
retry: false, // Don't retry NOT_FOUND — the link is invalid or expired
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the share URL for a MemoShare resource.
|
||||
* The token is the last path segment of the share name (memos/{uid}/shares/{token}).
|
||||
*/
|
||||
export function getShareUrl(share: MemoShare): string {
|
||||
const token = share.name.split("/").pop() ?? "";
|
||||
return `${window.location.origin}/memos/shares/${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token portion of a MemoShare resource name.
|
||||
* Format: memos/{memo}/shares/{token}
|
||||
*/
|
||||
export function getShareToken(share: MemoShare): string {
|
||||
return share.name.split("/").pop() ?? "";
|
||||
}
|
||||
|
|
@ -51,11 +51,17 @@ const LazyImportPlugin: BackendModule = {
|
|||
read: function (language, _, callback) {
|
||||
const matchedLanguage = findNearestMatchedLanguage(language);
|
||||
import(`./locales/${matchedLanguage}.json`)
|
||||
.then((translation: Record<string, unknown>) => {
|
||||
callback(null, translation);
|
||||
.then((translationModule: Record<string, unknown>) => {
|
||||
callback(null, (translationModule.default as Record<string, unknown>) ?? translationModule);
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to English.
|
||||
import("./locales/en.json")
|
||||
.then((translationModule: Record<string, unknown>) => {
|
||||
callback(null, (translationModule.default as Record<string, unknown>) ?? translationModule);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
callback(error as Error, false);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -67,6 +73,9 @@ i18n
|
|||
detection: {
|
||||
order: ["navigator"],
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
fallbackLng: {
|
||||
...fallbacks,
|
||||
...{ default: ["en"] },
|
||||
|
|
|
|||
|
|
@ -468,5 +468,30 @@
|
|||
"connected": "Live updates active",
|
||||
"connecting": "Connecting to live updates...",
|
||||
"disconnected": "Live updates unavailable"
|
||||
},
|
||||
"memo-share": {
|
||||
"share": "Share",
|
||||
"section-label": "Sharing",
|
||||
"open-panel": "Manage share links",
|
||||
"title": "Share this memo",
|
||||
"active-links": "Active share links",
|
||||
"no-links": "No share links yet. Create one below.",
|
||||
"create-link": "Create new link",
|
||||
"copy": "Copy link",
|
||||
"copied": "Copied!",
|
||||
"revoke": "Revoke",
|
||||
"expiry-label": "Expires",
|
||||
"expiry-never": "Never",
|
||||
"expiry-1-day": "1 day",
|
||||
"expiry-7-days": "7 days",
|
||||
"expiry-30-days": "30 days",
|
||||
"never-expires": "Never expires",
|
||||
"expires-on": "Expires {{date}}",
|
||||
"creating": "Creating…",
|
||||
"revoked": "Share link revoked",
|
||||
"revoke-failed": "Failed to revoke link",
|
||||
"create-failed": "Failed to create share link",
|
||||
"invalid-link": "This link is invalid or has expired.",
|
||||
"shared-by": "Shared by {{creator}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
import { Code, ConnectError } from "@connectrpc/connect";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import MemoContent from "@/components/MemoContent";
|
||||
import AttachmentList from "@/components/MemoView/components/metadata/AttachmentList";
|
||||
import UserAvatar from "@/components/UserAvatar";
|
||||
import { useSharedMemo } from "@/hooks/useMemoShareQueries";
|
||||
import { useUser } from "@/hooks/useUserQueries";
|
||||
import i18n from "@/i18n";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
function withShareAttachmentLinks(attachments: Attachment[], token: string): Attachment[] {
|
||||
return attachments.map((a) => {
|
||||
if (a.externalLink) return a;
|
||||
return { ...a, externalLink: `${window.location.origin}/file/${a.name}/${a.filename}?share_token=${encodeURIComponent(token)}` };
|
||||
});
|
||||
}
|
||||
|
||||
const SharedMemo = () => {
|
||||
const t = useTranslate();
|
||||
const { token = "" } = useParams<{ token: string }>();
|
||||
|
||||
const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token });
|
||||
const { data: creator } = useUser(memo?.creator ?? "", { enabled: !!memo?.creator });
|
||||
|
||||
const isNotFound = error instanceof ConnectError && (error.code === Code.NotFound || error.code === Code.Unauthenticated);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNotFound || (!isLoading && !memo)) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-3 text-center">
|
||||
<AlertCircleIcon className="h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">{t("memo-share.invalid-link")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !memo) return null;
|
||||
|
||||
const displayDate = (memo.displayTime as Timestamp | undefined)
|
||||
? timestampDate(memo.displayTime as Timestamp)?.toLocaleString(i18n.language)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full min-w-80 max-w-2xl px-4 py-8">
|
||||
{/* Creator + date above the card */}
|
||||
<div className="mb-3 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<UserAvatar className="shrink-0" avatarUrl={creator?.avatarUrl} />
|
||||
<span className="text-sm text-muted-foreground">{creator?.displayName || creator?.username || memo.creator}</span>
|
||||
</div>
|
||||
{displayDate && <span className="text-xs text-muted-foreground">{displayDate}</span>}
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col items-start gap-2 rounded-lg border border-border bg-card px-4 py-3 text-card-foreground">
|
||||
<MemoContent content={memo.content} />
|
||||
{memo.attachments.length > 0 && <AttachmentList attachments={withShareAttachmentLinks(memo.attachments, token)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedMemo;
|
||||
|
|
@ -33,6 +33,7 @@ const NotFound = lazyWithReload(() => import("@/pages/NotFound"));
|
|||
const PermissionDenied = lazyWithReload(() => import("@/pages/PermissionDenied"));
|
||||
const Attachments = lazyWithReload(() => import("@/pages/Attachments"));
|
||||
const Setting = lazyWithReload(() => import("@/pages/Setting"));
|
||||
const SharedMemo = lazyWithReload(() => import("@/pages/SharedMemo"));
|
||||
const SignIn = lazyWithReload(() => import("@/pages/SignIn"));
|
||||
const SignUp = lazyWithReload(() => import("@/pages/SignUp"));
|
||||
const UserProfile = lazyWithReload(() => import("@/pages/UserProfile"));
|
||||
|
|
@ -80,6 +81,9 @@ const router = createBrowserRouter([
|
|||
{ path: "*", element: <NotFound /> },
|
||||
],
|
||||
},
|
||||
// Public share-link viewer — outside RootLayout to bypass auth-gating
|
||||
// (including when disallowPublicVisibility is enabled on the instance)
|
||||
{ path: "memos/shares/:token", element: <SharedMemo /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const ROUTES = {
|
|||
SETTING: "/setting",
|
||||
EXPLORE: "/explore",
|
||||
AUTH: "/auth",
|
||||
SHARED_MEMO: "/memos/shares",
|
||||
} as const;
|
||||
|
||||
export type RouteKey = keyof typeof ROUTES;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,6 +4,7 @@ import { ROUTES } from "@/router/routes";
|
|||
const PUBLIC_ROUTES = [
|
||||
ROUTES.AUTH, // Authentication pages
|
||||
ROUTES.EXPLORE, // Explore page
|
||||
ROUTES.SHARED_MEMO + "/", // Shared memo pages (share-link viewer)
|
||||
"/u/", // User profile pages (dynamic)
|
||||
"/memos/", // Individual memo detail pages (dynamic)
|
||||
] as const;
|
||||
|
|
|
|||
Loading…
Reference in New Issue