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.
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/MANIFEST.in +2 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/PKG-INFO +8 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/README.md +6 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/PKG-INFO +8 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/SOURCES.txt +5 -11
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/requires.txt +1 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/setup.py +1 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/_version.py +2 -2
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/consumers/webchat_consumer.py +59 -8
- django_unicom-25.4.2.dev2/unicom/migrations/0025_message_unicom_message_channel_imap_uid_unique.py +19 -0
- django_unicom-25.4.2.dev2/unicom/migrations/0026_message_email_sender_authenticated.py +18 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/message.py +24 -6
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/reply_to_message.py +4 -0
- django_unicom-25.4.2.dev2/unicom/services/email/auth_helpers.py +25 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/listen_to_IMAP.py +19 -3
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/save_email_message.py +91 -25
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/send_email_message.py +9 -4
- django_unicom-25.4.2.dev2/unicom/services/email/system_channel.py +29 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/validate_email_config.py +25 -12
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/save_telegram_message.py +3 -3
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/send_webchat_message.py +29 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/signals.py +21 -21
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/channel_config.js +6 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-item.js +100 -10
- django_unicom-25.4.2.dev2/unicom/static/unicom/webchat/utils/morphdom.js +84 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/realtime-client.js +9 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-styles.js +383 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-with-sidebar.js +87 -17
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/unicom/webchat_demo.html +1 -1
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/webchat_views.py +2 -2
- django_unicom-25.4.2.dev0/smoke/consumer-install/.env.example +0 -2
- django_unicom-25.4.2.dev0/smoke/consumer-install/Dockerfile +0 -28
- django_unicom-25.4.2.dev0/smoke/consumer-install/README.md +0 -54
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/manage.py +0 -13
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/seed_webchat_channel.py +0 -21
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/smoke_test.py +0 -44
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/__init__.py +0 -1
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/settings.py +0 -59
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/urls.py +0 -6
- django_unicom-25.4.2.dev0/smoke/consumer-install/app/smokeproject/wsgi.py +0 -8
- django_unicom-25.4.2.dev0/smoke/consumer-install/docker-compose.yaml +0 -59
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/match_codebase_style.mdc +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/.cursor/rules/require_context_before_changes.mdc +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/Dockerfile +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/Makefile +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/conftest.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/data/definitions/bots/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/data/definitions/tools/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/dependency_links.txt +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/django_unicom.egg-info/top_level.txt +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/docker-compose.yaml +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/entrypoint.sh +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/internal-channel-modernization.md +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/manage.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/pyproject.toml +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/pytest.ini +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/requirements.txt +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/scripts/reacher_validate.sh +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/setup.cfg +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_cross_platform_buttons.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_authentication.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_live.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_email_request_processing.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_telegram_live.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_webchat.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/test_webchat_branching.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/tests/utils.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/todo.md +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/account_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/channel_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/chat_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/draft_message_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/email_inline_image_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/filters.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/member_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/message_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/message_template_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/admin/request_admin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/apps.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/consumers/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/mail_inbox_listen.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/run_as_llm_chat.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/send_scheduled_messages.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/management/commands/start_imap_listeners.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0001_initial.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0002_emailinlineimage.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
- {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
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0007_message_imap_uid.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0008_add_all_members_group.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0009_add_tool_call_types.py +0 -0
- {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
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
- {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
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0013_callbackexecution.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0014_rename_authorized_user_to_intended_account.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0015_simplify_callback_execution.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0016_callbackexecution_tool_call.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0017_add_chat_metadata.py +0 -0
- {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
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0019_draftmessage_skip_reacher_validation.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0020_message_provider_message_id.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0021_message_open_count.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0022_toolcall_result_status.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0023_toolcall_progress_updates_for_user.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/0024_email_from_name.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/migrations/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/account.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/account_chat.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/callback_execution.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/channel.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/chat.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/constants.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/draft_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/fields.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/member.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/member_group.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/message_template.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/request.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/request_category.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/tool_call.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/models/update.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/chat_summary.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/scheduler.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/crossplatform/send_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/decode_base64_image.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/IMAP_thread_manager.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/email_tracking.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/quote_filter.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/get_public_origin.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/html_inline_images.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/generate_text_message_data.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/save_internal_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/internal/send_internal_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/llm/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/llm/tool_calls.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/answer_callback_query.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/create_inline_keyboard.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/download_file.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/edit_telegram_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/escape_markdown.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/get_file_path.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/handle_telegram_callback.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/send_telegram_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/set_telegram_webhook.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/template_renderer.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/generate_chat_title.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/get_or_create_account.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/migrate_guest_to_user.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/webchat/save_webchat_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/get_template.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/js/tinymce_init.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/chat-list.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/media-preview.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-input.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/message-list.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/components/voice-recorder.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/api-client.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/datetime-formatter.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/utils/font-awesome-loader.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/static/unicom/webchat/webchat-component.js +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat/compose.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/chat_history.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templates/code_templates/category_processor.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templatetags/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/templatetags/unicom_tags.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/urls.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/chat_history_view.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/compose_view.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/email_tracking.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/inline_image.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/message_template.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/telegram_webhook.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/webchat_demo_view.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/views/whatsapp_webhook.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/apps.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/asgi.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/callback_handlers.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/bots/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/bots/assistant_bot.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/__init__.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/cross_platform_buttons.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/get_system_info.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/interactive_menu.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/interval_alarm.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/ip_lookup.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/definitions/tools/simple_timer.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/settings.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/test_button_handlers.py +0 -0
- {django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom_project/urls.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
32
|
-
__version_tuple__ = version_tuple = (25, 4, 2, '
|
|
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
|
{django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/consumers/webchat_consumer.py
RENAMED
|
@@ -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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
)
|
django_unicom-25.4.2.dev2/unicom/migrations/0025_message_unicom_message_channel_imap_uid_unique.py
ADDED
|
@@ -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
|
-
|
|
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
|
|
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=
|
|
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=
|
|
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 =
|
|
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
|
{django_unicom-25.4.2.dev0 → django_unicom-25.4.2.dev2}/unicom/services/email/listen_to_IMAP.py
RENAMED
|
@@ -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
|
-
|
|
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(
|
|
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()
|