Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## 0.8.3 - Unreleased

### JSON Output
- feat: include `reply_to_text` and `reply_to_sender` on message payloads
emitted by `history`, `search`, `watch`, and `rpc` so consumers can quote
the parent of a threaded reply (or non-reaction association) without a
follow-up chat.db lookup. The parent is resolved by joining
`thread_originator_guid` or the non-reaction `associated_message_guid`
back to the message table; absent parents leave the fields nil (#115, thanks
@omarshahine).

### JSON-RPC
- feat: expose bridge-backed message RPC methods for rich sends, attachments, tapbacks, edits, unsends, deletes, and notify-anyways; include the CLI version in `imsg status --json` so callers can gate newer RPC action surfaces.

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ Message objects include:
- `id`, `chat_id`, `chat_identifier`, `chat_guid`, `chat_name`
- `participants`, `is_group`
- `guid`, `reply_to_guid`, `thread_originator_guid`, `destination_caller_id`
- `reply_to_text`, `reply_to_sender` (parent body + handle for threaded
replies and non-reaction associations, when the parent row is still in
chat.db)
- `sender`, `sender_name`, `is_from_me`, `text`, `created_at`
- `attachments`, `reactions`

Expand Down
188 changes: 47 additions & 141 deletions Sources/IMsgCore/MessageStore+Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,134 +101,6 @@ struct MessageRowSelection {
}
}

private struct ChatMessagesQuery {
let sql: String
let bindings: [Binding?]
let selection: MessageRowSelection
let fallbackChatID: Int64

init(store: MessageStore, chatID: ChatID, limit: Int, filter: MessageFilter?) {
self.selection = MessageRowSelection(store: store, includeChatID: false)
let destinationCallerColumn =
store.schema.hasDestinationCallerID ? "m.destination_caller_id" : "NULL"
let reactionFilter =
store.schema.hasReactionColumns
? " AND (m.associated_message_type IS NULL OR m.associated_message_type < 2000 OR m.associated_message_type > 3006)"
: ""
var sql = """
SELECT \(selection.selectList)
FROM message m
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE cmj.chat_id = ?\(reactionFilter)
"""
var bindings: [Binding?] = [chatID.rawValue]

if let filter {
if let startDate = filter.startDate {
sql += " AND m.date >= ?"
bindings.append(MessageStore.appleEpoch(startDate))
}
if let endDate = filter.endDate {
sql += " AND m.date < ?"
bindings.append(MessageStore.appleEpoch(endDate))
}
if !filter.participants.isEmpty {
let placeholders = Array(repeating: "?", count: filter.participants.count).joined(
separator: ",")
sql +=
" AND COALESCE(NULLIF(h.id,''), \(destinationCallerColumn)) COLLATE NOCASE IN (\(placeholders))"
for participant in filter.participants {
bindings.append(participant)
}
}
}

sql += " ORDER BY m.date DESC LIMIT ?"
bindings.append(limit)

self.sql = sql
self.bindings = bindings
self.fallbackChatID = chatID.rawValue
}
}

private struct MessagesAfterQuery {
let sql: String
let bindings: [Binding?]
let selection: MessageRowSelection
let fallbackChatID: Int64?

init(
store: MessageStore,
afterRowID: MessageID,
chatID: ChatID?,
limit: Int,
includeReactions: Bool
) {
self.selection = MessageRowSelection(
store: store,
includeChatID: true,
includeBalloonBundleID: true
)
let reactionFilter: String
if includeReactions || !store.schema.hasReactionColumns {
reactionFilter = ""
} else {
reactionFilter =
" AND (m.associated_message_type IS NULL OR m.associated_message_type < 2000 OR m.associated_message_type > 3006)"
}
var sql = """
SELECT \(selection.selectList)
FROM message m
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE m.ROWID > ?\(reactionFilter)
"""
var bindings: [Binding?] = [afterRowID.rawValue]
if let chatID {
sql += " AND cmj.chat_id = ?"
bindings.append(chatID.rawValue)
}
sql += " ORDER BY m.ROWID ASC LIMIT ?"
bindings.append(limit)

self.sql = sql
self.bindings = bindings
self.fallbackChatID = chatID?.rawValue
}
}

