Pull groups from WhatsApp
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -163,3 +163,4 @@ static/
|
||||
auth_debug.log
|
||||
genv
|
||||
oom
|
||||
node_modules/
|
||||
|
||||
89
artifacts/checkpoints/C1_route_response_map.tsv
Normal file
89
artifacts/checkpoints/C1_route_response_map.tsv
Normal file
@@ -0,0 +1,89 @@
|
||||
name route view response_kind templates
|
||||
signup accounts/signup/ base.Signup html registration/registration_closed.html
|
||||
notifications_update notifications/<str:type>/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/<str:type>/ signal.SignalAccounts unknown -
|
||||
whatsapp_accounts services/whatsapp/<str:type>/ whatsapp.WhatsAppAccounts unknown -
|
||||
instagram_accounts services/instagram/<str:type>/ instagram.InstagramAccounts unknown -
|
||||
signal_contacts services/signal/<str:type>/contacts/<str:pk>/ signal.SignalContactsList unknown -
|
||||
whatsapp_contacts services/whatsapp/<str:type>/contacts/<str:pk>/ whatsapp.WhatsAppContactsList unknown -
|
||||
signal_chats services/signal/<str:type>/chats/<str:pk>/ signal.SignalChatsList unknown -
|
||||
whatsapp_chats services/whatsapp/<str:type>/chats/<str:pk>/ whatsapp.WhatsAppChatsList unknown -
|
||||
signal_messages services/signal/<str:type>/messages/<str:pk>/<str:chat_id>/ signal.SignalMessagesList unknown -
|
||||
signal_account_add services/signal/<str:type>/add/ signal.SignalAccountAdd unknown -
|
||||
whatsapp_account_add services/whatsapp/<str:type>/add/ whatsapp.WhatsAppAccountAdd html -
|
||||
whatsapp_account_unlink services/whatsapp/<str:type>/unlink/<path:account>/ whatsapp.WhatsAppAccountUnlink html partials/signal-accounts.html
|
||||
instagram_account_add services/instagram/<str:type>/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/<str:type>/contacts/ workspace.AIWorkspaceContactsWidget html mixins/wm/widget.html
|
||||
ai_workspace_person ai/workspace/<str:type>/person/<uuid:person_id>/ workspace.AIWorkspacePersonWidget html mixins/wm/widget.html
|
||||
ai_workspace_person_timeline ai/workspace/<str:type>/person/<uuid:person_id>/timeline/ workspace.AIWorkspacePersonTimelineWidget html mixins/wm/widget.html
|
||||
ai_workspace_insight_graphs ai/workspace/<str:type>/person/<uuid:person_id>/insights/graphs/ workspace.AIWorkspaceInsightGraphs html pages/ai-workspace-insight-graphs.html
|
||||
ai_workspace_information ai/workspace/<str:type>/person/<uuid:person_id>/information/ workspace.AIWorkspaceInformation html pages/ai-workspace-information.html
|
||||
ai_workspace_insight_help ai/workspace/<str:type>/person/<uuid:person_id>/insights/help/ workspace.AIWorkspaceInsightHelp html pages/ai-workspace-insight-help.html
|
||||
ai_workspace_insight_detail ai/workspace/<str:type>/person/<uuid:person_id>/insights/<str:metric>/ workspace.AIWorkspaceInsightDetail html pages/ai-workspace-insight-detail.html
|
||||
ai_workspace_run ai/workspace/<str:type>/person/<uuid:person_id>/run/<str:operation>/ workspace.AIWorkspaceRunOperation html partials/ai-workspace-ai-result.html
|
||||
ai_workspace_send ai/workspace/<str:type>/person/<uuid:person_id>/send/ workspace.AIWorkspaceSendDraft unknown -
|
||||
ai_workspace_queue ai/workspace/<str:type>/person/<uuid:person_id>/queue/ workspace.AIWorkspaceQueueDraft unknown -
|
||||
ai_workspace_mitigation_create ai/workspace/<str:type>/person/<uuid:person_id>/mitigation/create/ workspace.AIWorkspaceCreateMitigation html partials/ai-workspace-mitigation-panel.html
|
||||
ais ai/<str:type>/ ais.AIList unknown -
|
||||
osint_search search/<str:type>/ 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/<str:type>/create/ ais.AICreate unknown -
|
||||
ai_update ai/<str:type>/update/<str:pk>/ ais.AIUpdate unknown -
|
||||
ai_delete ai/<str:type>/delete/<str:pk>/ ais.AIDelete unknown -
|
||||
people person/<str:type>/ people.PersonList unknown -
|
||||
person_create person/<str:type>/create/ people.PersonCreate unknown -
|
||||
person_update person/<str:type>/update/<str:pk>/ people.PersonUpdate unknown -
|
||||
person_delete person/<str:type>/delete/<str:pk>/ people.PersonDelete unknown -
|
||||
groups group/<str:type>/ groups.GroupList unknown -
|
||||
group_create group/<str:type>/create/ groups.GroupCreate unknown -
|
||||
group_update group/<str:type>/update/<str:pk>/ groups.GroupUpdate unknown -
|
||||
group_delete group/<str:type>/delete/<str:pk>/ groups.GroupDelete unknown -
|
||||
personas persona/<str:type>/ personas.PersonaList unknown -
|
||||
persona_create persona/<str:type>/create/ personas.PersonaCreate unknown -
|
||||
persona_update persona/<str:type>/update/<str:pk>/ personas.PersonaUpdate unknown -
|
||||
persona_delete persona/<str:type>/delete/<str:pk>/ personas.PersonaDelete unknown -
|
||||
manipulations manipulation/<str:type>/ manipulations.ManipulationList unknown -
|
||||
manipulation_create manipulation/<str:type>/create/ manipulations.ManipulationCreate unknown -
|
||||
manipulation_update manipulation/<str:type>/update/<str:pk>/ manipulations.ManipulationUpdate unknown -
|
||||
manipulation_delete manipulation/<str:type>/delete/<str:pk>/ manipulations.ManipulationDelete unknown -
|
||||
sessions session/<str:type>/ sessions.SessionList unknown -
|
||||
session_create session/<str:type>/create/ sessions.SessionCreate unknown -
|
||||
session_update session/<str:type>/update/<str:pk>/ sessions.SessionUpdate unknown -
|
||||
session_delete session/<str:type>/delete/<str:pk>/ sessions.SessionDelete unknown -
|
||||
person_identifiers person/<str:type>/identifiers/<str:person>/ identifiers.PersonIdentifierList unknown -
|
||||
person_identifier_create person/<str:type>/identifiers/create/<str:person> identifiers.PersonIdentifierCreate unknown -
|
||||
person_identifier_update person/<str:type>/identifiers/update/<str:person>/<str:pk>/ identifiers.PersonIdentifierUpdate unknown -
|
||||
person_identifier_delete person/<str:type>/identifiers/delete/<str:person>/<str:pk>/ identifiers.PersonIdentifierDelete unknown -
|
||||
messages session/<str:type>/messages/<str:session>/ messages.MessageList unknown -
|
||||
message_create session/<str:type>/messages/create/<str:session> messages.MessageCreate unknown -
|
||||
message_update session/<str:type>/messages/update/<str:session>/<str:pk>/ messages.MessageUpdate unknown -
|
||||
message_delete session/<str:type>/messages/delete/<str:session>/<str:pk>/ messages.MessageDelete unknown -
|
||||
message_accept_api api/v1/queue/message/accept/<str:message_id>/ queues.AcceptMessageAPI http -
|
||||
message_reject_api api/v1/queue/message/reject/<str:message_id>/ queues.RejectMessageAPI http -
|
||||
queues queue/<str:type>/ queues.QueueList unknown -
|
||||
queue_create queue/<str:type>/create/ queues.QueueCreate unknown -
|
||||
queue_update queue/<str:type>/update/<str:pk>/ queues.QueueUpdate unknown -
|
||||
queue_delete queue/<str:type>/delete/<str:pk>/ queues.QueueDelete unknown -
|
||||
|
121
artifacts/checkpoints/C2_duplicate_blocks.tsv
Normal file
121
artifacts/checkpoints/C2_duplicate_blocks.tsv
Normal file
@@ -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
|
||||
|
@@ -1465,13 +1465,10 @@ 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:
|
||||
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
|
||||
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),
|
||||
@@ -1480,12 +1477,29 @@ class WhatsAppClient(ClientBase):
|
||||
self._publish_state(
|
||||
contacts=contacts,
|
||||
lid_map=lid_map,
|
||||
contacts_synced_at=int(time.time()),
|
||||
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",
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
56
core/migrations/0025_platformchatlink.py
Normal file
56
core/migrations/0025_platformchatlink.py
Normal file
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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."""
|
||||
|
||||
|
||||
84
core/templates/pages/whatsapp-chat-link.html
Normal file
84
core/templates/pages/whatsapp-chat-link.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container" style="max-width: 44rem;">
|
||||
<div class="level" style="margin-bottom: 0.75rem;">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<h1 class="title is-4" style="margin-bottom: 0.2rem;">WhatsApp Chat Link</h1>
|
||||
<p class="is-size-7 has-text-grey">Link a WhatsApp chat identifier to a person. This link is WhatsApp-only.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<a class="button is-light" href="{% url 'whatsapp' %}">
|
||||
<span class="icon is-small"><i class="fa-solid fa-arrow-left"></i></span>
|
||||
<span>Back To WhatsApp</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if notice_message %}
|
||||
<article class="notification is-{{ notice_level|default:'info' }} is-light">
|
||||
{{ notice_message }}
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<article class="box">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label is-small">Chat Identifier</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="identifier" value="{{ identifier }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small">Existing Person</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select name="person_id">
|
||||
<option value="">- Select person -</option>
|
||||
{% for person in people %}
|
||||
<option value="{{ person.id }}" {% if existing and existing.person_id == person.id %}selected{% endif %}>{{ person.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small">Or Create Person</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="person_name" placeholder="New person name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small">Chat JID (optional)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="chat_jid" value="{% if existing %}{{ existing.chat_jid|default:'' }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-small">Display Name (optional)</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="chat_name" value="{% if existing %}{{ existing.chat_name|default:'' }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button is-link" type="submit">
|
||||
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
|
||||
<span>Save WhatsApp Chat Link</span>
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
{% if existing %}
|
||||
<article class="notification is-light" style="margin-top: 0.8rem;">
|
||||
Current link: <strong>{{ existing.person.name }}</strong> ← <code>{{ existing.chat_identifier }}</code>
|
||||
</article>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -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()
|
||||
);
|
||||
setCardLoading(card, false);
|
||||
const container = cardContentNode(card);
|
||||
if (!container) {
|
||||
return;
|
||||
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 = 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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
|
||||
|
||||
@@ -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,
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
20
openspec/config.yaml
Normal file
20
openspec/config.yaml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user