diff --git a/.gitignore b/.gitignore index b07fe68..be91e04 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ static/ auth_debug.log genv oom +node_modules/ diff --git a/artifacts/checkpoints/C1_route_response_map.tsv b/artifacts/checkpoints/C1_route_response_map.tsv new file mode 100644 index 0000000..a5e136f --- /dev/null +++ b/artifacts/checkpoints/C1_route_response_map.tsv @@ -0,0 +1,89 @@ +name route view response_kind templates +signup accounts/signup/ base.Signup html registration/registration_closed.html +notifications_update notifications//update/ notifications.NotificationsUpdate unknown - +system_settings settings/system/ system.SystemSettings html - +signal services/signal/ signal.Signal html - +whatsapp services/whatsapp/ whatsapp.WhatsApp html partials/signal-accounts.html +instagram services/instagram/ instagram.Instagram unknown - +signal_accounts services/signal// signal.SignalAccounts unknown - +whatsapp_accounts services/whatsapp// whatsapp.WhatsAppAccounts unknown - +instagram_accounts services/instagram// instagram.InstagramAccounts unknown - +signal_contacts services/signal//contacts// signal.SignalContactsList unknown - +whatsapp_contacts services/whatsapp//contacts// whatsapp.WhatsAppContactsList unknown - +signal_chats services/signal//chats// signal.SignalChatsList unknown - +whatsapp_chats services/whatsapp//chats// whatsapp.WhatsAppChatsList unknown - +signal_messages services/signal//messages/// signal.SignalMessagesList unknown - +signal_account_add services/signal//add/ signal.SignalAccountAdd unknown - +whatsapp_account_add services/whatsapp//add/ whatsapp.WhatsAppAccountAdd html - +whatsapp_account_unlink services/whatsapp//unlink// whatsapp.WhatsAppAccountUnlink html partials/signal-accounts.html +instagram_account_add services/instagram//add/ instagram.InstagramAccountAdd unknown - +compose_page compose/page/ compose.ComposePage html - +compose_workspace compose/workspace/ compose.ComposeWorkspace html - +compose_widget compose/widget/ compose.ComposeWidget html mixins/wm/widget.html +compose_workspace_contacts_widget compose/workspace/widget/contacts/ compose.ComposeWorkspaceContactsWidget html mixins/wm/widget.html +compose_send compose/send/ compose.ComposeSend html partials/compose-send-status.html +compose_cancel_send compose/cancel-send/ compose.ComposeCancelSend json - +compose_command_result compose/command-result/ compose.ComposeCommandResult html partials/compose-send-status.html +compose_drafts compose/drafts/ compose.ComposeDrafts json - +compose_summary compose/summary/ compose.ComposeSummary json - +compose_quick_insights compose/quick-insights/ compose.ComposeQuickInsights json - +compose_engage_preview compose/engage/preview/ compose.ComposeEngagePreview json - +compose_engage_send compose/engage/send/ compose.ComposeEngageSend json - +compose_thread compose/thread/ compose.ComposeThread json - +compose_history_sync compose/history-sync/ compose.ComposeHistorySync json - +compose_media_blob compose/media/blob/ compose.ComposeMediaBlob http - +compose_contacts_dropdown compose/widget/contacts/ compose.ComposeContactsDropdown html partials/nav-contacts-dropdown.html +compose_contact_match compose/contacts/match/ compose.ComposeContactMatch html - +ai_workspace ai/workspace/ workspace.AIWorkspace html - +ai_workspace_contacts ai/workspace//contacts/ workspace.AIWorkspaceContactsWidget html mixins/wm/widget.html +ai_workspace_person ai/workspace//person// workspace.AIWorkspacePersonWidget html mixins/wm/widget.html +ai_workspace_person_timeline ai/workspace//person//timeline/ workspace.AIWorkspacePersonTimelineWidget html mixins/wm/widget.html +ai_workspace_insight_graphs ai/workspace//person//insights/graphs/ workspace.AIWorkspaceInsightGraphs html pages/ai-workspace-insight-graphs.html +ai_workspace_information ai/workspace//person//information/ workspace.AIWorkspaceInformation html pages/ai-workspace-information.html +ai_workspace_insight_help ai/workspace//person//insights/help/ workspace.AIWorkspaceInsightHelp html pages/ai-workspace-insight-help.html +ai_workspace_insight_detail ai/workspace//person//insights// workspace.AIWorkspaceInsightDetail html pages/ai-workspace-insight-detail.html +ai_workspace_run ai/workspace//person//run// workspace.AIWorkspaceRunOperation html partials/ai-workspace-ai-result.html +ai_workspace_send ai/workspace//person//send/ workspace.AIWorkspaceSendDraft unknown - +ai_workspace_queue ai/workspace//person//queue/ workspace.AIWorkspaceQueueDraft unknown - +ai_workspace_mitigation_create ai/workspace//person//mitigation/create/ workspace.AIWorkspaceCreateMitigation html partials/ai-workspace-mitigation-panel.html +ais ai// ais.AIList unknown - +osint_search search// osint.OSINTSearch html - +osint_workspace osint/workspace/ osint.OSINTWorkspace html - +osint_workspace_tabs_widget osint/workspace/widget/tabs/ osint.OSINTWorkspaceTabsWidget html mixins/wm/widget.html +ai_create ai//create/ ais.AICreate unknown - +ai_update ai//update// ais.AIUpdate unknown - +ai_delete ai//delete// ais.AIDelete unknown - +people person// people.PersonList unknown - +person_create person//create/ people.PersonCreate unknown - +person_update person//update// people.PersonUpdate unknown - +person_delete person//delete// people.PersonDelete unknown - +groups group// groups.GroupList unknown - +group_create group//create/ groups.GroupCreate unknown - +group_update group//update// groups.GroupUpdate unknown - +group_delete group//delete// groups.GroupDelete unknown - +personas persona// personas.PersonaList unknown - +persona_create persona//create/ personas.PersonaCreate unknown - +persona_update persona//update// personas.PersonaUpdate unknown - +persona_delete persona//delete// personas.PersonaDelete unknown - +manipulations manipulation// manipulations.ManipulationList unknown - +manipulation_create manipulation//create/ manipulations.ManipulationCreate unknown - +manipulation_update manipulation//update// manipulations.ManipulationUpdate unknown - +manipulation_delete manipulation//delete// manipulations.ManipulationDelete unknown - +sessions session// sessions.SessionList unknown - +session_create session//create/ sessions.SessionCreate unknown - +session_update session//update// sessions.SessionUpdate unknown - +session_delete session//delete// sessions.SessionDelete unknown - +person_identifiers person//identifiers// identifiers.PersonIdentifierList unknown - +person_identifier_create person//identifiers/create/ identifiers.PersonIdentifierCreate unknown - +person_identifier_update person//identifiers/update/// identifiers.PersonIdentifierUpdate unknown - +person_identifier_delete person//identifiers/delete/// identifiers.PersonIdentifierDelete unknown - +messages session//messages// messages.MessageList unknown - +message_create session//messages/create/ messages.MessageCreate unknown - +message_update session//messages/update/// messages.MessageUpdate unknown - +message_delete session//messages/delete/// messages.MessageDelete unknown - +message_accept_api api/v1/queue/message/accept// queues.AcceptMessageAPI http - +message_reject_api api/v1/queue/message/reject// queues.RejectMessageAPI http - +queues queue// queues.QueueList unknown - +queue_create queue//create/ queues.QueueCreate unknown - +queue_update queue//update// queues.QueueUpdate unknown - +queue_delete queue//delete// queues.QueueDelete unknown - diff --git a/artifacts/checkpoints/C2_duplicate_blocks.tsv b/artifacts/checkpoints/C2_duplicate_blocks.tsv new file mode 100644 index 0000000..815e89b --- /dev/null +++ b/artifacts/checkpoints/C2_duplicate_blocks.tsv @@ -0,0 +1,121 @@ +occurrences files hash sample_locations +10 10 03b0f9bc20964f2440461604aa0ffb6c6ee7c3cd core/templates/partials/ai-list.html:47 | core/templates/partials/identifier-list.html:47 | core/templates/partials/group-list.html:57 | core/templates/partials/manipulation-list.html:85 | core/templates/partials/message-list.html:77 | core/templates/partials/person-list.html:68 | core/templates/partials/persona-list.html:65 | core/templates/partials/session-list.html:66 | core/templates/partials/signal-accounts.html:37 | core/templates/partials/signal-chats-list.html:44 +9 9 f0d371d430810ce9eae58adc2d12965161068d13 core/templates/partials/ai-list.html:46 | core/templates/partials/identifier-list.html:46 | core/templates/partials/group-list.html:56 | core/templates/partials/manipulation-list.html:84 | core/templates/partials/message-list.html:76 | core/templates/partials/person-list.html:67 | core/templates/partials/persona-list.html:64 | core/templates/partials/session-list.html:65 | core/templates/partials/signal-chats-list.html:43 +9 9 cff5da77248dead32e9fdc06f8fe42cddca30a30 core/templates/partials/ai-list.html:8 | core/templates/partials/identifier-list.html:8 | core/templates/partials/group-list.html:8 | core/templates/partials/manipulation-list.html:8 | core/templates/partials/message-list.html:8 | core/templates/partials/person-list.html:8 | core/templates/partials/persona-list.html:8 | core/templates/partials/session-list.html:8 | core/templates/partials/signal-chats-list.html:6 +9 9 3d3fd1673ee12c057f8054da11425c2c2dfeedb0 core/templates/partials/ai-list.html:7 | core/templates/partials/identifier-list.html:7 | core/templates/partials/group-list.html:7 | core/templates/partials/manipulation-list.html:7 | core/templates/partials/message-list.html:7 | core/templates/partials/person-list.html:7 | core/templates/partials/persona-list.html:7 | core/templates/partials/session-list.html:7 | core/templates/partials/signal-chats-list.html:5 +9 9 15b35686e496e5401cc946aacf6e9ce6ae44aae8 core/templates/partials/ai-list.html:6 | core/templates/partials/identifier-list.html:6 | core/templates/partials/group-list.html:6 | core/templates/partials/manipulation-list.html:6 | core/templates/partials/message-list.html:6 | core/templates/partials/person-list.html:6 | core/templates/partials/persona-list.html:6 | core/templates/partials/session-list.html:6 | core/templates/partials/signal-chats-list.html:4 +8 8 f1690409577850c0ce9239566578d49ad9925a1a core/templates/partials/ai-list.html:31 | core/templates/partials/identifier-list.html:31 | core/templates/partials/group-list.html:41 | core/templates/partials/manipulation-list.html:69 | core/templates/partials/message-list.html:61 | core/templates/partials/person-list.html:43 | core/templates/partials/persona-list.html:49 | core/templates/partials/session-list.html:41 +8 8 bc0b56758f9920642c9f185536d6cbf22f9b3396 core/templates/partials/ai-list.html:32 | core/templates/partials/identifier-list.html:32 | core/templates/partials/group-list.html:42 | core/templates/partials/manipulation-list.html:70 | core/templates/partials/message-list.html:62 | core/templates/partials/person-list.html:44 | core/templates/partials/persona-list.html:50 | core/templates/partials/session-list.html:42 +8 8 a7a050a861dfa0a6281229e0eee563c603ab379b core/templates/partials/ai-list.html:9 | core/templates/partials/identifier-list.html:9 | core/templates/partials/group-list.html:9 | core/templates/partials/manipulation-list.html:9 | core/templates/partials/message-list.html:9 | core/templates/partials/person-list.html:9 | core/templates/partials/persona-list.html:9 | core/templates/partials/session-list.html:9 +8 8 5c3b1591c15273065fdd44dd7733191989ba06d3 core/templates/partials/ai-list.html:51 | core/templates/partials/identifier-list.html:51 | core/templates/partials/group-list.html:61 | core/templates/partials/manipulation-list.html:89 | core/templates/partials/message-list.html:81 | core/templates/partials/person-list.html:72 | core/templates/partials/persona-list.html:69 | core/templates/partials/session-list.html:70 +8 8 5335dd928d6669e16e59528846f9a98102da4981 core/templates/partials/ai-list.html:49 | core/templates/partials/identifier-list.html:49 | core/templates/partials/group-list.html:59 | core/templates/partials/manipulation-list.html:87 | core/templates/partials/message-list.html:79 | core/templates/partials/person-list.html:70 | core/templates/partials/persona-list.html:67 | core/templates/partials/session-list.html:68 +8 8 4fc780e792516038cf09edc3dd9448701ee26e1c core/templates/partials/ai-list.html:30 | core/templates/partials/identifier-list.html:30 | core/templates/partials/group-list.html:40 | core/templates/partials/manipulation-list.html:68 | core/templates/partials/message-list.html:60 | core/templates/partials/person-list.html:42 | core/templates/partials/persona-list.html:48 | core/templates/partials/session-list.html:40 +8 8 48b2a53f87354bf9691c37af1c9be5f81cf1ce7c core/templates/partials/ai-list.html:48 | core/templates/partials/identifier-list.html:48 | core/templates/partials/group-list.html:58 | core/templates/partials/manipulation-list.html:86 | core/templates/partials/message-list.html:78 | core/templates/partials/person-list.html:69 | core/templates/partials/persona-list.html:66 | core/templates/partials/session-list.html:67 +8 8 3b4bddcd4ff63b68b226b8ded1236ffcc4bfd40f core/templates/partials/ai-list.html:50 | core/templates/partials/identifier-list.html:50 | core/templates/partials/group-list.html:60 | core/templates/partials/manipulation-list.html:88 | core/templates/partials/message-list.html:80 | core/templates/partials/person-list.html:71 | core/templates/partials/persona-list.html:68 | core/templates/partials/session-list.html:69 +8 8 2d766ff045ee8134a71845d7f58ffe06bc83cf5a core/templates/partials/ai-list.html:33 | core/templates/partials/identifier-list.html:33 | core/templates/partials/group-list.html:43 | core/templates/partials/manipulation-list.html:71 | core/templates/partials/message-list.html:63 | core/templates/partials/person-list.html:45 | core/templates/partials/persona-list.html:51 | core/templates/partials/session-list.html:43 +8 8 07c707357a23b0e9a988243ec392f0551d8f5ae8 core/templates/partials/ai-list.html:29 | core/templates/partials/identifier-list.html:29 | core/templates/partials/group-list.html:39 | core/templates/partials/manipulation-list.html:67 | core/templates/partials/message-list.html:59 | core/templates/partials/person-list.html:41 | core/templates/partials/persona-list.html:47 | core/templates/partials/session-list.html:39 +8 1 9abf8a2f282656184f30aa39a5691f1ba3c5f751 core/views/workspace.py:4435 | core/views/workspace.py:4532 | core/views/workspace.py:4533 | core/views/workspace.py:4784 | core/views/workspace.py:4785 | core/views/workspace.py:5049 | core/views/workspace.py:5119 | core/views/workspace.py:5143 +6 6 fd8babf68e334229e8760b124291f08ba139e8da core/templates/partials/group-list.html:18 | core/templates/partials/manipulation-list.html:22 | core/templates/partials/message-list.html:24 | core/templates/partials/person-list.html:19 | core/templates/partials/persona-list.html:22 | core/templates/partials/session-list.html:18 +6 6 f6142cb779dc039ab3c55f0cd88686d17b0bf715 core/templates/partials/group-list.html:22 | core/templates/partials/manipulation-list.html:26 | core/templates/partials/message-list.html:28 | core/templates/partials/person-list.html:23 | core/templates/partials/persona-list.html:26 | core/templates/partials/session-list.html:22 +6 6 efeb0fa9dbc193eff23b464d67ca1fbf81d5bdbf core/templates/partials/group-list.html:25 | core/templates/partials/manipulation-list.html:29 | core/templates/partials/message-list.html:31 | core/templates/partials/person-list.html:26 | core/templates/partials/persona-list.html:29 | core/templates/partials/session-list.html:25 +6 6 ea9c94038a099db8763e33d44239d2fc1ec791b5 core/templates/partials/group-list.html:19 | core/templates/partials/manipulation-list.html:23 | core/templates/partials/message-list.html:25 | core/templates/partials/person-list.html:20 | core/templates/partials/persona-list.html:23 | core/templates/partials/session-list.html:19 +6 6 d34851ba01afdef301f4d95b748ac93567759315 core/templates/partials/group-list.html:21 | core/templates/partials/manipulation-list.html:25 | core/templates/partials/message-list.html:27 | core/templates/partials/person-list.html:22 | core/templates/partials/persona-list.html:25 | core/templates/partials/session-list.html:21 +6 6 c198cfd05631c42f80c1cac33060bcae2a8314e1 core/templates/partials/group-list.html:24 | core/templates/partials/manipulation-list.html:28 | core/templates/partials/message-list.html:30 | core/templates/partials/person-list.html:25 | core/templates/partials/persona-list.html:28 | core/templates/partials/session-list.html:24 +6 6 b7ffcc018c542592630ae4aa179df63fd73124c6 core/templates/partials/ai-list.html:35 | core/templates/partials/identifier-list.html:35 | core/templates/partials/group-list.html:45 | core/templates/partials/manipulation-list.html:73 | core/templates/partials/message-list.html:65 | core/templates/partials/persona-list.html:53 +6 6 b0e7bdb0253104d309381cd1b92b8b1361011497 core/templates/partials/group-list.html:20 | core/templates/partials/manipulation-list.html:24 | core/templates/partials/message-list.html:26 | core/templates/partials/person-list.html:21 | core/templates/partials/persona-list.html:24 | core/templates/partials/session-list.html:20 +6 6 642e4fd07d36ac1672d41a357245fe65d548a167 core/templates/partials/ai-list.html:34 | core/templates/partials/identifier-list.html:34 | core/templates/partials/group-list.html:44 | core/templates/partials/manipulation-list.html:72 | core/templates/partials/message-list.html:64 | core/templates/partials/persona-list.html:52 +6 6 5a0aa6e4c55089cc2bc930f5b41e6460c03377be core/templates/partials/group-list.html:23 | core/templates/partials/manipulation-list.html:27 | core/templates/partials/message-list.html:29 | core/templates/partials/person-list.html:24 | core/templates/partials/persona-list.html:27 | core/templates/partials/session-list.html:23 +6 3 d59a18a73345a4d9ee14cf53c0651e265283500f core/templates/two_factor/core/login.html:25 | core/templates/two_factor/core/login.html:26 | core/templates/two_factor/core/phone_register.html:15 | core/templates/two_factor/core/phone_register.html:16 | core/templates/two_factor/core/setup.html:47 | core/templates/two_factor/core/setup.html:48 +6 1 f508ae701315bdaff81c2ed4fc909dcfe3063e12 core/views/workspace.py:4434 | core/views/workspace.py:4531 | core/views/workspace.py:4783 | core/views/workspace.py:5048 | core/views/workspace.py:5118 | core/views/workspace.py:5142 +5 1 9e2147b111b0b4cfde1741ff031ed5eeee0fb90c core/views/workspace.py:3565 | core/views/workspace.py:3612 | core/views/workspace.py:3673 | core/views/workspace.py:3692 | core/views/workspace.py:3734 +4 4 f1aa79edd9798ce0335811883d9edf5641caa560 core/views/groups.py:6 | core/views/manipulations.py:6 | core/views/people.py:6 | core/views/personas.py:6 +4 4 f0ec5c6490de79592d8bc01b014f7dcccd0ef038 core/templates/partials/ai-list.html:44 | core/templates/partials/identifier-list.html:44 | core/templates/partials/message-list.html:74 | core/templates/partials/session-list.html:63 +4 4 e8c131c539e745e9225a33c05a8eaa7373258d54 core/templates/pages/ai-workspace-insight-detail.html:4 | core/templates/pages/ai-workspace-insight-graphs.html:4 | core/templates/pages/ai-workspace-insight-help.html:3 | core/templates/pages/ai-workspace-information.html:3 +4 4 decebca418375e5a29ab01c3754b762d2657233f core/templates/pages/ai-workspace-insight-detail.html:3 | core/templates/pages/ai-workspace-insight-graphs.html:3 | core/templates/pages/ai-workspace-insight-help.html:2 | core/templates/pages/ai-workspace-information.html:2 +4 4 b7577e6b11ec4d0fa29bb51c1a83825ce02fd524 core/templates/partials/ai-list.html:42 | core/templates/partials/identifier-list.html:42 | core/templates/partials/message-list.html:72 | core/templates/partials/session-list.html:61 +4 4 75767eb7e8ab76bbe8ef7adbdbcb3885d5f49928 core/templates/partials/ai-list.html:45 | core/templates/partials/identifier-list.html:45 | core/templates/partials/message-list.html:75 | core/templates/partials/session-list.html:64 +4 4 1eb40cc61e3b91955ce88faa8fd0046ec6551929 core/templates/partials/ai-list.html:43 | core/templates/partials/identifier-list.html:43 | core/templates/partials/message-list.html:73 | core/templates/partials/session-list.html:62 +4 2 cee0c018559f005cebe0b2d6b3b50dbaebc40d66 core/templates/pages/compose-contact-match.html:22 | core/templates/pages/compose-contact-match.html:23 | core/templates/pages/system-settings.html:8 | core/templates/pages/system-settings.html:9 +4 2 c9cfa57fda2e778ee9b5eada947cbf891b6664e9 core/views/signal.py:53 | core/views/whatsapp.py:52 | core/views/whatsapp.py:93 | core/views/whatsapp.py:127 +4 2 445d072fad4a4544aa92401013bc2a116fa85412 core/views/signal.py:54 | core/views/whatsapp.py:53 | core/views/whatsapp.py:94 | core/views/whatsapp.py:128 +4 2 07396bab972b3c399891794476328d05274a7b24 core/views/signal.py:55 | core/views/whatsapp.py:54 | core/views/whatsapp.py:95 | core/views/whatsapp.py:129 +4 1 fc8954f193dbb953ac30413e97ca67dcc1995196 core/views/whatsapp.py:214 | core/views/whatsapp.py:245 | core/views/whatsapp.py:311 | core/views/whatsapp.py:346 +4 1 f6b88ff8738ebf9460faad2c3e174be762c67293 core/views/osint.py:147 | core/views/osint.py:205 | core/views/osint.py:244 | core/views/osint.py:322 +4 1 e439fdd1e1150add02fabd3c9eba6b2042956e5b core/views/workspace.py:4581 | core/views/workspace.py:4635 | core/views/workspace.py:4710 | core/views/workspace.py:4749 +4 1 dbcf5eed0e71ad85b4292e3046783ef1c09313e7 core/views/workspace.py:4582 | core/views/workspace.py:4636 | core/views/workspace.py:4711 | core/views/workspace.py:4750 +4 1 c6fd3be26b4dac1b081caf2c23fe4017cb32c79d core/views/osint.py:151 | core/views/osint.py:209 | core/views/osint.py:248 | core/views/osint.py:326 +4 1 619a8132534bf4cf49cb9025caefc4368d23027a core/views/workspace.py:3983 | core/views/workspace.py:4001 | core/views/workspace.py:4112 | core/views/workspace.py:4149 +4 1 55794b46a1c1398e2854efcd5956b72457efafbc core/views/osint.py:150 | core/views/osint.py:208 | core/views/osint.py:247 | core/views/osint.py:325 +4 1 51ed1c5494a45d890fab39751a78358c70a665d2 core/views/workspace.py:4583 | core/views/workspace.py:4637 | core/views/workspace.py:4712 | core/views/workspace.py:4751 +4 1 362730b92c130e505e810721f050388d6dca84ea core/views/workspace.py:3566 | core/views/workspace.py:3674 | core/views/workspace.py:3693 | core/views/workspace.py:3735 +4 1 28331d9d604508bf90644afe2bca3459260288d3 core/views/osint.py:148 | core/views/osint.py:206 | core/views/osint.py:245 | core/views/osint.py:323 +4 1 24492ca0de0ef20daca922e99241c19e2b4476d9 core/views/compose.py:2829 | core/views/compose.py:2890 | core/views/compose.py:2953 | core/views/compose.py:3069 +4 1 0429ccd645a7c781c66863de386fff245096acb8 core/templates/partials/signal-chats-list.html:55 | core/templates/partials/signal-chats-list.html:64 | core/templates/partials/signal-chats-list.html:100 | core/templates/partials/signal-chats-list.html:108 +4 1 00402420dfa95d67fb1fa047200c2c7ebec9e09f core/views/osint.py:149 | core/views/osint.py:207 | core/views/osint.py:246 | core/views/osint.py:324 +3 3 f72bfbb1fa3aaba54774035a28fa32d40d07782e core/templates/partials/whatsapp-contacts-list.html:188 | core/templates/pages/compose-contact-match.html:322 | core/templates/partials/osint/list-table.html:320 +3 3 ef0d9a8dd3eba020022cc111353fac4dede707aa core/templates/partials/whatsapp-contacts-list.html:189 | core/templates/pages/compose-contact-match.html:323 | core/templates/partials/osint/list-table.html:321 +3 3 e93f48a528a0b829c15e836f52b0688302294cd7 core/templates/mixins/window-content/persona-form.html:3 | core/templates/mixins/window-content/queue-form-inline.html:3 | core/templates/mixins/window-content/person-form.html:3 +3 3 e78a723340a9cdd4c8342598dd77df9905695934 core/templates/partials/group-list.html:54 | core/templates/partials/manipulation-list.html:82 | core/templates/partials/person-list.html:65 +3 3 d2066ceb8eb4188b1dda46b55168a971bab01fa8 core/templates/partials/group-list.html:55 | core/templates/partials/manipulation-list.html:83 | core/templates/partials/person-list.html:66 +3 3 c28449d0cb2fbbd1ae9a2fcf1894e23fe691b28f core/templates/mixins/window-content/persona-form.html:120 | core/templates/mixins/window-content/queue-form-inline.html:36 | core/templates/mixins/window-content/person-form.html:127 +3 3 b95648f6e16638f41aa87271ea707743183c6c07 core/templates/mixins/window-content/persona-form.html:35 | core/templates/mixins/window-content/queue-form-inline.html:20 | core/templates/mixins/window-content/person-form.html:41 +3 3 9bf9b960c86ce18ec24c06722eae061ba6c53b0d core/templates/mixins/window-content/persona-form.html:33 | core/templates/mixins/window-content/queue-form-inline.html:18 | core/templates/mixins/window-content/person-form.html:39 +3 3 90ecfd0cc6216452e3e1cf70802b6c5bf31bb6e1 core/templates/registration/login.html:1 | core/templates/registration/registration_closed.html:1 | core/templates/registration/signup.html:1 +3 3 908efc2243afc21fdb03a03590b487053bd8df62 core/views/signal.py:27 | core/views/system.py:136 | core/views/whatsapp.py:23 +3 3 8d46d1c156fa9b3f9ada7cfa9a2307e47cbd1d7d core/templates/mixins/window-content/persona-form.html:4 | core/templates/mixins/window-content/queue-form-inline.html:4 | core/templates/mixins/window-content/person-form.html:4 +3 3 88de0e7ee40636f90fdc006951eecceab664b9d9 core/templates/registration/login.html:2 | core/templates/registration/registration_closed.html:2 | core/templates/registration/signup.html:2 +3 3 8298593e6112ae5b1b79d784687dbe0adf81986c core/templates/mixins/window-content/persona-form.html:31 | core/templates/mixins/window-content/queue-form-inline.html:16 | core/templates/mixins/window-content/person-form.html:37 +3 3 7faa673ea237e033b1c17cfedd0ea28aa663a47f core/templates/two_factor/core/login.html:24 | core/templates/two_factor/core/phone_register.html:14 | core/templates/two_factor/core/setup.html:46 +3 3 70dd01d1657eb83f2e2683cb57ddb48c6c096461 core/templates/mixins/window-content/persona-form.html:2 | core/templates/mixins/window-content/queue-form-inline.html:2 | core/templates/mixins/window-content/person-form.html:2 +3 3 57a7ed538e23b8b8fe2fe43f7de9a034ca5dc09e core/templates/registration/login.html:3 | core/templates/registration/registration_closed.html:3 | core/templates/registration/signup.html:3 +3 3 46899ae970e49b3abf0ee240164475e500a508af core/templates/mixins/window-content/persona-form.html:32 | core/templates/mixins/window-content/queue-form-inline.html:17 | core/templates/mixins/window-content/person-form.html:38 +3 3 39756b7a97a94b78848422a3db963aca123f793f core/templates/partials/whatsapp-contacts-list.html:163 | core/templates/pages/compose-contact-match.html:293 | core/templates/partials/osint/list-table.html:301 +3 3 1c17b1165c7bdf1f43b33059f711f6f85b9cc218 core/templates/registration/login.html:4 | core/templates/registration/registration_closed.html:4 | core/templates/registration/signup.html:4 +3 3 1be799390181879613097b249220ae621ff07cdb core/templates/partials/whatsapp-contacts-list.html:127 | core/templates/pages/compose-contact-match.html:447 | core/templates/partials/osint/list-table.html:261 +3 3 19aab351002986a405384ecd3ce7608eb76d8bc5 core/templates/partials/group-list.html:53 | core/templates/partials/manipulation-list.html:81 | core/templates/partials/person-list.html:64 +3 3 14aff65587a9bbd6eb7756ac5c8efd169b560a3a core/templates/partials/whatsapp-contacts-list.html:126 | core/templates/pages/compose-contact-match.html:446 | core/templates/partials/osint/list-table.html:260 +3 3 14919fe1ff418d66f3d00bc460657f29d2fd8526 core/templates/partials/whatsapp-contacts-list.html:187 | core/templates/pages/compose-contact-match.html:321 | core/templates/partials/osint/list-table.html:319 +3 3 0aca7b3de6e51826f65cb5b05b2384c26dfbbe46 core/templates/mixins/window-content/persona-form.html:34 | core/templates/mixins/window-content/queue-form-inline.html:19 | core/templates/mixins/window-content/person-form.html:40 +3 3 0641ecc380ba0553c7878bf54bf39cb608f34fec core/templates/partials/group-list.html:52 | core/templates/partials/manipulation-list.html:80 | core/templates/partials/person-list.html:63 +3 1 ff9e899aa632040f0c8c04c35c724a584e96170e core/views/workspace.py:4613 | core/views/workspace.py:4727 | core/views/workspace.py:4769 +3 1 fd59c665fdc5e87a6d79967a146b4bdce2522e26 core/views/workspace.py:2277 | core/views/workspace.py:2278 | core/views/workspace.py:2279 +3 1 e9480c8124135a1145d5b7f1adbad08d521efef4 core/views/whatsapp.py:66 | core/views/whatsapp.py:110 | core/views/whatsapp.py:142 +3 1 e5fea1892e0910137b87aec081e0f617647d9e3c core/views/workspace.py:977 | core/views/workspace.py:978 | core/views/workspace.py:979 +3 1 e28608b35e727724aecee511c796d18764cf672e core/templates/partials/compose-panel.html:126 | core/templates/partials/compose-panel.html:135 | core/templates/partials/compose-panel.html:144 +3 1 dcd7eefc5742cbb8365f3f1f75183a7cc7615749 core/views/osint.py:155 | core/views/osint.py:213 | core/views/osint.py:330 +3 1 d9b54261b181ce1f8c5072e7c8a1df8b3d001a77 core/views/workspace.py:3498 | core/views/workspace.py:3499 | core/views/workspace.py:3500 +3 1 d975831bc528875ccfb5b7de5482552df7e943d5 core/views/compose.py:2861 | core/views/compose.py:2916 | core/views/compose.py:3077 +3 1 d931b65a39674b3ee1691f50052d5e14b7df0c75 core/views/osint.py:146 | core/views/osint.py:204 | core/views/osint.py:321 +3 1 d7908443bf8c1ca621dd346a0f86ef0bff940ab3 core/views/osint.py:156 | core/views/osint.py:214 | core/views/osint.py:331 +3 1 d6b5f871e150b61a5e188e49f6dff02004193b3f core/views/compose.py:1001 | core/views/compose.py:1025 | core/views/compose.py:1351 +3 1 d5c2ada1785825894266d98c57132b2544285032 core/views/compose.py:2830 | core/views/compose.py:2891 | core/views/compose.py:3070 +3 1 d0b3c17ee7292bc9b72a6b225e7a1e60e9b205df core/views/compose.py:1002 | core/views/compose.py:1026 | core/views/compose.py:1352 +3 1 cf1c4409875350836b698858dd25c6ecbde16919 core/views/compose.py:1000 | core/views/compose.py:1024 | core/views/compose.py:1350 +3 1 c3d51fd612a0951656a5ecfa90f43e6d01f74397 core/views/signal.py:14 | core/views/signal.py:15 | core/views/signal.py:16 +3 1 c07232d37f44c24fd72a425d5f3118b44ea878f4 core/views/workspace.py:3572 | core/views/workspace.py:3618 | core/views/workspace.py:4012 +3 1 9c0c4d771d15fd5b044aaa34f60455b6a0162e41 core/views/workspace.py:4903 | core/views/workspace.py:4928 | core/views/workspace.py:4992 +3 1 9b90da14fc1b8dbb3a77a0fe5e33ff8100ce8521 core/views/workspace.py:829 | core/views/workspace.py:830 | core/views/workspace.py:831 +3 1 93f4f5c3e371c438ae0c43856c3828f6d117f72a core/views/compose.py:2831 | core/views/compose.py:2892 | core/views/compose.py:3071 +3 1 90816cacf56a5067ae41905f8b55fe9d29537860 core/views/workspace.py:4612 | core/views/workspace.py:4726 | core/views/workspace.py:4768 +3 1 8684b5111e67349fe997efcd70aafcea4fdf83e5 core/views/whatsapp.py:65 | core/views/whatsapp.py:109 | core/views/whatsapp.py:141 +3 1 84ec68e536092f636e85f5fa8b8c62e132f8cf44 core/views/whatsapp.py:64 | core/views/whatsapp.py:108 | core/views/whatsapp.py:140 +3 1 833c0a4b126b5483e2d9a371decbb6e64d8be31b core/views/osint.py:154 | core/views/osint.py:212 | core/views/osint.py:329 +3 1 7fd26a8f251b682cc6532ea3e4b940e3c9d724f8 core/views/osint.py:158 | core/views/osint.py:216 | core/views/osint.py:333 +3 1 6761ee7ea5444b7947203328998c87732446c26b core/views/osint.py:152 | core/views/osint.py:210 | core/views/osint.py:327 +3 1 5d3621d8d1df3d692b03d7496a88f1983344f910 core/views/compose.py:997 | core/views/compose.py:1021 | core/views/compose.py:1347 +3 1 5bf9dc3e0e46be35ae97b3d487cd626913f4917e core/views/instagram.py:3 | core/views/instagram.py:4 | core/views/instagram.py:5 +3 1 59eb8ce9add8ac3ca30c2bd7910d926f4c1acf71 core/views/osint.py:153 | core/views/osint.py:211 | core/views/osint.py:328 +3 1 595d7274dc3c2036cb637962dbdb26e5b72cd48d core/views/compose.py:2371 | core/views/compose.py:2580 | core/views/compose.py:2641 +3 1 58d137eecf70203c332d9ac8eedf22185a5de4b3 core/views/workspace.py:3672 | core/views/workspace.py:3691 | core/views/workspace.py:3733 +3 1 49cbc2c109a4ef6091f5fef70963ffbde0fa802d core/views/workspace.py:4164 | core/views/workspace.py:4243 | core/views/workspace.py:4300 +3 1 481567b5ee2d425e4c04ce614ec8190ac6736a58 core/templates/partials/compose-panel.html:2667 | core/templates/partials/compose-panel.html:2729 | core/templates/partials/compose-panel.html:2751 +3 1 42c0f869021d8ad0c15da6c4463dba6d2ec11643 core/views/compose.py:1003 | core/views/compose.py:1027 | core/views/compose.py:1353 +3 1 421098a2efd474573beaccceeb159a576ee45114 core/views/workspace.py:4163 | core/views/workspace.py:4242 | core/views/workspace.py:4299 +3 1 3ce072494ff8ce4a9e08df7924cb4038c02a0bc9 core/views/workspace.py:4614 | core/views/workspace.py:4728 | core/views/workspace.py:4770 +3 1 3907486b4a80932f68c6b9354b3d9b0325c21d39 core/templates/partials/compose-panel.html:125 | core/templates/partials/compose-panel.html:134 | core/templates/partials/compose-panel.html:143 +3 1 2c60e1087d82418be7362e9258da08450bc81c86 core/views/compose.py:2862 | core/views/compose.py:2917 | core/views/compose.py:3078 +3 1 28720796d0312972c14eb707f03025ac3f574d16 core/views/osint.py:157 | core/views/osint.py:215 | core/views/osint.py:332 +3 1 2492a12ee36707345d81b61907f4190db03c91cf core/views/workspace.py:1625 | core/views/workspace.py:1626 | core/views/workspace.py:1627 +3 1 136f738cdbb963f2f131083bc6a4914baa489a99 core/templates/partials/ai-workspace-person-widget.html:581 | core/templates/partials/ai-workspace-person-widget.html:610 | core/templates/partials/ai-workspace-person-widget.html:647 +3 1 112897fea978b734511faa475861bc87fb740ce4 core/views/compose.py:998 | core/views/compose.py:1022 | core/views/compose.py:1348 +3 1 1112dde96781a149bcb3aa9da756f7f8a1d8cfc9 core/views/workspace.py:4520 | core/views/workspace.py:4559 | core/views/workspace.py:5033 diff --git a/core/clients/whatsapp.py b/core/clients/whatsapp.py index 7d70a2d..f169c21 100644 --- a/core/clients/whatsapp.py +++ b/core/clients/whatsapp.py @@ -1465,27 +1465,41 @@ class WhatsAppClient(ClientBase): # NOTE: Neonize get_all_contacts has crashed some runtime builds with a Go panic. # Read contact-like rows directly from the session sqlite DB instead. contacts, source, lid_map = await self._sync_contacts_from_sqlite() - if not contacts: + groups, groups_source = await self._sync_groups_from_client() + now_ts = int(time.time()) + + if contacts: + self.log.debug( + "whatsapp contacts synced: count=%s source=%s", + len(contacts), + source or "unknown", + ) + self._publish_state( + contacts=contacts, + lid_map=lid_map, + contacts_synced_at=now_ts, + contacts_sync_count=len(contacts), + last_event="contacts_synced", + contacts_source=source or "unknown", + last_error="", + ) + else: self.log.debug("whatsapp contacts sync empty (%s)", source or "unknown") self._publish_state( last_event="contacts_sync_empty", contacts_source=source or "unknown", ) - return - self.log.debug( - "whatsapp contacts synced: count=%s source=%s", - len(contacts), - source or "unknown", - ) - self._publish_state( - contacts=contacts, - lid_map=lid_map, - contacts_synced_at=int(time.time()), - contacts_sync_count=len(contacts), - last_event="contacts_synced", - contacts_source=source or "unknown", - last_error="", - ) + + if groups_source: + event_name = "groups_synced" if groups else "groups_sync_empty" + self._publish_state( + groups=groups, + groups_source=groups_source, + groups_sync_count=len(groups), + groups_synced_at=now_ts, + last_event=event_name, + last_error="" if groups else "", + ) async def _sync_contacts_from_sqlite(self): def _extract(): @@ -1700,6 +1714,51 @@ class WhatsAppClient(ClientBase): return await asyncio.to_thread(_extract) + async def _sync_groups_from_client(self): + if self._client is None: + return [], "client_missing" + getter = getattr(self._client, "get_joined_groups", None) + if getter is None: + return [], "get_joined_groups_missing" + try: + group_rows = await self._maybe_await(getter()) + except Exception as exc: + self._publish_state( + last_event="groups_sync_failed", + last_error=str(exc), + ) + return [], "get_joined_groups_failed" + + out = [] + now_ts = int(time.time()) + for group in group_rows or []: + jid_value = self._jid_to_identifier( + self._pluck(group, "JID") or self._pluck(group, "jid") + ) + identifier = ( + jid_value.split("@", 1)[0].strip() if jid_value else "" + ) + if not identifier: + continue + name = ( + str(self._pluck(group, "GroupName", "Name") or "").strip() + or str(self._pluck(group, "GroupTopic", "Topic") or "").strip() + or identifier + ) + out.append( + { + "identifier": identifier, + "jid": jid_value or f"{identifier}@g.us", + "name": name, + "chat": name, + "type": "group", + "seen_at": now_ts, + } + ) + if len(out) >= 500: + break + return out, "get_joined_groups" + async def _is_contact_sync_ready(self) -> bool: if self._client is None: return False diff --git a/core/migrations/0025_platformchatlink.py b/core/migrations/0025_platformchatlink.py new file mode 100644 index 0000000..f302d1c --- /dev/null +++ b/core/migrations/0025_platformchatlink.py @@ -0,0 +1,56 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0024_workspacemetricsnapshot"), + ] + + operations = [ + migrations.CreateModel( + name="PlatformChatLink", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("service", models.CharField(choices=[("signal", "Signal"), ("whatsapp", "WhatsApp"), ("xmpp", "XMPP"), ("instagram", "Instagram")], max_length=255)), + ("chat_identifier", models.CharField(max_length=255)), + ("chat_jid", models.CharField(blank=True, max_length=255, null=True)), + ("chat_name", models.CharField(blank=True, max_length=255, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "person", + models.ForeignKey(on_delete=models.deletion.CASCADE, to="core.person"), + ), + ( + "person_identifier", + models.ForeignKey(blank=True, null=True, on_delete=models.deletion.SET_NULL, to="core.personidentifier"), + ), + ( + "user", + models.ForeignKey(on_delete=models.deletion.CASCADE, to="core.user"), + ), + ], + ), + migrations.AddConstraint( + model_name="platformchatlink", + constraint=models.UniqueConstraint( + fields=("user", "service", "chat_identifier"), + name="unique_platform_chat_link", + ), + ), + migrations.AddIndex( + model_name="platformchatlink", + index=models.Index( + fields=["user", "service", "chat_identifier"], + name="core_platfo_user_id_0436ca_idx", + ), + ), + ] diff --git a/core/models.py b/core/models.py index cbfba14..4358640 100644 --- a/core/models.py +++ b/core/models.py @@ -4,6 +4,7 @@ import uuid from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError from django.db import models from core.clients import transport @@ -170,6 +171,62 @@ class PersonIdentifier(models.Model): ) +class PlatformChatLink(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + person = models.ForeignKey(Person, on_delete=models.CASCADE) + person_identifier = models.ForeignKey( + PersonIdentifier, + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + service = models.CharField(choices=SERVICE_CHOICES, max_length=255) + chat_identifier = models.CharField(max_length=255) + chat_jid = models.CharField(max_length=255, blank=True, null=True) + chat_name = models.CharField(max_length=255, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["user", "service", "chat_identifier"], + name="unique_platform_chat_link", + ) + ] + indexes = [ + models.Index(fields=["user", "service", "chat_identifier"]), + ] + + def clean(self): + if self.person_id and self.user_id and self.person.user_id != self.user_id: + raise ValidationError("Person must belong to the same user.") + if self.person_identifier_id: + if self.person_identifier.user_id != self.user_id: + raise ValidationError( + "Person identifier must belong to the same user." + ) + if self.person_identifier.person_id != self.person_id: + raise ValidationError( + "Person identifier must belong to the selected person." + ) + if self.person_identifier.service != self.service: + raise ValidationError( + "Chat links cannot be linked across platforms." + ) + + def save(self, *args, **kwargs): + value = str(self.chat_identifier or "").strip() + if "@" in value: + value = value.split("@", 1)[0] + self.chat_identifier = value + self.full_clean() + return super().save(*args, **kwargs) + + def __str__(self): + return f"{self.person.name} ({self.service}: {self.chat_identifier})" + + class ChatSession(models.Model): """Represents an ongoing chat session for persisted message history.""" diff --git a/core/templates/pages/whatsapp-chat-link.html b/core/templates/pages/whatsapp-chat-link.html new file mode 100644 index 0000000..4a4f915 --- /dev/null +++ b/core/templates/pages/whatsapp-chat-link.html @@ -0,0 +1,84 @@ +{% extends "index.html" %} + +{% block content %} +
+
+
+
+
+

