contextbase-plugin-whatsapp-local 0.2.8__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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.3
2
+ Name: contextbase-plugin-whatsapp-local
3
+ Version: 0.2.8
4
+ Summary: WhatsApp local plugin for ContextBase
5
+ Author: Alizain Feerasta
6
+ Author-email: Alizain Feerasta <alizain.feerasta@gmail.com>
7
+ Requires-Dist: contextbase-shared-plugins==0.2.8
8
+ Requires-Dist: dagster==1.12.14
9
+ Requires-Dist: dagster-dlt==0.28.14
10
+ Requires-Dist: dlt>=1.26.0
11
+ Requires-Dist: pydantic>=2.12.0
12
+ Requires-Dist: sqlalchemy>=2.0.0
13
+ Requires-Python: >=3.14, <3.15
@@ -0,0 +1,16 @@
1
+ plugin_whatsapp_local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ plugin_whatsapp_local/binding_config.py,sha256=HKdBxiJBVodGfmKU81wMEgqkVVAe2K8-b-D4OGQL1g8,442
3
+ plugin_whatsapp_local/component.py,sha256=PaRcFXZmLtlAJQqyJgZ82Mx9Zkqqwt_OhBabegFZw2Y,4753
4
+ plugin_whatsapp_local/defs/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ plugin_whatsapp_local/defs/defs.yaml,sha256=38drNqOp9m6Xxo8pIWwX66iyQQlT-Q8MUqjBoQZ5uc4,66
6
+ plugin_whatsapp_local/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ plugin_whatsapp_local/models/base.py,sha256=Fp7n4EfxQOxzqm34-D-rYyu60P88eZUIewCBNP_Iuc4,119
8
+ plugin_whatsapp_local/models/ctx.py,sha256=tYoXhusFPtBAbGo1NbrtpKFfWfIez6MTPpoytqfLExA,8511
9
+ plugin_whatsapp_local/models/ingress.py,sha256=kbaUxUyP9VI_UNQMdYhES2xcbJQ_2eOoK7PDTXQFVHk,14763
10
+ plugin_whatsapp_local/models/translators.py,sha256=gIeqiQjnmI7poAniTKyoukeV_pZhiOFZp4DCSIVAnqk,10138
11
+ plugin_whatsapp_local/plugin.json,sha256=xb5rdK4thxvUaS18kYgiua4d7Ac-QnUA5P6beCUiZXY,87
12
+ plugin_whatsapp_local/sources/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
13
+ plugin_whatsapp_local/sources/snapshot.py,sha256=3iDy_FeIUZSzqUJfdiuZaKWP1yLVkkJ4Uwa6SI3wNzU,8969
14
+ contextbase_plugin_whatsapp_local-0.2.8.dist-info/WHEEL,sha256=i9aSRDivn5iP9LaR1BLQX2GNAuriQWPsFwbbWygTX2k,81
15
+ contextbase_plugin_whatsapp_local-0.2.8.dist-info/METADATA,sha256=otkPzLQCU92lRUrGp_8v-5i_duodSSf69U6HEu8Xzi4,449
16
+ contextbase_plugin_whatsapp_local-0.2.8.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
File without changes
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pydantic import Field
6
+
7
+ from shared_plugins.bindings import BaseBindingConfigModel, ResolvedPath
8
+
9
+
10
+ class WhatsAppLocalBindingConfig(BaseBindingConfigModel):
11
+ data_dir: ResolvedPath = Field(
12
+ default_factory=lambda: (
13
+ Path.home()
14
+ / "Library"
15
+ / "Group Containers"
16
+ / "group.net.whatsapp.WhatsApp.shared"
17
+ ),
18
+ )
@@ -0,0 +1,137 @@
1
+ import dagster as dg
2
+ from dagster import AssetExecutionContext
3
+ from dagster_dlt import DagsterDltResource
4
+ from shared_plugins.automation import non_overlapping_automation_condition
5
+ from shared_plugins.bindings import parse_binding_config
6
+ from shared_plugins.control_plane import ControlPlaneClient
7
+ from shared_plugins.dlt import resolve_partition_binding, run_dlt_pipeline
8
+ from shared_plugins.naming import (
9
+ dagster_asset_group_name,
10
+ dagster_asset_tags,
11
+ dagster_dlt_asset_key,
12
+ dagster_partition_def_name,
13
+ dagster_pool_name,
14
+ dlt_source_name,
15
+ plugin_id_from_module,
16
+ )
17
+ from shared_plugins.resources import DLT_RESOURCE
18
+
19
+ from .binding_config import WhatsAppLocalBindingConfig
20
+ from .sources.snapshot import whatsapp_local_snapshot_source
21
+
22
+ PLUGIN_ID = plugin_id_from_module(__file__)
23
+ SNAPSHOT_JOB = "snapshot"
24
+
25
+
26
+ def _build_snapshot_specs(
27
+ partitions_def: dg.PartitionsDefinition,
28
+ automation_condition: dg.AutomationCondition,
29
+ ) -> list[dg.AssetSpec]:
30
+ snapshot_source_name = dlt_source_name(PLUGIN_ID, SNAPSHOT_JOB)
31
+ shared = dict(
32
+ group_name=dagster_asset_group_name(PLUGIN_ID),
33
+ tags=dagster_asset_tags(PLUGIN_ID),
34
+ automation_condition=automation_condition,
35
+ partitions_def=partitions_def,
36
+ )
37
+
38
+ return [
39
+ dg.AssetSpec(
40
+ key=dagster_dlt_asset_key(snapshot_source_name, "chat_session"),
41
+ **shared,
42
+ ),
43
+ dg.AssetSpec(
44
+ key=dagster_dlt_asset_key(snapshot_source_name, "message"),
45
+ **shared,
46
+ ),
47
+ dg.AssetSpec(
48
+ key=dagster_dlt_asset_key(snapshot_source_name, "contact"),
49
+ **shared,
50
+ ),
51
+ dg.AssetSpec(
52
+ key=dagster_dlt_asset_key(snapshot_source_name, "group_info"),
53
+ **shared,
54
+ ),
55
+ dg.AssetSpec(
56
+ key=dagster_dlt_asset_key(snapshot_source_name, "group_member"),
57
+ **shared,
58
+ ),
59
+ dg.AssetSpec(
60
+ key=dagster_dlt_asset_key(snapshot_source_name, "profile_push_name"),
61
+ **shared,
62
+ ),
63
+ dg.AssetSpec(
64
+ key=dagster_dlt_asset_key(snapshot_source_name, "message_data_item"),
65
+ **shared,
66
+ ),
67
+ dg.AssetSpec(
68
+ key=dagster_dlt_asset_key(snapshot_source_name, "aggregate_call_event"),
69
+ **shared,
70
+ ),
71
+ dg.AssetSpec(
72
+ key=dagster_dlt_asset_key(snapshot_source_name, "call_event"),
73
+ **shared,
74
+ ),
75
+ dg.AssetSpec(
76
+ key=dagster_dlt_asset_key(snapshot_source_name, "call_event_participant"),
77
+ **shared,
78
+ ),
79
+ ]
80
+
81
+
82
+ class WhatsAppLocalSyncComponent(dg.Component):
83
+ def build_defs(self, context: dg.ComponentLoadContext) -> dg.Definitions:
84
+ partitions_def = dg.DynamicPartitionsDefinition(
85
+ name=dagster_partition_def_name(PLUGIN_ID)
86
+ )
87
+ snapshot_specs = _build_snapshot_specs(
88
+ partitions_def=partitions_def,
89
+ automation_condition=non_overlapping_automation_condition(
90
+ dg.AutomationCondition.on_missing()
91
+ | dg.AutomationCondition.on_cron("*/15 * * * *")
92
+ ),
93
+ )
94
+
95
+ @dg.multi_asset(
96
+ specs=snapshot_specs,
97
+ can_subset=True,
98
+ name="whatsapp_local_snapshot",
99
+ pool=dagster_pool_name(PLUGIN_ID),
100
+ )
101
+ def whatsapp_local_snapshot_assets(
102
+ context: AssetExecutionContext,
103
+ dlt_resource: DagsterDltResource,
104
+ control_plane: dg.ResourceParam[ControlPlaneClient],
105
+ ):
106
+ binding = resolve_partition_binding(
107
+ context=context,
108
+ control_plane=control_plane,
109
+ plugin_id=PLUGIN_ID,
110
+ )
111
+ binding_id = str(binding.binding_id)
112
+ cfg = parse_binding_config(binding, WhatsAppLocalBindingConfig)
113
+
114
+ source = whatsapp_local_snapshot_source(binding_id, cfg)
115
+ yield from run_dlt_pipeline(
116
+ context=context,
117
+ dlt_resource=dlt_resource,
118
+ source=source,
119
+ plugin_id=PLUGIN_ID,
120
+ binding_id=binding_id,
121
+ job_name=SNAPSHOT_JOB,
122
+ )
123
+
124
+ automation_sensor = dg.AutomationConditionSensorDefinition(
125
+ name="whatsapp_local_automation_sensor",
126
+ target=dg.AssetSelection.assets(whatsapp_local_snapshot_assets),
127
+ default_status=dg.DefaultSensorStatus.RUNNING,
128
+ minimum_interval_seconds=30,
129
+ )
130
+
131
+ return dg.Definitions(
132
+ assets=[whatsapp_local_snapshot_assets],
133
+ sensors=[automation_sensor],
134
+ resources={
135
+ "dlt_resource": DLT_RESOURCE,
136
+ },
137
+ )
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,2 @@
1
+ type: plugin_whatsapp_local.component.WhatsAppLocalSyncComponent
2
+
File without changes
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.orm import DeclarativeBase
4
+
5
+
6
+ class Base(DeclarativeBase):
7
+ pass
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import AwareDatetime
4
+ from shared_plugins.models import CtxModel
5
+
6
+
7
+ class ChatSessionRow(CtxModel):
8
+ pk: int
9
+ ent: int | None = None
10
+ opt: int | None = None
11
+ archived: int | None = None
12
+ contact_abid: int | None = None
13
+ flags: int | None = None
14
+ hidden: int | None = None
15
+ identity_verification_epoch: int | None = None
16
+ identity_verification_state: int | None = None
17
+ message_counter: int | None = None
18
+ removed: int | None = None
19
+ session_type: int | None = None
20
+ spotlight_status: int | None = None
21
+ unread_count: int | None = None
22
+ group_info_pk: int | None = None
23
+ last_message_pk: int | None = None
24
+ properties_pk: int | None = None
25
+ last_message_date: AwareDatetime | None = None
26
+ location_sharing_end_date: AwareDatetime | None = None
27
+ contact_identifier: str | None = None
28
+ contact_jid: str | None = None
29
+ etag: str | None = None
30
+ last_message_text: str | None = None
31
+ partner_name: str | None = None
32
+ saved_input: str | None = None
33
+
34
+
35
+ class MessageRow(CtxModel):
36
+ pk: int
37
+ ent: int | None = None
38
+ opt: int | None = None
39
+ child_messages_delivered_count: int | None = None
40
+ child_messages_played_count: int | None = None
41
+ child_messages_read_count: int | None = None
42
+ data_item_version: int | None = None
43
+ doc_id: int | None = None
44
+ enc_retry_count: int | None = None
45
+ filtered_recipient_count: int | None = None
46
+ flags: int | None = None
47
+ group_event_type: int | None = None
48
+ is_from_me: int | None = None
49
+ message_error_status: int | None = None
50
+ message_status: int | None = None
51
+ message_type: int | None = None
52
+ sort: int | None = None
53
+ spotlight_status: int | None = None
54
+ starred: int | None = None
55
+ chat_session_pk: int | None = None
56
+ group_member_pk: int | None = None
57
+ last_session_pk: int | None = None
58
+ media_item_pk: int | None = None
59
+ message_info_pk: int | None = None
60
+ parent_message_pk: int | None = None
61
+ message_date: AwareDatetime | None = None
62
+ sent_date: AwareDatetime | None = None
63
+ from_jid: str | None = None
64
+ media_section_id: str | None = None
65
+ phash: str | None = None
66
+ stanza_id: str | None = None
67
+ text: str | None = None
68
+ to_jid: str | None = None
69
+
70
+
71
+ class ContactRow(CtxModel):
72
+ pk: int
73
+ ent: int | None = None
74
+ opt: int | None = None
75
+ disappearing_mode_duration: int | None = None
76
+ phone_status: int | None = None
77
+ section_sort: int | None = None
78
+ sort: int | None = None
79
+ source: int | None = None
80
+ spotlight_status: int | None = None
81
+ sync_policy: int | None = None
82
+ sync_with_server_state: int | None = None
83
+ about_expiration_timestamp: AwareDatetime | None = None
84
+ about_timestamp: AwareDatetime | None = None
85
+ disappearing_mode_timestamp: AwareDatetime | None = None
86
+ last_updated: AwareDatetime | None = None
87
+ about_emoji: str | None = None
88
+ about_text: str | None = None
89
+ business_name: str | None = None
90
+ full_name: str | None = None
91
+ given_name: str | None = None
92
+ highlighted_name: str | None = None
93
+ identifier: str | None = None
94
+ last_name: str | None = None
95
+ lid: str | None = None
96
+ lid_hash: str | None = None
97
+ localized_phone_number: str | None = None
98
+ notes: str | None = None
99
+ phone_number: str | None = None
100
+ phone_number_label: str | None = None
101
+ pn_hash: str | None = None
102
+ search_token_list: str | None = None
103
+ section_title: str | None = None
104
+ unique_id: str | None = None
105
+ username: str | None = None
106
+ whatsapp_id: str | None = None
107
+ device_list: bytes | None = None
108
+
109
+
110
+ class GroupInfoRow(CtxModel):
111
+ pk: int
112
+ ent: int | None = None
113
+ opt: int | None = None
114
+ state: int | None = None
115
+ chat_session_pk: int | None = None
116
+ last_message_owner_pk: int | None = None
117
+ creation_date: AwareDatetime | None = None
118
+ subject_timestamp: AwareDatetime | None = None
119
+ creator_jid: str | None = None
120
+ owner_jid: str | None = None
121
+ picture_id: str | None = None
122
+ picture_path: str | None = None
123
+ source_jid: str | None = None
124
+ subject_owner_jid: str | None = None
125
+
126
+
127
+ class GroupMemberRow(CtxModel):
128
+ pk: int
129
+ ent: int | None = None
130
+ opt: int | None = None
131
+ contact_abid: int | None = None
132
+ is_active: int | None = None
133
+ is_admin: int | None = None
134
+ sender_key_sent: int | None = None
135
+ chat_session_pk: int | None = None
136
+ recent_group_chat_pk: int | None = None
137
+ contact_identifier: str | None = None
138
+ member_jid: str | None = None
139
+
140
+
141
+ class ProfilePushNameRow(CtxModel):
142
+ pk: int
143
+ ent: int | None = None
144
+ opt: int | None = None
145
+ jid: str | None = None
146
+ push_name: str | None = None
147
+
148
+
149
+ class MessageDataItemRow(CtxModel):
150
+ pk: int
151
+ ent: int | None = None
152
+ opt: int | None = None
153
+ index: int | None = None
154
+ owns_thumbnail: int | None = None
155
+ type: int | None = None
156
+ message_pk: int | None = None
157
+ date: AwareDatetime | None = None
158
+ chat_jid: str | None = None
159
+ content1: str | None = None
160
+ content2: str | None = None
161
+ matched_text: str | None = None
162
+ section_id: str | None = None
163
+ sender_jid: str | None = None
164
+ summary: str | None = None
165
+ thumbnail_path: str | None = None
166
+ title: str | None = None
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # CallHistory.sqlite
171
+ # ---------------------------------------------------------------------------
172
+
173
+
174
+ class AggregateCallEventRow(CtxModel):
175
+ pk: int
176
+ ent: int | None = None
177
+ opt: int | None = None
178
+ incoming: int | None = None
179
+ missed: int | None = None
180
+ missed_reason: int | None = None
181
+ video: int | None = None
182
+ first_date: AwareDatetime | None = None
183
+ link_token: str | None = None
184
+
185
+
186
+ class CallEventRow(CtxModel):
187
+ pk: int
188
+ ent: int | None = None
189
+ opt: int | None = None
190
+ bytes_received: int | None = None
191
+ bytes_sent: int | None = None
192
+ is_lightweight: int | None = None
193
+ outcome: int | None = None
194
+ aggregate_call_event_pk: int | None = None
195
+ aggregate_call_event_fok: int | None = None
196
+ date: AwareDatetime | None = None
197
+ duration: float | None = None
198
+ call_id_string: str | None = None
199
+ group_call_creator_user_jid_string: str | None = None
200
+ group_jid_string: str | None = None
201
+ reminder_id: str | None = None
202
+ scheduled_id: str | None = None
203
+
204
+
205
+ class CallEventParticipantRow(CtxModel):
206
+ pk: int
207
+ ent: int | None = None
208
+ opt: int | None = None
209
+ outcome: int | None = None
210
+ call_event_pk: int | None = None
211
+ call_event_fok: int | None = None
212
+ jid_string: str | None = None
213
+
214
+
215
+ CHAT_SESSION_COLUMNS: dict[str, dict[str, str]] = {
216
+ "session_type": {
217
+ "description": "1 = group chat (1:1 with group_info), 0 = 1:1 DM. Other values exist for broadcasts/statuses."
218
+ },
219
+ }
220
+
221
+
222
+ MESSAGE_COLUMNS: dict[str, dict[str, str]] = {
223
+ "is_from_me": {
224
+ "description": "When 1, the phone owner sent the message and group_member_pk is NULL. When 0, resolve sender via group_member_pk."
225
+ },
226
+ "from_jid": {
227
+ "description": "In group messages this is the GROUP's jid, not the sender's. Use group_member_pk for sender identity."
228
+ },
229
+ }
230
+
231
+
232
+ GROUP_MEMBER_COLUMNS: dict[str, dict[str, str]] = {
233
+ "member_jid": {
234
+ "description": "Join to contact via lid for @lid jids, via whatsapp_id for @s.whatsapp.net jids — neither column alone covers both."
235
+ },
236
+ }
237
+
238
+
239
+ AGGREGATE_CALL_EVENT_COLUMNS: dict[str, dict[str, str]] = {
240
+ "incoming": {
241
+ "description": "1 if the call group was received, 0 if placed by self. Combine with call_event.group_call_creator_user_jid_string to identify the peer."
242
+ },
243
+ }
244
+
245
+
246
+ CALL_EVENT_COLUMNS: dict[str, dict[str, str]] = {
247
+ "group_call_creator_user_jid_string": {
248
+ "description": "Call initiator's jid — set for ALL calls despite the name, not just group calls. For aggregate_call_event.incoming=0 (outgoing) it equals self; for incoming=1 it equals the peer."
249
+ },
250
+ "outcome": {
251
+ "description": "Undocumented enum at the source (values 0, 1, 4, 5 observed). Prefer `duration > 0` to detect an answered call."
252
+ },
253
+ }
254
+
255
+
256
+ CALL_EVENT_PARTICIPANT_COLUMNS: dict[str, dict[str, str]] = {
257
+ "jid_string": {
258
+ "description": "Other-party jid for this call leg. For outgoing 1:1 this is the peer; if it differs from call_event.group_call_creator_user_jid_string on an incoming call, the call is almost always multi-party (not 1:1)."
259
+ },
260
+ }