contextbase-plugin-imessage-local 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contextbase_plugin_imessage_local-0.2.9.dist-info/METADATA +14 -0
- contextbase_plugin_imessage_local-0.2.9.dist-info/RECORD +17 -0
- contextbase_plugin_imessage_local-0.2.9.dist-info/WHEEL +4 -0
- plugin_imessage_local/__init__.py +0 -0
- plugin_imessage_local/attributed_body.py +269 -0
- plugin_imessage_local/binding_config.py +13 -0
- plugin_imessage_local/component.py +125 -0
- plugin_imessage_local/defs/__init__.py +0 -0
- plugin_imessage_local/defs/defs.yaml +1 -0
- plugin_imessage_local/models/__init__.py +0 -0
- plugin_imessage_local/models/base.py +7 -0
- plugin_imessage_local/models/ctx.py +193 -0
- plugin_imessage_local/models/ingress.py +321 -0
- plugin_imessage_local/models/translators.py +384 -0
- plugin_imessage_local/plugin.json +7 -0
- plugin_imessage_local/sources/__init__.py +0 -0
- plugin_imessage_local/sources/snapshot.py +234 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..attributed_body import decode_attributed_body
|
|
9
|
+
from .ctx import (
|
|
10
|
+
AttachmentRow,
|
|
11
|
+
ChatHandleJoinRow,
|
|
12
|
+
ChatMessageJoinRow,
|
|
13
|
+
ChatRow,
|
|
14
|
+
HandleRow,
|
|
15
|
+
MessageAttachmentJoinRow,
|
|
16
|
+
MessageRow,
|
|
17
|
+
)
|
|
18
|
+
from .ingress import (
|
|
19
|
+
IMessageAttachment,
|
|
20
|
+
IMessageChat,
|
|
21
|
+
IMessageChatHandleJoin,
|
|
22
|
+
IMessageChatMessageJoin,
|
|
23
|
+
IMessageHandle,
|
|
24
|
+
IMessageMessage,
|
|
25
|
+
IMessageMessageAttachmentJoin,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _json_safe(value: Any) -> Any:
|
|
30
|
+
"""Convert a decoded-attribute structure to JSON-serializable values.
|
|
31
|
+
|
|
32
|
+
``bytes`` inside attribute dicts (e.g. DataDetected bplist blobs) become
|
|
33
|
+
base64 strings so the whole tree fits into a JSONB column without
|
|
34
|
+
silently dropping any bytes.
|
|
35
|
+
"""
|
|
36
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
37
|
+
return value
|
|
38
|
+
if isinstance(value, bytes):
|
|
39
|
+
return base64.b64encode(value).decode("ascii")
|
|
40
|
+
if isinstance(value, (datetime, date)):
|
|
41
|
+
return value.isoformat()
|
|
42
|
+
if isinstance(value, Mapping):
|
|
43
|
+
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
44
|
+
if isinstance(value, (list, tuple)):
|
|
45
|
+
return [_json_safe(v) for v in value]
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def handles_to_ctx_models(
|
|
50
|
+
*,
|
|
51
|
+
binding_id: str,
|
|
52
|
+
rows: Iterable[IMessageHandle],
|
|
53
|
+
) -> Iterator[HandleRow]:
|
|
54
|
+
for handle in rows:
|
|
55
|
+
yield HandleRow(
|
|
56
|
+
ctx_binding_id=binding_id,
|
|
57
|
+
ctx_source_updated_at=None,
|
|
58
|
+
id=handle.id,
|
|
59
|
+
service=handle.service,
|
|
60
|
+
country=handle.country,
|
|
61
|
+
uncanonicalized_id=handle.uncanonicalized_id,
|
|
62
|
+
person_centric_id=handle.person_centric_id,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def chats_to_ctx_models(
|
|
67
|
+
*,
|
|
68
|
+
binding_id: str,
|
|
69
|
+
rows: Iterable[IMessageChat],
|
|
70
|
+
) -> Iterator[ChatRow]:
|
|
71
|
+
for chat in rows:
|
|
72
|
+
yield ChatRow(
|
|
73
|
+
ctx_binding_id=binding_id,
|
|
74
|
+
ctx_source_updated_at=None,
|
|
75
|
+
guid=chat.guid,
|
|
76
|
+
style=chat.style,
|
|
77
|
+
state=chat.state,
|
|
78
|
+
account_id=chat.account_id,
|
|
79
|
+
properties=chat.properties,
|
|
80
|
+
chat_identifier=chat.chat_identifier,
|
|
81
|
+
service_name=chat.service_name,
|
|
82
|
+
room_name=chat.room_name,
|
|
83
|
+
account_login=chat.account_login,
|
|
84
|
+
is_archived=chat.is_archived,
|
|
85
|
+
last_addressed_handle=chat.last_addressed_handle,
|
|
86
|
+
display_name=chat.display_name,
|
|
87
|
+
group_id=chat.group_id,
|
|
88
|
+
is_filtered=chat.is_filtered,
|
|
89
|
+
successful_query=chat.successful_query,
|
|
90
|
+
engram_id=chat.engram_id,
|
|
91
|
+
server_change_token=chat.server_change_token,
|
|
92
|
+
ck_sync_state=chat.ck_sync_state,
|
|
93
|
+
original_group_id=chat.original_group_id,
|
|
94
|
+
last_read_message_timestamp=chat.last_read_message_timestamp,
|
|
95
|
+
sr_server_change_token=chat.sr_server_change_token,
|
|
96
|
+
sr_ck_sync_state=chat.sr_ck_sync_state,
|
|
97
|
+
cloudkit_record_id=chat.cloudkit_record_id,
|
|
98
|
+
sr_cloudkit_record_id=chat.sr_cloudkit_record_id,
|
|
99
|
+
last_addressed_sim_id=chat.last_addressed_sim_id,
|
|
100
|
+
is_blackholed=chat.is_blackholed,
|
|
101
|
+
syndication_date=chat.syndication_date,
|
|
102
|
+
syndication_type=chat.syndication_type,
|
|
103
|
+
is_recovered=chat.is_recovered,
|
|
104
|
+
is_deleting_incoming_messages=chat.is_deleting_incoming_messages,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def attachments_to_ctx_models(
|
|
109
|
+
*,
|
|
110
|
+
binding_id: str,
|
|
111
|
+
rows: Iterable[IMessageAttachment],
|
|
112
|
+
) -> Iterator[AttachmentRow]:
|
|
113
|
+
for attachment in rows:
|
|
114
|
+
yield AttachmentRow(
|
|
115
|
+
ctx_binding_id=binding_id,
|
|
116
|
+
ctx_source_updated_at=attachment.created_date,
|
|
117
|
+
guid=attachment.guid,
|
|
118
|
+
created_date=attachment.created_date,
|
|
119
|
+
start_date=attachment.start_date,
|
|
120
|
+
filename=attachment.filename,
|
|
121
|
+
uti=attachment.uti,
|
|
122
|
+
mime_type=attachment.mime_type,
|
|
123
|
+
transfer_state=attachment.transfer_state,
|
|
124
|
+
is_outgoing=attachment.is_outgoing,
|
|
125
|
+
user_info=attachment.user_info,
|
|
126
|
+
transfer_name=attachment.transfer_name,
|
|
127
|
+
total_bytes=attachment.total_bytes,
|
|
128
|
+
is_sticker=attachment.is_sticker,
|
|
129
|
+
sticker_user_info=attachment.sticker_user_info,
|
|
130
|
+
attribution_info=attachment.attribution_info,
|
|
131
|
+
hide_attachment=attachment.hide_attachment,
|
|
132
|
+
ck_sync_state=attachment.ck_sync_state,
|
|
133
|
+
ck_server_change_token_blob=attachment.ck_server_change_token_blob,
|
|
134
|
+
ck_record_id=attachment.ck_record_id,
|
|
135
|
+
original_guid=attachment.original_guid,
|
|
136
|
+
sr_ck_sync_state=attachment.sr_ck_sync_state,
|
|
137
|
+
sr_ck_server_change_token_blob=attachment.sr_ck_server_change_token_blob,
|
|
138
|
+
sr_ck_record_id=attachment.sr_ck_record_id,
|
|
139
|
+
is_commsafety_sensitive=attachment.is_commsafety_sensitive,
|
|
140
|
+
emoji_image_content_identifier=attachment.emoji_image_content_identifier,
|
|
141
|
+
emoji_image_short_description=attachment.emoji_image_short_description,
|
|
142
|
+
preview_generation_state=attachment.preview_generation_state,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def messages_to_ctx_models(
|
|
147
|
+
*,
|
|
148
|
+
binding_id: str,
|
|
149
|
+
rows: Iterable[IMessageMessage],
|
|
150
|
+
handle_lookup: Mapping[int, tuple[str, str]],
|
|
151
|
+
) -> Iterator[MessageRow]:
|
|
152
|
+
for msg in rows:
|
|
153
|
+
decoded = (
|
|
154
|
+
decode_attributed_body(msg.attributed_body) if msg.attributed_body else None
|
|
155
|
+
)
|
|
156
|
+
if decoded is None:
|
|
157
|
+
decoded_text: str | None = None
|
|
158
|
+
attribute_runs: list[dict[str, Any]] | None = None
|
|
159
|
+
else:
|
|
160
|
+
decoded_text = decoded.text
|
|
161
|
+
attribute_runs = [
|
|
162
|
+
{
|
|
163
|
+
"location": run.location,
|
|
164
|
+
"length": run.length,
|
|
165
|
+
"attributes": _json_safe(dict(run.attributes)),
|
|
166
|
+
}
|
|
167
|
+
for run in decoded.runs
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
text = decoded_text if decoded_text is not None else msg.text
|
|
171
|
+
|
|
172
|
+
handle_natural_id, handle_natural_service = handle_lookup.get(
|
|
173
|
+
msg.handle_id, (None, None)
|
|
174
|
+
)
|
|
175
|
+
other_handle_natural_id, other_handle_natural_service = handle_lookup.get(
|
|
176
|
+
msg.other_handle, (None, None)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
source_updated_at = max(
|
|
180
|
+
(
|
|
181
|
+
timestamp
|
|
182
|
+
for timestamp in (msg.date, msg.date_edited, msg.date_retracted)
|
|
183
|
+
if timestamp is not None
|
|
184
|
+
),
|
|
185
|
+
default=None,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
yield MessageRow(
|
|
189
|
+
ctx_binding_id=binding_id,
|
|
190
|
+
ctx_source_updated_at=source_updated_at,
|
|
191
|
+
guid=msg.guid,
|
|
192
|
+
text=text,
|
|
193
|
+
replace=msg.replace,
|
|
194
|
+
service_center=msg.service_center,
|
|
195
|
+
handle_id=msg.handle_id,
|
|
196
|
+
handle_natural_id=handle_natural_id,
|
|
197
|
+
handle_natural_service=handle_natural_service,
|
|
198
|
+
subject=msg.subject,
|
|
199
|
+
country=msg.country,
|
|
200
|
+
attributed_body=msg.attributed_body,
|
|
201
|
+
attribute_runs=attribute_runs,
|
|
202
|
+
version=msg.version,
|
|
203
|
+
type=msg.type,
|
|
204
|
+
service=msg.service,
|
|
205
|
+
account=msg.account,
|
|
206
|
+
account_guid=msg.account_guid,
|
|
207
|
+
error=msg.error,
|
|
208
|
+
date=msg.date,
|
|
209
|
+
date_read=msg.date_read,
|
|
210
|
+
date_delivered=msg.date_delivered,
|
|
211
|
+
is_delivered=msg.is_delivered,
|
|
212
|
+
is_finished=msg.is_finished,
|
|
213
|
+
is_emote=msg.is_emote,
|
|
214
|
+
is_from_me=msg.is_from_me,
|
|
215
|
+
is_empty=msg.is_empty,
|
|
216
|
+
is_delayed=msg.is_delayed,
|
|
217
|
+
is_auto_reply=msg.is_auto_reply,
|
|
218
|
+
is_prepared=msg.is_prepared,
|
|
219
|
+
is_read=msg.is_read,
|
|
220
|
+
is_system_message=msg.is_system_message,
|
|
221
|
+
is_sent=msg.is_sent,
|
|
222
|
+
has_dd_results=msg.has_dd_results,
|
|
223
|
+
is_service_message=msg.is_service_message,
|
|
224
|
+
is_forward=msg.is_forward,
|
|
225
|
+
was_downgraded=msg.was_downgraded,
|
|
226
|
+
is_archive=msg.is_archive,
|
|
227
|
+
was_data_detected=msg.was_data_detected,
|
|
228
|
+
was_deduplicated=msg.was_deduplicated,
|
|
229
|
+
is_audio_message=msg.is_audio_message,
|
|
230
|
+
is_played=msg.is_played,
|
|
231
|
+
date_played=msg.date_played,
|
|
232
|
+
item_type=msg.item_type,
|
|
233
|
+
other_handle=msg.other_handle,
|
|
234
|
+
other_handle_natural_id=other_handle_natural_id,
|
|
235
|
+
other_handle_natural_service=other_handle_natural_service,
|
|
236
|
+
group_title=msg.group_title,
|
|
237
|
+
group_action_type=msg.group_action_type,
|
|
238
|
+
share_status=msg.share_status,
|
|
239
|
+
share_direction=msg.share_direction,
|
|
240
|
+
is_expirable=msg.is_expirable,
|
|
241
|
+
expire_state=msg.expire_state,
|
|
242
|
+
message_action_type=msg.message_action_type,
|
|
243
|
+
message_source=msg.message_source,
|
|
244
|
+
associated_message_guid=msg.associated_message_guid,
|
|
245
|
+
associated_message_type=msg.associated_message_type,
|
|
246
|
+
balloon_bundle_id=msg.balloon_bundle_id,
|
|
247
|
+
payload_data=msg.payload_data,
|
|
248
|
+
expressive_send_style_id=msg.expressive_send_style_id,
|
|
249
|
+
associated_message_range_location=msg.associated_message_range_location,
|
|
250
|
+
associated_message_range_length=msg.associated_message_range_length,
|
|
251
|
+
time_expressive_send_played=msg.time_expressive_send_played,
|
|
252
|
+
message_summary_info=msg.message_summary_info,
|
|
253
|
+
destination_caller_id=msg.destination_caller_id,
|
|
254
|
+
is_corrupt=msg.is_corrupt,
|
|
255
|
+
reply_to_guid=msg.reply_to_guid,
|
|
256
|
+
sort_id=msg.sort_id,
|
|
257
|
+
is_spam=msg.is_spam,
|
|
258
|
+
has_unseen_mention=msg.has_unseen_mention,
|
|
259
|
+
thread_originator_guid=msg.thread_originator_guid,
|
|
260
|
+
thread_originator_part=msg.thread_originator_part,
|
|
261
|
+
syndication_ranges=msg.syndication_ranges,
|
|
262
|
+
was_delivered_quietly=msg.was_delivered_quietly,
|
|
263
|
+
did_notify_recipient=msg.did_notify_recipient,
|
|
264
|
+
synced_syndication_ranges=msg.synced_syndication_ranges,
|
|
265
|
+
date_retracted=msg.date_retracted,
|
|
266
|
+
date_edited=msg.date_edited,
|
|
267
|
+
was_detonated=msg.was_detonated,
|
|
268
|
+
part_count=msg.part_count,
|
|
269
|
+
is_stewie=msg.is_stewie,
|
|
270
|
+
is_kt_verified=msg.is_kt_verified,
|
|
271
|
+
is_sos=msg.is_sos,
|
|
272
|
+
is_critical=msg.is_critical,
|
|
273
|
+
bia_reference_id=msg.bia_reference_id,
|
|
274
|
+
fallback_hash=msg.fallback_hash,
|
|
275
|
+
associated_message_emoji=msg.associated_message_emoji,
|
|
276
|
+
is_pending_satellite_send=msg.is_pending_satellite_send,
|
|
277
|
+
needs_relay=msg.needs_relay,
|
|
278
|
+
schedule_type=msg.schedule_type,
|
|
279
|
+
schedule_state=msg.schedule_state,
|
|
280
|
+
sent_or_received_off_grid=msg.sent_or_received_off_grid,
|
|
281
|
+
date_recovered=msg.date_recovered,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def chat_handle_joins_to_ctx_models(
|
|
286
|
+
*,
|
|
287
|
+
binding_id: str,
|
|
288
|
+
rows: Iterable[IMessageChatHandleJoin],
|
|
289
|
+
chat_lookup: Mapping[int, str],
|
|
290
|
+
handle_lookup: Mapping[int, tuple[str, str]],
|
|
291
|
+
) -> Iterator[ChatHandleJoinRow]:
|
|
292
|
+
"""Resolve chat rowid -> guid and handle rowid -> (id, service).
|
|
293
|
+
|
|
294
|
+
Raises if a join row references a missing chat or handle — the source
|
|
295
|
+
enforces FK integrity, so an unresolvable row indicates either
|
|
296
|
+
corruption or a pre-load race. Fail loudly per the "errors must be
|
|
297
|
+
loud" rule.
|
|
298
|
+
"""
|
|
299
|
+
for row in rows:
|
|
300
|
+
chat_guid = chat_lookup.get(row.chat_id)
|
|
301
|
+
if chat_guid is None:
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"chat_handle_join row references missing chat_id={row.chat_id}"
|
|
304
|
+
)
|
|
305
|
+
handle_pair = handle_lookup.get(row.handle_id)
|
|
306
|
+
if handle_pair is None:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"chat_handle_join row references missing handle_id={row.handle_id}"
|
|
309
|
+
)
|
|
310
|
+
handle_natural_id, handle_natural_service = handle_pair
|
|
311
|
+
yield ChatHandleJoinRow(
|
|
312
|
+
ctx_binding_id=binding_id,
|
|
313
|
+
ctx_source_updated_at=None,
|
|
314
|
+
chat_id=row.chat_id,
|
|
315
|
+
chat_guid=chat_guid,
|
|
316
|
+
handle_id=row.handle_id,
|
|
317
|
+
handle_natural_id=handle_natural_id,
|
|
318
|
+
handle_natural_service=handle_natural_service,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def chat_message_joins_to_ctx_models(
|
|
323
|
+
*,
|
|
324
|
+
binding_id: str,
|
|
325
|
+
rows: Iterable[IMessageChatMessageJoin],
|
|
326
|
+
chat_lookup: Mapping[int, str],
|
|
327
|
+
message_lookup: Mapping[int, str],
|
|
328
|
+
) -> Iterator[ChatMessageJoinRow]:
|
|
329
|
+
"""Resolve chat rowid -> guid and message rowid -> guid.
|
|
330
|
+
|
|
331
|
+
Fails loudly on unresolvable rowids — the source declares FKs and the
|
|
332
|
+
triggers CASCADE on delete, so missing refs indicate corruption.
|
|
333
|
+
"""
|
|
334
|
+
for row in rows:
|
|
335
|
+
chat_guid = chat_lookup.get(row.chat_id)
|
|
336
|
+
if chat_guid is None:
|
|
337
|
+
raise RuntimeError(
|
|
338
|
+
f"chat_message_join row references missing chat_id={row.chat_id}"
|
|
339
|
+
)
|
|
340
|
+
message_guid = message_lookup.get(row.message_id)
|
|
341
|
+
if message_guid is None:
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
f"chat_message_join row references missing message_id={row.message_id}"
|
|
344
|
+
)
|
|
345
|
+
yield ChatMessageJoinRow(
|
|
346
|
+
ctx_binding_id=binding_id,
|
|
347
|
+
ctx_source_updated_at=row.message_date,
|
|
348
|
+
chat_id=row.chat_id,
|
|
349
|
+
chat_guid=chat_guid,
|
|
350
|
+
message_id=row.message_id,
|
|
351
|
+
message_guid=message_guid,
|
|
352
|
+
message_date=row.message_date,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def message_attachment_joins_to_ctx_models(
|
|
357
|
+
*,
|
|
358
|
+
binding_id: str,
|
|
359
|
+
rows: Iterable[IMessageMessageAttachmentJoin],
|
|
360
|
+
message_lookup: Mapping[int, str],
|
|
361
|
+
attachment_lookup: Mapping[int, str],
|
|
362
|
+
) -> Iterator[MessageAttachmentJoinRow]:
|
|
363
|
+
"""Resolve message rowid -> guid and attachment rowid -> guid."""
|
|
364
|
+
for row in rows:
|
|
365
|
+
message_guid = message_lookup.get(row.message_id)
|
|
366
|
+
if message_guid is None:
|
|
367
|
+
raise RuntimeError(
|
|
368
|
+
f"message_attachment_join row references missing "
|
|
369
|
+
f"message_id={row.message_id}"
|
|
370
|
+
)
|
|
371
|
+
attachment_guid = attachment_lookup.get(row.attachment_id)
|
|
372
|
+
if attachment_guid is None:
|
|
373
|
+
raise RuntimeError(
|
|
374
|
+
f"message_attachment_join row references missing "
|
|
375
|
+
f"attachment_id={row.attachment_id}"
|
|
376
|
+
)
|
|
377
|
+
yield MessageAttachmentJoinRow(
|
|
378
|
+
ctx_binding_id=binding_id,
|
|
379
|
+
ctx_source_updated_at=None,
|
|
380
|
+
message_id=row.message_id,
|
|
381
|
+
message_guid=message_guid,
|
|
382
|
+
attachment_id=row.attachment_id,
|
|
383
|
+
attachment_guid=attachment_guid,
|
|
384
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import dlt
|
|
10
|
+
from shared_plugins.naming import (
|
|
11
|
+
dlt_resource_name,
|
|
12
|
+
dlt_source_name,
|
|
13
|
+
plugin_id_from_module,
|
|
14
|
+
)
|
|
15
|
+
from shared_plugins.resources import ctx_dlt_resource
|
|
16
|
+
from shared_plugins.sqlite import sqlite_snapshot
|
|
17
|
+
from sqlalchemy import Engine, create_engine, select
|
|
18
|
+
from sqlalchemy.orm import Session
|
|
19
|
+
|
|
20
|
+
from ..binding_config import IMessageLocalBindingConfig
|
|
21
|
+
from ..models.ctx import (
|
|
22
|
+
AttachmentRow,
|
|
23
|
+
ChatHandleJoinRow,
|
|
24
|
+
ChatMessageJoinRow,
|
|
25
|
+
ChatRow,
|
|
26
|
+
HandleRow,
|
|
27
|
+
MessageAttachmentJoinRow,
|
|
28
|
+
MessageRow,
|
|
29
|
+
)
|
|
30
|
+
from ..models.ingress import (
|
|
31
|
+
IMessageAttachment,
|
|
32
|
+
IMessageChat,
|
|
33
|
+
IMessageChatHandleJoin,
|
|
34
|
+
IMessageChatMessageJoin,
|
|
35
|
+
IMessageHandle,
|
|
36
|
+
IMessageMessage,
|
|
37
|
+
IMessageMessageAttachmentJoin,
|
|
38
|
+
)
|
|
39
|
+
from ..models.translators import (
|
|
40
|
+
attachments_to_ctx_models,
|
|
41
|
+
chat_handle_joins_to_ctx_models,
|
|
42
|
+
chat_message_joins_to_ctx_models,
|
|
43
|
+
chats_to_ctx_models,
|
|
44
|
+
handles_to_ctx_models,
|
|
45
|
+
message_attachment_joins_to_ctx_models,
|
|
46
|
+
messages_to_ctx_models,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
PLUGIN_ID = plugin_id_from_module(__file__)
|
|
50
|
+
JOB = "snapshot"
|
|
51
|
+
LOGGER = logging.getLogger(__name__)
|
|
52
|
+
MERGE_WRITE_DISPOSITION = {"disposition": "merge", "strategy": "delete-insert"}
|
|
53
|
+
MERGE_KEY = ("_ctx_binding_id",)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@contextmanager
|
|
57
|
+
def _db_snapshot(db_path: Path) -> Iterator[Engine]:
|
|
58
|
+
if not db_path.is_file():
|
|
59
|
+
raise RuntimeError(f"Missing iMessage database: '{db_path}'.")
|
|
60
|
+
with sqlite_snapshot(db_path) as snapshot_path:
|
|
61
|
+
engine = create_engine(f"sqlite:///{snapshot_path}")
|
|
62
|
+
try:
|
|
63
|
+
yield engine
|
|
64
|
+
finally:
|
|
65
|
+
engine.dispose()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dlt.source(name=dlt_source_name(PLUGIN_ID, JOB))
|
|
69
|
+
def imessage_local_snapshot_source(
|
|
70
|
+
binding_id: str,
|
|
71
|
+
cfg: IMessageLocalBindingConfig,
|
|
72
|
+
) -> tuple[Any, ...]:
|
|
73
|
+
chat_db = cfg.chat_db
|
|
74
|
+
|
|
75
|
+
@ctx_dlt_resource(
|
|
76
|
+
name=dlt_resource_name("handle"),
|
|
77
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
78
|
+
merge_key=MERGE_KEY,
|
|
79
|
+
primary_key=("_ctx_binding_id", "id", "service"),
|
|
80
|
+
)
|
|
81
|
+
def handle_resource() -> Iterator[HandleRow]:
|
|
82
|
+
with _db_snapshot(chat_db) as engine:
|
|
83
|
+
with Session(engine) as session:
|
|
84
|
+
yield from handles_to_ctx_models(
|
|
85
|
+
binding_id=binding_id,
|
|
86
|
+
rows=session.scalars(select(IMessageHandle)),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@ctx_dlt_resource(
|
|
90
|
+
name=dlt_resource_name("chat"),
|
|
91
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
92
|
+
merge_key=MERGE_KEY,
|
|
93
|
+
primary_key=("_ctx_binding_id", "guid"),
|
|
94
|
+
)
|
|
95
|
+
def chat_resource() -> Iterator[ChatRow]:
|
|
96
|
+
with _db_snapshot(chat_db) as engine:
|
|
97
|
+
with Session(engine) as session:
|
|
98
|
+
yield from chats_to_ctx_models(
|
|
99
|
+
binding_id=binding_id,
|
|
100
|
+
rows=session.scalars(select(IMessageChat)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@ctx_dlt_resource(
|
|
104
|
+
name=dlt_resource_name("attachment"),
|
|
105
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
106
|
+
merge_key=MERGE_KEY,
|
|
107
|
+
primary_key=("_ctx_binding_id", "guid"),
|
|
108
|
+
)
|
|
109
|
+
def attachment_resource() -> Iterator[AttachmentRow]:
|
|
110
|
+
with _db_snapshot(chat_db) as engine:
|
|
111
|
+
with Session(engine) as session:
|
|
112
|
+
yield from attachments_to_ctx_models(
|
|
113
|
+
binding_id=binding_id,
|
|
114
|
+
rows=session.scalars(select(IMessageAttachment)),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@ctx_dlt_resource(
|
|
118
|
+
name=dlt_resource_name("message"),
|
|
119
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
120
|
+
merge_key=MERGE_KEY,
|
|
121
|
+
primary_key=("_ctx_binding_id", "guid"),
|
|
122
|
+
)
|
|
123
|
+
def message_resource() -> Iterator[MessageRow]:
|
|
124
|
+
with _db_snapshot(chat_db) as engine:
|
|
125
|
+
with Session(engine) as session:
|
|
126
|
+
handle_lookup = {
|
|
127
|
+
rowid: (id_, service)
|
|
128
|
+
for rowid, id_, service in session.execute(
|
|
129
|
+
select(
|
|
130
|
+
IMessageHandle.rowid,
|
|
131
|
+
IMessageHandle.id,
|
|
132
|
+
IMessageHandle.service,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
yield from messages_to_ctx_models(
|
|
137
|
+
binding_id=binding_id,
|
|
138
|
+
rows=session.scalars(select(IMessageMessage)),
|
|
139
|
+
handle_lookup=handle_lookup,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@ctx_dlt_resource(
|
|
143
|
+
name=dlt_resource_name("chat_handle_join"),
|
|
144
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
145
|
+
merge_key=MERGE_KEY,
|
|
146
|
+
primary_key=(
|
|
147
|
+
"_ctx_binding_id",
|
|
148
|
+
"chat_guid",
|
|
149
|
+
"handle_natural_id",
|
|
150
|
+
"handle_natural_service",
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
def chat_handle_join_resource() -> Iterator[ChatHandleJoinRow]:
|
|
154
|
+
with _db_snapshot(chat_db) as engine:
|
|
155
|
+
with Session(engine) as session:
|
|
156
|
+
chat_lookup = dict(
|
|
157
|
+
session.execute(select(IMessageChat.rowid, IMessageChat.guid)).all()
|
|
158
|
+
)
|
|
159
|
+
handle_lookup = {
|
|
160
|
+
rowid: (id_, service)
|
|
161
|
+
for rowid, id_, service in session.execute(
|
|
162
|
+
select(
|
|
163
|
+
IMessageHandle.rowid,
|
|
164
|
+
IMessageHandle.id,
|
|
165
|
+
IMessageHandle.service,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
yield from chat_handle_joins_to_ctx_models(
|
|
170
|
+
binding_id=binding_id,
|
|
171
|
+
rows=session.scalars(select(IMessageChatHandleJoin)),
|
|
172
|
+
chat_lookup=chat_lookup,
|
|
173
|
+
handle_lookup=handle_lookup,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@ctx_dlt_resource(
|
|
177
|
+
name=dlt_resource_name("chat_message_join"),
|
|
178
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
179
|
+
merge_key=MERGE_KEY,
|
|
180
|
+
primary_key=("_ctx_binding_id", "chat_guid", "message_guid"),
|
|
181
|
+
)
|
|
182
|
+
def chat_message_join_resource() -> Iterator[ChatMessageJoinRow]:
|
|
183
|
+
with _db_snapshot(chat_db) as engine:
|
|
184
|
+
with Session(engine) as session:
|
|
185
|
+
chat_lookup = dict(
|
|
186
|
+
session.execute(select(IMessageChat.rowid, IMessageChat.guid)).all()
|
|
187
|
+
)
|
|
188
|
+
message_lookup = dict(
|
|
189
|
+
session.execute(
|
|
190
|
+
select(IMessageMessage.rowid, IMessageMessage.guid)
|
|
191
|
+
).all()
|
|
192
|
+
)
|
|
193
|
+
yield from chat_message_joins_to_ctx_models(
|
|
194
|
+
binding_id=binding_id,
|
|
195
|
+
rows=session.scalars(select(IMessageChatMessageJoin)),
|
|
196
|
+
chat_lookup=chat_lookup,
|
|
197
|
+
message_lookup=message_lookup,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@ctx_dlt_resource(
|
|
201
|
+
name=dlt_resource_name("message_attachment_join"),
|
|
202
|
+
write_disposition=MERGE_WRITE_DISPOSITION,
|
|
203
|
+
merge_key=MERGE_KEY,
|
|
204
|
+
primary_key=("_ctx_binding_id", "message_guid", "attachment_guid"),
|
|
205
|
+
)
|
|
206
|
+
def message_attachment_join_resource() -> Iterator[MessageAttachmentJoinRow]:
|
|
207
|
+
with _db_snapshot(chat_db) as engine:
|
|
208
|
+
with Session(engine) as session:
|
|
209
|
+
message_lookup = dict(
|
|
210
|
+
session.execute(
|
|
211
|
+
select(IMessageMessage.rowid, IMessageMessage.guid)
|
|
212
|
+
).all()
|
|
213
|
+
)
|
|
214
|
+
attachment_lookup = dict(
|
|
215
|
+
session.execute(
|
|
216
|
+
select(IMessageAttachment.rowid, IMessageAttachment.guid)
|
|
217
|
+
).all()
|
|
218
|
+
)
|
|
219
|
+
yield from message_attachment_joins_to_ctx_models(
|
|
220
|
+
binding_id=binding_id,
|
|
221
|
+
rows=session.scalars(select(IMessageMessageAttachmentJoin)),
|
|
222
|
+
message_lookup=message_lookup,
|
|
223
|
+
attachment_lookup=attachment_lookup,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
handle_resource,
|
|
228
|
+
chat_resource,
|
|
229
|
+
attachment_resource,
|
|
230
|
+
message_resource,
|
|
231
|
+
chat_handle_join_resource,
|
|
232
|
+
chat_message_join_resource,
|
|
233
|
+
message_attachment_join_resource,
|
|
234
|
+
)
|