feat: add msglib bounded message listing
This commit is contained in:
@@ -22,6 +22,9 @@ const (
|
||||
|
||||
DefaultDisplayEntityLimit = 25
|
||||
MaxDisplayEntityLimit = 100
|
||||
|
||||
DefaultListMessagesLimit = 20
|
||||
MaxListMessagesLimit = 50
|
||||
)
|
||||
|
||||
var requestCounter uint64
|
||||
@@ -97,6 +100,54 @@ type MessageSource struct {
|
||||
RowCount string `json:"row_count"`
|
||||
}
|
||||
|
||||
type ListMessagesOptions struct {
|
||||
ConversationID string
|
||||
ConversationType string
|
||||
Query string
|
||||
Since string
|
||||
Cursor string
|
||||
Limit int
|
||||
IncludeBody bool
|
||||
IncludeAttachmentMetadata bool
|
||||
}
|
||||
|
||||
type ListMessagesResult struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
MessageBodyValuesReturned bool `json:"message_body_values_returned"`
|
||||
RawRowsReturned bool `json:"raw_rows_returned"`
|
||||
FilePathsReturned bool `json:"file_paths_returned"`
|
||||
SourceTables []string `json:"source_tables"`
|
||||
Messages []ListMessage `json:"messages"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
}
|
||||
|
||||
type ListMessage struct {
|
||||
MessageID string `json:"message_id"`
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ConversationType string `json:"conversation_type"`
|
||||
SenderID string `json:"sender_id"`
|
||||
SenderName string `json:"sender_name"`
|
||||
ReceiverID string `json:"receiver_id"`
|
||||
Text string `json:"text"`
|
||||
ContentText string `json:"content_text"`
|
||||
ContentType string `json:"content_type"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Subject string `json:"subject"`
|
||||
Read bool `json:"read"`
|
||||
Source string `json:"source"`
|
||||
RawRef string `json:"raw_ref"`
|
||||
Attachments []ListMessageAttachment `json:"attachments"`
|
||||
}
|
||||
|
||||
type ListMessageAttachment struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
DownloadRef string `json:"download_ref"`
|
||||
}
|
||||
|
||||
type DisplayEntitiesOptions struct {
|
||||
EntityType string
|
||||
Query string
|
||||
@@ -162,6 +213,46 @@ func (c *Client) MessageSources(ctx context.Context) ([]MessageSource, error) {
|
||||
return out.MessageSources, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListMessages(ctx context.Context, opts ListMessagesOptions) (ListMessagesResult, error) {
|
||||
if strings.TrimSpace(opts.Cursor) != "" {
|
||||
return ListMessagesResult{}, errors.New("msglib list_messages cursor pagination is not available yet")
|
||||
}
|
||||
limit := opts.Limit
|
||||
if limit <= 0 {
|
||||
limit = DefaultListMessagesLimit
|
||||
}
|
||||
if limit > MaxListMessagesLimit {
|
||||
limit = MaxListMessagesLimit
|
||||
}
|
||||
|
||||
args := map[string]any{
|
||||
"sqlite_dll_path": c.config.SQLiteDLLPath,
|
||||
"db_path": c.config.DBPath,
|
||||
"password": c.config.Password,
|
||||
"limit": limit,
|
||||
"include_body": opts.IncludeBody,
|
||||
"include_attachment_metadata": opts.IncludeAttachmentMetadata,
|
||||
}
|
||||
if strings.TrimSpace(opts.ConversationID) != "" {
|
||||
args["conversation_id"] = strings.TrimSpace(opts.ConversationID)
|
||||
}
|
||||
if strings.TrimSpace(opts.ConversationType) != "" {
|
||||
args["conversation_type"] = strings.TrimSpace(opts.ConversationType)
|
||||
}
|
||||
if strings.TrimSpace(opts.Query) != "" {
|
||||
args["query"] = strings.TrimSpace(opts.Query)
|
||||
}
|
||||
if strings.TrimSpace(opts.Since) != "" {
|
||||
args["since"] = strings.TrimSpace(opts.Since)
|
||||
}
|
||||
|
||||
var out ListMessagesResult
|
||||
if err := c.call(ctx, "list_messages", args, &out); err != nil {
|
||||
return ListMessagesResult{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) DisplayEntities(ctx context.Context, opts DisplayEntitiesOptions) ([]DisplayEntity, error) {
|
||||
entityType := strings.TrimSpace(opts.EntityType)
|
||||
if entityType != EntityTypeContacts && entityType != EntityTypeGroups {
|
||||
|
||||
@@ -81,6 +81,46 @@ func TestMessageSourcesPassesConfiguredPathsAndDecodesMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMessagesPassesBoundedArgsAndDecodesNormalizedMessages(t *testing.T) {
|
||||
cfg := Config{
|
||||
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
|
||||
DBPath: `C:\fixture\MsgLib.db`,
|
||||
Password: "123",
|
||||
}
|
||||
client := newFakeClient(t, "list_messages_ok", cfg)
|
||||
|
||||
got, err := client.ListMessages(context.Background(), ListMessagesOptions{
|
||||
ConversationType: "direct",
|
||||
Query: "redacted",
|
||||
Since: "2026-07-10T00:00:00Z",
|
||||
Limit: 99,
|
||||
IncludeBody: false,
|
||||
IncludeAttachmentMetadata: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListMessages returned error: %v", err)
|
||||
}
|
||||
if !got.ReadOnly || got.MessageBodyValuesReturned || got.RawRowsReturned || got.FilePathsReturned {
|
||||
t.Fatalf("unexpected safety flags: %+v", got)
|
||||
}
|
||||
if strings.Join(got.SourceTables, ",") != "tblPersonMsg" {
|
||||
t.Fatalf("SourceTables = %#v", got.SourceTables)
|
||||
}
|
||||
if len(got.Messages) != 1 {
|
||||
t.Fatalf("len(Messages) = %d, want 1", len(got.Messages))
|
||||
}
|
||||
msg := got.Messages[0]
|
||||
if msg.MessageID != "msg-redacted-1" || msg.ConversationType != "direct" || msg.RawRef != "msglib:tblPersonMsg" {
|
||||
t.Fatalf("unexpected message identity: %+v", msg)
|
||||
}
|
||||
if msg.ContentText != "" || msg.Text != "" {
|
||||
t.Fatalf("content should be omitted when include_body=false: %+v", msg)
|
||||
}
|
||||
if len(msg.Attachments) != 1 || msg.Attachments[0].FileName != "redacted.docx" || msg.Attachments[0].DownloadRef != "" {
|
||||
t.Fatalf("unexpected attachments: %+v", msg.Attachments)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayEntitiesPassesBoundedArgsAndDecodesMetadata(t *testing.T) {
|
||||
cfg := Config{
|
||||
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
|
||||
@@ -307,6 +347,49 @@ func fakeSidecarMain() int {
|
||||
"error": nil,
|
||||
"warnings": []any{},
|
||||
})
|
||||
case "list_messages":
|
||||
args, _ := req["args"].(map[string]any)
|
||||
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["conversation_type"] != "direct" || args["query"] != "redacted" || args["since"] != "2026-07-10T00:00:00Z" || args["limit"] != float64(50) || args["include_body"] != false || args["include_attachment_metadata"] != true {
|
||||
fmt.Fprintf(os.Stderr, "bad list_messages args: %#v\n", args)
|
||||
return 2
|
||||
}
|
||||
return writeFakeResponse(map[string]any{
|
||||
"protocol": Protocol,
|
||||
"request_id": requestID,
|
||||
"op": op,
|
||||
"ok": true,
|
||||
"data": map[string]any{
|
||||
"read_only": true,
|
||||
"message_body_values_returned": false,
|
||||
"raw_rows_returned": false,
|
||||
"file_paths_returned": false,
|
||||
"source_tables": []any{"tblPersonMsg"},
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"message_id": "msg-redacted-1",
|
||||
"id": "msg-redacted-1",
|
||||
"conversation_id": "contact-redacted",
|
||||
"conversation_type": "direct",
|
||||
"sender_id": "sender-redacted",
|
||||
"sender_name": "Sender Redacted",
|
||||
"receiver_id": "receiver-redacted",
|
||||
"text": "",
|
||||
"content_text": "",
|
||||
"content_type": "file",
|
||||
"timestamp": "2026-07-10T00:01:02Z",
|
||||
"created_at": "2026-07-10T00:01:02Z",
|
||||
"read": true,
|
||||
"source": "local_readonly",
|
||||
"raw_ref": "msglib:tblPersonMsg",
|
||||
"attachments": []any{
|
||||
map[string]any{"file_id": "msg-redacted-1:redacted.docx", "file_name": "redacted.docx", "size_bytes": float64(1234), "download_ref": ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"error": nil,
|
||||
"warnings": []any{},
|
||||
})
|
||||
case "display_entities":
|
||||
args, _ := req["args"].(map[string]any)
|
||||
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["entity_type"] != "contacts" || args["query"] != "alice" || args["limit"] != float64(2) {
|
||||
|
||||
Reference in New Issue
Block a user