django-unicom 25.4.2.dev0__tar.gz → 25.4.2.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/MANIFEST.in +2 -1
  2. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/PKG-INFO +8 -1
  3. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/README.md +6 -0
  4. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/PKG-INFO +8 -1
  5. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/SOURCES.txt +5 -11
  6. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/requires.txt +1 -0
  7. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/setup.py +1 -0
  8. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/_version.py +2 -2
  9. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/consumers/webchat_consumer.py +59 -8
  10. django_unicom-25.4.2.dev2/unicom/migrations/0025_message_unicom_message_channel_imap_uid_unique.py +19 -0
  11. django_unicom-25.4.2.dev2/unicom/migrations/0026_message_email_sender_authenticated.py +18 -0
  12. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/message.py +24 -6
  13. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/reply_to_message.py +4 -0
  14. django_unicom-25.4.2.dev2/unicom/services/email/auth_helpers.py +25 -0
  15. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/listen_to_IMAP.py +19 -3
  16. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/save_email_message.py +91 -25
  17. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/send_email_message.py +9 -4
  18. django_unicom-25.4.2.dev2/unicom/services/email/system_channel.py +29 -0
  19. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/validate_email_config.py +25 -12
  20. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/save_telegram_message.py +3 -3
  21. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/send_webchat_message.py +29 -1
  22. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/signals.py +21 -21
  23. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/channel_config.js +6 -1
  24. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-item.js +100 -10
  25. django_unicom-25.4.2.dev2/unicom/static/unicom/webchat/utils/morphdom.js +84 -0
  26. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/realtime-client.js +9 -0
  27. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-styles.js +383 -0
  28. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-with-sidebar.js +87 -17
  29. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/unicom/webchat_demo.html +1 -1
  30. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/webchat_views.py +2 -2
  31. django_unicom-25.4.2.dev0/smoke/consumer-install/.env.example +0 -2
  32. django_unicom-25.4.2.dev0/smoke/consumer-install/Dockerfile +0 -28
  33. django_unicom-25.4.2.dev0/smoke/consumer-install/README.md +0 -54
  34. django_unicom-25.4.2.dev0/smoke/consumer-install/app/manage.py +0 -13
  35. django_unicom-25.4.2.dev0/smoke/consumer-install/app/seed_webchat_channel.py +0 -21
  36. django_unicom-25.4.2.dev0/smoke/consumer-install/app/smoke_test.py +0 -44
  37. django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/__init__.py +0 -1
  38. django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/settings.py +0 -59
  39. django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/urls.py +0 -6
  40. django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/wsgi.py +0 -8
  41. django_unicom-25.4.2.dev0/smoke/consumer-install/docker-compose.yaml +0 -59
  42. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/match_codebase_style.mdc +0 -0
  43. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
  44. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/require_context_before_changes.mdc +0 -0
  45. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/Dockerfile +0 -0
  46. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/Makefile +0 -0
  47. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/conftest.py +0 -0
  48. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/data/definitions/bots/__init__.py +0 -0
  49. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/data/definitions/tools/__init__.py +0 -0
  50. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/dependency_links.txt +0 -0
  51. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/top_level.txt +0 -0
  52. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/docker-compose.yaml +0 -0
  53. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/entrypoint.sh +0 -0
  54. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/internal-channel-modernization.md +0 -0
  55. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/manage.py +0 -0
  56. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/pyproject.toml +0 -0
  57. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/pytest.ini +0 -0
  58. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/requirements.txt +0 -0
  59. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/scripts/reacher_validate.sh +0 -0
  60. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/setup.cfg +0 -0
  61. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/__init__.py +0 -0
  62. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_cross_platform_buttons.py +0 -0
  63. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_authentication.py +0 -0
  64. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_live.py +0 -0
  65. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_request_processing.py +0 -0
  66. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_telegram_live.py +0 -0
  67. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_webchat.py +0 -0
  68. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_webchat_branching.py +0 -0
  69. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/utils.py +0 -0
  70. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/todo.md +0 -0
  71. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/__init__.py +0 -0
  72. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/__init__.py +0 -0
  73. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/account_admin.py +0 -0
  74. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/channel_admin.py +0 -0
  75. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/chat_admin.py +0 -0
  76. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/draft_message_admin.py +0 -0
  77. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/email_inline_image_admin.py +0 -0
  78. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/filters.py +0 -0
  79. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/member_admin.py +0 -0
  80. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/message_admin.py +0 -0
  81. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/message_template_admin.py +0 -0
  82. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/request_admin.py +0 -0
  83. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/apps.py +0 -0
  84. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/consumers/__init__.py +0 -0
  85. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/__init__.py +0 -0
  86. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/__init__.py +0 -0
  87. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/mail_inbox_listen.py +0 -0
  88. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/run_as_llm_chat.py +0 -0
  89. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/send_scheduled_messages.py +0 -0
  90. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/start_imap_listeners.py +0 -0
  91. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0001_initial.py +0 -0
  92. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0002_emailinlineimage.py +0 -0
  93. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
  94. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
  95. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
  96. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0006_channel_created_at_channel_created_by_and_more.py +0 -0
  97. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0007_message_imap_uid.py +0 -0
  98. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0008_add_all_members_group.py +0 -0
  99. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0009_add_tool_call_types.py +0 -0
  100. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0010_request_initial_request_request_llm_calls_count_and_more.py +0 -0
  101. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
  102. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0012_message_response_to_tool_call_and_more.py +0 -0
  103. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0013_callbackexecution.py +0 -0
  104. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0014_rename_authorized_user_to_intended_account.py +0 -0
  105. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0015_simplify_callback_execution.py +0 -0
  106. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0016_callbackexecution_tool_call.py +0 -0
  107. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0017_add_chat_metadata.py +0 -0
  108. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0018_message_bounce_details_message_bounce_reason_and_more.py +0 -0
  109. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0019_draftmessage_skip_reacher_validation.py +0 -0
  110. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0020_message_provider_message_id.py +0 -0
  111. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0021_message_open_count.py +0 -0
  112. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0022_toolcall_result_status.py +0 -0
  113. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0023_toolcall_progress_updates_for_user.py +0 -0
  114. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0024_email_from_name.py +0 -0
  115. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/__init__.py +0 -0
  116. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/__init__.py +0 -0
  117. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/account.py +0 -0
  118. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/account_chat.py +0 -0
  119. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/callback_execution.py +0 -0
  120. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/channel.py +0 -0
  121. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/chat.py +0 -0
  122. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/constants.py +0 -0
  123. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/draft_message.py +0 -0
  124. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/fields.py +0 -0
  125. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/member.py +0 -0
  126. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/member_group.py +0 -0
  127. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/message_template.py +0 -0
  128. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/request.py +0 -0
  129. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/request_category.py +0 -0
  130. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/tool_call.py +0 -0
  131. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/update.py +0 -0
  132. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/__init__.py +0 -0
  133. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/chat_summary.py +0 -0
  134. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/__init__.py +0 -0
  135. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/scheduler.py +0 -0
  136. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/send_message.py +0 -0
  137. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/decode_base64_image.py +0 -0
  138. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/IMAP_thread_manager.py +0 -0
  139. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/__init__.py +0 -0
  140. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/email_tracking.py +0 -0
  141. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/quote_filter.py +0 -0
  142. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
  143. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/get_public_origin.py +0 -0
  144. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/html_inline_images.py +0 -0
  145. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/__init__.py +0 -0
  146. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/generate_text_message_data.py +0 -0
  147. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/save_internal_message.py +0 -0
  148. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/send_internal_message.py +0 -0
  149. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/llm/__init__.py +0 -0
  150. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/llm/tool_calls.py +0 -0
  151. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/__init__.py +0 -0
  152. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/answer_callback_query.py +0 -0
  153. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/create_inline_keyboard.py +0 -0
  154. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/download_file.py +0 -0
  155. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/edit_telegram_message.py +0 -0
  156. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/escape_markdown.py +0 -0
  157. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/get_file_path.py +0 -0
  158. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/handle_telegram_callback.py +0 -0
  159. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/send_telegram_message.py +0 -0
  160. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/set_telegram_webhook.py +0 -0
  161. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
  162. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
  163. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/template_renderer.py +0 -0
  164. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/__init__.py +0 -0
  165. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/generate_chat_title.py +0 -0
  166. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/get_or_create_account.py +0 -0
  167. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/migrate_guest_to_user.py +0 -0
  168. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/save_webchat_message.py +0 -0
  169. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/__init__.py +0 -0
  170. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/get_template.py +0 -0
  171. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
  172. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
  173. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
  174. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
  175. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
  176. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
  177. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/tinymce_init.js +0 -0
  178. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/chat-list.js +0 -0
  179. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/media-preview.js +0 -0
  180. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-input.js +0 -0
  181. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-list.js +0 -0
  182. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/voice-recorder.js +0 -0
  183. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/api-client.js +0 -0
  184. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/datetime-formatter.js +0 -0
  185. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/font-awesome-loader.js +0 -0
  186. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-component.js +0 -0
  187. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
  188. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat/compose.html +0 -0
  189. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat_history.html +0 -0
  190. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
  191. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
  192. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
  193. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
  194. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
  195. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
  196. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/code_templates/category_processor.py +0 -0
  197. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templatetags/__init__.py +0 -0
  198. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templatetags/unicom_tags.py +0 -0
  199. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/urls.py +0 -0
  200. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/__init__.py +0 -0
  201. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/chat_history_view.py +0 -0
  202. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/compose_view.py +0 -0
  203. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/email_tracking.py +0 -0
  204. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/inline_image.py +0 -0
  205. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/message_template.py +0 -0
  206. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/telegram_webhook.py +0 -0
  207. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/webchat_demo_view.py +0 -0
  208. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/whatsapp_webhook.py +0 -0
  209. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/__init__.py +0 -0
  210. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/apps.py +0 -0
  211. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/asgi.py +0 -0
  212. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/callback_handlers.py +0 -0
  213. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/__init__.py +0 -0
  214. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/bots/__init__.py +0 -0
  215. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/bots/assistant_bot.py +0 -0
  216. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/__init__.py +0 -0
  217. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/cross_platform_buttons.py +0 -0
  218. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/get_system_info.py +0 -0
  219. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/interactive_menu.py +0 -0
  220. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/interval_alarm.py +0 -0
  221. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/ip_lookup.py +0 -0
  222. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/simple_timer.py +0 -0
  223. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/settings.py +0 -0
  224. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/test_button_handlers.py +0 -0
  225. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/urls.py +0 -0
  226. {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/wsgi.py +0 -0
@@ -1,6 +1,7 @@
1
1
  include LICENSE
2
2
  include README.md
3
3
  include MANIFEST.in
4
+ prune smoke
4
5
  recursive-include unicom/static *
5
6
  recursive-include unicom/templates *
6
7
  recursive-include unicom/locale *
@@ -8,4 +9,4 @@ recursive-include unicom/migrations *.py
8
9
  global-exclude *.pyc
9
10
  global-exclude __pycache__
10
11
  global-exclude *.pyo
11
- global-exclude .git*
12
+ global-exclude .git*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-unicom
3
- Version: 25.4.2.dev0
3
+ Version: 25.4.2.dev2
4
4
  Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
5
5
  Home-page: https://github.com/meena-erian/django-unicom
6
6
  Author: Meena (Menas) Erian
@@ -23,6 +23,7 @@ Requires-Dist: Pillow>=10.4.0
23
23
  Requires-Dist: django-ace>=1.39.2
24
24
  Requires-Dist: fa2svg==0.1.10
25
25
  Requires-Dist: pytz>=2024.1
26
+ Requires-Dist: Jinja2>=3.1.0
26
27
  Requires-Dist: openai
27
28
  Requires-Dist: pydub>=0.25.1
28
29
  Requires-Dist: audioop-lts; python_version >= "3.13"
@@ -104,6 +105,12 @@ Dynamic: summary
104
105
  python -m playwright install --with-deps
105
106
  ```
106
107
 
108
+ If you use the email icon-rendering or PDF-export features, you also need
109
+ native Cairo libraries available on the host image. On Debian/Ubuntu-based
110
+ systems this typically means installing `libcairo2`. Those libraries are not
111
+ required for basic messaging setup and are loaded lazily when the relevant
112
+ feature paths are used.
113
+
107
114
  2. **Add required apps to your Django settings:**
108
115
 
109
116
  ```python
@@ -59,6 +59,12 @@
59
59
  python -m playwright install --with-deps
60
60
  ```
61
61
 
62
+ If you use the email icon-rendering or PDF-export features, you also need
63
+ native Cairo libraries available on the host image. On Debian/Ubuntu-based
64
+ systems this typically means installing `libcairo2`. Those libraries are not
65
+ required for basic messaging setup and are loaded lazily when the relevant
66
+ feature paths are used.
67
+
62
68
  2. **Add required apps to your Django settings:**
63
69
 
64
70
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-unicom
3
- Version: 25.4.2.dev0
3
+ Version: 25.4.2.dev2
4
4
  Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
5
5
  Home-page: https://github.com/meena-erian/django-unicom
6
6
  Author: Meena (Menas) Erian
@@ -23,6 +23,7 @@ Requires-Dist: Pillow>=10.4.0
23
23
  Requires-Dist: django-ace>=1.39.2
24
24
  Requires-Dist: fa2svg==0.1.10
25
25
  Requires-Dist: pytz>=2024.1
26
+ Requires-Dist: Jinja2>=3.1.0
26
27
  Requires-Dist: openai
27
28
  Requires-Dist: pydub>=0.25.1
28
29
  Requires-Dist: audioop-lts; python_version >= "3.13"
@@ -104,6 +105,12 @@ Dynamic: summary
104
105
  python -m playwright install --with-deps
105
106
  ```
106
107
 
108
+ If you use the email icon-rendering or PDF-export features, you also need
109
+ native Cairo libraries available on the host image. On Debian/Ubuntu-based
110
+ systems this typically means installing `libcairo2`. Those libraries are not
111
+ required for basic messaging setup and are loaded lazily when the relevant
112
+ feature paths are used.
113
+
107
114
  2. **Add required apps to your Django settings:**
108
115
 
109
116
  ```python
@@ -23,17 +23,6 @@ django_unicom.egg-info/dependency_links.txt
23
23
  django_unicom.egg-info/requires.txt
24
24
  django_unicom.egg-info/top_level.txt
25
25
  scripts/reacher_validate.sh
26
- smoke/consumer-install/.env.example
27
- smoke/consumer-install/Dockerfile
28
- smoke/consumer-install/README.md
29
- smoke/consumer-install/docker-compose.yaml
30
- smoke/consumer-install/app/manage.py
31
- smoke/consumer-install/app/seed_webchat_channel.py
32
- smoke/consumer-install/app/smoke_test.py
33
- smoke/consumer-install/app/smokeproject/__init__.py
34
- smoke/consumer-install/app/smokeproject/settings.py
35
- smoke/consumer-install/app/smokeproject/urls.py
36
- smoke/consumer-install/app/smokeproject/wsgi.py
37
26
  tests/__init__.py
38
27
  tests/test_cross_platform_buttons.py
39
28
  tests/test_email_authentication.py
@@ -91,6 +80,8 @@ unicom/migrations/0021_message_open_count.py
91
80
  unicom/migrations/0022_toolcall_result_status.py
92
81
  unicom/migrations/0023_toolcall_progress_updates_for_user.py
93
82
  unicom/migrations/0024_email_from_name.py
83
+ unicom/migrations/0025_message_unicom_message_channel_imap_uid_unique.py
84
+ unicom/migrations/0026_message_email_sender_authenticated.py
94
85
  unicom/migrations/__init__.py
95
86
  unicom/models/__init__.py
96
87
  unicom/models/account.py
@@ -121,12 +112,14 @@ unicom/services/crossplatform/scheduler.py
121
112
  unicom/services/crossplatform/send_message.py
122
113
  unicom/services/email/IMAP_thread_manager.py
123
114
  unicom/services/email/__init__.py
115
+ unicom/services/email/auth_helpers.py
124
116
  unicom/services/email/email_tracking.py
125
117
  unicom/services/email/listen_to_IMAP.py
126
118
  unicom/services/email/quote_filter.py
127
119
  unicom/services/email/replace_cid_images_with_base64.py
128
120
  unicom/services/email/save_email_message.py
129
121
  unicom/services/email/send_email_message.py
122
+ unicom/services/email/system_channel.py
130
123
  unicom/services/email/validate_email_config.py
131
124
  unicom/services/internal/__init__.py
132
125
  unicom/services/internal/generate_text_message_data.py
@@ -175,6 +168,7 @@ unicom/static/unicom/webchat/components/voice-recorder.js
175
168
  unicom/static/unicom/webchat/utils/api-client.js
176
169
  unicom/static/unicom/webchat/utils/datetime-formatter.js
177
170
  unicom/static/unicom/webchat/utils/font-awesome-loader.js
171
+ unicom/static/unicom/webchat/utils/morphdom.js
178
172
  unicom/static/unicom/webchat/utils/realtime-client.js
179
173
  unicom/templates/admin/unicom/chat_history.html
180
174
  unicom/templates/admin/unicom/chat/change_list.html
@@ -12,6 +12,7 @@ Pillow>=10.4.0
12
12
  django-ace>=1.39.2
13
13
  fa2svg==0.1.10
14
14
  pytz>=2024.1
15
+ Jinja2>=3.1.0
15
16
  openai
16
17
  pydub>=0.25.1
17
18
  WeasyPrint==52.5
@@ -27,6 +27,7 @@ setup(
27
27
  'django-ace>=1.39.2',
28
28
  'fa2svg==0.1.10',
29
29
  'pytz>=2024.1',
30
+ 'Jinja2>=3.1.0',
30
31
  'openai',
31
32
  'pydub>=0.25.1',
32
33
  'audioop-lts; python_version >= "3.13"',
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '25.4.2.dev0'
32
- __version_tuple__ = version_tuple = (25, 4, 2, 'dev0')
31
+ __version__ = version = '25.4.2.dev2'
32
+ __version_tuple__ = version_tuple = (25, 4, 2, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -30,6 +30,7 @@ from typing import Iterable, Optional, Tuple
30
30
  try:
31
31
  from channels.generic.websocket import AsyncJsonWebsocketConsumer
32
32
  from channels.db import database_sync_to_async
33
+ from channels.layers import get_channel_layer
33
34
 
34
35
  CHANNELS_AVAILABLE = True
35
36
  except ImportError: # pragma: no cover - channels is optional
@@ -85,6 +86,11 @@ class WebChatConsumer(AsyncJsonWebsocketConsumer):
85
86
  self._recent_ids = deque(maxlen=self.warm_cache_limit)
86
87
  self._recent_id_set: set[str] = set()
87
88
 
89
+ def _group_name(self) -> Optional[str]:
90
+ if not self.chat_id:
91
+ return None
92
+ return f"webchat_chat_{self.chat_id}"
93
+
88
94
  # ------------------------------------------------------------------ WS API
89
95
  async def connect(self):
90
96
  """Validate access and start background polling."""
@@ -106,6 +112,9 @@ class WebChatConsumer(AsyncJsonWebsocketConsumer):
106
112
  return
107
113
 
108
114
  await self.accept()
115
+ group_name = self._group_name()
116
+ if group_name and self.channel_layer:
117
+ await self.channel_layer.group_add(group_name, self.channel_name)
109
118
  await self._warm_seen_cache()
110
119
  await self.send_json({"type": "ready", "chat_id": self.chat_id})
111
120
 
@@ -114,6 +123,12 @@ class WebChatConsumer(AsyncJsonWebsocketConsumer):
114
123
 
115
124
  async def disconnect(self, code):
116
125
  """Stop background polling gracefully."""
126
+ group_name = self._group_name()
127
+ if group_name and self.channel_layer:
128
+ try:
129
+ await self.channel_layer.group_discard(group_name, self.channel_name)
130
+ except Exception:
131
+ pass
117
132
  if self._polling_task and not self._polling_task.done():
118
133
  self._polling_task.cancel()
119
134
  try:
@@ -121,6 +136,20 @@ class WebChatConsumer(AsyncJsonWebsocketConsumer):
121
136
  except asyncio.CancelledError:
122
137
  pass
123
138
 
139
+ async def webchat_message_updated(self, event):
140
+ """Receive a message update via channel layer and forward to client."""
141
+ message_payload = event.get("message")
142
+ chat_id = event.get("chat_id") or self.chat_id
143
+ if not message_payload:
144
+ return
145
+ await self.send_json(
146
+ {
147
+ "type": "message_updated",
148
+ "chat_id": chat_id,
149
+ "message": message_payload,
150
+ }
151
+ )
152
+
124
153
  async def receive_json(self, content, **kwargs):
125
154
  """
126
155
  No client commands are required for this consumer. Respond to optional
@@ -305,11 +334,33 @@ def is_channels_available() -> bool:
305
334
 
306
335
 
307
336
  async def broadcast_message_to_chat(chat_id: str, message) -> None:
308
- """
309
- Legacy helper retained for backwards compatibility.
310
-
311
- The simplified consumer no longer relies on channel layers, so this helper
312
- simply exists to avoid import errors in projects that may still reference
313
- it. Messages are delivered by the consumer's periodic polling loop instead.
314
- """
315
- return None
337
+ """Broadcast a message update to WebSocket clients subscribed to this chat."""
338
+ if not CHANNELS_AVAILABLE:
339
+ return
340
+ channel_layer = get_channel_layer()
341
+ if channel_layer is None:
342
+ return
343
+
344
+ payload = {
345
+ "id": message.id,
346
+ "text": message.text,
347
+ "html": message.html,
348
+ "is_outgoing": message.is_outgoing,
349
+ "sender_name": message.sender_name,
350
+ "timestamp": message.timestamp.isoformat(),
351
+ "media_type": message.media_type,
352
+ "media_url": message.media.url if message.media else None,
353
+ # Never dereference related objects in async context; use *_id fields only.
354
+ "reply_to_message_id": message.reply_to_message_id,
355
+ "interactive_buttons": message.raw.get("interactive_buttons") if message.raw else None,
356
+ "progress_updates_for_user": (message.raw or {}).get("tool_call", {}).get("arguments", {}).get("progress_updates_for_user") if message.media_type == "tool_call" else None,
357
+ "result_status": (message.raw or {}).get("tool_response", {}).get("result", {}).get("status") if message.media_type == "tool_response" else None,
358
+ }
359
+ await channel_layer.group_send(
360
+ f"webchat_chat_{chat_id}",
361
+ {
362
+ "type": "webchat.message_updated",
363
+ "chat_id": chat_id,
364
+ "message": payload,
365
+ },
366
+ )
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.9 on 2025-12-27 07:06
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('unicom', '0024_email_from_name'),
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddConstraint(
16
+ model_name='message',
17
+ constraint=models.UniqueConstraint(fields=('channel', 'imap_uid'), name='unicom_message_channel_imap_uid_unique'),
18
+ ),
19
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.9 on 2025-12-27 08:18
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('unicom', '0025_message_unicom_message_channel_imap_uid_unique'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='message',
15
+ name='email_sender_authenticated',
16
+ field=models.BooleanField(default=True, help_text='Email-only: True when sender identity passed SPF/DKIM/DMARC (always True for other platforms).'),
17
+ ),
18
+ ]
@@ -5,7 +5,6 @@ from django.contrib.auth.models import User
5
5
  from unicom.models.constants import channels
6
6
  from django.contrib.postgres.fields import ArrayField
7
7
  from django.core.validators import validate_email
8
- from fa2svg.converter import revert_to_original_fa
9
8
  import uuid
10
9
  import re
11
10
  import os
@@ -14,6 +13,7 @@ import base64
14
13
  from .fields import DedupFileField, only_delete_file_if_unused
15
14
  from unicom.services.get_public_origin import get_public_origin
16
15
  from openai import OpenAI
16
+ from openai import OpenAIError
17
17
  from django.conf import settings
18
18
  from pydub import AudioSegment
19
19
  import io
@@ -21,7 +21,13 @@ import io
21
21
  if TYPE_CHECKING:
22
22
  from unicom.models import Channel
23
23
 
24
- openai_client = OpenAI(api_key=getattr(settings, 'OPENAI_API_KEY', None))
24
+ def get_openai_client():
25
+ api_key = getattr(settings, 'OPENAI_API_KEY', None)
26
+ if not api_key:
27
+ raise OpenAIError(
28
+ "OPENAI_API_KEY is required only for LLM features such as Message.reply_using_llm()."
29
+ )
30
+ return OpenAI(api_key=api_key)
25
31
 
26
32
 
27
33
  class Message(models.Model):
@@ -52,6 +58,10 @@ class Message(models.Model):
52
58
  chat = models.ForeignKey('unicom.Chat', on_delete=models.CASCADE, related_name='messages')
53
59
  is_outgoing = models.BooleanField(null=True, default=None, help_text="True for outgoing messages, False for incoming, None for internal")
54
60
  sender_name = models.CharField(max_length=100)
61
+ email_sender_authenticated = models.BooleanField(
62
+ default=True,
63
+ help_text="Email-only: True when sender identity passed SPF/DKIM/DMARC (always True for other platforms).",
64
+ )
55
65
  subject = models.CharField(max_length=512, blank=True, null=True, help_text="Subject of the message (only for email messages)")
56
66
  text = models.TextField()
57
67
  html = models.TextField(
@@ -457,11 +467,13 @@ class Message(models.Model):
457
467
  latest_tool_call_time = max(tool_call_timestamps)
458
468
  chain_message_ids = [m.id for m in chain]
459
469
 
460
- # Look for user messages after the latest tool call, before this tool response,
470
+ # Look for user messages after (tool call time - grace period), before this tool response,
461
471
  # that reply to any message in our chain
472
+ from django.utils import timezone
473
+ grace_start = latest_tool_call_time - timezone.timedelta(minutes=5)
462
474
  user_interrupt = self.chat.messages.filter(
463
475
  is_outgoing=False,
464
- timestamp__gt=latest_tool_call_time,
476
+ timestamp__gt=grace_start,
465
477
  timestamp__lt=self.timestamp,
466
478
  reply_to_message_id__in=chain_message_ids
467
479
  ).exclude(
@@ -472,7 +484,7 @@ class Message(models.Model):
472
484
  if not user_interrupt:
473
485
  user_interrupt = self.chat.messages.filter(
474
486
  is_outgoing=False,
475
- timestamp__gt=latest_tool_call_time,
487
+ timestamp__gt=grace_start,
476
488
  timestamp__lt=self.timestamp
477
489
  ).exclude(
478
490
  media_type__in=['tool_call', 'tool_response']
@@ -593,7 +605,7 @@ class Message(models.Model):
593
605
  openai_kwargs["modalities"] = ["text", "audio"]
594
606
  openai_kwargs["audio"] = {"voice": voice, "format": "opus"}
595
607
  # Call OpenAI ChatCompletion API
596
- response = openai_client.chat.completions.create(
608
+ response = get_openai_client().chat.completions.create(
597
609
  model=model,
598
610
  messages=messages,
599
611
  **openai_kwargs
@@ -658,6 +670,12 @@ class Message(models.Model):
658
670
 
659
671
  class Meta:
660
672
  ordering = ['-timestamp']
673
+ constraints = [
674
+ models.UniqueConstraint(
675
+ fields=['channel', 'imap_uid'],
676
+ name='unicom_message_channel_imap_uid_unique',
677
+ ),
678
+ ]
661
679
 
662
680
  def __str__(self) -> str:
663
681
  return f"{self.platform}:{self.chat.name}->{self.sender_name}: {self.text}"
@@ -28,6 +28,8 @@ def reply_to_message(channel:Channel , message: Message, response: dict) -> Mess
28
28
  - If 'base64_audio' is present and type is 'audio', decodes and saves the audio to media folder, sets 'file_path'.
29
29
  """
30
30
 
31
+ tool_call_id = response.pop('_tool_call_id', None)
32
+
31
33
  # If it's an image in base64, decode it:
32
34
  if response.get("type") == "image" and "base64_image" in response:
33
35
  from unicom.services.decode_base64_image import decode_base64_media
@@ -91,6 +93,8 @@ def reply_to_message(channel:Channel , message: Message, response: dict) -> Mess
91
93
  }, source_function_call=source_function_call)
92
94
  elif platform == 'WebChat':
93
95
  payload = {**response, 'chat_id': message.chat_id}
96
+ if tool_call_id:
97
+ payload['_tool_call_id'] = tool_call_id
94
98
  media_type = payload.pop('type', None)
95
99
  if media_type and 'media_type' not in payload:
96
100
  payload['media_type'] = media_type
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+ from typing import Tuple
3
+
4
+
5
+ def get_email_service_credentials(config: dict, service: str) -> Tuple[str | None, str | None]:
6
+ """
7
+ Return the credentials that should be used for the given service (IMAP or SMTP),
8
+ falling back to the shared EMAIL_ADDRESS/EMAIL_PASSWORD values when overrides
9
+ like IMAP_USERNAME or SMTP_PASSWORD are not provided.
10
+ """
11
+ service_key = service.upper()
12
+ if service_key not in {'IMAP', 'SMTP'}:
13
+ raise ValueError(f"Unknown service for email credentials: {service}")
14
+
15
+ shared_username = config.get('EMAIL_ADDRESS')
16
+ shared_password = config.get('EMAIL_PASSWORD')
17
+ username = config.get(f'{service_key}_USERNAME')
18
+ password = config.get(f'{service_key}_PASSWORD')
19
+
20
+ if username is None:
21
+ username = shared_username
22
+ if password is None:
23
+ password = shared_password
24
+
25
+ return username, password
@@ -1,4 +1,6 @@
1
+ from unicom.services.email.auth_helpers import get_email_service_credentials
1
2
  from unicom.services.email.save_email_message import save_email_message
3
+ from unicom.models import Message
2
4
  from imapclient import IMAPClient, SEEN
3
5
  from imapclient.exceptions import IMAPClientError
4
6
  from django.db import connections
@@ -16,7 +18,7 @@ def listen_to_IMAP(channel):
16
18
  This runs indefinitely, with automatic reconnects on failure.
17
19
  """
18
20
  email_address = channel.config['EMAIL_ADDRESS']
19
- password = channel.config['EMAIL_PASSWORD']
21
+ imap_username, imap_password = get_email_service_credentials(channel.config, 'IMAP')
20
22
  imap_conf = channel.config['IMAP']
21
23
  host = imap_conf['host']
22
24
  port = imap_conf['port']
@@ -27,13 +29,20 @@ def listen_to_IMAP(channel):
27
29
  while True:
28
30
  try:
29
31
  with IMAPClient(host, port=port, ssl=use_ssl) as server:
30
- server.login(email_address, password)
32
+ server.login(imap_username, imap_password)
31
33
  server.select_folder('INBOX')
32
34
  # caps = server.capabilities()
33
35
  mark_seen_on = channel.config.get('mark_seen_on', 'never')
34
36
  # Immediately fetch any older unseen messages on startup
35
37
  uids = server.search(['UNSEEN'])
38
+ existing_uids = set()
39
+ if uids:
40
+ existing_uids = set(
41
+ Message.objects.filter(channel=channel, imap_uid__in=uids).values_list('imap_uid', flat=True)
42
+ )
36
43
  for uid in uids:
44
+ if uid in existing_uids:
45
+ continue
37
46
  try:
38
47
  resp = server.fetch(uid, ['BODY.PEEK[]'])
39
48
  raw = resp[uid][b'BODY[]']
@@ -75,7 +84,14 @@ def listen_to_IMAP(channel):
75
84
  continue
76
85
 
77
86
  uids = server.search(['UNSEEN'])
87
+ existing_uids = set()
88
+ if uids:
89
+ existing_uids = set(
90
+ Message.objects.filter(channel=channel, imap_uid__in=uids).values_list('imap_uid', flat=True)
91
+ )
78
92
  for uid in uids:
93
+ if uid in existing_uids:
94
+ continue
79
95
  try:
80
96
  resp = server.fetch(uid, ['BODY.PEEK[]'])
81
97
  raw = resp[uid][b'BODY[]']
@@ -95,4 +111,4 @@ def listen_to_IMAP(channel):
95
111
  time.sleep(3)
96
112
  finally:
97
113
  # Ensure we close all connections to avoid leaks
98
- connections.close_all()
114
+ connections.close_all()