private struct LatestSentMessageQuery {
let sql: String
let bindings: [Binding?]
let selection: MessageRowSelection
let fallbackChatID: Int64?

init(store: MessageStore, text: String, chatID: ChatID?, since date: Date) {
self.selection = MessageRowSelection(store: store, includeChatID: true)
var sql = """
SELECT \(selection.selectList)
FROM message m
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE m.is_from_me = 1
AND IFNULL(m.text, '') = ?
AND m.date >= ?
"""
var bindings: [Binding?] = [text, MessageStore.appleEpoch(date)]
if let chatID {
sql += " AND cmj.chat_id = ?"
bindings.append(chatID.rawValue)
}
sql += " ORDER BY m.date DESC, m.ROWID DESC LIMIT 1"

self.sql = sql
self.bindings = bindings
self.fallbackChatID = chatID?.rawValue
}
}

extension MessageStore {
public func maxRowID() throws -> Int64 {
return try withConnection { db in
Expand All @@ -251,6 +123,7 @@ extension MessageStore {

return try withConnection { db in
var messages: [Message] = []
var parentCache: ReplyParentCache = [:]
let rows = try db.prepareRowIterator(query.sql, bindings: query.bindings)
while let row = try rows.failableNext() {
let decoded = try decodeMessageRow(
Expand All @@ -262,6 +135,14 @@ extension MessageStore {
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let threadOriginatorGUID =
decoded.threadOriginatorGUID.isEmpty ? nil : decoded.threadOriginatorGUID
let parent = enrichedReplyContext(
db,
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
cache: &parentCache
)
messages.append(
Message(
rowID: decoded.rowID,
Expand All @@ -276,10 +157,11 @@ extension MessageStore {
guid: decoded.guid,
routing: Message.RoutingMetadata(
replyToGUID: replyToGUID,
threadOriginatorGUID: decoded.threadOriginatorGUID.isEmpty
? nil : decoded.threadOriginatorGUID,
threadOriginatorGUID: threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID
? nil : decoded.destinationCallerID,
replyToText: parent?.text,
replyToSender: parent?.sender
)
))
}
Expand Down Expand Up @@ -312,6 +194,7 @@ extension MessageStore {

return try withConnection { db in
var messages: [Message] = []
var parentCache: ReplyParentCache = [:]
let urlBalloonProvider = "com.apple.messages.URLBalloonProvider"

let rows = try db.prepareRowIterator(query.sql, bindings: query.bindings)
Expand All @@ -335,15 +218,27 @@ extension MessageStore {
continue
}

let replyToGUID = replyToGUID(
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let reaction = decodeReaction(
associatedType: decoded.associatedType,
associatedGUID: decoded.associatedGUID,
text: decoded.text
)
let replyToGUID = replyToGUID(
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let threadOriginatorGUID =
reaction.isReaction || decoded.threadOriginatorGUID.isEmpty
? nil : decoded.threadOriginatorGUID
let parent =
reaction.isReaction
? nil
: enrichedReplyContext(
db,
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
cache: &parentCache
)

messages.append(
Message(
Expand All @@ -359,10 +254,11 @@ extension MessageStore {
guid: decoded.guid,
routing: Message.RoutingMetadata(
replyToGUID: replyToGUID,
threadOriginatorGUID: decoded.threadOriginatorGUID.isEmpty
? nil : decoded.threadOriginatorGUID,
threadOriginatorGUID: threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID
? nil : decoded.destinationCallerID,
replyToText: parent?.text,
replyToSender: parent?.sender
),
reaction: Message.ReactionMetadata(
isReaction: reaction.isReaction,
Expand Down Expand Up @@ -400,6 +296,15 @@ extension MessageStore {
associatedGuid: decoded.associatedGUID,
associatedType: decoded.associatedType
)
let threadOriginatorGUID =
decoded.threadOriginatorGUID.isEmpty ? nil : decoded.threadOriginatorGUID
var parentCache: ReplyParentCache = [:]
let parent = enrichedReplyContext(
db,
replyToGUID: replyToGUID,
threadOriginatorGUID: threadOriginatorGUID,
cache: &parentCache
)
return Message(
rowID: decoded.rowID,
chatID: decoded.chatID,
Expand All @@ -413,10 +318,11 @@ extension MessageStore {
guid: decoded.guid,
routing: Message.RoutingMetadata(
replyToGUID: replyToGUID,
threadOriginatorGUID: decoded.threadOriginatorGUID.isEmpty
? nil : decoded.threadOriginatorGUID,
threadOriginatorGUID: threadOriginatorGUID,
destinationCallerID: decoded.destinationCallerID.isEmpty
? nil : decoded.destinationCallerID
? nil : decoded.destinationCallerID,
replyToText: parent?.text,
replyToSender: parent?.sender
)
)
}
Expand Down
Loading