feat: add msglib receive adapter
This commit is contained in:
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
|
||||
ReceiptID string
|
||||
Read bool
|
||||
Source string
|
||||
RawRef string
|
||||
Attachments []MessageAttachment
|
||||
}
|
||||
|
||||
type MessageAttachment struct {
|
||||
FileID string
|
||||
FileName string
|
||||
SizeBytes *int64
|
||||
DownloadRef string
|
||||
}
|
||||
|
||||
type ReceiveMessagesQuery struct {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user