diff --git a/docs/current-status-card.md b/docs/current-status-card.md index 6044f95..5ff5d6e 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -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_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. -- `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. - 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: `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`. @@ -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. 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. +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 -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. 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. diff --git a/docs/msglib-read-sidecar-contract.md b/docs/msglib-read-sidecar-contract.md index bd30d5c..c788df9 100644 --- a/docs/msglib-read-sidecar-contract.md +++ b/docs/msglib-read-sidecar-contract.md @@ -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. - 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 Default verification builds and checks `self_check` only: diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index 1491c4a..bf346df 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -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 | | --- | --- | --- | --- | --- | --- | -| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `` 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 `` 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_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\` 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 -`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. diff --git a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md index 38ff6bf..3d18a0d 100644 --- a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md +++ b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md @@ -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. - 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 diff --git a/internal/isphere/msglib_source.go b/internal/isphere/msglib_source.go new file mode 100644 index 0000000..5c8b0ab --- /dev/null +++ b/internal/isphere/msglib_source.go @@ -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 "" +} diff --git a/internal/isphere/msglib_source_test.go b/internal/isphere/msglib_source_test.go new file mode 100644 index 0000000..bc28fe9 --- /dev/null +++ b/internal/isphere/msglib_source_test.go @@ -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 +} diff --git a/internal/isphere/source.go b/internal/isphere/source.go index fc1416d..af9f0e6 100644 --- a/internal/isphere/source.go +++ b/internal/isphere/source.go @@ -22,6 +22,16 @@ type Message struct { Subject string ReceiptID string Read bool + Source string + RawRef string + Attachments []MessageAttachment +} + +type MessageAttachment struct { + FileID string + FileName string + SizeBytes *int64 + DownloadRef string } type ReceiveMessagesQuery struct { diff --git a/internal/tools/isphere_read.go b/internal/tools/isphere_read.go index f667eb8..9c92067 100644 --- a/internal/tools/isphere_read.go +++ b/internal/tools/isphere_read.go @@ -131,8 +131,8 @@ func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started ti "subject": message.Subject, "receipt_id": message.ReceiptID, "read": message.Read, - "source": "local_readonly", - "raw_ref": "packetlog_message", + "source": receiveMessageSource(message), + "raw_ref": receiveMessageRawRef(message), }) } return map[string]any{ @@ -244,6 +244,18 @@ func contractConversationType(value string) string { } 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{}) attachments := make([]map[string]any, 0, len(files.Files)) for _, file := range files.Files { @@ -257,6 +269,20 @@ func receiveMessageAttachments(message isphere.Message) []map[string]any { 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 { if len(attachments) > 0 { return "file"