mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Implement SiriKit intents for CarPlay messaging (#1664)
* Add SiriKit intent handlers for CarPlay messaging (INSendMessageIntent, INSearchForMessagesIntent, INSetMessageAttributeIntent) Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Address code review: consolidate intent routing, support multiple recipients, improve error for long messages Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix unnecessary nil-coalescing in conversationIdentifier Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Restrict INSendMessageIntent to single recipient (channel or direct message, not both) Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/1798a03a-53b3-4a97-94e1-8281b552217a Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix Mac Catalyst build errors in SiriKit intent handlers - SendMessageIntentHandler: guard `.noHandlesForValue` with #if targetEnvironment(macCatalyst) since the reason enum is iOS-only - IntentMessageConverters: use .text instead of .tapback; INMessageType.tapback is unavailable on Mac Catalyst - SearchForMessagesIntentHandler: replace .startDate/.endDate (iOS-only) with .startDateComponents/.endDateComponents + Calendar.date(from:) which work on all platforms Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/9b61aad5-652c-4330-83b3-2303f10e4f12 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Add Siri authorization request at startup and NSSiriUsageDescription in Info.plist Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/be245ecb-2f0a-48d4-b931-4df889a6b6cc Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
parent
27b59fbb72
commit
1ae2b4bfef
8 changed files with 494 additions and 1 deletions
106
Meshtastic/Intents/SearchForMessagesIntentHandler.swift
Normal file
106
Meshtastic/Intents/SearchForMessagesIntentHandler.swift
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// SearchForMessagesIntentHandler.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Handles INSearchForMessagesIntent for CarPlay and Siri.
|
||||
// Queries Core Data for messages matching the intent criteria
|
||||
// and returns them as INMessage objects.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Intents
|
||||
import OSLog
|
||||
|
||||
final class SearchForMessagesIntentHandler: NSObject, INSearchForMessagesIntentHandling {
|
||||
|
||||
/// Maximum number of messages to return in a single search.
|
||||
private static let maxResults = 20
|
||||
|
||||
// MARK: - Handling
|
||||
|
||||
func handle(intent: INSearchForMessagesIntent) async -> INSearchForMessagesIntentResponse {
|
||||
let context = PersistenceController.shared.container.viewContext
|
||||
|
||||
let messages: [INMessage] = await MainActor.run {
|
||||
let fetchRequest: NSFetchRequest<MessageEntity> = MessageEntity.fetchRequest()
|
||||
var predicates: [NSPredicate] = []
|
||||
|
||||
// Exclude admin and emoji messages
|
||||
predicates.append(NSPredicate(format: "admin == NO"))
|
||||
predicates.append(NSPredicate(format: "isEmoji == NO"))
|
||||
|
||||
// Filter by identifiers (specific message IDs)
|
||||
if let identifiers = intent.identifiers, !identifiers.isEmpty {
|
||||
let messageIds = identifiers.compactMap { Int64($0) }
|
||||
if !messageIds.isEmpty {
|
||||
predicates.append(NSPredicate(format: "messageId IN %@", messageIds))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by sender
|
||||
if let senders = intent.senders, !senders.isEmpty {
|
||||
let senderNums = senders.compactMap { $0.personHandle?.value }.compactMap { Int64($0) }
|
||||
if !senderNums.isEmpty {
|
||||
predicates.append(NSPredicate(format: "fromUser.num IN %@", senderNums))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by date range.
|
||||
// INDateComponentsRange exposes DateComponents on all platforms;
|
||||
// .startDate/.endDate are iOS-only and unavailable on Mac Catalyst.
|
||||
if let dateRange = intent.dateTimeRange {
|
||||
let calendar = Calendar.current
|
||||
if let startComponents = dateRange.startDateComponents,
|
||||
let startDate = calendar.date(from: startComponents) {
|
||||
let startTimestamp = Int32(startDate.timeIntervalSince1970)
|
||||
predicates.append(NSPredicate(format: "messageTimestamp >= %d", startTimestamp))
|
||||
}
|
||||
if let endComponents = dateRange.endDateComponents,
|
||||
let endDate = calendar.date(from: endComponents) {
|
||||
let endTimestamp = Int32(endDate.timeIntervalSince1970)
|
||||
predicates.append(NSPredicate(format: "messageTimestamp <= %d", endTimestamp))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by group/channel name
|
||||
if let groupNames = intent.speakableGroupNames, !groupNames.isEmpty {
|
||||
let channelIndices: [Int32] = groupNames.compactMap { groupName in
|
||||
let channels = IntentMessageConverters.findChannels(
|
||||
matching: groupName.spokenPhrase, in: context
|
||||
)
|
||||
return channels.first.map { Int32($0.index) }
|
||||
}
|
||||
if !channelIndices.isEmpty {
|
||||
predicates.append(NSPredicate(format: "channel IN %@", channelIndices))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by read/unread attribute
|
||||
let attributes = intent.attributes
|
||||
if attributes.contains(.read) {
|
||||
predicates.append(NSPredicate(format: "read == YES"))
|
||||
} else if attributes.contains(.unread) {
|
||||
predicates.append(NSPredicate(format: "read == NO"))
|
||||
}
|
||||
|
||||
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
fetchRequest.sortDescriptors = [
|
||||
NSSortDescriptor(key: "messageTimestamp", ascending: false)
|
||||
]
|
||||
fetchRequest.fetchLimit = Self.maxResults
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = ["fromUser", "toUser"]
|
||||
|
||||
do {
|
||||
let results = try context.fetch(fetchRequest)
|
||||
return results.map { IntentMessageConverters.inMessage(from: $0) }
|
||||
} catch {
|
||||
Logger.services.error("CarPlay/Siri: Failed to search messages: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: nil)
|
||||
response.messages = messages
|
||||
return response
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue