feat: add msglib receive adapter
This commit is contained in:
@@ -29,7 +29,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
|||||||
- `isphere_receive_messages` accepts the read-contract argument set for the current local-readonly path: `conversation_id`, `query`, `since`, `limit`, `include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`.
|
- `isphere_receive_messages` accepts the read-contract argument set for the current local-readonly path: `conversation_id`, `query`, `since`, `limit`, `include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`.
|
||||||
- `isphere_search_contacts` and `isphere_search_groups` accept and validate the safe search-contract args for the current local-readonly path and can optionally enrich results from the env-configured C29 `MsgLib.db` display-entity reader; default behavior remains log/JID-backed when MsgLib env vars are absent.
|
- `isphere_search_contacts` and `isphere_search_groups` accept and validate the safe search-contract args for the current local-readonly path and can optionally enrich results from the env-configured C29 `MsgLib.db` display-entity reader; default behavior remains log/JID-backed when MsgLib env vars are absent.
|
||||||
- `isphere_receive_files` list mode accepts and validates the safe file-list contract args for the current local-readonly path while keeping download blocked.
|
- `isphere_receive_files` list mode accepts and validates the safe file-list contract args for the current local-readonly path while keeping download blocked.
|
||||||
- `native/MsgLibReadSidecar` provides a bounded x86 .NET read-only boundary for `MsgLib.db` schema/display-source checks, bounded contact/group display-entity reads, message-source metadata, and sidecar-level bounded message listing; `internal/msglib` wraps it from Go, and C30 wires display metadata as an optional MCP contact/group enrichment source.
|
- `native/MsgLibReadSidecar` provides a bounded x86 .NET read-only boundary for `MsgLib.db` schema/display-source checks, bounded contact/group display-entity reads, message-source metadata, and sidecar-level bounded message listing; `internal/msglib` wraps it from Go, C30 wires display metadata as an optional MCP contact/group enrichment source, and C39 adds a Go receive-source adapter around `ListMessages` without changing the MCP default source.
|
||||||
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
|
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
|
||||||
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
|
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
|||||||
|
|
||||||
## Current loop
|
## Current loop
|
||||||
|
|
||||||
Current loop: `Stage C / Loop C39 - DB-backed receive source adapter test slice`.
|
Current loop: `Stage C / Loop C40 - explicit MsgLib receive source selection design/test slice`.
|
||||||
|
|
||||||
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
||||||
|
|
||||||
@@ -107,10 +107,11 @@ Current loop output so far:
|
|||||||
36. C36: added metadata-only MsgLib `message_sources` across the x86 sidecar, Go wrapper, and sidecar verification; evidence-backed smoke returned six message-source summaries with source/table/role/availability/row-count metadata only.
|
36. C36: added metadata-only MsgLib `message_sources` across the x86 sidecar, Go wrapper, and sidecar verification; evidence-backed smoke returned six message-source summaries with source/table/role/availability/row-count metadata only.
|
||||||
37. C37: `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` defines the bounded DB-backed receive-message design: sidecar op `list_messages`, hard limit, no cursor initially, explicit `include_body`, no raw rows, no file path values, PacketReader remains default until reconciliation.
|
37. C37: `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` defines the bounded DB-backed receive-message design: sidecar op `list_messages`, hard limit, no cursor initially, explicit `include_body`, no raw rows, no file path values, PacketReader remains default until reconciliation.
|
||||||
38. C38: added sidecar/Go-wrapper `list_messages`, clamped to `1..50`, cursor rejected, optional attachment metadata without path/download refs, and copied-DB verification with `include_body=false`; printed evidence is only count/source-table/safety booleans.
|
38. C38: added sidecar/Go-wrapper `list_messages`, clamped to `1..50`, cursor rejected, optional attachment metadata without path/download refs, and copied-DB verification with `include_body=false`; printed evidence is only count/source-table/safety booleans.
|
||||||
|
39. C39: added `internal/isphere.MsgLibMessageSource`, an injected-provider adapter that maps `msglib.ListMessages` results into the existing receive-message domain model, preserving explicit source refs and attachment metadata while leaving MCP default PacketReader behavior unchanged.
|
||||||
|
|
||||||
## Next business mainline
|
## Next business mainline
|
||||||
|
|
||||||
1. Run Stage C Loop C39: design and test a DB-backed receive source adapter around `msglib.ListMessages`, with source selection explicit and PacketReader remaining the default until reconciliation is verified.
|
1. Run Stage C Loop C40: define and test explicit MsgLib receive source selection behavior, keeping `auto`/default on PacketReader and enabling DB rows only through an explicit operator-selected path.
|
||||||
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
|
2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated.
|
||||||
3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
|
3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
|
||||||
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
||||||
|
|||||||
@@ -293,6 +293,13 @@ Committed C38 implementation:
|
|||||||
- `scripts/verify-msglib-sidecar.ps1` calls `list_messages` only when copied DB env paths are supplied, with `include_body=false`, and prints only `message_list_count`, `message_list_sources`, safety booleans, and omission booleans.
|
- `scripts/verify-msglib-sidecar.ps1` calls `list_messages` only when copied DB env paths are supplied, with `include_body=false`, and prints only `message_list_count`, `message_list_sources`, safety booleans, and omission booleans.
|
||||||
- Evidence-backed C38 smoke returned normalized message count/source-table evidence without printing message text, body values, file paths, download refs, raw rows, sends, downloads, hooks, injection, or DB writes.
|
- Evidence-backed C38 smoke returned normalized message count/source-table evidence without printing message text, body values, file paths, download refs, raw rows, sends, downloads, hooks, injection, or DB writes.
|
||||||
|
|
||||||
|
Committed C39 implementation:
|
||||||
|
|
||||||
|
- Go adapter: `internal/isphere.MsgLibMessageSource`.
|
||||||
|
- The adapter uses an injected `ListMessages` provider and maps sidecar-normalized messages into the existing receive-message domain model.
|
||||||
|
- It preserves explicit `source`/`raw_ref` values and bounded attachment metadata while rejecting provider results that report non-read-only mode, raw rows, or file path values.
|
||||||
|
- It is not wired into MCP default receive behavior; `auto` remains PacketReader/log-backed until an explicit source-selection loop is completed.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Default verification builds and checks `self_check` only:
|
Default verification builds and checks `self_check` only:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
|
|||||||
|
|
||||||
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice |
|
| MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | copied `MsgLib.db` message tables through x86 read-only sidecar `list_messages`; decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md`; `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; C33 proves this path through a real copied-DB MCP smoke; C35 maps MsgLib message tables; C36 adds metadata-only `message_sources`; C37 defines bounded `list_messages` design; C38 implements sidecar/Go-wrapper `list_messages` with sanitized copied-DB verification, while PacketReader remains MCP default until reconciliation | C39 DB-backed receive source adapter design/test slice |
|
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | copied `MsgLib.db` message tables through x86 read-only sidecar `list_messages`; decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence`; `docs/source-discovery/2026-07-10-dotnet-wrapper-static-analysis.md`; `docs/source-discovery/2026-07-10-db-backed-receive-source-design.md` | C20 supports and validates the current safe receive-message contract args; C32 wires optional MsgLib display metadata into receive-message `sender_name` and `conversation.display_name`; C33 proves this path through a real copied-DB MCP smoke; C35 maps MsgLib message tables; C36 adds metadata-only `message_sources`; C37 defines bounded `list_messages` design; C38 implements sidecar/Go-wrapper `list_messages` with sanitized copied-DB verification; C39 adds a Go adapter from `ListMessages` to the receive-message domain model, while PacketReader remains MCP default until explicit source selection is tested | C40 explicit MsgLib receive source selection design/test slice |
|
||||||
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C34 documents how to enable optional MsgLib contact display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md` | C34 documents how to enable optional MsgLib contact display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
||||||
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents how to enable optional MsgLib group display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents how to enable optional MsgLib group display enrichment and verify sanitized MCP output; search enrichment remains available | C37 bounded DB-backed receive-source design |
|
||||||
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence |
|
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\<hash>` cache entries remain unlinked | decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence |
|
||||||
@@ -39,4 +39,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac
|
|||||||
|
|
||||||
## Deferred source work
|
## Deferred source work
|
||||||
|
|
||||||
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, and C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification. The next step is a DB-backed receive source adapter test slice; do not silently merge MsgLib rows into the MCP default until PacketReader-vs-DB reconciliation and operator source-selection behavior are explicit.
|
`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, and C39 adds a Go adapter from MsgLib list results to the receive-message domain model. The next step is explicit source-selection behavior; do not silently merge MsgLib rows into the MCP default until PacketReader-vs-DB reconciliation and operator selection are explicit.
|
||||||
|
|||||||
@@ -2720,17 +2720,59 @@ Sidecar bounded DB message listing result:
|
|||||||
- Keep source selection explicit. Do not silently merge MsgLib rows into default MCP receive output.
|
- Keep source selection explicit. Do not silently merge MsgLib rows into default MCP receive output.
|
||||||
- Verification should continue to clear MsgLib env in the standard `verify-go-mcp.ps1`; optional copied-DB verification remains separate.
|
- Verification should continue to clear MsgLib env in the standard `verify-go-mcp.ps1`; optional copied-DB verification remains separate.
|
||||||
|
|
||||||
- [ ] **Step 1: Locate current receive-message model boundary**
|
- [x] **Step 1: Locate current receive-message model boundary**
|
||||||
|
|
||||||
Inspect `internal/isphere` and `internal/tools` receive-message code and decide whether the adapter belongs below the tool layer or inside the tool registration layer.
|
Confirmed the domain boundary is `internal/isphere.Message` plus `ReceiveMessagesQuery/Result`; MCP tools depend on a `ReceiveMessagesSource` interface and can keep PacketReader as the default source.
|
||||||
|
|
||||||
- [ ] **Step 2: Write failing adapter test**
|
- [x] **Step 2: Write failing adapter test**
|
||||||
|
|
||||||
Use a fake MsgLib list provider returning redacted normalized messages and assert mapped receive-message fields, source refs, attachment metadata behavior, and explicit source-selection handling.
|
Added `TestMsgLibMessageSourceMapsListMessagesToReceiveMessages`. RED failed because `MsgLibMessageSource` did not exist.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement minimal adapter and update docs**
|
- [x] **Step 3: Implement minimal adapter and update docs**
|
||||||
|
|
||||||
Implement only enough code to pass the adapter test; defer MCP default wiring until reconciliation/source-selection behavior is proven.
|
Added `internal/isphere.MsgLibMessageSource` with injected `ListMessages` provider, explicit include flags, safety-flag checks, source/raw-ref mapping, and bounded attachment metadata mapping. Tool output now honors explicit message source/raw-ref/attachments when present while preserving PacketReader fallbacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C39 Result
|
||||||
|
|
||||||
|
DB-backed receive source adapter result:
|
||||||
|
|
||||||
|
- Added `internal/isphere.MsgLibMessageSource` and `MsgLibMessageLister` interface.
|
||||||
|
- Extended `internal/isphere.Message` with optional `Source`, `RawRef`, and `Attachments` fields so DB-backed rows can preserve sidecar refs without changing PacketReader behavior.
|
||||||
|
- `internal/tools` receive-message mapping now uses explicit message attachments/source/raw-ref when present and falls back to existing PacketReader-derived behavior otherwise.
|
||||||
|
- Unit tests use a fake MsgLib provider only; no real DB is required for C39.
|
||||||
|
- MCP default source is unchanged: `auto` still resolves to PacketReader/log-backed behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C40: Explicit MsgLib receive source selection design/test slice
|
||||||
|
|
||||||
|
**Goal:** Define and test the operator-facing path that enables MsgLib DB receive rows only through explicit source selection, while preserving PacketReader as the default `auto` behavior.
|
||||||
|
|
||||||
|
**Planned files:**
|
||||||
|
- Likely modify: `internal/tools/isphere_read.go` and tests for source-selection behavior.
|
||||||
|
- Possibly modify: `internal/mcpserver/server.go` only after the tool-layer contract is clear.
|
||||||
|
- Modify docs/status/matrix/plan after implementation.
|
||||||
|
|
||||||
|
**Planned behavior:**
|
||||||
|
- Keep `source_preference="auto"` and empty source preference on the existing PacketReader/log-backed source.
|
||||||
|
- Add or design an explicit source value such as `msglib_readonly` for the DB adapter; reject it when no MsgLib receive source is configured.
|
||||||
|
- Do not silently merge PacketReader and MsgLib rows.
|
||||||
|
- Standard `verify-go-mcp.ps1` should remain deterministic and should not require copied DB env.
|
||||||
|
- Optional copied-DB verification can be added only if its printed output stays sanitized.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write source-selection RED test**
|
||||||
|
|
||||||
|
Create a fake primary PacketReader source and fake MsgLib receive source; assert default/auto uses primary and explicit MsgLib selection uses the MsgLib source.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement minimal selector**
|
||||||
|
|
||||||
|
Add the smallest selector layer needed by the test. Keep current rejection behavior when explicit MsgLib is requested but not configured.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify and update operator docs**
|
||||||
|
|
||||||
|
Run full verification and update docs to describe the explicit opt-in route and the unchanged default route.
|
||||||
|
|
||||||
---
|
---
|
||||||
## Self-Review
|
## Self-Review
|
||||||
|
|||||||
98
internal/isphere/msglib_source.go
Normal file
98
internal/isphere/msglib_source.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package isphere
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MsgLibMessageLister interface {
|
||||||
|
ListMessages(ctx context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgLibMessageSource struct {
|
||||||
|
Lister MsgLibMessageLister
|
||||||
|
IncludeBody bool
|
||||||
|
IncludeAttachmentMetadata bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MsgLibMessageSource) ReceiveMessages(ctx context.Context, query ReceiveMessagesQuery) (ReceiveMessagesResult, error) {
|
||||||
|
if s.Lister == nil {
|
||||||
|
return ReceiveMessagesResult{}, fmt.Errorf("msglib message source requires a lister")
|
||||||
|
}
|
||||||
|
result, err := s.Lister.ListMessages(ctx, msglib.ListMessagesOptions{
|
||||||
|
ConversationID: query.ConversationID,
|
||||||
|
ConversationType: "auto",
|
||||||
|
Query: query.Query,
|
||||||
|
Since: query.Since,
|
||||||
|
Limit: query.Limit,
|
||||||
|
IncludeBody: s.IncludeBody,
|
||||||
|
IncludeAttachmentMetadata: s.IncludeAttachmentMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ReceiveMessagesResult{}, err
|
||||||
|
}
|
||||||
|
if !result.ReadOnly {
|
||||||
|
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages did not report read_only=true")
|
||||||
|
}
|
||||||
|
if result.RawRowsReturned {
|
||||||
|
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages returned raw rows")
|
||||||
|
}
|
||||||
|
if result.FilePathsReturned {
|
||||||
|
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages returned file path values")
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := make([]Message, 0, len(result.Messages))
|
||||||
|
for _, item := range result.Messages {
|
||||||
|
messages = append(messages, msgLibListMessageToReceiveMessage(item))
|
||||||
|
}
|
||||||
|
return ReceiveMessagesResult{Messages: messages}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func msgLibListMessageToReceiveMessage(item msglib.ListMessage) Message {
|
||||||
|
return Message{
|
||||||
|
ID: firstNonEmpty(item.ID, item.MessageID),
|
||||||
|
ConversationID: item.ConversationID,
|
||||||
|
ConversationType: item.ConversationType,
|
||||||
|
SenderID: item.SenderID,
|
||||||
|
ReceiverID: item.ReceiverID,
|
||||||
|
Text: firstNonEmpty(item.ContentText, item.Text),
|
||||||
|
Timestamp: firstNonEmpty(item.Timestamp, item.CreatedAt),
|
||||||
|
Subject: item.Subject,
|
||||||
|
Read: item.Read,
|
||||||
|
Source: firstNonEmpty(item.Source, "local_readonly"),
|
||||||
|
RawRef: item.RawRef,
|
||||||
|
Attachments: msgLibAttachmentsToReceiveAttachments(item.Attachments),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func msgLibAttachmentsToReceiveAttachments(items []msglib.ListMessageAttachment) []MessageAttachment {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
attachments := make([]MessageAttachment, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
attachment := MessageAttachment{
|
||||||
|
FileID: item.FileID,
|
||||||
|
FileName: item.FileName,
|
||||||
|
DownloadRef: item.DownloadRef,
|
||||||
|
}
|
||||||
|
if item.SizeBytes > 0 {
|
||||||
|
size := item.SizeBytes
|
||||||
|
attachment.SizeBytes = &size
|
||||||
|
}
|
||||||
|
attachments = append(attachments, attachment)
|
||||||
|
}
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
98
internal/isphere/msglib_source_test.go
Normal file
98
internal/isphere/msglib_source_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package isphere
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"isphere-ai-bridge/internal/msglib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMsgLibMessageSourceMapsListMessagesToReceiveMessages(t *testing.T) {
|
||||||
|
fake := &fakeMsgLibMessageLister{
|
||||||
|
result: msglib.ListMessagesResult{
|
||||||
|
ReadOnly: true,
|
||||||
|
MessageBodyValuesReturned: true,
|
||||||
|
RawRowsReturned: false,
|
||||||
|
FilePathsReturned: false,
|
||||||
|
SourceTables: []string{"tblMsgGroupPersonMsg"},
|
||||||
|
Messages: []msglib.ListMessage{{
|
||||||
|
MessageID: "db-msg-1",
|
||||||
|
ID: "db-msg-1",
|
||||||
|
ConversationID: "group-redacted",
|
||||||
|
ConversationType: "group",
|
||||||
|
SenderID: "sender-redacted",
|
||||||
|
SenderName: "Sender Redacted",
|
||||||
|
ReceiverID: "",
|
||||||
|
Text: "redacted body",
|
||||||
|
ContentText: "redacted body",
|
||||||
|
ContentType: "file",
|
||||||
|
Timestamp: "1783423807000",
|
||||||
|
CreatedAt: "2026-07-10T00:00:00Z",
|
||||||
|
Subject: "redacted subject",
|
||||||
|
Read: true,
|
||||||
|
Source: "local_readonly",
|
||||||
|
RawRef: "msglib:tblMsgGroupPersonMsg",
|
||||||
|
Attachments: []msglib.ListMessageAttachment{{
|
||||||
|
FileID: "file-redacted-1",
|
||||||
|
FileName: "redacted.docx",
|
||||||
|
SizeBytes: 1234,
|
||||||
|
DownloadRef: "",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
source := MsgLibMessageSource{
|
||||||
|
Lister: fake,
|
||||||
|
IncludeBody: true,
|
||||||
|
IncludeAttachmentMetadata: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := source.ReceiveMessages(context.Background(), ReceiveMessagesQuery{
|
||||||
|
ConversationID: "group-redacted",
|
||||||
|
Query: "redacted",
|
||||||
|
Since: "2026-07-10T00:00:00Z",
|
||||||
|
Limit: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReceiveMessages returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(fake.calls) != 1 {
|
||||||
|
t.Fatalf("ListMessages calls = %d, want 1", len(fake.calls))
|
||||||
|
}
|
||||||
|
call := fake.calls[0]
|
||||||
|
if call.ConversationID != "group-redacted" || call.Query != "redacted" || call.Since != "2026-07-10T00:00:00Z" || call.Limit != 5 {
|
||||||
|
t.Fatalf("ListMessages options = %+v", call)
|
||||||
|
}
|
||||||
|
if !call.IncludeBody || !call.IncludeAttachmentMetadata {
|
||||||
|
t.Fatalf("ListMessages include flags = body:%v attachments:%v", call.IncludeBody, call.IncludeAttachmentMetadata)
|
||||||
|
}
|
||||||
|
if len(got.Messages) != 1 {
|
||||||
|
t.Fatalf("messages = %+v, want one", got.Messages)
|
||||||
|
}
|
||||||
|
msg := got.Messages[0]
|
||||||
|
if msg.ID != "db-msg-1" || msg.ConversationID != "group-redacted" || msg.ConversationType != "group" {
|
||||||
|
t.Fatalf("unexpected message identity: %+v", msg)
|
||||||
|
}
|
||||||
|
if msg.SenderID != "sender-redacted" || msg.ReceiverID != "" || msg.Text != "redacted body" || msg.Subject != "redacted subject" || !msg.Read {
|
||||||
|
t.Fatalf("unexpected message fields: %+v", msg)
|
||||||
|
}
|
||||||
|
if msg.Source != "local_readonly" || msg.RawRef != "msglib:tblMsgGroupPersonMsg" {
|
||||||
|
t.Fatalf("source refs = %q/%q", msg.Source, msg.RawRef)
|
||||||
|
}
|
||||||
|
if len(msg.Attachments) != 1 || msg.Attachments[0].FileName != "redacted.docx" || msg.Attachments[0].DownloadRef != "" {
|
||||||
|
t.Fatalf("attachments = %+v", msg.Attachments)
|
||||||
|
}
|
||||||
|
if msg.Attachments[0].SizeBytes == nil || *msg.Attachments[0].SizeBytes != 1234 {
|
||||||
|
t.Fatalf("attachment size = %+v", msg.Attachments[0].SizeBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeMsgLibMessageLister struct {
|
||||||
|
calls []msglib.ListMessagesOptions
|
||||||
|
result msglib.ListMessagesResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeMsgLibMessageLister) ListMessages(_ context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error) {
|
||||||
|
f.calls = append(f.calls, opts)
|
||||||
|
return f.result, nil
|
||||||
|
}
|
||||||
@@ -22,6 +22,16 @@ type Message struct {
|
|||||||
Subject string
|
Subject string
|
||||||
ReceiptID string
|
ReceiptID string
|
||||||
Read bool
|
Read bool
|
||||||
|
Source string
|
||||||
|
RawRef string
|
||||||
|
Attachments []MessageAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageAttachment struct {
|
||||||
|
FileID string
|
||||||
|
FileName string
|
||||||
|
SizeBytes *int64
|
||||||
|
DownloadRef string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceiveMessagesQuery struct {
|
type ReceiveMessagesQuery struct {
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started ti
|
|||||||
"subject": message.Subject,
|
"subject": message.Subject,
|
||||||
"receipt_id": message.ReceiptID,
|
"receipt_id": message.ReceiptID,
|
||||||
"read": message.Read,
|
"read": message.Read,
|
||||||
"source": "local_readonly",
|
"source": receiveMessageSource(message),
|
||||||
"raw_ref": "packetlog_message",
|
"raw_ref": receiveMessageRawRef(message),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
@@ -244,6 +244,18 @@ func contractConversationType(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func receiveMessageAttachments(message isphere.Message) []map[string]any {
|
func receiveMessageAttachments(message isphere.Message) []map[string]any {
|
||||||
|
if len(message.Attachments) > 0 {
|
||||||
|
attachments := make([]map[string]any, 0, len(message.Attachments))
|
||||||
|
for _, attachment := range message.Attachments {
|
||||||
|
attachments = append(attachments, map[string]any{
|
||||||
|
"file_id": attachment.FileID,
|
||||||
|
"file_name": attachment.FileName,
|
||||||
|
"size_bytes": attachment.SizeBytes,
|
||||||
|
"download_ref": nullableString(attachment.DownloadRef),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
files := isphere.ListFilesFromMessages([]isphere.Message{message}, isphere.ListFilesQuery{})
|
files := isphere.ListFilesFromMessages([]isphere.Message{message}, isphere.ListFilesQuery{})
|
||||||
attachments := make([]map[string]any, 0, len(files.Files))
|
attachments := make([]map[string]any, 0, len(files.Files))
|
||||||
for _, file := range files.Files {
|
for _, file := range files.Files {
|
||||||
@@ -257,6 +269,20 @@ func receiveMessageAttachments(message isphere.Message) []map[string]any {
|
|||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func receiveMessageSource(message isphere.Message) string {
|
||||||
|
if strings.TrimSpace(message.Source) != "" {
|
||||||
|
return message.Source
|
||||||
|
}
|
||||||
|
return "local_readonly"
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveMessageRawRef(message isphere.Message) string {
|
||||||
|
if strings.TrimSpace(message.RawRef) != "" {
|
||||||
|
return message.RawRef
|
||||||
|
}
|
||||||
|
return "packetlog_message"
|
||||||
|
}
|
||||||
|
|
||||||
func receiveMessageContentType(message isphere.Message, attachments []map[string]any) string {
|
func receiveMessageContentType(message isphere.Message, attachments []map[string]any) string {
|
||||||
if len(attachments) > 0 {
|
if len(attachments) > 0 {
|
||||||
return "file"
|
return "file"
|
||||||
|
|||||||
Reference in New Issue
Block a user