WhatsApp Chat Link

+

Link a WhatsApp chat identifier to a person. This link is WhatsApp-only.

+
+
+ +
+ + {% if notice_message %} +
+ {{ notice_message }} +
+ {% endif %} + +
+
+ {% csrf_token %} +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+
+ + {% if existing %} +
+ Current link: {{ existing.person.name }}{{ existing.chat_identifier }} +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/partials/compose-panel.html b/core/templates/partials/compose-panel.html index 2a657ce..de79fa2 100644 --- a/core/templates/partials/compose-panel.html +++ b/core/templates/partials/compose-panel.html @@ -2545,18 +2545,6 @@ return response.json(); }; - const getJson = async function (url) { - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - headers: { Accept: "application/json" } - }); - if (!response.ok) { - throw new Error("Request failed"); - } - return response.json(); - }; - const titleCase = function (value) { const raw = String(value || "").trim().toLowerCase(); if (!raw) { @@ -2639,17 +2627,6 @@ } }; - const cardContentNode = function (card) { - return card ? card.querySelector(".compose-ai-content") : null; - }; - - const setCardMessage = function (card, message) { - const node = cardContentNode(card); - if (node) { - node.textContent = String(message || ""); - } - }; - const openEngage = function (sourceRef) { const engageCard = showCard("engage"); if (!engageCard) { @@ -2669,19 +2646,19 @@ } setCardLoading(card, true); try { - const payload = await getJson( - thread.dataset.draftsUrl + "?" + queryParams().toString() - ); + const response = await fetch(thread.dataset.draftsUrl + "?" + queryParams().toString(), { + method: "GET", + credentials: "same-origin", + headers: { Accept: "application/json" } + }); + const payload = await response.json(); setCardLoading(card, false); if (!payload.ok) { - setCardMessage(card, payload.error || "Failed to load drafts."); + card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load drafts."; return; } const drafts = Array.isArray(payload.drafts) ? payload.drafts : []; - const container = cardContentNode(card); - if (!container) { - return; - } + const container = card.querySelector(".compose-ai-content"); container.innerHTML = ""; const engageButton = document.createElement("button"); engageButton.type = "button"; @@ -2720,7 +2697,7 @@ }); } catch (err) { setCardLoading(card, false); - setCardMessage(card, "Failed to load drafts."); + card.querySelector(".compose-ai-content").textContent = "Failed to load drafts."; } }; @@ -2731,18 +2708,21 @@ } setCardLoading(card, true); try { - const payload = await getJson( - thread.dataset.summaryUrl + "?" + queryParams().toString() - ); + const response = await fetch(thread.dataset.summaryUrl + "?" + queryParams().toString(), { + method: "GET", + credentials: "same-origin", + headers: { Accept: "application/json" } + }); + const payload = await response.json(); setCardLoading(card, false); if (!payload.ok) { - setCardMessage(card, payload.error || "Failed to load summary."); + card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load summary."; return; } - setCardMessage(card, String(payload.summary || "")); + card.querySelector(".compose-ai-content").textContent = String(payload.summary || ""); } catch (err) { setCardLoading(card, false); - setCardMessage(card, "Failed to load summary."); + card.querySelector(".compose-ai-content").textContent = "Failed to load summary."; } }; @@ -2753,14 +2733,17 @@ } setCardLoading(card, true); try { - const payload = await getJson( - thread.dataset.quickInsightsUrl + "?" + queryParams().toString() + const response = await fetch( + thread.dataset.quickInsightsUrl + "?" + queryParams().toString(), + { + method: "GET", + credentials: "same-origin", + headers: { Accept: "application/json" } + } ); + const payload = await response.json(); setCardLoading(card, false); - const container = cardContentNode(card); - if (!container) { - return; - } + const container = card.querySelector(".compose-ai-content"); if (!payload.ok) { container.textContent = payload.error || "Failed to load quick insights."; return; @@ -3013,7 +2996,8 @@ } } catch (err) { setCardLoading(card, false); - setCardMessage(card, "Failed to load quick insights."); + card.querySelector(".compose-ai-content").textContent = + "Failed to load quick insights."; } }; @@ -3053,12 +3037,18 @@ if (showCustom && customValue) { params.set("custom_text", customValue); } - const payload = await getJson( - thread.dataset.engagePreviewUrl + "?" + params.toString() + const response = await fetch( + thread.dataset.engagePreviewUrl + "?" + params.toString(), + { + method: "GET", + credentials: "same-origin", + headers: { Accept: "application/json" } + } ); + const payload = await response.json(); setCardLoading(card, false); if (!payload.ok) { - setCardMessage(card, payload.error || "Failed to load engage preview."); + card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load engage preview."; panelState.engageToken = ""; return; } @@ -3093,11 +3083,11 @@ if (payload.artifact) { text = text + "\n\nSource: " + String(payload.artifact); } - setCardMessage(card, text); + card.querySelector(".compose-ai-content").textContent = text; sendBtn.disabled = !(confirm.checked && panelState.engageToken); } catch (err) { setCardLoading(card, false); - setCardMessage(card, "Failed to load engage preview."); + card.querySelector(".compose-ai-content").textContent = "Failed to load engage preview."; panelState.engageToken = ""; } finally { if (refreshBtn) { diff --git a/core/views/whatsapp.py b/core/views/whatsapp.py index 2cb8784..db9b2bd 100644 --- a/core/views/whatsapp.py +++ b/core/views/whatsapp.py @@ -7,7 +7,7 @@ from django.views import View from mixins.views import ObjectList, ObjectRead from core.clients import transport -from core.models import ChatSession, Message, PersonIdentifier +from core.models import PersonIdentifier from core.util import logs from core.views.compose import _compose_urls, _service_icon_class from core.views.manage.permissions import SuperUserRequiredMixin @@ -263,84 +263,102 @@ class WhatsAppChatsList(WhatsAppContactsList): rows = [] seen = set() state = transport.get_runtime_state("whatsapp") + runtime_contacts = state.get("contacts") or [] - runtime_name_map = {} - for item in runtime_contacts: - if not isinstance(item, dict): - continue - identifier = str(item.get("identifier") or "").strip() + runtime_groups = state.get("groups") or [] + combined_contacts = [] + for item in runtime_contacts + runtime_groups: + if isinstance(item, dict): + combined_contacts.append(item) + contact_index = {} + for item in combined_contacts: + raw_identifier = str( + item.get("identifier") or item.get("jid") or item.get("chat") or "" + ).strip() + jid = str(item.get("jid") or "").strip() + name = str(item.get("name") or item.get("chat") or "").strip() + base_id = raw_identifier.split("@", 1)[0].strip() + jid_base = jid.split("@", 1)[0].strip() + for key in {raw_identifier, base_id, jid, jid_base}: + if key: + contact_index[key] = {"name": name, "jid": jid} + + history_anchors = state.get("history_anchors") or {} + for key, anchor in (history_anchors.items() if isinstance(history_anchors, dict) else []): + identifier = str(key or "").strip() if not identifier: continue - runtime_name_map[identifier] = str(item.get("name") or "").strip() - - sessions = ( - ChatSession.objects.filter( - user=self.request.user, - identifier__service="whatsapp", - ) - .select_related("identifier", "identifier__person") - .order_by("-last_interaction", "-id") - ) - for session in sessions: - identifier = str(session.identifier.identifier or "").strip() - if not identifier or identifier in seen: + identifier = identifier.split("@", 1)[0].strip() or identifier + if identifier in seen: continue seen.add(identifier) - latest = ( - Message.objects.filter(user=self.request.user, session=session) - .order_by("-ts") - .first() + anchor_jid = str((anchor or {}).get("chat_jid") or "").strip() + contact = contact_index.get(identifier) or contact_index.get(anchor_jid) + jid = (contact or {}).get("jid") or anchor_jid or identifier + linked = self._linked_identifier(identifier, jid) + urls = _compose_urls( + "whatsapp", + identifier, + linked.person_id if linked else None, ) - urls = _compose_urls("whatsapp", identifier, session.identifier.person_id) - preview = str((latest.text if latest else "") or "").strip() - if len(preview) > 80: - preview = f"{preview[:77]}..." - display_name = ( - preview - or runtime_name_map.get(identifier) - or session.identifier.person.name + name = ( + (contact or {}).get("name") + or (linked.person.name if linked else "") + or jid + or identifier or "WhatsApp Chat" ) rows.append( { "identifier": identifier, - "jid": identifier, - "name": display_name, + "jid": jid, + "name": name, "service_icon_class": _service_icon_class("whatsapp"), - "person_name": session.identifier.person.name, + "person_name": linked.person.name if linked else "", "compose_page_url": urls["page_url"], "compose_widget_url": urls["widget_url"], "match_url": ( f"{reverse('compose_contact_match')}?" f"{urlencode({'service': 'whatsapp', 'identifier': identifier})}" ), - "last_ts": int(latest.ts or 0) if latest else 0, + "last_ts": int((anchor or {}).get("ts") or (anchor or {}).get("updated_at") or 0), } ) - # Fallback: show synced WhatsApp contacts as chat entries even when no - # local message history exists yet. - for item in runtime_contacts: - if not isinstance(item, dict): - continue - identifier = str(item.get("identifier") or item.get("jid") or "").strip() + + if rows: + rows.sort(key=lambda row: row.get("last_ts", 0), reverse=True) + return rows + + # Fallback: if no anchors yet, surface the runtime contacts (best effort live state) + for item in combined_contacts: + identifier = str( + item.get("identifier") or item.get("jid") or item.get("chat") or "" + ).strip() if not identifier: continue identifier = identifier.split("@", 1)[0].strip() if not identifier or identifier in seen: continue seen.add(identifier) - linked = self._linked_identifier(identifier, str(item.get("jid") or "")) + jid = str(item.get("jid") or "").strip() + linked = self._linked_identifier(identifier, jid) urls = _compose_urls( "whatsapp", identifier, linked.person_id if linked else None, ) + name = ( + str(item.get("name") or item.get("chat") or "").strip() + or (linked.person.name if linked else "") + or jid + or identifier + or "WhatsApp Chat" + ) rows.append( { "identifier": identifier, - "jid": str(item.get("jid") or identifier).strip(), - "name": str(item.get("name") or "WhatsApp Chat").strip() - or "WhatsApp Chat", + "jid": jid or identifier, + "name": name, "service_icon_class": _service_icon_class("whatsapp"), "person_name": linked.person.name if linked else "", "compose_page_url": urls["page_url"], @@ -352,10 +370,7 @@ class WhatsAppChatsList(WhatsAppContactsList): "last_ts": 0, } ) - if rows: - rows.sort(key=lambda row: row.get("last_ts", 0), reverse=True) - return rows - return super().get_queryset(*args, **kwargs) + return rows class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead): diff --git a/core/views/workspace.py b/core/views/workspace.py index 4e65926..84c2605 100644 --- a/core/views/workspace.py +++ b/core/views/workspace.py @@ -3497,12 +3497,6 @@ def _workspace_nav_urls(person): } -def _person_plan_or_404(request, person_id, plan_id): - person = get_object_or_404(Person, pk=person_id, user=request.user) - plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) - return person, plan - - class AIWorkspace(LoginRequiredMixin, View): template_name = "pages/ai-workspace.html" @@ -4437,7 +4431,12 @@ class AIWorkspaceMitigationChat(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404( + PatternMitigationPlan, + id=plan_id, + user=request.user, + ) text = (request.POST.get("message") or "").strip() active_tab = _sanitize_active_tab( request.POST.get("active_tab"), default="ask_ai" @@ -4519,11 +4518,14 @@ class AIWorkspaceMitigationChat(LoginRequiredMixin, View): text=assistant_text, ) - return _render_mitigation_panel( + return render( request, - person, - plan, - active_tab=active_tab, + "partials/ai-workspace-mitigation-panel.html", + _mitigation_panel_context( + person=person, + plan=plan, + active_tab=active_tab, + ), ) @@ -4534,7 +4536,12 @@ class AIWorkspaceExportArtifact(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404( + PatternMitigationPlan, + id=plan_id, + user=request.user, + ) artifact_type = (request.POST.get("artifact_type") or "rulebook").strip() if artifact_type not in {"rulebook", "rules", "games", "corrections"}: @@ -4581,7 +4588,8 @@ class AIWorkspaceCreateArtifact(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) kind_key = (kind or "").strip().lower() if kind_key not in self.kind_map: return HttpResponseBadRequest("Invalid artifact kind") @@ -4635,7 +4643,8 @@ class AIWorkspaceUpdateArtifact(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) kind_key = (kind or "").strip().lower() if kind_key not in self.kind_map: return HttpResponseBadRequest("Invalid artifact kind") @@ -4710,7 +4719,8 @@ class AIWorkspaceDeleteArtifact(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) kind_key = (kind or "").strip().lower() if kind_key not in self.kind_map: return HttpResponseBadRequest("Invalid artifact kind") @@ -4749,7 +4759,8 @@ class AIWorkspaceDeleteArtifactList(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) kind_key = (kind or "").strip().lower() if kind_key not in self.kind_map: return HttpResponseBadRequest("Invalid artifact kind") @@ -4786,7 +4797,8 @@ class AIWorkspaceEngageShare(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) source_ref = (request.POST.get("source_ref") or "").strip() share_target = (request.POST.get("share_target") or "self").strip() @@ -5051,7 +5063,8 @@ class AIWorkspaceAutoSettings(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) auto_settings = _get_or_create_auto_settings(request.user, plan.conversation) auto_settings.enabled = _is_truthy(request.POST.get("enabled")) @@ -5121,7 +5134,8 @@ class AIWorkspaceUpdateFundamentals(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) fundamentals_text = request.POST.get("fundamentals_text") or "" active_tab = _sanitize_active_tab( request.POST.get("active_tab"), default="fundamentals" @@ -5145,7 +5159,8 @@ class AIWorkspaceUpdatePlanMeta(LoginRequiredMixin, View): if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") - person, plan = _person_plan_or_404(request, person_id, plan_id) + person = get_object_or_404(Person, pk=person_id, user=request.user) + plan = get_object_or_404(PatternMitigationPlan, id=plan_id, user=request.user) active_tab = _sanitize_active_tab( request.POST.get("active_tab"), default="plan_board" ) diff --git a/docker-compose.yml b/docker-compose.yml index 00fe09d..0ed552b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -336,7 +336,7 @@ services: source: /code/vrun target: /var/run healthcheck: - test: "redis-cli ping" + test: "CMD-SHELL redis-cli -s /var/run/gia-redis.sock ping" interval: 2s timeout: 2s retries: 15 diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours