mirror of
				https://github.com/usememos/memos.git
				synced 2025-10-31 08:46:39 +08:00 
			
		
		
		
	refactor: inbox service
This commit is contained in:
		
							parent
							
								
									a4920d464b
								
							
						
					
					
						commit
						91c2a4cef9
					
				
					 10 changed files with 1176 additions and 152 deletions
				
			
		|  | @ -4,6 +4,8 @@ package memos.api.v1; | |||
| 
 | ||||
| import "google/api/annotations.proto"; | ||||
| import "google/api/client.proto"; | ||||
| import "google/api/field_behavior.proto"; | ||||
| import "google/api/resource.proto"; | ||||
| import "google/protobuf/empty.proto"; | ||||
| import "google/protobuf/field_mask.proto"; | ||||
| import "google/protobuf/timestamp.proto"; | ||||
|  | @ -13,7 +15,8 @@ option go_package = "gen/api/v1"; | |||
| service InboxService { | ||||
|   // ListInboxes lists inboxes for a user. | ||||
|   rpc ListInboxes(ListInboxesRequest) returns (ListInboxesResponse) { | ||||
|     option (google.api.http) = {get: "/api/v1/inboxes"}; | ||||
|     option (google.api.http) = {get: "/api/v1/{parent=users/*}/inboxes"}; | ||||
|     option (google.api.method_signature) = "parent"; | ||||
|   } | ||||
|   // UpdateInbox updates an inbox. | ||||
|   rpc UpdateInbox(UpdateInboxRequest) returns (Inbox) { | ||||
|  | @ -31,59 +34,116 @@ service InboxService { | |||
| } | ||||
| 
 | ||||
| message Inbox { | ||||
|   // The name of the inbox. | ||||
|   // Format: inboxes/{id}, id is the system generated auto-incremented id. | ||||
|   string name = 1; | ||||
|   // Format: users/{user} | ||||
|   string sender = 2; | ||||
|   // Format: users/{user} | ||||
|   string receiver = 3; | ||||
|   option (google.api.resource) = { | ||||
|     type: "memos.api.v1/Inbox" | ||||
|     pattern: "inboxes/{inbox}" | ||||
|     name_field: "name" | ||||
|     singular: "inbox" | ||||
|     plural: "inboxes" | ||||
|   }; | ||||
| 
 | ||||
|   // The resource name of the inbox. | ||||
|   // Format: inboxes/{inbox} | ||||
|   string name = 1 [(google.api.field_behavior) = IDENTIFIER]; | ||||
| 
 | ||||
|   // The sender of the inbox notification. | ||||
|   // Format: users/{user} | ||||
|   string sender = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; | ||||
| 
 | ||||
|   // The receiver of the inbox notification. | ||||
|   // Format: users/{user} | ||||
|   string receiver = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; | ||||
| 
 | ||||
|   // The status of the inbox notification. | ||||
|   Status status = 4 [(google.api.field_behavior) = OPTIONAL]; | ||||
| 
 | ||||
|   // Output only. The creation timestamp. | ||||
|   google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; | ||||
| 
 | ||||
|   // The type of the inbox notification. | ||||
|   Type type = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; | ||||
| 
 | ||||
|   // Optional. The activity ID associated with this inbox notification. | ||||
|   optional int32 activity_id = 7 [(google.api.field_behavior) = OPTIONAL]; | ||||
| 
 | ||||
|   // Status enumeration for inbox notifications. | ||||
|   enum Status { | ||||
|     // Unspecified status. | ||||
|     STATUS_UNSPECIFIED = 0; | ||||
|     // The notification is unread. | ||||
|     UNREAD = 1; | ||||
|     // The notification is archived. | ||||
|     ARCHIVED = 2; | ||||
|   } | ||||
|   Status status = 4; | ||||
| 
 | ||||
|   google.protobuf.Timestamp create_time = 5; | ||||
| 
 | ||||
|   // Type enumeration for inbox notifications. | ||||
|   enum Type { | ||||
|     // Unspecified type. | ||||
|     TYPE_UNSPECIFIED = 0; | ||||
|     // Memo comment notification. | ||||
|     MEMO_COMMENT = 1; | ||||
|     // Version update notification. | ||||
|     VERSION_UPDATE = 2; | ||||
|   } | ||||
|   Type type = 6; | ||||
| 
 | ||||
|   optional int32 activity_id = 7; | ||||
| } | ||||
| 
 | ||||
| message ListInboxesRequest { | ||||
|   // Required. The parent resource whose inboxes will be listed. | ||||
|   // Format: users/{user} | ||||
|   string user = 1; | ||||
|   string parent = 1 [ | ||||
|     (google.api.field_behavior) = REQUIRED, | ||||
|     (google.api.resource_reference) = {type: "memos.api.v1/User"} | ||||
|   ]; | ||||
| 
 | ||||
|   // The maximum number of inbox to return. | ||||
|   int32 page_size = 2; | ||||
|   // Optional. The maximum number of inboxes to return. | ||||
|   // The service may return fewer than this value. | ||||
|   // If unspecified, at most 50 inboxes will be returned. | ||||
|   // The maximum value is 1000; values above 1000 will be coerced to 1000. | ||||
|   int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; | ||||
| 
 | ||||
|   // Optional. A page token, received from a previous `ListInboxes` call. | ||||
|   // Provide this to retrieve the subsequent page. | ||||
|   string page_token = 3; | ||||
|   string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; | ||||
| 
 | ||||
|   // Optional. Filter to apply to the list results. | ||||
|   // Example: "status=UNREAD" or "type=MEMO_COMMENT" | ||||
|   // Supported operators: =, != | ||||
|   // Supported fields: status, type, sender, create_time | ||||
|   string filter = 4 [(google.api.field_behavior) = OPTIONAL]; | ||||
| 
 | ||||
|   // Optional. The order to sort results by. | ||||
|   // Example: "create_time desc" or "status asc" | ||||
|   string order_by = 5 [(google.api.field_behavior) = OPTIONAL]; | ||||
| } | ||||
| 
 | ||||
| message ListInboxesResponse { | ||||
|   // The list of inboxes. | ||||
|   repeated Inbox inboxes = 1; | ||||
| 
 | ||||
|   // A token, which can be sent as `page_token` to retrieve the next page. | ||||
|   // A token that can be sent as `page_token` to retrieve the next page. | ||||
|   // If this field is omitted, there are no subsequent pages. | ||||
|   string next_page_token = 2; | ||||
| 
 | ||||
|   // The total count of inboxes (may be approximate). | ||||
|   int32 total_size = 3; | ||||
| } | ||||
| 
 | ||||
| message UpdateInboxRequest { | ||||
|   Inbox inbox = 1; | ||||
|   // Required. The inbox to update. | ||||
|   Inbox inbox = 1 [(google.api.field_behavior) = REQUIRED]; | ||||
| 
 | ||||
|   google.protobuf.FieldMask update_mask = 2; | ||||
|   // Required. The list of fields to update. | ||||
|   google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; | ||||
| 
 | ||||
|   // Optional. If set to true, allows updating missing fields. | ||||
|   bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL]; | ||||
| } | ||||
| 
 | ||||
| message DeleteInboxRequest { | ||||
|   // The name of the inbox to delete. | ||||
|   string name = 1; | ||||
|   // Required. The resource name of the inbox to delete. | ||||
|   // Format: inboxes/{inbox} | ||||
|   string name = 1 [ | ||||
|     (google.api.field_behavior) = REQUIRED, | ||||
|     (google.api.resource_reference) = {type: "memos.api.v1/Inbox"} | ||||
|   ]; | ||||
| } | ||||
|  |  | |||
|  | @ -25,12 +25,16 @@ const ( | |||
| 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) | ||||
| ) | ||||
| 
 | ||||
| // Status enumeration for inbox notifications. | ||||
| type Inbox_Status int32 | ||||
| 
 | ||||
| const ( | ||||
| 	// Unspecified status. | ||||
| 	Inbox_STATUS_UNSPECIFIED Inbox_Status = 0 | ||||
| 	Inbox_UNREAD             Inbox_Status = 1 | ||||
| 	Inbox_ARCHIVED           Inbox_Status = 2 | ||||
| 	// The notification is unread. | ||||
| 	Inbox_UNREAD Inbox_Status = 1 | ||||
| 	// The notification is archived. | ||||
| 	Inbox_ARCHIVED Inbox_Status = 2 | ||||
| ) | ||||
| 
 | ||||
