django-nativemojo 0.1.10__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.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- 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 +319 -15
- mojo/apps/account/models/member.py +29 -5
- 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 +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- 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 +7 -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/s3.py +64 -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/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- 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 +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- 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/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- 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 +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -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 +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- 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/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- 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/bounce.py +0 -0
- 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 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -1,370 +0,0 @@
|
|
1
|
-
from django.core.management.base import BaseCommand, CommandError
|
2
|
-
from django.utils import timezone
|
3
|
-
from django.db import transaction, models
|
4
|
-
from django.conf import settings
|
5
|
-
import time
|
6
|
-
import logging
|
7
|
-
import signal
|
8
|
-
import sys
|
9
|
-
from datetime import timedelta
|
10
|
-
from typing import Optional
|
11
|
-
|
12
|
-
from mojo.apps.notify.models import (
|
13
|
-
Account, Inbox, InboxMessage, Outbox, OutboxMessage
|
14
|
-
)
|
15
|
-
from mojo.apps.notify.utils.notifications import MessageProcessor, _import_handler
|
16
|
-
|
17
|
-
logger = logging.getLogger(__name__)
|
18
|
-
|
19
|
-
|
20
|
-
class Command(BaseCommand):
|
21
|
-
help = 'Process notification messages (inbox and outbox)'
|
22
|
-
|
23
|
-
def __init__(self, *args, **kwargs):
|
24
|
-
super().__init__(*args, **kwargs)
|
25
|
-
self.should_stop = False
|
26
|
-
|
27
|
-
def add_arguments(self, parser):
|
28
|
-
parser.add_argument(
|
29
|
-
'--daemon',
|
30
|
-
action='store_true',
|
31
|
-
help='Run as daemon (continuous processing)',
|
32
|
-
)
|
33
|
-
|
34
|
-
parser.add_argument(
|
35
|
-
'--interval',
|
36
|
-
type=int,
|
37
|
-
default=30,
|
38
|
-
help='Processing interval in seconds (default: 30)',
|
39
|
-
)
|
40
|
-
|
41
|
-
parser.add_argument(
|
42
|
-
'--inbox-only',
|
43
|
-
action='store_true',
|
44
|
-
help='Process only inbox messages',
|
45
|
-
)
|
46
|
-
|
47
|
-
parser.add_argument(
|
48
|
-
'--outbox-only',
|
49
|
-
action='store_true',
|
50
|
-
help='Process only outbox messages',
|
51
|
-
)
|
52
|
-
|
53
|
-
parser.add_argument(
|
54
|
-
'--kind',
|
55
|
-
type=str,
|
56
|
-
choices=['email', 'sms', 'whatsapp', 'signal', 'ws', 'push'],
|
57
|
-
help='Process only messages of specific kind',
|
58
|
-
)
|
59
|
-
|
60
|
-
parser.add_argument(
|
61
|
-
'--limit',
|
62
|
-
type=int,
|
63
|
-
default=100,
|
64
|
-
help='Maximum messages to process per batch (default: 100)',
|
65
|
-
)
|
66
|
-
|
67
|
-
parser.add_argument(
|
68
|
-
'--retry-failed',
|
69
|
-
action='store_true',
|
70
|
-
help='Process failed messages for retry',
|
71
|
-
)
|
72
|
-
|
73
|
-
parser.add_argument(
|
74
|
-
'--max-age-hours',
|
75
|
-
type=int,
|
76
|
-
default=24,
|
77
|
-
help='Maximum age of messages to retry in hours (default: 24)',
|
78
|
-
)
|
79
|
-
|
80
|
-
def handle(self, *args, **options):
|
81
|
-
self.setup_signal_handlers()
|
82
|
-
|
83
|
-
if options['daemon']:
|
84
|
-
self.run_daemon(options)
|
85
|
-
else:
|
86
|
-
self.run_once(options)
|
87
|
-
|
88
|
-
def setup_signal_handlers(self):
|
89
|
-
"""Setup signal handlers for graceful shutdown"""
|
90
|
-
def signal_handler(signum, frame):
|
91
|
-
self.stdout.write(
|
92
|
-
self.style.WARNING(f'Received signal {signum}, shutting down gracefully...')
|
93
|
-
)
|
94
|
-
self.should_stop = True
|
95
|
-
|
96
|
-
signal.signal(signal.SIGINT, signal_handler)
|
97
|
-
signal.signal(signal.SIGTERM, signal_handler)
|
98
|
-
|
99
|
-
def run_daemon(self, options):
|
100
|
-
"""Run as daemon with continuous processing"""
|
101
|
-
interval = options['interval']
|
102
|
-
|
103
|
-
self.stdout.write(
|
104
|
-
self.style.SUCCESS(f'Starting notification processor daemon (interval: {interval}s)')
|
105
|
-
)
|
106
|
-
|
107
|
-
while not self.should_stop:
|
108
|
-
try:
|
109
|
-
stats = self.run_once(options)
|
110
|
-
|
111
|
-
if stats['total_processed'] > 0:
|
112
|
-
self.stdout.write(
|
113
|
-
f'Processed {stats["total_processed"]} messages '
|
114
|
-
f'(inbox: {stats["inbox_processed"]}, outbox: {stats["outbox_processed"]})'
|
115
|
-
)
|
116
|
-
|
117
|
-
# Sleep with interruption check
|
118
|
-
for _ in range(interval):
|
119
|
-
if self.should_stop:
|
120
|
-
break
|
121
|
-
time.sleep(1)
|
122
|
-
|
123
|
-
except Exception as e:
|
124
|
-
logger.error(f'Error in daemon loop: {e}')
|
125
|
-
self.stderr.write(
|
126
|
-
self.style.ERROR(f'Error in processing: {e}')
|
127
|
-
)
|
128
|
-
time.sleep(interval)
|
129
|
-
|
130
|
-
self.stdout.write(
|
131
|
-
self.style.SUCCESS('Notification processor daemon stopped')
|
132
|
-
)
|
133
|
-
|
134
|
-
def run_once(self, options):
|
135
|
-
"""Run processing once"""
|
136
|
-
stats = {
|
137
|
-
'inbox_processed': 0,
|
138
|
-
'outbox_processed': 0,
|
139
|
-
'total_processed': 0,
|
140
|
-
'inbox_failed': 0,
|
141
|
-
'outbox_failed': 0,
|
142
|
-
}
|
143
|
-
|
144
|
-
try:
|
145
|
-
if not options['outbox_only']:
|
146
|
-
inbox_stats = self.process_inbox_messages(options)
|
147
|
-
stats['inbox_processed'] = inbox_stats['processed']
|
148
|
-
stats['inbox_failed'] = inbox_stats['failed']
|
149
|
-
|
150
|
-
if not options['inbox_only']:
|
151
|
-
outbox_stats = self.process_outbox_messages(options)
|
152
|
-
stats['outbox_processed'] = outbox_stats['processed']
|
153
|
-
stats['outbox_failed'] = outbox_stats['failed']
|
154
|
-
|
155
|
-
if options['retry_failed']:
|
156
|
-
retry_stats = self.process_failed_messages(options)
|
157
|
-
stats['outbox_processed'] += retry_stats['processed']
|
158
|
-
stats['outbox_failed'] += retry_stats['failed']
|
159
|
-
|
160
|
-
stats['total_processed'] = stats['inbox_processed'] + stats['outbox_processed']
|
161
|
-
|
162
|
-
except Exception as e:
|
163
|
-
logger.error(f'Error in processing cycle: {e}')
|
164
|
-
raise CommandError(f'Processing failed: {e}')
|
165
|
-
|
166
|
-
return stats
|
167
|
-
|
168
|
-
def process_inbox_messages(self, options):
|
169
|
-
"""Process unprocessed inbox messages"""
|
170
|
-
stats = {'processed': 0, 'failed': 0}
|
171
|
-
limit = options['limit']
|
172
|
-
kind = options.get('kind')
|
173
|
-
|
174
|
-
# Build query
|
175
|
-
query = InboxMessage.objects.filter(processed=False).select_related(
|
176
|
-
'inbox', 'inbox__account'
|
177
|
-
).order_by('created')
|
178
|
-
|
179
|
-
if kind:
|
180
|
-
query = query.filter(inbox__account__kind=kind)
|
181
|
-
|
182
|
-
messages = query[:limit]
|
183
|
-
|
184
|
-
self.stdout.write(f'Processing {len(messages)} inbox messages...')
|
185
|
-
|
186
|
-
for message in messages:
|
187
|
-
if self.should_stop:
|
188
|
-
break
|
189
|
-
|
190
|
-
try:
|
191
|
-
with transaction.atomic():
|
192
|
-
success = MessageProcessor.process_inbox_message(message)
|
193
|
-
if success:
|
194
|
-
stats['processed'] += 1
|
195
|
-
self.stdout.write(
|
196
|
-
f' ✓ Processed inbox message {message.id} from {message.from_address}'
|
197
|
-
)
|
198
|
-
else:
|
199
|
-
stats['failed'] += 1
|
200
|
-
self.stdout.write(
|
201
|
-
self.style.WARNING(
|
202
|
-
f' ✗ Failed to process inbox message {message.id}'
|
203
|
-
)
|
204
|
-
)
|
205
|
-
|
206
|
-
except Exception as e:
|
207
|
-
stats['failed'] += 1
|
208
|
-
logger.error(f'Error processing inbox message {message.id}: {e}')
|
209
|
-
self.stderr.write(
|
210
|
-
self.style.ERROR(f' ✗ Error processing inbox message {message.id}: {e}')
|
211
|
-
)
|
212
|
-
|
213
|
-
return stats
|
214
|
-
|
215
|
-
def process_outbox_messages(self, options):
|
216
|
-
"""Process pending outbox messages"""
|
217
|
-
stats = {'processed': 0, 'failed': 0}
|
218
|
-
limit = options['limit']
|
219
|
-
kind = options.get('kind')
|
220
|
-
|
221
|
-
# Build query for ready-to-send messages
|
222
|
-
query = OutboxMessage.objects.filter(
|
223
|
-
status=OutboxMessage.PENDING
|
224
|
-
).select_related('outbox', 'outbox__account').order_by('created')
|
225
|
-
|
226
|
-
if kind:
|
227
|
-
query = query.filter(outbox__account__kind=kind)
|
228
|
-
|
229
|
-
# Filter messages that are ready to send (scheduled_at <= now or null)
|
230
|
-
now = timezone.now()
|
231
|
-
query = query.filter(
|
232
|
-
models.Q(scheduled_at__isnull=True) | models.Q(scheduled_at__lte=now)
|
233
|
-
)
|
234
|
-
|
235
|
-
messages = query[:limit]
|
236
|
-
|
237
|
-
self.stdout.write(f'Processing {len(messages)} outbox messages...')
|
238
|
-
|
239
|
-
for message in messages:
|
240
|
-
if self.should_stop:
|
241
|
-
break
|
242
|
-
|
243
|
-
try:
|
244
|
-
with transaction.atomic():
|
245
|
-
success = self.send_outbox_message(message)
|
246
|
-
if success:
|
247
|
-
stats['processed'] += 1
|
248
|
-
self.stdout.write(
|
249
|
-
f' ✓ Sent {message.outbox.account.kind} message to {message.to_address}'
|
250
|
-
)
|
251
|
-
else:
|
252
|
-
stats['failed'] += 1
|
253
|
-
|
254
|
-
except Exception as e:
|
255
|
-
stats['failed'] += 1
|
256
|
-
logger.error(f'Error sending outbox message {message.id}: {e}')
|
257
|
-
self.stderr.write(
|
258
|
-
self.style.ERROR(f' ✗ Error sending message {message.id}: {e}')
|
259
|
-
)
|
260
|
-
|
261
|
-
return stats
|
262
|
-
|
263
|
-
def process_failed_messages(self, options):
|
264
|
-
"""Process failed messages for retry"""
|
265
|
-
stats = {'processed': 0, 'failed': 0}
|
266
|
-
limit = options['limit']
|
267
|
-
max_age_hours = options['max_age_hours']
|
268
|
-
kind = options.get('kind')
|
269
|
-
|
270
|
-
# Find failed messages that can be retried
|
271
|
-
cutoff_time = timezone.now() - timedelta(hours=max_age_hours)
|
272
|
-
|
273
|
-
query = OutboxMessage.objects.filter(
|
274
|
-
status=OutboxMessage.FAILED,
|
275
|
-
failed_at__gte=cutoff_time
|
276
|
-
).select_related('outbox', 'outbox__account').order_by('failed_at')
|
277
|
-
|
278
|
-
if kind:
|
279
|
-
query = query.filter(outbox__account__kind=kind)
|
280
|
-
|
281
|
-
messages = [msg for msg in query[:limit] if msg.can_retry]
|
282
|
-
|
283
|
-
self.stdout.write(f'Retrying {len(messages)} failed messages...')
|
284
|
-
|
285
|
-
for message in messages:
|
286
|
-
if self.should_stop:
|
287
|
-
break
|
288
|
-
|
289
|
-
try:
|
290
|
-
with transaction.atomic():
|
291
|
-
# Reset for retry
|
292
|
-
message.reset_for_retry()
|
293
|
-
|
294
|
-
# Try to send again
|
295
|
-
success = self.send_outbox_message(message)
|
296
|
-
if success:
|
297
|
-
stats['processed'] += 1
|
298
|
-
self.stdout.write(
|
299
|
-
f' ✓ Retry successful for message {message.id}'
|
300
|
-
)
|
301
|
-
else:
|
302
|
-
stats['failed'] += 1
|
303
|
-
|
304
|
-
except Exception as e:
|
305
|
-
stats['failed'] += 1
|
306
|
-
logger.error(f'Error retrying message {message.id}: {e}')
|
307
|
-
self.stderr.write(
|
308
|
-
self.style.ERROR(f' ✗ Error retrying message {message.id}: {e}')
|
309
|
-
)
|
310
|
-
|
311
|
-
return stats
|
312
|
-
|
313
|
-
def send_outbox_message(self, message: OutboxMessage) -> bool:
|
314
|
-
"""Send an outbox message using its handler"""
|
315
|
-
try:
|
316
|
-
outbox = message.outbox
|
317
|
-
|
318
|
-
# Check if outbox can send messages
|
319
|
-
if not outbox.can_send_messages():
|
320
|
-
message.mark_failed("Outbox is not active or account is disabled")
|
321
|
-
return False
|
322
|
-
|
323
|
-
# Check rate limits
|
324
|
-
if not outbox.check_rate_limit():
|
325
|
-
# Don't mark as failed, just skip for now
|
326
|
-
logger.warning(f'Rate limit exceeded for outbox {outbox.id}')
|
327
|
-
return False
|
328
|
-
|
329
|
-
# Get handler
|
330
|
-
handler_path = outbox.handler
|
331
|
-
if not handler_path:
|
332
|
-
message.mark_failed("No handler configured for outbox")
|
333
|
-
return False
|
334
|
-
|
335
|
-
# Import and call handler
|
336
|
-
handler_func = _import_handler(handler_path)
|
337
|
-
if not handler_func:
|
338
|
-
message.mark_failed(f"Could not import handler: {handler_path}")
|
339
|
-
return False
|
340
|
-
|
341
|
-
# Call the handler
|
342
|
-
return handler_func(message)
|
343
|
-
|
344
|
-
except Exception as e:
|
345
|
-
message.mark_failed(str(e))
|
346
|
-
logger.error(f'Error sending message {message.id}: {e}')
|
347
|
-
return False
|
348
|
-
|
349
|
-
def get_processing_stats(self):
|
350
|
-
"""Get current processing statistics"""
|
351
|
-
stats = {}
|
352
|
-
|
353
|
-
# Inbox stats
|
354
|
-
stats['inbox_unprocessed'] = InboxMessage.objects.filter(processed=False).count()
|
355
|
-
|
356
|
-
# Outbox stats
|
357
|
-
stats['outbox_pending'] = OutboxMessage.objects.filter(
|
358
|
-
status=OutboxMessage.PENDING
|
359
|
-
).count()
|
360
|
-
|
361
|
-
stats['outbox_failed'] = OutboxMessage.objects.filter(
|
362
|
-
status=OutboxMessage.FAILED
|
363
|
-
).count()
|
364
|
-
|
365
|
-
stats['outbox_ready'] = OutboxMessage.objects.filter(
|
366
|
-
status=OutboxMessage.PENDING,
|
367
|
-
scheduled_at__lte=timezone.now()
|
368
|
-
).count()
|
369
|
-
|
370
|
-
return stats
|
mojo/apps/notify/mod
DELETED
File without changes
|
@@ -1,12 +0,0 @@
|
|
1
|
-
from .account import Account
|
2
|
-
from .bounce import Bounce
|
3
|
-
from .complaint import Complaint
|
4
|
-
from .inbox import Inbox
|
5
|
-
from .inbox_message import InboxMessage
|
6
|
-
from .message import Message, Attachment
|
7
|
-
from .outbox import Outbox
|
8
|
-
from .outbox_message import OutboxMessage
|
9
|
-
from .template import NotifyTemplate
|
10
|
-
|
11
|
-
# Backward compatibility alias
|
12
|
-
MailTemplate = NotifyTemplate
|
@@ -1,128 +0,0 @@
|
|
1
|
-
from django.db import models
|
2
|
-
from mojo.models import MojoModel
|
3
|
-
|
4
|
-
|
5
|
-
class Account(models.Model, MojoModel):
|
6
|
-
"""
|
7
|
-
Notification service account for sending/receiving messages across different channels
|
8
|
-
"""
|
9
|
-
|
10
|
-
class RestMeta:
|
11
|
-
CAN_SAVE = CAN_CREATE = True
|
12
|
-
CAN_DELETE = True
|
13
|
-
DEFAULT_SORT = "-id"
|
14
|
-
VIEW_PERMS = ["view_notify"]
|
15
|
-
SEARCH_FIELDS = ["domain", "kind"]
|
16
|
-
SEARCH_TERMS = [
|
17
|
-
"kind", "domain",
|
18
|
-
("group", "group__name")]
|
19
|
-
|
20
|
-
GRAPHS = {
|
21
|
-
"default": {
|
22
|
-
"graphs": {
|
23
|
-
"group": "basic"
|
24
|
-
}
|
25
|
-
},
|
26
|
-
"list": {
|
27
|
-
"graphs": {
|
28
|
-
"group": "basic"
|
29
|
-
}
|
30
|
-
}
|
31
|
-
}
|
32
|
-
|
33
|
-
# Message service types
|
34
|
-
EMAIL = 'email'
|
35
|
-
SMS = 'sms'
|
36
|
-
WHATSAPP = 'whatsapp'
|
37
|
-
SIGNAL = 'signal'
|
38
|
-
WEBSOCKET = 'ws'
|
39
|
-
PUSH = 'push'
|
40
|
-
|
41
|
-
KIND_CHOICES = [
|
42
|
-
(EMAIL, 'Email'),
|
43
|
-
(SMS, 'SMS'),
|
44
|
-
(WHATSAPP, 'WhatsApp'),
|
45
|
-
(SIGNAL, 'Signal'),
|
46
|
-
(WEBSOCKET, 'WebSocket'),
|
47
|
-
(PUSH, 'Push Notification'),
|
48
|
-
]
|
49
|
-
|
50
|
-
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
51
|
-
modified = models.DateTimeField(auto_now=True)
|
52
|
-
|
53
|
-
group = models.ForeignKey(
|
54
|
-
"account.Group",
|
55
|
-
related_name="notify_accounts",
|
56
|
-
null=True,
|
57
|
-
blank=True,
|
58
|
-
default=None,
|
59
|
-
on_delete=models.CASCADE,
|
60
|
-
help_text="Group that owns this notification account"
|
61
|
-
)
|
62
|
-
|
63
|
-
kind = models.CharField(
|
64
|
-
max_length=32,
|
65
|
-
choices=KIND_CHOICES,
|
66
|
-
db_index=True,
|
67
|
-
help_text="Type of notification service (email, sms, whatsapp, etc.)"
|
68
|
-
)
|
69
|
-
|
70
|
-
domain = models.CharField(
|
71
|
-
max_length=255,
|
72
|
-
db_index=True,
|
73
|
-
help_text="Domain for email (example.com) or phone number for SMS (9493211234)"
|
74
|
-
)
|
75
|
-
|
76
|
-
settings = models.JSONField(
|
77
|
-
default=dict,
|
78
|
-
blank=True,
|
79
|
-
help_text="Service-specific configuration settings"
|
80
|
-
)
|
81
|
-
|
82
|
-
is_active = models.BooleanField(
|
83
|
-
default=True,
|
84
|
-
help_text="Whether this account is active and can send/receive messages"
|
85
|
-
)
|
86
|
-
|
87
|
-
class Meta:
|
88
|
-
unique_together = ['group', 'kind', 'domain']
|
89
|
-
indexes = [
|
90
|
-
models.Index(fields=['kind', 'domain']),
|
91
|
-
models.Index(fields=['group', 'kind']),
|
92
|
-
]
|
93
|
-
|
94
|
-
def __str__(self):
|
95
|
-
group_name = self.group.name if self.group else "No Group"
|
96
|
-
return f"{self.get_kind_display()} account: {self.domain} ({group_name})"
|
97
|
-
|
98
|
-
def get_setting(self, key, default=None):
|
99
|
-
"""Get a specific setting value"""
|
100
|
-
return self.settings.get(key, default)
|
101
|
-
|
102
|
-
def set_setting(self, key, value):
|
103
|
-
"""Set a specific setting value"""
|
104
|
-
self.settings[key] = value
|
105
|
-
|
106
|
-
@property
|
107
|
-
def is_email(self):
|
108
|
-
return self.kind == self.EMAIL
|
109
|
-
|
110
|
-
@property
|
111
|
-
def is_sms(self):
|
112
|
-
return self.kind == self.SMS
|
113
|
-
|
114
|
-
@property
|
115
|
-
def is_whatsapp(self):
|
116
|
-
return self.kind == self.WHATSAPP
|
117
|
-
|
118
|
-
@property
|
119
|
-
def is_signal(self):
|
120
|
-
return self.kind == self.SIGNAL
|
121
|
-
|
122
|
-
@property
|
123
|
-
def is_websocket(self):
|
124
|
-
return self.kind == self.WEBSOCKET
|
125
|
-
|
126
|
-
@property
|
127
|
-
def is_push(self):
|
128
|
-
return self.kind == self.PUSH
|
@@ -1,24 +0,0 @@
|
|
1
|
-
from django.db import models
|
2
|
-
from mojo.models import MojoModel
|
3
|
-
|
4
|
-
|
5
|
-
class Attachment(models.Model, MojoModel):
|
6
|
-
class RestMeta:
|
7
|
-
CAN_SAVE = CAN_CREATE = False
|
8
|
-
DEFAULT_SORT = "-id"
|
9
|
-
GRAPHS = {
|
10
|
-
"default": {
|
11
|
-
"graphs": {
|
12
|
-
"media": "basic"
|
13
|
-
},
|
14
|
-
}
|
15
|
-
}
|
16
|
-
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
17
|
-
name = models.CharField(max_length=255, null=True, default=None)
|
18
|
-
content_type = models.CharField(max_length=128, null=True, default=None)
|
19
|
-
message = models.ForeignKey(Message, related_name="attachments",
|
20
|
-
on_delete=models.CASCADE)
|
21
|
-
file = models.ForeignKey("fileman.File", related_name="attachments", on_delete=models.CASCADE)
|
22
|
-
|
23
|
-
def __str__(self):
|
24
|
-
return f"attachment: to:{self.message.to_email} from:{self.message.from_email} filename: {self.name}"
|
@@ -1,68 +0,0 @@
|
|
1
|
-
from django.db import models
|
2
|
-
from mojo.models import MojoModel
|
3
|
-
from mojo.helpers import dates
|
4
|
-
from mojo.apps.account.models import User
|
5
|
-
|
6
|
-
|
7
|
-
class Bounce(models.Model, MojoModel):
|
8
|
-
class RestMeta:
|
9
|
-
CAN_SAVE = CAN_CREATE = False
|
10
|
-
DEFAULT_SORT = "-id"
|
11
|
-
VIEW_PERMS = ["view_logs", "view_email"]
|
12
|
-
SEARCH_FIELDS = ["address"]
|
13
|
-
SEARCH_TERMS = [
|
14
|
-
("email", "address"),
|
15
|
-
("to", "address"), "source", "reason", "state",
|
16
|
-
("user", "user__username")]
|
17
|
-
|
18
|
-
GRAPHS = {
|
19
|
-
"default": {
|
20
|
-
"graphs": {
|
21
|
-
"user": "basic"
|
22
|
-
}
|
23
|
-
},
|
24
|
-
"list": {
|
25
|
-
"graphs": {
|
26
|
-
"user": "basic"
|
27
|
-
}
|
28
|
-
}
|
29
|
-
}
|
30
|
-
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
31
|
-
user = models.ForeignKey("account.User", related_name="emails_bounced",
|
32
|
-
null=True, blank=True, default=None, on_delete=models.CASCADE)
|
33
|
-
address = models.CharField(max_length=255, db_index=True)
|
34
|
-
kind = models.CharField(max_length=32, db_index=True)
|
35
|
-
reason = models.TextField(null=True, blank=True, default=None)
|
36
|
-
reporter = models.CharField(max_length=255, null=True, blank=True, default=None)
|
37
|
-
code = models.CharField(max_length=32, null=True, blank=True, default=None)
|
38
|
-
source = models.CharField(max_length=255, null=True, blank=True, default=None)
|
39
|
-
source_ip = models.CharField(max_length=64, null=True, blank=True, default=None)
|
40
|
-
|
41
|
-
def __str__(self):
|
42
|
-
return f"bounce: address:{self.address} reason:{self.reason}"
|
43
|
-
|
44
|
-
@staticmethod
|
45
|
-
def record(kind, address, reason, reporter=None, code=None, source=None, source_ip=None, user=None):
|
46
|
-
bounce = Bounce(
|
47
|
-
kind=kind,
|
48
|
-
address=address,
|
49
|
-
reason=reason,
|
50
|
-
reporter=reporter,
|
51
|
-
code=code,
|
52
|
-
source=source,
|
53
|
-
source_ip=source_ip,
|
54
|
-
user=None
|
55
|
-
)
|
56
|
-
|
57
|
-
if user is None:
|
58
|
-
user = User.objects.filter(email=address).last()
|
59
|
-
if user:
|
60
|
-
user.log("bounced", f"{kind} bounced to {address} from {source_ip}", method=kind)
|
61
|
-
recent_bounce_count = Bounce.objects.filter(user=user, created__gte=dates.add(dates.utcnow(), days=14)).count()
|
62
|
-
if recent_bounce_count > 2:
|
63
|
-
user.is_email_verified = False
|
64
|
-
user.save()
|
65
|
-
user.log("Email notifications have been disabled because of repeated bounces.", kind="email", level="warning")
|
66
|
-
|
67
|
-
bounce.user = user
|
68
|
-
bounce.save()
|
@@ -1,40 +0,0 @@
|
|
1
|
-
from django.db import models
|
2
|
-
from mojo.models import MojoModel
|
3
|
-
from mojo.apps.account.models import User
|
4
|
-
|
5
|
-
|
6
|
-
class Complaint(models.Model, MojoModel):
|
7
|
-
class RestMeta:
|
8
|
-
CAN_SAVE = CAN_CREATE = False
|
9
|
-
DEFAULT_SORT = "-id"
|
10
|
-
SEARCH_FIELDS = ["address"]
|
11
|
-
VIEW_PERMS = ["view_logs", "view_email"]
|
12
|
-
SEARCH_TERMS = [
|
13
|
-
("email", "address"),
|
14
|
-
("to", "address"), "source", "reason", "state",
|
15
|
-
("user", "user__username")]
|
16
|
-
|
17
|
-
GRAPHS = {
|
18
|
-
"default": {
|
19
|
-
"graphs": {
|
20
|
-
"user": "basic"
|
21
|
-
}
|
22
|
-
},
|
23
|
-
"list": {
|
24
|
-
"graphs": {
|
25
|
-
"user": "basic"
|
26
|
-
}
|
27
|
-
}
|
28
|
-
}
|
29
|
-
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
30
|
-
user = models.ForeignKey("account.User", related_name="emails_complaints",
|
31
|
-
null=True, blank=True, default=None, on_delete=models.CASCADE)
|
32
|
-
address = models.CharField(max_length=255, db_index=True)
|
33
|
-
kind = models.CharField(max_length=32, db_index=True)
|
34
|
-
reason = models.TextField(null=True, blank=True, default=None)
|
35
|
-
user_agent = models.CharField(max_length=255, null=True, blank=True, default=None)
|
36
|
-
source = models.CharField(max_length=255, null=True, blank=True, default=None)
|
37
|
-
source_ip = models.CharField(max_length=64, null=True, blank=True, default=None)
|
38
|
-
|
39
|
-
def __str__(self):
|
40
|
-
return f"complaint: address:{self.address} reason:{self.reason}"
|