django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.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}"