django-nativemojo 0.1.15__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,30 +0,0 @@
|
|
1
|
-
from django.db import models
|
2
|
-
from django.template import Context
|
3
|
-
from django.template import Template
|
4
|
-
from mojo.models import MojoModel
|
5
|
-
|
6
|
-
|
7
|
-
try:
|
8
|
-
import css_inline
|
9
|
-
inliner = css_inline.CSSInliner(remove_style_tags=True)
|
10
|
-
except Exception:
|
11
|
-
inliner = None
|
12
|
-
|
13
|
-
|
14
|
-
class NotifyTemplate(models.Model, MojoModel):
|
15
|
-
created = models.DateTimeField(auto_now_add=True, editable=False)
|
16
|
-
modified = models.DateTimeField(auto_now=True)
|
17
|
-
group = models.ForeignKey("account.Group", blank=True, null=True, default=None,
|
18
|
-
related_name="templates", on_delete=models.CASCADE)
|
19
|
-
name = models.CharField(max_length=255, db_index=True)
|
20
|
-
kind = models.CharField(max_length=124, default="email")
|
21
|
-
template = models.TextField()
|
22
|
-
|
23
|
-
def render(self, context):
|
24
|
-
template = Template(self.template)
|
25
|
-
if context is None:
|
26
|
-
context = {}
|
27
|
-
context = Context(context)
|
28
|
-
if inliner is not None:
|
29
|
-
return inliner.inline(template.render(context))
|
30
|
-
return template.render(context)
|
@@ -1,73 +0,0 @@
|
|
1
|
-
from mojo.helpers.settings import settings
|
2
|
-
from mojo.helpers.logit import get_logger
|
3
|
-
import metrics
|
4
|
-
import boto3
|
5
|
-
from . import render as mr
|
6
|
-
|
7
|
-
# Initialize logger for email notifications
|
8
|
-
EMAIL_LOGGER = get_logger("email", filename="email.log")
|
9
|
-
|
10
|
-
# Retrieve SES settings from configuration
|
11
|
-
SES_ACCESS_KEY = settings.SES_ACCESS_KEY
|
12
|
-
SES_SECRET_KEY = settings.SES_SECRET_KEY
|
13
|
-
SES_REGION = settings.SES_REGION
|
14
|
-
EMAIL_METRICS = settings.EMAIL_METRICS
|
15
|
-
EMAIL_ASYNC_AS_TASK = settings.EMAIL_ASYNC_AS_TASK
|
16
|
-
|
17
|
-
def get_ses_client(access_key, secret_key, region):
|
18
|
-
"""Create a new SES client with the provided credentials."""
|
19
|
-
return boto3.client('ses',
|
20
|
-
aws_access_key_id=access_key,
|
21
|
-
aws_secret_access_key=secret_key,
|
22
|
-
region_name=region)
|
23
|
-
|
24
|
-
def send_mail_via_ses(msg, sender, recipients, fail_silently=True):
|
25
|
-
"""Send an email using Amazon SES service."""
|
26
|
-
try:
|
27
|
-
ses_client = get_ses_client(SES_ACCESS_KEY, SES_SECRET_KEY, SES_REGION)
|
28
|
-
ses_client.send_raw_email(
|
29
|
-
Source=sender,
|
30
|
-
Destinations=recipients,
|
31
|
-
RawMessage={'Data': msg.as_string()}
|
32
|
-
)
|
33
|
-
if EMAIL_METRICS:
|
34
|
-
metrics.record("emails_sent", category="email", min_granularity="hours")
|
35
|
-
return True
|
36
|
-
except Exception as err:
|
37
|
-
if EMAIL_METRICS:
|
38
|
-
metrics.record("email_errors", category="email", min_granularity="hours")
|
39
|
-
EMAIL_LOGGER.exception(err)
|
40
|
-
EMAIL_LOGGER.error(msg.as_string())
|
41
|
-
if not fail_silently:
|
42
|
-
raise err
|
43
|
-
return False
|
44
|
-
|
45
|
-
def send_mail(msg, sender, recipients, fail_silently=True):
|
46
|
-
"""Send an email via SES, defaulting to fail silently."""
|
47
|
-
return send_mail_via_ses(msg, sender, recipients, fail_silently)
|
48
|
-
|
49
|
-
def send(sender, recipients, subject, message, attachments=None, replyto=None, fail_silently=False, do_async=False):
|
50
|
-
"""
|
51
|
-
Prepare and send an email message.
|
52
|
-
|
53
|
-
:param sender: Email address of the sender.
|
54
|
-
:param recipients: List of recipient email addresses.
|
55
|
-
:param subject: Subject of the email.
|
56
|
-
:param message: Body of the email.
|
57
|
-
:param attachments: List of attachments.
|
58
|
-
:param replyto: Email address to reply to.
|
59
|
-
:param fail_silently: Flag to suppress exceptions.
|
60
|
-
:param do_async: Flag to send email asynchronously (not implemented).
|
61
|
-
"""
|
62
|
-
html = None
|
63
|
-
text = None
|
64
|
-
|
65
|
-
if mr.isHTML(message):
|
66
|
-
html = message
|
67
|
-
else:
|
68
|
-
text = message
|
69
|
-
|
70
|
-
msg = mr.createMessage(sender, recipients, subject, text, html,
|
71
|
-
attachments=attachments, replyto=replyto)
|
72
|
-
|
73
|
-
return send_mail(msg.msg, msg.sender, msg.recipients, fail_silently=fail_silently)
|
mojo/apps/notify/rest/ses.py
DELETED
File without changes
|
@@ -1,404 +0,0 @@
|
|
1
|
-
from django.utils import timezone
|
2
|
-
from django.db import transaction
|
3
|
-
from django.core.exceptions import ValidationError
|
4
|
-
from typing import Optional, Dict, Any, List, Union
|
5
|
-
import logging
|
6
|
-
|
7
|
-
from ..models import Account, Inbox, InboxMessage, Outbox, OutboxMessage
|
8
|
-
|
9
|
-
|
10
|
-
logger = logging.getLogger(__name__)
|
11
|
-
|
12
|
-
|
13
|
-
class NotificationError(Exception):
|
14
|
-
"""Base exception for notification errors"""
|
15
|
-
pass
|
16
|
-
|
17
|
-
|
18
|
-
class MessageSender:
|
19
|
-
"""
|
20
|
-
High-level interface for sending messages through various notification channels
|
21
|
-
"""
|
22
|
-
|
23
|
-
@staticmethod
|
24
|
-
def send_email(
|
25
|
-
to_address: str,
|
26
|
-
subject: str,
|
27
|
-
message: str,
|
28
|
-
from_address: Optional[str] = None,
|
29
|
-
group=None,
|
30
|
-
user=None,
|
31
|
-
metadata: Optional[Dict[str, Any]] = None,
|
32
|
-
scheduled_at: Optional[timezone.datetime] = None
|
33
|
-
) -> OutboxMessage:
|
34
|
-
"""Send an email message"""
|
35
|
-
return MessageSender._send_message(
|
36
|
-
kind=Account.EMAIL,
|
37
|
-
to_address=to_address,
|
38
|
-
subject=subject,
|
39
|
-
message=message,
|
40
|
-
from_address=from_address,
|
41
|
-
group=group,
|
42
|
-
user=user,
|
43
|
-
metadata=metadata or {},
|
44
|
-
scheduled_at=scheduled_at
|
45
|
-
)
|
46
|
-
|
47
|
-
@staticmethod
|
48
|
-
def send_sms(
|
49
|
-
to_address: str,
|
50
|
-
message: str,
|
51
|
-
from_address: Optional[str] = None,
|
52
|
-
group=None,
|
53
|
-
user=None,
|
54
|
-
metadata: Optional[Dict[str, Any]] = None,
|
55
|
-
scheduled_at: Optional[timezone.datetime] = None
|
56
|
-
) -> OutboxMessage:
|
57
|
-
"""Send an SMS message"""
|
58
|
-
return MessageSender._send_message(
|
59
|
-
kind=Account.SMS,
|
60
|
-
to_address=to_address,
|
61
|
-
subject=None,
|
62
|
-
message=message,
|
63
|
-
from_address=from_address,
|
64
|
-
group=group,
|
65
|
-
user=user,
|
66
|
-
metadata=metadata or {},
|
67
|
-
scheduled_at=scheduled_at
|
68
|
-
)
|
69
|
-
|
70
|
-
@staticmethod
|
71
|
-
def send_whatsapp(
|
72
|
-
to_address: str,
|
73
|
-
message: str,
|
74
|
-
from_address: Optional[str] = None,
|
75
|
-
group=None,
|
76
|
-
user=None,
|
77
|
-
metadata: Optional[Dict[str, Any]] = None,
|
78
|
-
scheduled_at: Optional[timezone.datetime] = None
|
79
|
-
) -> OutboxMessage:
|
80
|
-
"""Send a WhatsApp message"""
|
81
|
-
return MessageSender._send_message(
|
82
|
-
kind=Account.WHATSAPP,
|
83
|
-
to_address=to_address,
|
84
|
-
subject=None,
|
85
|
-
message=message,
|
86
|
-
from_address=from_address,
|
87
|
-
group=group,
|
88
|
-
user=user,
|
89
|
-
metadata=metadata or {},
|
90
|
-
scheduled_at=scheduled_at
|
91
|
-
)
|
92
|
-
|
93
|
-
@staticmethod
|
94
|
-
def send_push(
|
95
|
-
to_address: str,
|
96
|
-
message: str,
|
97
|
-
title: Optional[str] = None,
|
98
|
-
from_address: Optional[str] = None,
|
99
|
-
group=None,
|
100
|
-
user=None,
|
101
|
-
metadata: Optional[Dict[str, Any]] = None,
|
102
|
-
scheduled_at: Optional[timezone.datetime] = None
|
103
|
-
) -> OutboxMessage:
|
104
|
-
"""Send a push notification"""
|
105
|
-
metadata = metadata or {}
|
106
|
-
if title:
|
107
|
-
metadata['title'] = title
|
108
|
-
|
109
|
-
return MessageSender._send_message(
|
110
|
-
kind=Account.PUSH,
|
111
|
-
to_address=to_address,
|
112
|
-
subject=title,
|
113
|
-
message=message,
|
114
|
-
from_address=from_address,
|
115
|
-
group=group,
|
116
|
-
user=user,
|
117
|
-
metadata=metadata,
|
118
|
-
scheduled_at=scheduled_at
|
119
|
-
)
|
120
|
-
|
121
|
-
@staticmethod
|
122
|
-
def _send_message(
|
123
|
-
kind: str,
|
124
|
-
to_address: str,
|
125
|
-
message: str,
|
126
|
-
subject: Optional[str] = None,
|
127
|
-
from_address: Optional[str] = None,
|
128
|
-
group=None,
|
129
|
-
user=None,
|
130
|
-
metadata: Optional[Dict[str, Any]] = None,
|
131
|
-
scheduled_at: Optional[timezone.datetime] = None
|
132
|
-
) -> OutboxMessage:
|
133
|
-
"""Internal method to send messages of any kind"""
|
134
|
-
|
135
|
-
# Find appropriate outbox
|
136
|
-
outbox = OutboxFinder.find_outbox(
|
137
|
-
kind=kind,
|
138
|
-
from_address=from_address,
|
139
|
-
group=group
|
140
|
-
)
|
141
|
-
|
142
|
-
if not outbox:
|
143
|
-
raise NotificationError(f"No active outbox found for {kind} messages")
|
144
|
-
|
145
|
-
if not outbox.can_send_messages():
|
146
|
-
raise NotificationError(f"Outbox {outbox} cannot send messages")
|
147
|
-
|
148
|
-
if not outbox.check_rate_limit():
|
149
|
-
raise NotificationError(f"Outbox {outbox} has exceeded rate limit")
|
150
|
-
|
151
|
-
# Use outbox address if no from_address specified
|
152
|
-
if not from_address:
|
153
|
-
from_address = outbox.address
|
154
|
-
|
155
|
-
# Create the outbox message
|
156
|
-
with transaction.atomic():
|
157
|
-
outbox_message = OutboxMessage.objects.create(
|
158
|
-
outbox=outbox,
|
159
|
-
user=user,
|
160
|
-
group=group or outbox.group,
|
161
|
-
to_address=to_address,
|
162
|
-
from_address=from_address,
|
163
|
-
subject=subject,
|
164
|
-
message=message,
|
165
|
-
metadata=metadata or {},
|
166
|
-
scheduled_at=scheduled_at
|
167
|
-
)
|
168
|
-
|
169
|
-
logger.info(f"Queued {kind} message from {from_address} to {to_address}")
|
170
|
-
return outbox_message
|
171
|
-
|
172
|
-
|
173
|
-
class OutboxFinder:
|
174
|
-
"""
|
175
|
-
Utility for finding appropriate outboxes for sending messages
|
176
|
-
"""
|
177
|
-
|
178
|
-
@staticmethod
|
179
|
-
def find_outbox(
|
180
|
-
kind: str,
|
181
|
-
from_address: Optional[str] = None,
|
182
|
-
group=None
|
183
|
-
) -> Optional[Outbox]:
|
184
|
-
"""Find the best outbox for sending a message"""
|
185
|
-
|
186
|
-
query = Outbox.objects.filter(
|
187
|
-
account__kind=kind,
|
188
|
-
account__is_active=True,
|
189
|
-
is_active=True
|
190
|
-
).select_related('account', 'group')
|
191
|
-
|
192
|
-
# Prefer outboxes with matching group
|
193
|
-
if group:
|
194
|
-
group_matches = query.filter(group=group)
|
195
|
-
if group_matches.exists():
|
196
|
-
query = group_matches
|
197
|
-
|
198
|
-
# Prefer outboxes with matching address
|
199
|
-
if from_address:
|
200
|
-
address_matches = query.filter(address=from_address)
|
201
|
-
if address_matches.exists():
|
202
|
-
return address_matches.first()
|
203
|
-
|
204
|
-
# Return any available outbox
|
205
|
-
return query.first()
|
206
|
-
|
207
|
-
@staticmethod
|
208
|
-
def get_outboxes_for_account(account: Account) -> List[Outbox]:
|
209
|
-
"""Get all active outboxes for an account"""
|
210
|
-
return list(
|
211
|
-
Outbox.objects.filter(
|
212
|
-
account=account,
|
213
|
-
is_active=True
|
214
|
-
).select_related('group')
|
215
|
-
)
|
216
|
-
|
217
|
-
|
218
|
-
class MessageProcessor:
|
219
|
-
"""
|
220
|
-
Utility for processing received messages
|
221
|
-
"""
|
222
|
-
|
223
|
-
@staticmethod
|
224
|
-
def process_inbox_message(inbox_message: InboxMessage) -> bool:
|
225
|
-
"""Process a received inbox message"""
|
226
|
-
if inbox_message.processed:
|
227
|
-
return True
|
228
|
-
|
229
|
-
inbox = inbox_message.inbox
|
230
|
-
handler_path = inbox.get_handler()
|
231
|
-
|
232
|
-
if not handler_path:
|
233
|
-
logger.warning(f"No handler configured for inbox {inbox}")
|
234
|
-
return False
|
235
|
-
|
236
|
-
try:
|
237
|
-
# Import and call the handler
|
238
|
-
handler_func = _import_handler(handler_path)
|
239
|
-
if handler_func:
|
240
|
-
result = handler_func(inbox_message)
|
241
|
-
inbox_message.mark_processed()
|
242
|
-
logger.info(f"Processed message {inbox_message.id} with handler {handler_path}")
|
243
|
-
return True
|
244
|
-
except Exception as e:
|
245
|
-
logger.error(f"Error processing message {inbox_message.id}: {e}")
|
246
|
-
return False
|
247
|
-
|
248
|
-
return False
|
249
|
-
|
250
|
-
@staticmethod
|
251
|
-
def bulk_process_inbox_messages(inbox: Inbox, limit: int = 100) -> int:
|
252
|
-
"""Process multiple unprocessed messages for an inbox"""
|
253
|
-
messages = InboxMessage.objects.filter(
|
254
|
-
inbox=inbox,
|
255
|
-
processed=False
|
256
|
-
).order_by('created')[:limit]
|
257
|
-
|
258
|
-
processed_count = 0
|
259
|
-
for message in messages:
|
260
|
-
if MessageProcessor.process_inbox_message(message):
|
261
|
-
processed_count += 1
|
262
|
-
|
263
|
-
return processed_count
|
264
|
-
|
265
|
-
|
266
|
-
class BulkNotifier:
|
267
|
-
"""
|
268
|
-
Utility for sending bulk notifications
|
269
|
-
"""
|
270
|
-
|
271
|
-
@staticmethod
|
272
|
-
def send_bulk_email(
|
273
|
-
recipients: List[str],
|
274
|
-
subject: str,
|
275
|
-
message: str,
|
276
|
-
from_address: Optional[str] = None,
|
277
|
-
group=None,
|
278
|
-
metadata: Optional[Dict[str, Any]] = None,
|
279
|
-
scheduled_at: Optional[timezone.datetime] = None,
|
280
|
-
batch_size: int = 100
|
281
|
-
) -> List[OutboxMessage]:
|
282
|
-
"""Send bulk email messages"""
|
283
|
-
|
284
|
-
messages = []
|
285
|
-
for i in range(0, len(recipients), batch_size):
|
286
|
-
batch = recipients[i:i + batch_size]
|
287
|
-
|
288
|
-
with transaction.atomic():
|
289
|
-
for recipient in batch:
|
290
|
-
try:
|
291
|
-
msg = MessageSender.send_email(
|
292
|
-
to_address=recipient,
|
293
|
-
subject=subject,
|
294
|
-
message=message,
|
295
|
-
from_address=from_address,
|
296
|
-
group=group,
|
297
|
-
metadata=metadata,
|
298
|
-
scheduled_at=scheduled_at
|
299
|
-
)
|
300
|
-
messages.append(msg)
|
301
|
-
except Exception as e:
|
302
|
-
logger.error(f"Failed to queue email to {recipient}: {e}")
|
303
|
-
|
304
|
-
return messages
|
305
|
-
|
306
|
-
@staticmethod
|
307
|
-
def send_bulk_sms(
|
308
|
-
recipients: List[str],
|
309
|
-
message: str,
|
310
|
-
from_address: Optional[str] = None,
|
311
|
-
group=None,
|
312
|
-
metadata: Optional[Dict[str, Any]] = None,
|
313
|
-
scheduled_at: Optional[timezone.datetime] = None,
|
314
|
-
batch_size: int = 100
|
315
|
-
) -> List[OutboxMessage]:
|
316
|
-
"""Send bulk SMS messages"""
|
317
|
-
|
318
|
-
messages = []
|
319
|
-
for i in range(0, len(recipients), batch_size):
|
320
|
-
batch = recipients[i:i + batch_size]
|
321
|
-
|
322
|
-
with transaction.atomic():
|
323
|
-
for recipient in batch:
|
324
|
-
try:
|
325
|
-
msg = MessageSender.send_sms(
|
326
|
-
to_address=recipient,
|
327
|
-
message=message,
|
328
|
-
from_address=from_address,
|
329
|
-
group=group,
|
330
|
-
metadata=metadata,
|
331
|
-
scheduled_at=scheduled_at
|
332
|
-
)
|
333
|
-
messages.append(msg)
|
334
|
-
except Exception as e:
|
335
|
-
logger.error(f"Failed to queue SMS to {recipient}: {e}")
|
336
|
-
|
337
|
-
return messages
|
338
|
-
|
339
|
-
|
340
|
-
class MessageStats:
|
341
|
-
"""
|
342
|
-
Utility for getting message statistics
|
343
|
-
"""
|
344
|
-
|
345
|
-
@staticmethod
|
346
|
-
def get_outbox_stats(outbox: Outbox) -> Dict[str, Any]:
|
347
|
-
"""Get statistics for an outbox"""
|
348
|
-
messages = outbox.messages.all()
|
349
|
-
|
350
|
-
return {
|
351
|
-
'total_messages': messages.count(),
|
352
|
-
'pending_messages': messages.filter(status=OutboxMessage.PENDING).count(),
|
353
|
-
'sent_messages': messages.filter(status=OutboxMessage.SENT).count(),
|
354
|
-
'failed_messages': messages.filter(status=OutboxMessage.FAILED).count(),
|
355
|
-
'recent_messages': messages.filter(
|
356
|
-
created__gte=timezone.now() - timezone.timedelta(hours=24)
|
357
|
-
).count(),
|
358
|
-
}
|
359
|
-
|
360
|
-
@staticmethod
|
361
|
-
def get_inbox_stats(inbox: Inbox) -> Dict[str, Any]:
|
362
|
-
"""Get statistics for an inbox"""
|
363
|
-
messages = inbox.messages.all()
|
364
|
-
|
365
|
-
return {
|
366
|
-
'total_messages': messages.count(),
|
367
|
-
'processed_messages': messages.filter(processed=True).count(),
|
368
|
-
'unprocessed_messages': messages.filter(processed=False).count(),
|
369
|
-
'recent_messages': messages.filter(
|
370
|
-
created__gte=timezone.now() - timezone.timedelta(hours=24)
|
371
|
-
).count(),
|
372
|
-
}
|
373
|
-
|
374
|
-
|
375
|
-
def _import_handler(handler_path: str):
|
376
|
-
"""Import a handler function from a module path"""
|
377
|
-
try:
|
378
|
-
module_path, function_name = handler_path.rsplit('.', 1)
|
379
|
-
module = __import__(module_path, fromlist=[function_name])
|
380
|
-
return getattr(module, function_name)
|
381
|
-
except (ImportError, AttributeError, ValueError) as e:
|
382
|
-
logger.error(f"Failed to import handler {handler_path}: {e}")
|
383
|
-
return None
|
384
|
-
|
385
|
-
|
386
|
-
# Convenience functions for common operations
|
387
|
-
def send_email(to_address: str, subject: str, message: str, **kwargs) -> OutboxMessage:
|
388
|
-
"""Convenience function to send an email"""
|
389
|
-
return MessageSender.send_email(to_address, subject, message, **kwargs)
|
390
|
-
|
391
|
-
|
392
|
-
def send_sms(to_address: str, message: str, **kwargs) -> OutboxMessage:
|
393
|
-
"""Convenience function to send an SMS"""
|
394
|
-
return MessageSender.send_sms(to_address, message, **kwargs)
|
395
|
-
|
396
|
-
|
397
|
-
def send_whatsapp(to_address: str, message: str, **kwargs) -> OutboxMessage:
|
398
|
-
"""Convenience function to send a WhatsApp message"""
|
399
|
-
return MessageSender.send_whatsapp(to_address, message, **kwargs)
|
400
|
-
|
401
|
-
|
402
|
-
def send_push(to_address: str, message: str, title: Optional[str] = None, **kwargs) -> OutboxMessage:
|
403
|
-
"""Convenience function to send a push notification"""
|
404
|
-
return MessageSender.send_push(to_address, message, title, **kwargs)
|