package isphere import ( "context" "fmt" "strconv" "strings" "time" "isphere-ai-bridge/internal/isphere/logcodec" "isphere-ai-bridge/internal/isphere/packetlog" ) type Message struct { ID string ConversationID string ConversationType string SenderID string ReceiverID string Text string Timestamp string 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 { ConversationID string Query string Since string Limit int } type ReceiveMessagesResult struct { Messages []Message } type EncryptedPacketLogSource struct { Lines []string } func (s EncryptedPacketLogSource) ReceiveMessages(ctx context.Context, query ReceiveMessagesQuery) (ReceiveMessagesResult, error) { since, hasSince, err := parseReceiveMessagesSince(query.Since) if err != nil { return ReceiveMessagesResult{}, err } limit := query.Limit if limit <= 0 || limit > len(s.Lines) { limit = len(s.Lines) } messages := make([]Message, 0, limit) for index, line := range s.Lines { select { case <-ctx.Done(): return ReceiveMessagesResult{}, ctx.Err() default: } plaintext, err := logcodec.DecryptLine(line) if err != nil { return ReceiveMessagesResult{}, fmt.Errorf("decrypt packet log line %d: %w", index, err) } parsed, err := packetlog.ParseMessageLog(plaintext) if err != nil { return ReceiveMessagesResult{}, fmt.Errorf("parse packet log line %d: %w", index, err) } message := normalizePacketMessage(parsed) if !receiveMessageMatchesQuery(message, query) || !receiveMessageMatchesSince(message, since, hasSince) { continue } messages = append(messages, message) if len(messages) == limit { break } } return ReceiveMessagesResult{Messages: messages}, nil } func parseReceiveMessagesSince(value string) (time.Time, bool, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return time.Time{}, false, nil } parsed, err := time.Parse(time.RFC3339Nano, trimmed) if err != nil { return time.Time{}, false, fmt.Errorf("parse receive messages since %q: %w", value, err) } return parsed.UTC(), true, nil } func receiveMessageMatchesQuery(message Message, query ReceiveMessagesQuery) bool { if strings.TrimSpace(query.ConversationID) != "" && message.ConversationID != strings.TrimSpace(query.ConversationID) { return false } keyword := strings.ToLower(strings.TrimSpace(query.Query)) if keyword == "" { return true } values := []string{ message.ID, message.ConversationID, message.SenderID, message.ReceiverID, message.Text, message.Subject, message.ReceiptID, } for _, value := range values { if strings.Contains(strings.ToLower(value), keyword) { return true } } return false } func receiveMessageMatchesSince(message Message, since time.Time, hasSince bool) bool { if !hasSince { return true } messageTime, ok := packetMessageTimestamp(message.Timestamp) if !ok { return false } return !messageTime.Before(since) } func packetMessageTimestamp(value string) (time.Time, bool) { trimmed := strings.TrimSpace(value) if trimmed == "" { return time.Time{}, false } epoch, err := strconv.ParseInt(trimmed, 10, 64) if err != nil { return time.Time{}, false } if epoch > 1_000_000_000_000 { return time.UnixMilli(epoch).UTC(), true } return time.Unix(epoch, 0).UTC(), true } func normalizePacketMessage(msg packetlog.Message) Message { sender := bareJID(msg.From) receiver := bareJID(msg.To) return Message{ ID: msg.ID, ConversationID: conversationID(sender, receiver), ConversationType: msg.Type, SenderID: sender, ReceiverID: receiver, Text: msg.Body, Timestamp: msg.SendTime, Subject: msg.Subject, ReceiptID: msg.ReceiptID, Read: msg.Read, } } func bareJID(value string) string { beforeResource, _, _ := strings.Cut(value, "/") return beforeResource } func conversationID(sender string, receiver string) string { if sender == "" { return receiver } if receiver == "" { return sender } return sender + "|" + receiver }