| // Enum value maps for Inbox_Status. | ||||
|  | @ -74,12 +78,16 @@ func (Inbox_Status) EnumDescriptor() ([]byte, []int) { | |||
| 	return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{0, 0} | ||||
| } | ||||
| 
 | ||||
| // Type enumeration for inbox notifications. | ||||
| type Inbox_Type int32 | ||||
| 
 | ||||
| const ( | ||||
| 	// Unspecified type. | ||||
| 	Inbox_TYPE_UNSPECIFIED Inbox_Type = 0 | ||||
| 	Inbox_MEMO_COMMENT     Inbox_Type = 1 | ||||
| 	Inbox_VERSION_UPDATE   Inbox_Type = 2 | ||||
| 	// Memo comment notification. | ||||
| 	Inbox_MEMO_COMMENT Inbox_Type = 1 | ||||
| 	// Version update notification. | ||||
| 	Inbox_VERSION_UPDATE Inbox_Type = 2 | ||||
| ) | ||||
| 
 | ||||
| // Enum value maps for Inbox_Type. | ||||
|  | @ -125,17 +133,23 @@ func (Inbox_Type) EnumDescriptor() ([]byte, []int) { | |||
| 
 | ||||
| type Inbox struct { | ||||
| 	state protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	// The name of the inbox. | ||||
| 	// Format: inboxes/{id}, id is the system generated auto-incremented id. | ||||
| 	// The resource name of the inbox. | ||||
| 	// Format: inboxes/{inbox} | ||||
| 	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` | ||||
| 	// The sender of the inbox notification. | ||||
| 	// Format: users/{user} | ||||
| 	Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` | ||||
| 	// The receiver of the inbox notification. | ||||
| 	// Format: users/{user} | ||||
| 	Receiver      string                 `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` | ||||
| 	Status        Inbox_Status           `protobuf:"varint,4,opt,name=status,proto3,enum=memos.api.v1.Inbox_Status" json:"status,omitempty"` | ||||
| 	CreateTime    *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` | ||||
| 	Type          Inbox_Type             `protobuf:"varint,6,opt,name=type,proto3,enum=memos.api.v1.Inbox_Type" json:"type,omitempty"` | ||||
| 	ActivityId    *int32                 `protobuf:"varint,7,opt,name=activity_id,json=activityId,proto3,oneof" json:"activity_id,omitempty"` | ||||
| 	Receiver string `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` | ||||
| 	// The status of the inbox notification. | ||||
| 	Status Inbox_Status `protobuf:"varint,4,opt,name=status,proto3,enum=memos.api.v1.Inbox_Status" json:"status,omitempty"` | ||||
| 	// Output only. The creation timestamp. | ||||
| 	CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` | ||||
| 	// The type of the inbox notification. | ||||
| 	Type Inbox_Type `protobuf:"varint,6,opt,name=type,proto3,enum=memos.api.v1.Inbox_Type" json:"type,omitempty"` | ||||
| 	// Optional. The activity ID associated with this inbox notification. | ||||
| 	ActivityId    *int32 `protobuf:"varint,7,opt,name=activity_id,json=activityId,proto3,oneof" json:"activity_id,omitempty"` | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| } | ||||
|  | @ -221,12 +235,25 @@ func (x *Inbox) GetActivityId() int32 { | |||
| 
 | ||||
| type ListInboxesRequest struct { | ||||
| 	state protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	// Required. The parent resource whose inboxes will be listed. | ||||
| 	// Format: users/{user} | ||||
| 	User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` | ||||
| 	// The maximum number of inbox to return. | ||||
| 	Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` | ||||
| 	// Optional. The maximum number of inboxes to return. | ||||
| 	// The service may return fewer than this value. | ||||
| 	// If unspecified, at most 50 inboxes will be returned. | ||||
| 	// The maximum value is 1000; values above 1000 will be coerced to 1000. | ||||
| 	PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` | ||||
| 	// Optional. A page token, received from a previous `ListInboxes` call. | ||||
| 	// Provide this to retrieve the subsequent page. | ||||
| 	PageToken     string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` | ||||
| 	PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` | ||||
| 	// Optional. Filter to apply to the list results. | ||||
| 	// Example: "status=UNREAD" or "type=MEMO_COMMENT" | ||||
| 	// Supported operators: =, != | ||||
| 	// Supported fields: status, type, sender, create_time | ||||
| 	Filter string `protobuf:"bytes,4,opt,name=filter,proto3" json:"filter,omitempty"` | ||||
| 	// Optional. The order to sort results by. | ||||
| 	// Example: "create_time desc" or "status asc" | ||||
| 	OrderBy       string `protobuf:"bytes,5,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| } | ||||
|  | @ -261,9 +288,9 @@ func (*ListInboxesRequest) Descriptor() ([]byte, []int) { | |||
| 	return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{1} | ||||
| } | ||||
| 
 | ||||
| func (x *ListInboxesRequest) GetUser() string { | ||||
| func (x *ListInboxesRequest) GetParent() string { | ||||
| 	if x != nil { | ||||
| 		return x.User | ||||
| 		return x.Parent | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | @ -282,12 +309,29 @@ func (x *ListInboxesRequest) GetPageToken() string { | |||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (x *ListInboxesRequest) GetFilter() string { | ||||
| 	if x != nil { | ||||
| 		return x.Filter | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (x *ListInboxesRequest) GetOrderBy() string { | ||||
| 	if x != nil { | ||||
| 		return x.OrderBy | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| type ListInboxesResponse struct { | ||||
| 	state   protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	Inboxes []*Inbox               `protobuf:"bytes,1,rep,name=inboxes,proto3" json:"inboxes,omitempty"` | ||||
| 	// A token, which can be sent as `page_token` to retrieve the next page. | ||||
| 	state protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	// The list of inboxes. | ||||
| 	Inboxes []*Inbox `protobuf:"bytes,1,rep,name=inboxes,proto3" json:"inboxes,omitempty"` | ||||
| 	// A token that can be sent as `page_token` to retrieve the next page. | ||||
| 	// If this field is omitted, there are no subsequent pages. | ||||
| 	NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` | ||||
| 	// The total count of inboxes (may be approximate). | ||||
| 	TotalSize     int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| } | ||||
|  | @ -336,10 +380,21 @@ func (x *ListInboxesResponse) GetNextPageToken() string { | |||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (x *ListInboxesResponse) GetTotalSize() int32 { | ||||
| 	if x != nil { | ||||
| 		return x.TotalSize | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
| 
 | ||||
| type UpdateInboxRequest struct { | ||||
| 	state         protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	Inbox         *Inbox                 `protobuf:"bytes,1,opt,name=inbox,proto3" json:"inbox,omitempty"` | ||||
| 	UpdateMask    *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` | ||||
| 	state protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	// Required. The inbox to update. | ||||
| 	Inbox *Inbox `protobuf:"bytes,1,opt,name=inbox,proto3" json:"inbox,omitempty"` | ||||
| 	// Required. The list of fields to update. | ||||
| 	UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` | ||||
| 	// Optional. If set to true, allows updating missing fields. | ||||
| 	AllowMissing  bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| } | ||||
|  | @ -388,9 +443,17 @@ func (x *UpdateInboxRequest) GetUpdateMask() *fieldmaskpb.FieldMask { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (x *UpdateInboxRequest) GetAllowMissing() bool { | ||||
| 	if x != nil { | ||||
| 		return x.AllowMissing | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| type DeleteInboxRequest struct { | ||||
| 	state protoimpl.MessageState `protogen:"open.v1"` | ||||
| 	// The name of the inbox to delete. | ||||
| 	// Required. The resource name of the inbox to delete. | ||||
| 	// Format: inboxes/{inbox} | ||||
| 	Name          string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
|  | @ -437,16 +500,16 @@ var File_api_v1_inbox_service_proto protoreflect.FileDescriptor | |||
| 
 | ||||
| const file_api_v1_inbox_service_proto_rawDesc = "" + | ||||
| 	"\n" + | ||||
| 	"\x1aapi/v1/inbox_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa4\x03\n" + | ||||
| 	"\x05Inbox\x12\x12\n" + | ||||
| 	"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + | ||||
| 	"\x06sender\x18\x02 \x01(\tR\x06sender\x12\x1a\n" + | ||||
| 	"\breceiver\x18\x03 \x01(\tR\breceiver\x122\n" + | ||||
| 	"\x06status\x18\x04 \x01(\x0e2\x1a.memos.api.v1.Inbox.StatusR\x06status\x12;\n" + | ||||
| 	"\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + | ||||
| 	"createTime\x12,\n" + | ||||
| 	"\x04type\x18\x06 \x01(\x0e2\x18.memos.api.v1.Inbox.TypeR\x04type\x12$\n" + | ||||
| 	"\vactivity_id\x18\a \x01(\x05H\x00R\n" + | ||||
| 	"\x1aapi/v1/inbox_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x87\x04\n" + | ||||
| 	"\x05Inbox\x12\x17\n" + | ||||
| 	"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1b\n" + | ||||
| 	"\x06sender\x18\x02 \x01(\tB\x03\xe0A\x03R\x06sender\x12\x1f\n" + | ||||
| 	"\breceiver\x18\x03 \x01(\tB\x03\xe0A\x03R\breceiver\x127\n" + | ||||
| 	"\x06status\x18\x04 \x01(\x0e2\x1a.memos.api.v1.Inbox.StatusB\x03\xe0A\x01R\x06status\x12@\n" + | ||||
| 	"\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + | ||||
| 	"createTime\x121\n" + | ||||
| 	"\x04type\x18\x06 \x01(\x0e2\x18.memos.api.v1.Inbox.TypeB\x03\xe0A\x03R\x04type\x12)\n" + | ||||
| 	"\vactivity_id\x18\a \x01(\x05B\x03\xe0A\x01H\x00R\n" + | ||||
| 	"activityId\x88\x01\x01\":\n" + | ||||
| 	"\x06Status\x12\x16\n" + | ||||
| 	"\x12STATUS_UNSPECIFIED\x10\x00\x12\n" + | ||||
|  | @ -456,24 +519,32 @@ const file_api_v1_inbox_service_proto_rawDesc = "" + | |||
| 	"\x04Type\x12\x14\n" + | ||||
| 	"\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + | ||||
| 	"\fMEMO_COMMENT\x10\x01\x12\x12\n" + | ||||
| 	"\x0eVERSION_UPDATE\x10\x02B\x0e\n" + | ||||
| 	"\f_activity_id\"d\n" + | ||||
| 	"\x12ListInboxesRequest\x12\x12\n" + | ||||
| 	"\x04user\x18\x01 \x01(\tR\x04user\x12\x1b\n" + | ||||
| 	"\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + | ||||
| 	"\x0eVERSION_UPDATE\x10\x02:>\xeaA;\n" + | ||||
| 	"\x12memos.api.v1/Inbox\x12\x0finboxes/{inbox}\x1a\x04name*\ainboxes2\x05inboxB\x0e\n" + | ||||
| 	"\f_activity_id\"\xca\x01\n" + | ||||
| 	"\x12ListInboxesRequest\x121\n" + | ||||
| 	"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + | ||||
| 	"\x11memos.api.v1/UserR\x06parent\x12 \n" + | ||||
| 	"\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + | ||||
| 	"\n" + | ||||
| 	"page_token\x18\x03 \x01(\tR\tpageToken\"l\n" + | ||||
| 	"page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1b\n" + | ||||
| 	"\x06filter\x18\x04 \x01(\tB\x03\xe0A\x01R\x06filter\x12\x1e\n" + | ||||
| 	"\border_by\x18\x05 \x01(\tB\x03\xe0A\x01R\aorderBy\"\x8b\x01\n" + | ||||
| 	"\x13ListInboxesResponse\x12-\n" + | ||||
| 	"\ainboxes\x18\x01 \x03(\v2\x13.memos.api.v1.InboxR\ainboxes\x12&\n" + | ||||
| 	"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"|\n" + | ||||
| 	"\x12UpdateInboxRequest\x12)\n" + | ||||
| 	"\x05inbox\x18\x01 \x01(\v2\x13.memos.api.v1.InboxR\x05inbox\x12;\n" + | ||||
| 	"\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" + | ||||
| 	"updateMask\"(\n" + | ||||
| 	"\x12DeleteInboxRequest\x12\x12\n" + | ||||
| 	"\x04name\x18\x01 \x01(\tR\x04name2\xf7\x02\n" + | ||||
| 	"\fInboxService\x12k\n" + | ||||
| 	"\vListInboxes\x12 .memos.api.v1.ListInboxesRequest\x1a!.memos.api.v1.ListInboxesResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/v1/inboxes\x12\x87\x01\n" + | ||||
| 	"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + | ||||
| 	"\n" + | ||||
| 	"total_size\x18\x03 \x01(\x05R\ttotalSize\"\xb0\x01\n" + | ||||
| 	"\x12UpdateInboxRequest\x12.\n" + | ||||
| 	"\x05inbox\x18\x01 \x01(\v2\x13.memos.api.v1.InboxB\x03\xe0A\x02R\x05inbox\x12@\n" + | ||||
| 	"\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + | ||||
| 	"updateMask\x12(\n" + | ||||
| 	"\rallow_missing\x18\x03 \x01(\bB\x03\xe0A\x01R\fallowMissing\"D\n" + | ||||
| 	"\x12DeleteInboxRequest\x12.\n" + | ||||
| 	"\x04name\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + | ||||
| 	"\x12memos.api.v1/InboxR\x04name2\x92\x03\n" + | ||||
| 	"\fInboxService\x12\x85\x01\n" + | ||||
| 	"\vListInboxes\x12 .memos.api.v1.ListInboxesRequest\x1a!.memos.api.v1.ListInboxesResponse\"1\xdaA\x06parent\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{parent=users/*}/inboxes\x12\x87\x01\n" + | ||||
| 	"\vUpdateInbox\x12 .memos.api.v1.UpdateInboxRequest\x1a\x13.memos.api.v1.Inbox\"A\xdaA\x11inbox,update_mask\x82\xd3\xe4\x93\x02':\x05inbox2\x1e/api/v1/{inbox.name=inboxes/*}\x12p\n" + | ||||
| 	"\vDeleteInbox\x12 .memos.api.v1.DeleteInboxRequest\x1a\x16.google.protobuf.Empty\"'\xdaA\x04name\x82\xd3\xe4\x93\x02\x1a*\x18/api/v1/{name=inboxes/*}B\xa9\x01\n" + | ||||
| 	"\x10com.memos.api.v1B\x11InboxServiceProtoP\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" | ||||
|  |  | |||
|  | @ -35,14 +35,23 @@ var ( | |||
| 	_ = metadata.Join | ||||
| ) | ||||
| 
 | ||||
| var filter_InboxService_ListInboxes_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} | ||||
| var filter_InboxService_ListInboxes_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} | ||||
| 
 | ||||
| func request_InboxService_ListInboxes_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { | ||||
| 	var ( | ||||
| 		protoReq ListInboxesRequest | ||||
| 		metadata runtime.ServerMetadata | ||||
| 		err      error | ||||
| 	) | ||||
| 	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) | ||||
| 	} | ||||
| 	if err := req.ParseForm(); err != nil { | ||||
| 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) | ||||
| 	} | ||||
|  | @ -57,7 +66,16 @@ func local_request_InboxService_ListInboxes_0(ctx context.Context, marshaler run | |||
| 	var ( | ||||
| 		protoReq ListInboxesRequest | ||||
| 		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) | ||||
| 	} | ||||
| 	if err := req.ParseForm(); err != nil { | ||||
| 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) | ||||
| 	} | ||||
|  | @ -195,7 +213,7 @@ func RegisterInboxServiceHandlerServer(ctx context.Context, mux *runtime.ServeMu | |||
| 		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.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/inboxes")) | ||||
| 		annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes")) | ||||
| 		if err != nil { | ||||
| 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) | ||||
| 			return | ||||
|  | @ -293,7 +311,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu | |||
| 		ctx, cancel := context.WithCancel(req.Context()) | ||||
| 		defer cancel() | ||||
| 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) | ||||
| 		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/inboxes")) | ||||
| 		annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes")) | ||||
| 		if err != nil { | ||||
| 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) | ||||
| 			return | ||||
|  | @ -344,7 +362,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu | |||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	pattern_InboxService_ListInboxes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "inboxes"}, "")) | ||||
| 	pattern_InboxService_ListInboxes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "inboxes"}, "")) | ||||
| 	pattern_InboxService_UpdateInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "inbox.name"}, "")) | ||||
| 	pattern_InboxService_DeleteInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "name"}, "")) | ||||
| ) | ||||
|  |  | |||
|  | @ -292,38 +292,6 @@ paths: | |||
|           type: string | ||||
|       tags: | ||||
|         - IdentityProviderService | ||||
|   /api/v1/inboxes: | ||||
|     get: | ||||
|       summary: ListInboxes lists inboxes for a user. | ||||
|       operationId: InboxService_ListInboxes | ||||
|       responses: | ||||
|         "200": | ||||
|           description: A successful response. | ||||
|           schema: | ||||
|             $ref: '#/definitions/v1ListInboxesResponse' | ||||
|         default: | ||||
|           description: An unexpected error response. | ||||
|           schema: | ||||
|             $ref: '#/definitions/googlerpcStatus' | ||||
|       parameters: | ||||
|         - name: user | ||||
|           description: 'Format: users/{user}' | ||||
|           in: query | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: pageSize | ||||
|           description: The maximum number of inbox to return. | ||||
|           in: query | ||||
|           required: false | ||||
|           type: integer | ||||
|           format: int32 | ||||
|         - name: pageToken | ||||
|           description: Provide this to retrieve the subsequent page. | ||||
|           in: query | ||||
|           required: false | ||||
|           type: string | ||||
|       tags: | ||||
|         - InboxService | ||||
|   /api/v1/markdown/link:metadata: | ||||
|     get: | ||||
|       summary: GetLinkMetadata returns metadata for a given link. | ||||
|  | @ -937,13 +905,14 @@ paths: | |||
|       parameters: | ||||
|         - name: inbox.name | ||||
|           description: |- | ||||
|             The name of the inbox. | ||||
|             Format: inboxes/{id}, id is the system generated auto-incremented id. | ||||
|             The resource name of the inbox. | ||||
|             Format: inboxes/{inbox} | ||||
|           in: path | ||||
|           required: true | ||||
|           type: string | ||||
|           pattern: inboxes/[^/]+ | ||||
|         - name: inbox | ||||
|           description: Required. The inbox to update. | ||||
|           in: body | ||||
|           required: true | ||||
|           schema: | ||||
|  | @ -951,20 +920,40 @@ paths: | |||
|             properties: | ||||
|               sender: | ||||
|                 type: string | ||||
|                 title: 'Format: users/{user}' | ||||
|                 title: |- | ||||
|                   The sender of the inbox notification. | ||||
|                   Format: users/{user} | ||||
|                 readOnly: true | ||||
|               receiver: | ||||
|                 type: string | ||||
|                 title: 'Format: users/{user}' | ||||
|                 title: |- | ||||
|                   The receiver of the inbox notification. | ||||
|                   Format: users/{user} | ||||
|                 readOnly: true | ||||
|               status: | ||||
|                 $ref: '#/definitions/v1InboxStatus' | ||||
|                 description: The status of the inbox notification. | ||||
|               createTime: | ||||
|                 type: string | ||||
|                 format: date-time | ||||
|                 description: Output only. The creation timestamp. | ||||
|                 readOnly: true | ||||
|               type: | ||||
|                 $ref: '#/definitions/v1InboxType' | ||||
|                 description: The type of the inbox notification. | ||||
|                 readOnly: true | ||||
|               activityId: | ||||
|                 type: integer | ||||
|                 format: int32 | ||||
|                 description: Optional. The activity ID associated with this inbox notification. | ||||
|             title: Required. The inbox to update. | ||||
|             required: | ||||
|               - inbox | ||||
|         - name: allowMissing | ||||
|           description: Optional. If set to true, allows updating missing fields. | ||||
|           in: query | ||||
|           required: false | ||||
|           type: boolean | ||||
|       tags: | ||||
|         - InboxService | ||||
|   /api/v1/{memo.name}: | ||||
|  | @ -1263,7 +1252,9 @@ paths: | |||
|             $ref: '#/definitions/googlerpcStatus' | ||||
|       parameters: | ||||
|         - name: name_4 | ||||
|           description: The name of the inbox to delete. | ||||
|           description: |- | ||||
|             Required. The resource name of the inbox to delete. | ||||
|             Format: inboxes/{inbox} | ||||
|           in: path | ||||
|           required: true | ||||
|           type: string | ||||
|  | @ -1810,6 +1801,63 @@ paths: | |||
|           type: string | ||||
|       tags: | ||||
|         - UserService | ||||
|   /api/v1/{parent}/inboxes: | ||||
|     get: | ||||
|       summary: ListInboxes lists inboxes for a user. | ||||
|       operationId: InboxService_ListInboxes | ||||
|       responses: | ||||
|         "200": | ||||
|           description: A successful response. | ||||
|           schema: | ||||
|             $ref: '#/definitions/v1ListInboxesResponse' | ||||
|         default: | ||||
|           description: An unexpected error response. | ||||
|           schema: | ||||
|             $ref: '#/definitions/googlerpcStatus' | ||||
|       parameters: | ||||
|         - name: parent | ||||
|           description: |- | ||||
|             Required. The parent resource whose inboxes will be listed. | ||||
|             Format: users/{user} | ||||
|           in: path | ||||
|           required: true | ||||
|           type: string | ||||
|           pattern: users/[^/]+ | ||||
|         - name: pageSize | ||||
|           description: |- | ||||
|             Optional. The maximum number of inboxes to return. | ||||
|             The service may return fewer than this value. | ||||
|             If unspecified, at most 50 inboxes will be returned. | ||||
|             The maximum value is 1000; values above 1000 will be coerced to 1000. | ||||
|           in: query | ||||
|           required: false | ||||
|           type: integer | ||||
|           format: int32 | ||||
|         - name: pageToken | ||||
|           description: |- | ||||
|             Optional. A page token, received from a previous `ListInboxes` call. | ||||
|             Provide this to retrieve the subsequent page. | ||||
|           in: query | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: filter | ||||
|           description: |- | ||||
|             Optional. Filter to apply to the list results. | ||||
|             Example: "status=UNREAD" or "type=MEMO_COMMENT" | ||||
|             Supported operators: =, != | ||||
|             Supported fields: status, type, sender, create_time | ||||
|           in: query | ||||
|           required: false | ||||
|           type: string | ||||
|         - name: orderBy | ||||
|           description: |- | ||||
|             Optional. The order to sort results by. | ||||
|             Example: "create_time desc" or "status asc" | ||||
|           in: query | ||||
|           required: false | ||||
|           type: string | ||||
|       tags: | ||||
|         - InboxService | ||||
|   /api/v1/{parent}/memos: | ||||
|     get: | ||||
|       summary: ListMemos lists memos with pagination and filter. | ||||
|  | @ -3170,25 +3218,37 @@ definitions: | |||
|     properties: | ||||
|       name: | ||||
|         type: string | ||||
|         description: |- | ||||
|           The name of the inbox. | ||||
|           Format: inboxes/{id}, id is the system generated auto-incremented id. | ||||
|         title: |- | ||||
|           The resource name of the inbox. | ||||
|           Format: inboxes/{inbox} | ||||
|       sender: | ||||
|         type: string | ||||
|         title: 'Format: users/{user}' | ||||
|         title: |- | ||||
|           The sender of the inbox notification. | ||||
|           Format: users/{user} | ||||
|         readOnly: true | ||||
|       receiver: | ||||
|         type: string | ||||
|         title: 'Format: users/{user}' | ||||
|         title: |- | ||||
|           The receiver of the inbox notification. | ||||
|           Format: users/{user} | ||||
|         readOnly: true | ||||
|       status: | ||||
|         $ref: '#/definitions/v1InboxStatus' | ||||
|         description: The status of the inbox notification. | ||||
|       createTime: | ||||
|         type: string | ||||
|         format: date-time | ||||
|         description: Output only. The creation timestamp. | ||||
|         readOnly: true | ||||
|       type: | ||||
|         $ref: '#/definitions/v1InboxType' | ||||
|         description: The type of the inbox notification. | ||||
|         readOnly: true | ||||
|       activityId: | ||||
|         type: integer | ||||
|         format: int32 | ||||
|         description: Optional. The activity ID associated with this inbox notification. | ||||
|   v1InboxStatus: | ||||
|     type: string | ||||
|     enum: | ||||
|  | @ -3196,6 +3256,12 @@ definitions: | |||
|       - UNREAD | ||||
|       - ARCHIVED | ||||
|     default: STATUS_UNSPECIFIED | ||||
|     description: |- | ||||
|       Status enumeration for inbox notifications. | ||||
| 
 | ||||
|        - STATUS_UNSPECIFIED: Unspecified status. | ||||
|        - UNREAD: The notification is unread. | ||||
|        - ARCHIVED: The notification is archived. | ||||
|   v1InboxType: | ||||
|     type: string | ||||
|     enum: | ||||
|  | @ -3203,6 +3269,12 @@ definitions: | |||
|       - MEMO_COMMENT | ||||
|       - VERSION_UPDATE | ||||
|     default: TYPE_UNSPECIFIED | ||||
|     description: |- | ||||
|       Type enumeration for inbox notifications. | ||||
| 
 | ||||
|        - TYPE_UNSPECIFIED: Unspecified type. | ||||
|        - MEMO_COMMENT: Memo comment notification. | ||||
|        - VERSION_UPDATE: Version update notification. | ||||
|   v1ItalicNode: | ||||
|     type: object | ||||
|     properties: | ||||
|  | @ -3303,11 +3375,16 @@ definitions: | |||
|         items: | ||||
|           type: object | ||||
|           $ref: '#/definitions/v1Inbox' | ||||
|         description: The list of inboxes. | ||||
|       nextPageToken: | ||||
|         type: string | ||||
|         description: |- | ||||
|           A token, which can be sent as `page_token` to retrieve the next page. | ||||
|           A token that can be sent as `page_token` to retrieve the next page. | ||||
|           If this field is omitted, there are no subsequent pages. | ||||
|       totalSize: | ||||
|         type: integer | ||||
|         format: int32 | ||||
|         description: The total count of inboxes (may be approximate). | ||||
|   v1ListMemoAttachmentsResponse: | ||||
|     type: object | ||||
|     properties: | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ import ( | |||
| const ( | ||||
| 	// DefaultPageSize is the default page size for requests. | ||||
| 	DefaultPageSize = 10 | ||||
| 	// MaxPageSize is the maximum page size for requests. | ||||
| 	MaxPageSize = 1000 | ||||
| ) | ||||
| 
 | ||||
| func convertStateFromStore(rowStatus store.RowStatus) v1pb.State { | ||||
|  |  | |||
|  | @ -15,9 +15,27 @@ import ( | |||
| ) | ||||
| 
 | ||||
| func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxesRequest) (*v1pb.ListInboxesResponse, error) { | ||||
| 	user, err := s.GetCurrentUser(ctx) | ||||
| 	// Extract user ID from parent resource name | ||||
| 	userID, err := ExtractUserIDFromName(request.Parent) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get user") | ||||
| 		return nil, status.Errorf(codes.InvalidArgument, "invalid parent name %q: %v", request.Parent, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get current user for authorization | ||||
| 	currentUser, err := s.GetCurrentUser(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get current user") | ||||
| 	} | ||||
| 	if currentUser == nil { | ||||
| 		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if current user can access the requested user's inboxes | ||||
| 	if currentUser.ID != userID { | ||||
| 		// Only allow hosts and admins to access other users' inboxes | ||||
| 		if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { | ||||
| 			return nil, status.Errorf(codes.PermissionDenied, "cannot access inboxes for user %q", request.Parent) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var limit, offset int | ||||
|  | @ -34,15 +52,20 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe | |||
| 	if limit <= 0 { | ||||
| 		limit = DefaultPageSize | ||||
| 	} | ||||
| 	if limit > MaxPageSize { | ||||
| 		limit = MaxPageSize | ||||
| 	} | ||||
| 	limitPlusOne := limit + 1 | ||||
| 
 | ||||
| 	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ | ||||
| 		ReceiverID: &user.ID, | ||||
| 	findInbox := &store.FindInbox{ | ||||
| 		ReceiverID: &userID, | ||||
| 		Limit:      &limitPlusOne, | ||||
| 		Offset:     &offset, | ||||
| 	}) | ||||
| 	} | ||||
| 
 | ||||
| 	inboxes, err := s.Store.ListInboxes(ctx, findInbox) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to list inbox: %v", err) | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	inboxMessages := []*v1pb.Inbox{} | ||||
|  | @ -51,7 +74,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe | |||
| 		inboxes = inboxes[:limit] | ||||
| 		nextPageToken, err = getPageToken(limit, offset+limit) | ||||
| 		if err != nil { | ||||
| 			return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err) | ||||
| 			return nil, status.Errorf(codes.Internal, "failed to get next page token: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	for _, inbox := range inboxes { | ||||
|  | @ -65,6 +88,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe | |||
| 	response := &v1pb.ListInboxesResponse{ | ||||
| 		Inboxes:       inboxMessages, | ||||
| 		NextPageToken: nextPageToken, | ||||
| 		TotalSize:     int32(len(inboxMessages)), // For now, use actual returned count | ||||
| 	} | ||||
| 	return response, nil | ||||
| } | ||||
|  | @ -76,17 +100,46 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo | |||
| 
 | ||||
| 	inboxID, err := ExtractInboxIDFromName(request.Inbox.Name) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) | ||||
| 		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Inbox.Name, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get current user for authorization | ||||
| 	currentUser, err := s.GetCurrentUser(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get current user") | ||||
| 	} | ||||
| 	if currentUser == nil { | ||||
| 		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") | ||||
| 	} | ||||
| 
 | ||||
| 	// Get the existing inbox to verify ownership | ||||
| 	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ | ||||
| 		ID: &inboxID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err) | ||||
| 	} | ||||
| 	if len(inboxes) == 0 { | ||||
| 		return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Inbox.Name) | ||||
| 	} | ||||
| 	existingInbox := inboxes[0] | ||||
| 
 | ||||
| 	// Check if current user can update this inbox (must be the receiver) | ||||
| 	if currentUser.ID != existingInbox.ReceiverID { | ||||
| 		return nil, status.Errorf(codes.PermissionDenied, "cannot update inbox for another user") | ||||
| 	} | ||||
| 
 | ||||
| 	update := &store.UpdateInbox{ | ||||
| 		ID: inboxID, | ||||
| 	} | ||||
| 	for _, field := range request.UpdateMask.Paths { | ||||
| 		if field == "status" { | ||||
| 			if request.Inbox.Status == v1pb.Inbox_STATUS_UNSPECIFIED { | ||||
| 				return nil, status.Errorf(codes.InvalidArgument, "status is required") | ||||
| 				return nil, status.Errorf(codes.InvalidArgument, "status cannot be unspecified") | ||||
| 			} | ||||
| 			update.Status = convertInboxStatusToStore(request.Inbox.Status) | ||||
| 		} else { | ||||
| 			return nil, status.Errorf(codes.InvalidArgument, "unsupported field in update mask: %q", field) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -101,13 +154,39 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo | |||
| func (s *APIV1Service) DeleteInbox(ctx context.Context, request *v1pb.DeleteInboxRequest) (*emptypb.Empty, error) { | ||||
| 	inboxID, err := ExtractInboxIDFromName(request.Name) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) | ||||
| 		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Name, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get current user for authorization | ||||
| 	currentUser, err := s.GetCurrentUser(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get current user") | ||||
| 	} | ||||
| 	if currentUser == nil { | ||||
| 		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") | ||||
| 	} | ||||
| 
 | ||||
| 	// Get the existing inbox to verify ownership | ||||
| 	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ | ||||
| 		ID: &inboxID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err) | ||||
| 	} | ||||
| 	if len(inboxes) == 0 { | ||||
| 		return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Name) | ||||
| 	} | ||||
| 	existingInbox := inboxes[0] | ||||
| 
 | ||||
| 	// Check if current user can delete this inbox (must be the receiver) | ||||
| 	if currentUser.ID != existingInbox.ReceiverID { | ||||
| 		return nil, status.Errorf(codes.PermissionDenied, "cannot delete inbox for another user") | ||||
| 	} | ||||
| 
 | ||||
| 	if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{ | ||||
| 		ID: inboxID, | ||||
| 	}); err != nil { | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err) | ||||
| 		return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err) | ||||
| 	} | ||||
| 	return &emptypb.Empty{}, nil | ||||
| } | ||||
|  |  | |||
							
								
								
									
										559
									
								
								server/router/api/v1/test/inbox_service_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								server/router/api/v1/test/inbox_service_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,559 @@ | |||
| package v1 | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"google.golang.org/grpc/codes" | ||||
| 	"google.golang.org/grpc/status" | ||||
| 	"google.golang.org/protobuf/types/known/fieldmaskpb" | ||||
| 
 | ||||
| 	v1pb "github.com/usememos/memos/proto/gen/api/v1" | ||||
| 	storepb "github.com/usememos/memos/proto/gen/store" | ||||
| 	"github.com/usememos/memos/store" | ||||
| ) | ||||
| 
 | ||||
| func TestListInboxes(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	t.Run("ListInboxes success", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		// List inboxes (should be empty initially) | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: fmt.Sprintf("users/%d", user.ID), | ||||
| 		} | ||||
| 
 | ||||
| 		resp, err := ts.Service.ListInboxes(userCtx, req) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, resp) | ||||
| 		require.Empty(t, resp.Inboxes) | ||||
| 		require.Equal(t, int32(0), resp.TotalSize) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("ListInboxes with pagination", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create some inbox entries | ||||
| 		const systemBotID int32 = 0 | ||||
| 		for i := 0; i < 3; i++ { | ||||
| 			_, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 				SenderID:   systemBotID, | ||||
| 				ReceiverID: user.ID, | ||||
| 				Status:     store.UNREAD, | ||||
| 				Message: &storepb.InboxMessage{ | ||||
| 					Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 				}, | ||||
| 			}) | ||||
| 			require.NoError(t, err) | ||||
| 		} | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		// List inboxes with page size limit | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent:   fmt.Sprintf("users/%d", user.ID), | ||||
| 			PageSize: 2, | ||||
| 		} | ||||
| 
 | ||||
| 		resp, err := ts.Service.ListInboxes(userCtx, req) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, resp) | ||||
| 		require.Equal(t, 2, len(resp.Inboxes)) | ||||
| 		require.NotEmpty(t, resp.NextPageToken) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("ListInboxes permission denied for different user", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create two users | ||||
| 		user1, err := ts.CreateRegularUser(ctx, "user1") | ||||
| 		require.NoError(t, err) | ||||
| 		user2, err := ts.CreateRegularUser(ctx, "user2") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user1 context but try to list user2's inboxes | ||||
| 		userCtx := ts.CreateUserContext(ctx, user1.Username) | ||||
| 
 | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: fmt.Sprintf("users/%d", user2.ID), | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.ListInboxes(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "cannot access inboxes") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("ListInboxes host can access other users' inboxes", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a host user and a regular user | ||||
| 		hostUser, err := ts.CreateHostUser(ctx, "hostuser") | ||||
| 		require.NoError(t, err) | ||||
| 		regularUser, err := ts.CreateRegularUser(ctx, "regularuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox for the regular user | ||||
| 		const systemBotID int32 = 0 | ||||
| 		_, err = ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: regularUser.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set host user context and try to list regular user's inboxes | ||||
| 		hostCtx := ts.CreateUserContext(ctx, hostUser.Username) | ||||
| 
 | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: fmt.Sprintf("users/%d", regularUser.ID), | ||||
| 		} | ||||
| 
 | ||||
| 		resp, err := ts.Service.ListInboxes(hostCtx, req) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, resp) | ||||
| 		require.Equal(t, 1, len(resp.Inboxes)) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("ListInboxes invalid parent format", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: "invalid-parent-format", | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.ListInboxes(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "invalid parent name") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("ListInboxes unauthenticated", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		req := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: "users/1", | ||||
| 		} | ||||
| 
 | ||||
| 		_, err := ts.Service.ListInboxes(ctx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "user not authenticated") | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestUpdateInbox(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	t.Run("UpdateInbox success", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		// Update inbox status | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"status"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		resp, err := ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, resp) | ||||
| 		require.Equal(t, v1pb.Inbox_ARCHIVED, resp.Status) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("UpdateInbox permission denied for different user", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create two users | ||||
| 		user1, err := ts.CreateRegularUser(ctx, "user1") | ||||
| 		require.NoError(t, err) | ||||
| 		user2, err := ts.CreateRegularUser(ctx, "user2") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry for user2 | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user2.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user1 context but try to update user2's inbox | ||||
| 		userCtx := ts.CreateUserContext(ctx, user1.Username) | ||||
| 
 | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"status"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "cannot update inbox") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("UpdateInbox missing update mask", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   "inboxes/1", | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "update mask is required") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("UpdateInbox invalid name format", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   "invalid-inbox-name", | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"status"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "invalid inbox name") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("UpdateInbox not found", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   "inboxes/99999", // Non-existent inbox | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"status"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		st, ok := status.FromError(err) | ||||
| 		require.True(t, ok) | ||||
| 		require.Equal(t, codes.NotFound, st.Code()) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("UpdateInbox unsupported field", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"unsupported_field"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.UpdateInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "unsupported field") | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestDeleteInbox(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	t.Run("DeleteInbox success", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		// Delete inbox | ||||
| 		req := &v1pb.DeleteInboxRequest{ | ||||
| 			Name: fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.DeleteInbox(userCtx, req) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Verify inbox is deleted | ||||
| 		inboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{ | ||||
| 			ReceiverID: &user.ID, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, 0, len(inboxes)) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteInbox permission denied for different user", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create two users | ||||
| 		user1, err := ts.CreateRegularUser(ctx, "user1") | ||||
| 		require.NoError(t, err) | ||||
| 		user2, err := ts.CreateRegularUser(ctx, "user2") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry for user2 | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user2.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user1 context but try to delete user2's inbox | ||||
| 		userCtx := ts.CreateUserContext(ctx, user1.Username) | ||||
| 
 | ||||
| 		req := &v1pb.DeleteInboxRequest{ | ||||
| 			Name: fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.DeleteInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "cannot delete inbox") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteInbox invalid name format", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.DeleteInboxRequest{ | ||||
| 			Name: "invalid-inbox-name", | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.DeleteInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		require.Contains(t, err.Error(), "invalid inbox name") | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteInbox not found", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		req := &v1pb.DeleteInboxRequest{ | ||||
| 			Name: "inboxes/99999", // Non-existent inbox | ||||
| 		} | ||||
| 
 | ||||
| 		_, err = ts.Service.DeleteInbox(userCtx, req) | ||||
| 		require.Error(t, err) | ||||
| 		st, ok := status.FromError(err) | ||||
| 		require.True(t, ok) | ||||
| 		require.Equal(t, codes.NotFound, st.Code()) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestInboxCRUDComplete(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	t.Run("Complete CRUD lifecycle", func(t *testing.T) { | ||||
| 		ts := NewTestService(t) | ||||
| 		defer ts.Cleanup() | ||||
| 
 | ||||
| 		// Create a user | ||||
| 		user, err := ts.CreateRegularUser(ctx, "testuser") | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Create an inbox entry directly in store | ||||
| 		const systemBotID int32 = 0 | ||||
| 		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ | ||||
| 			SenderID:   systemBotID, | ||||
| 			ReceiverID: user.ID, | ||||
| 			Status:     store.UNREAD, | ||||
| 			Message: &storepb.InboxMessage{ | ||||
| 				Type: storepb.InboxMessage_MEMO_COMMENT, | ||||
| 			}, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// Set user context | ||||
| 		userCtx := ts.CreateUserContext(ctx, user.Username) | ||||
| 
 | ||||
| 		// 1. List inboxes - should have 1 | ||||
| 		listReq := &v1pb.ListInboxesRequest{ | ||||
| 			Parent: fmt.Sprintf("users/%d", user.ID), | ||||
| 		} | ||||
| 		listResp, err := ts.Service.ListInboxes(userCtx, listReq) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, 1, len(listResp.Inboxes)) | ||||
| 		require.Equal(t, v1pb.Inbox_UNREAD, listResp.Inboxes[0].Status) | ||||
| 
 | ||||
| 		// 2. Update inbox status to ARCHIVED | ||||
| 		updateReq := &v1pb.UpdateInboxRequest{ | ||||
| 			Inbox: &v1pb.Inbox{ | ||||
| 				Name:   fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 				Status: v1pb.Inbox_ARCHIVED, | ||||
| 			}, | ||||
| 			UpdateMask: &fieldmaskpb.FieldMask{ | ||||
| 				Paths: []string{"status"}, | ||||
| 			}, | ||||
| 		} | ||||
| 		updateResp, err := ts.Service.UpdateInbox(userCtx, updateReq) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, v1pb.Inbox_ARCHIVED, updateResp.Status) | ||||
| 
 | ||||
| 		// 3. List inboxes again - should still have 1 but ARCHIVED | ||||
| 		listResp, err = ts.Service.ListInboxes(userCtx, listReq) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, 1, len(listResp.Inboxes)) | ||||
| 		require.Equal(t, v1pb.Inbox_ARCHIVED, listResp.Inboxes[0].Status) | ||||
| 
 | ||||
| 		// 4. Delete inbox | ||||
| 		deleteReq := &v1pb.DeleteInboxRequest{ | ||||
| 			Name: fmt.Sprintf("inboxes/%d", inbox.ID), | ||||
| 		} | ||||
| 		_, err = ts.Service.DeleteInbox(userCtx, deleteReq) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		// 5. List inboxes - should be empty | ||||
| 		listResp, err = ts.Service.ListInboxes(userCtx, listReq) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, 0, len(listResp.Inboxes)) | ||||
| 		require.Equal(t, int32(0), listResp.TotalSize) | ||||
| 	}) | ||||
| } | ||||
|  | @ -13,14 +13,23 @@ import { useTranslate } from "@/utils/i18n"; | |||
| const Inboxes = observer(() => { | ||||
|   const t = useTranslate(); | ||||
|   const { md } = useResponsiveWidth(); | ||||
| 
 | ||||
|   const inboxes = sortBy(userStore.state.inboxes, (inbox) => { | ||||
|     if (inbox.status === Inbox_Status.UNREAD) return 0; | ||||
|     if (inbox.status === Inbox_Status.ARCHIVED) return 1; | ||||
|     return 2; | ||||
|   }); | ||||
| 
 | ||||
|   const fetchInboxes = async () => { | ||||
|     try { | ||||
|       await userStore.fetchInboxes(); | ||||
|     } catch (error) { | ||||
|       console.error("Failed to fetch inboxes:", error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     userStore.fetchInboxes(); | ||||
|     fetchInboxes(); | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|  |  | |||
|  | @ -160,7 +160,14 @@ const userStore = (() => { | |||
|   }; | ||||
| 
 | ||||
|   const fetchInboxes = async () => { | ||||
|     const { inboxes } = await inboxServiceClient.listInboxes({}); | ||||
|     if (!state.currentUser) { | ||||
|       throw new Error("No current user available"); | ||||
|     } | ||||
| 
 | ||||
|     const { inboxes } = await inboxServiceClient.listInboxes({ | ||||
|       parent: state.currentUser, | ||||
|     }); | ||||
| 
 | ||||
|     state.setPartial({ | ||||
|       inboxes, | ||||
|     }); | ||||
|  |  | |||
|  | @ -14,23 +14,39 @@ export const protobufPackage = "memos.api.v1"; | |||
| 
 | ||||
| export interface Inbox { | ||||
|   /** | ||||
|    * The name of the inbox. | ||||
|    * Format: inboxes/{id}, id is the system generated auto-incremented id. | ||||
|    * The resource name of the inbox. | ||||
|    * Format: inboxes/{inbox} | ||||
|    */ | ||||
|   name: string; | ||||
|   /** Format: users/{user} */ | ||||
|   /** | ||||
|    * The sender of the inbox notification. | ||||
|    * Format: users/{user} | ||||
|    */ | ||||
|   sender: string; | ||||
|   /** Format: users/{user} */ | ||||
|   /** | ||||
|    * The receiver of the inbox notification. | ||||
|    * Format: users/{user} | ||||
|    */ | ||||
|   receiver: string; | ||||
|   /** The status of the inbox notification. */ | ||||
|   status: Inbox_Status; | ||||
|   createTime?: Date | undefined; | ||||
|   /** Output only. The creation timestamp. */ | ||||
|   createTime?: | ||||
|     | Date | ||||
|     | undefined; | ||||
|   /** The type of the inbox notification. */ | ||||
|   type: Inbox_Type; | ||||
|   /** Optional. The activity ID associated with this inbox notification. */ | ||||
|   activityId?: number | undefined; | ||||
| } | ||||
| 
 | ||||
| /** Status enumeration for inbox notifications. */ | ||||
| export enum Inbox_Status { | ||||
|   /** STATUS_UNSPECIFIED - Unspecified status. */ | ||||
|   STATUS_UNSPECIFIED = "STATUS_UNSPECIFIED", | ||||
|   /** UNREAD - The notification is unread. */ | ||||
|   UNREAD = "UNREAD", | ||||
|   /** ARCHIVED - The notification is archived. */ | ||||
|   ARCHIVED = "ARCHIVED", | ||||
|   UNRECOGNIZED = "UNRECOGNIZED", | ||||
| } | ||||
|  | @ -67,9 +83,13 @@ export function inbox_StatusToNumber(object: Inbox_Status): number { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Type enumeration for inbox notifications. */ | ||||
| export enum Inbox_Type { | ||||
|   /** TYPE_UNSPECIFIED - Unspecified type. */ | ||||
|   TYPE_UNSPECIFIED = "TYPE_UNSPECIFIED", | ||||
|   /** MEMO_COMMENT - Memo comment notification. */ | ||||
|   MEMO_COMMENT = "MEMO_COMMENT", | ||||
|   /** VERSION_UPDATE - Version update notification. */ | ||||
|   VERSION_UPDATE = "VERSION_UPDATE", | ||||
|   UNRECOGNIZED = "UNRECOGNIZED", | ||||
| } | ||||
|  | @ -107,30 +127,67 @@ export function inbox_TypeToNumber(object: Inbox_Type): number { | |||
| } | ||||
| 
 | ||||
| export interface ListInboxesRequest { | ||||
|   /** Format: users/{user} */ | ||||
|   user: string; | ||||
|   /** The maximum number of inbox to return. */ | ||||
|   /** | ||||
|    * Required. The parent resource whose inboxes will be listed. | ||||
|    * Format: users/{user} | ||||
|    */ | ||||
|   parent: string; | ||||
|   /** | ||||
|    * Optional. The maximum number of inboxes to return. | ||||
|    * The service may return fewer than this value. | ||||
|    * If unspecified, at most 50 inboxes will be returned. | ||||
|    * The maximum value is 1000; values above 1000 will be coerced to 1000. | ||||
|    */ | ||||
|   pageSize: number; | ||||
|   /** Provide this to retrieve the subsequent page. */ | ||||
|   /** | ||||
|    * Optional. A page token, received from a previous `ListInboxes` call. | ||||
|    * Provide this to retrieve the subsequent page. | ||||
|    */ | ||||
|   pageToken: string; | ||||
|   /** | ||||
|    * Optional. Filter to apply to the list results. | ||||
|    * Example: "status=UNREAD" or "type=MEMO_COMMENT" | ||||
|    * Supported operators: =, != | ||||
|    * Supported fields: status, type, sender, create_time | ||||
|    */ | ||||
|   filter: string; | ||||
|   /** | ||||
|    * Optional. The order to sort results by. | ||||
|    * Example: "create_time desc" or "status asc" | ||||
|    */ | ||||
|   orderBy: string; | ||||
| } | ||||
| 
 | ||||
| export interface ListInboxesResponse { | ||||
|   /** The list of inboxes. */ | ||||
|   inboxes: Inbox[]; | ||||
|   /** | ||||
|    * A token, which can be sent as `page_token` to retrieve the next page. | ||||
|    * A token that can be sent as `page_token` to retrieve the next page. | ||||
|    * If this field is omitted, there are no subsequent pages. | ||||
|    */ | ||||
|   nextPageToken: string; | ||||
|   /** The total count of inboxes (may be approximate). */ | ||||
|   totalSize: number; | ||||
| } | ||||
| 
 | ||||
| export interface UpdateInboxRequest { | ||||
|   inbox?: Inbox | undefined; | ||||
|   updateMask?: string[] | undefined; | ||||
|   /** Required. The inbox to update. */ | ||||
|   inbox?: | ||||
|     | Inbox | ||||
|     | undefined; | ||||
|   /** Required. The list of fields to update. */ | ||||
|   updateMask?: | ||||
|     | string[] | ||||
|     | undefined; | ||||
|   /** Optional. If set to true, allows updating missing fields. */ | ||||
|   allowMissing: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface DeleteInboxRequest { | ||||
|   /** The name of the inbox to delete. */ | ||||
|   /** | ||||
|    * Required. The resource name of the inbox to delete. | ||||
|    * Format: inboxes/{inbox} | ||||
|    */ | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
|  | @ -261,13 +318,13 @@ export const Inbox: MessageFns<Inbox> = { | |||
| }; | ||||
| 
 | ||||
| function createBaseListInboxesRequest(): ListInboxesRequest { | ||||
|   return { user: "", pageSize: 0, pageToken: "" }; | ||||
|   return { parent: "", pageSize: 0, pageToken: "", filter: "", orderBy: "" }; | ||||
| } | ||||
| 
 | ||||
| export const ListInboxesRequest: MessageFns<ListInboxesRequest> = { | ||||
|   encode(message: ListInboxesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { | ||||
|     if (message.user !== "") { | ||||
|       writer.uint32(10).string(message.user); | ||||
|     if (message.parent !== "") { | ||||
|       writer.uint32(10).string(message.parent); | ||||
|     } | ||||
|     if (message.pageSize !== 0) { | ||||
|       writer.uint32(16).int32(message.pageSize); | ||||
|  | @ -275,6 +332,12 @@ export const ListInboxesRequest: MessageFns<ListInboxesRequest> = { | |||
|     if (message.pageToken !== "") { | ||||
|       writer.uint32(26).string(message.pageToken); | ||||
|     } | ||||
|     if (message.filter !== "") { | ||||
|       writer.uint32(34).string(message.filter); | ||||
|     } | ||||
|     if (message.orderBy !== "") { | ||||
|       writer.uint32(42).string(message.orderBy); | ||||
|     } | ||||
|     return writer; | ||||
|   }, | ||||
| 
 | ||||
|  | @ -290,7 +353,7 @@ export const ListInboxesRequest: MessageFns<ListInboxesRequest> = { | |||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           message.user = reader.string(); | ||||
|           message.parent = reader.string(); | ||||
|           continue; | ||||
|         } | ||||
|         case 2: { | ||||
|  | @ -309,6 +372,22 @@ export const ListInboxesRequest: MessageFns<ListInboxesRequest> = { | |||
|           message.pageToken = reader.string(); | ||||
|           continue; | ||||
|         } | ||||
|         case 4: { | ||||
|           if (tag !== 34) { | ||||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           message.filter = reader.string(); | ||||
|           continue; | ||||
|         } | ||||
|         case 5: { | ||||
|           if (tag !== 42) { | ||||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           message.orderBy = reader.string(); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|       if ((tag & 7) === 4 || tag === 0) { | ||||
|         break; | ||||
|  | @ -323,15 +402,17 @@ export const ListInboxesRequest: MessageFns<ListInboxesRequest> = { | |||
|   }, | ||||
|   fromPartial(object: DeepPartial<ListInboxesRequest>): ListInboxesRequest { | ||||
|     const message = createBaseListInboxesRequest(); | ||||
|     message.user = object.user ?? ""; | ||||
|     message.parent = object.parent ?? ""; | ||||
|     message.pageSize = object.pageSize ?? 0; | ||||
|     message.pageToken = object.pageToken ?? ""; | ||||
|     message.filter = object.filter ?? ""; | ||||
|     message.orderBy = object.orderBy ?? ""; | ||||
|     return message; | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| function createBaseListInboxesResponse(): ListInboxesResponse { | ||||
|   return { inboxes: [], nextPageToken: "" }; | ||||
|   return { inboxes: [], nextPageToken: "", totalSize: 0 }; | ||||
| } | ||||
| 
 | ||||
| export const ListInboxesResponse: MessageFns<ListInboxesResponse> = { | ||||
|  | @ -342,6 +423,9 @@ export const ListInboxesResponse: MessageFns<ListInboxesResponse> = { | |||
|     if (message.nextPageToken !== "") { | ||||
|       writer.uint32(18).string(message.nextPageToken); | ||||
|     } | ||||
|     if (message.totalSize !== 0) { | ||||
|       writer.uint32(24).int32(message.totalSize); | ||||
|     } | ||||
|     return writer; | ||||
|   }, | ||||
| 
 | ||||
|  | @ -368,6 +452,14 @@ export const ListInboxesResponse: MessageFns<ListInboxesResponse> = { | |||
|           message.nextPageToken = reader.string(); | ||||
|           continue; | ||||
|         } | ||||
|         case 3: { | ||||
|           if (tag !== 24) { | ||||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           message.totalSize = reader.int32(); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|       if ((tag & 7) === 4 || tag === 0) { | ||||
|         break; | ||||
|  | @ -384,12 +476,13 @@ export const ListInboxesResponse: MessageFns<ListInboxesResponse> = { | |||
|     const message = createBaseListInboxesResponse(); | ||||
|     message.inboxes = object.inboxes?.map((e) => Inbox.fromPartial(e)) || []; | ||||
|     message.nextPageToken = object.nextPageToken ?? ""; | ||||
|     message.totalSize = object.totalSize ?? 0; | ||||
|     return message; | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| function createBaseUpdateInboxRequest(): UpdateInboxRequest { | ||||
|   return { inbox: undefined, updateMask: undefined }; | ||||
|   return { inbox: undefined, updateMask: undefined, allowMissing: false }; | ||||
| } | ||||
| 
 | ||||
| export const UpdateInboxRequest: MessageFns<UpdateInboxRequest> = { | ||||
|  | @ -400,6 +493,9 @@ export const UpdateInboxRequest: MessageFns<UpdateInboxRequest> = { | |||
|     if (message.updateMask !== undefined) { | ||||
|       FieldMask.encode(FieldMask.wrap(message.updateMask), writer.uint32(18).fork()).join(); | ||||
|     } | ||||
|     if (message.allowMissing !== false) { | ||||
|       writer.uint32(24).bool(message.allowMissing); | ||||
|     } | ||||
|     return writer; | ||||
|   }, | ||||
| 
 | ||||
|  | @ -426,6 +522,14 @@ export const UpdateInboxRequest: MessageFns<UpdateInboxRequest> = { | |||
|           message.updateMask = FieldMask.unwrap(FieldMask.decode(reader, reader.uint32())); | ||||
|           continue; | ||||
|         } | ||||
|         case 3: { | ||||
|           if (tag !== 24) { | ||||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           message.allowMissing = reader.bool(); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|       if ((tag & 7) === 4 || tag === 0) { | ||||
|         break; | ||||
|  | @ -442,6 +546,7 @@ export const UpdateInboxRequest: MessageFns<UpdateInboxRequest> = { | |||
|     const message = createBaseUpdateInboxRequest(); | ||||
|     message.inbox = (object.inbox !== undefined && object.inbox !== null) ? Inbox.fromPartial(object.inbox) : undefined; | ||||
|     message.updateMask = object.updateMask ?? undefined; | ||||
|     message.allowMissing = object.allowMissing ?? false; | ||||
|     return message; | ||||
|   }, | ||||
| }; | ||||
|  | @ -506,8 +611,45 @@ export const InboxServiceDefinition = { | |||
|       responseStream: false, | ||||
|       options: { | ||||
|         _unknownFields: { | ||||
|           8410: [new Uint8Array([6, 112, 97, 114, 101, 110, 116])], | ||||
|           578365826: [ | ||||
|             new Uint8Array([17, 18, 15, 47, 97, 112, 105, 47, 118, 49, 47, 105, 110, 98, 111, 120, 101, 115]), | ||||
|             new Uint8Array([ | ||||
|               34, | ||||
|               18, | ||||
|               32, | ||||
|               47, | ||||
|               97, | ||||
|               112, | ||||
|               105, | ||||
|               47, | ||||
|               118, | ||||
|               49, | ||||
|               47, | ||||
|               123, | ||||
|               112, | ||||
|               97, | ||||
|               114, | ||||
|               101, | ||||
|               110, | ||||
|               116, | ||||
|               61, | ||||
|               117, | ||||
|               115, | ||||
|               101, | ||||
|               114, | ||||
|               115, | ||||
|               47, | ||||
|               42, | ||||
|               125, | ||||
|               47, | ||||
|               105, | ||||
|               110, | ||||
|               98, | ||||
|               111, | ||||
|               120, | ||||
|               101, | ||||
|               115, | ||||
|             ]), | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue