stash
This commit is contained in:
parent
e361556188
commit
432b61c70a
16 changed files with 181 additions and 78 deletions
|
@ -61,6 +61,7 @@ INSTALLED_APPS = [
|
|||
'drf_yasg',
|
||||
'channels',
|
||||
'authentication',
|
||||
'notifications',
|
||||
'files',
|
||||
'tickets',
|
||||
'inventory',
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 4.2.7 on 2024-04-26 13:08
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('mail', '0004_alter_emailattachment_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserNotificationChannel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('channel_type', models.CharField(choices=[('telegram', 'telegram'), ('email', 'email')], max_length=255)),
|
||||
('channel_target', models.CharField(max_length=255)),
|
||||
('event_filter', models.CharField(max_length=255)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -41,13 +41,4 @@ class EmailAttachment(AbstractFile):
|
|||
name = models.CharField(max_length=255)
|
||||
|
||||
|
||||
class UserNotificationChannel(models.Model):
|
||||
user = models.ForeignKey(ExtendedUser, models.CASCADE)
|
||||
channel_type = models.CharField(choices=[('telegram', 'telegram'), ('email', 'email')], max_length=255)
|
||||
channel_target = models.CharField(max_length=255)
|
||||
event_filter = models.CharField(max_length=255)
|
||||
active = models.BooleanField(default=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def validate_constraints(self, exclude=None): # TODO: email -> emailaddress, telegram -> chatid
|
||||
return True
|
||||
|
|
|
@ -5,8 +5,8 @@ from channels.layers import get_channel_layer
|
|||
from channels.db import database_sync_to_async
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from core.settings import PRIMARY_HOST
|
||||
from mail.models import Email, EventAddress, EmailAttachment
|
||||
from notifications.templates import render_auto_reply
|
||||
from notify_sessions.models import SystemEvent
|
||||
from tickets.models import IssueThread
|
||||
|
||||
|
@ -83,14 +83,13 @@ def make_reply(reply_email, references=None, event=None):
|
|||
return reply
|
||||
|
||||
|
||||
def make_notification(message, to, event=None): # TODO where should replies to this go
|
||||
def make_notification(message, to, title): # TODO where should replies to this go
|
||||
from email.message import EmailMessage
|
||||
from core.settings import MAIL_DOMAIN
|
||||
event = event or "mail"
|
||||
notification = EmailMessage()
|
||||
notification["From"] = "notifications@%s" % MAIL_DOMAIN
|
||||
notification["To"] = to
|
||||
notification["Subject"] = f"System3 Notification"
|
||||
notification["Subject"] = f"[C3LF Notification]%s" % title
|
||||
# notification["Reply-To"] = f"{event}@{MAIL_DOMAIN}"
|
||||
# notification["In-Reply-To"] = email.reference
|
||||
# notification["Message-ID"] = email.id + "@" + MAIL_DOMAIN
|
||||
|
@ -198,11 +197,11 @@ def receive_email(envelope, log=None):
|
|||
header_in_reply_to = parsed.get('In-Reply-To')
|
||||
header_message_id = parsed.get('Message-ID')
|
||||
|
||||
#if header_from != envelope.mail_from:
|
||||
# if header_from != envelope.mail_from:
|
||||
# log.warning("Header from does not match envelope from")
|
||||
# log.info(f"Header from: {header_from}, envelope from: {envelope.mail_from}")
|
||||
#
|
||||
#if header_to != envelope.rcpt_tos[0]:
|
||||
#
|
||||
# if header_to != envelope.rcpt_tos[0]:
|
||||
# log.warning("Header to does not match envelope to")
|
||||
# log.info(f"Header to: {header_to}, envelope to: {envelope.rcpt_tos[0]}")
|
||||
|
||||
|
@ -231,16 +230,7 @@ def receive_email(envelope, log=None):
|
|||
references = collect_references(active_issue_thread)
|
||||
if not sender.startswith('noreply'):
|
||||
subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]"
|
||||
body = '''Your request (#{}) has been received and will be reviewed by our lost&found angels.
|
||||
|
||||
We are reviewing incoming requests during the event and teardown. Immediately after the event, expect a delay as the \
|
||||
workload is high. We will not forget about your request and get back in touch once we have updated information on your \
|
||||
request. Requests for devices, wallets, credit cards or similar items will be handled with priority.
|
||||
|
||||
If you happen to find your lost item or just want to add additional information, please reply to this email. Please \
|
||||
do not create a new request.
|
||||
|
||||
Your c3lf (Cloakroom + Lost&Found) Team'''.format(active_issue_thread.short_uuid())
|
||||
body = render_auto_reply(active_issue_thread)
|
||||
reply_email = Email.objects.create(
|
||||
sender=recipient, recipient=sender, body=body, subject=subject,
|
||||
in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread)
|
||||
|
@ -251,20 +241,7 @@ Your c3lf (Cloakroom + Lost&Found) Team'''.format(active_issue_thread.short_uuid
|
|||
active_issue_thread.state = 'pending_open'
|
||||
active_issue_thread.save()
|
||||
|
||||
notification = None
|
||||
if len(active_issue_thread.name) > 50:
|
||||
notify_subject = active_issue_thread.name[:47] + "..."
|
||||
else:
|
||||
notify_subject = active_issue_thread.name
|
||||
eventslug = target_event.slug if target_event else "37C3" # TODO 37C3 should not be hardcoded
|
||||
if new:
|
||||
notification = f"""New issue \"{active_issue_thread.name}\" [{active_issue_thread.short_uuid()}] created
|
||||
https://{PRIMARY_HOST}/{eventslug}/ticket/{active_issue_thread.id}/"""
|
||||
else:
|
||||
notification = f"""Reply to issue \"{active_issue_thread.name}\" [{active_issue_thread.short_uuid()}]
|
||||
https://{PRIMARY_HOST}/{eventslug}/ticket/{active_issue_thread.id}/"""
|
||||
|
||||
return email, new, reply, notification
|
||||
return email, new, reply, active_issue_thread
|
||||
|
||||
|
||||
class LMTPHandler:
|
||||
|
@ -286,7 +263,7 @@ class LMTPHandler:
|
|||
content = None
|
||||
try:
|
||||
content = envelope.content
|
||||
email, new, reply, notification = await database_sync_to_async(receive_email)(envelope, log)
|
||||
email, new, reply, thread = await database_sync_to_async(receive_email)(envelope, log)
|
||||
log.info(f"Created email {email.id}")
|
||||
systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received',
|
||||
reference=email.id)
|
||||
|
@ -296,11 +273,10 @@ class LMTPHandler:
|
|||
'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id,
|
||||
"message": "email received"})
|
||||
log.info(f"Sent message to frontend")
|
||||
if notification:
|
||||
log.info(f"Sending notification {notification}")
|
||||
if thread:
|
||||
await channel_layer.group_send(
|
||||
'general', {"type": "generic.event", "name": "user_notification", "event_id": systemevent.id,
|
||||
"message": notification})
|
||||
"ticket": thread, "new": new})
|
||||
if new and reply:
|
||||
log.info('Sending message to %s' % reply['To'])
|
||||
await send_smtp(reply)
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib.auth.models import Permission
|
|||
from django.test import TestCase
|
||||
|
||||
from authentication.models import ExtendedUser
|
||||
from mail.models import UserNotificationChannel
|
||||
from notifications.models import UserNotificationChannel
|
||||
|
||||
|
||||
class UserNotificationTestCase(TestCase):
|
||||
|
|
0
core/notifications/__init__.py
Normal file
0
core/notifications/__init__.py
Normal file
16
core/notifications/defauls.py
Normal file
16
core/notifications/defauls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
auto_reply_body = '''Your request (#{{ ticket_uuid }}) has been received and will be reviewed by our lost&found angels.
|
||||
|
||||
We are reviewing incoming requests during the event and teardown. Immediately after the event, expect a delay as the \
|
||||
workload is high. We will not forget about your request and get back in touch once we have updated information on your \
|
||||
request. Requests for devices, wallets, credit cards or similar items will be handled with priority.
|
||||
|
||||
If you happen to find your lost item or just want to add additional information, please reply to this email. Please \
|
||||
do not create a new request.
|
||||
|
||||
Your c3lf (Cloakroom + Lost&Found) Team'''
|
||||
|
||||
new_issue_notification = '''New issue "{{ ticket_name | limit_length }}" [{{ ticket_uuid }}] created
|
||||
{{ ticket_url }}'''
|
||||
|
||||
reply_issue_notification = '''Reply to issue "{{ ticket_name }}" [{{ ticket_uuid }}]
|
||||
{{ ticket_url }}'''
|
|
@ -4,8 +4,9 @@ from channels.db import database_sync_to_async
|
|||
from urllib.parse import quote as urlencode
|
||||
|
||||
from core.settings import TELEGRAM_BOT_TOKEN, TELEGRAM_GROUP_CHAT_ID
|
||||
from mail.models import UserNotificationChannel
|
||||
from mail.protocol import send_smtp, make_notification
|
||||
from notifications.models import UserNotificationChannel
|
||||
from notifications.templates import render_notification_new_ticket, render_notification_reply_ticket
|
||||
|
||||
|
||||
async def http_get(url):
|
||||
|
@ -51,12 +52,13 @@ class NotificationDispatcher:
|
|||
|
||||
if (message and 'type' in message and message['type'] == 'generic.event' and 'name' in message and
|
||||
message['name'] == 'user_notification'):
|
||||
if 'message' in message and 'event_id' in message:
|
||||
await self.dispatch(message['message'], message['event_id'])
|
||||
if 'ticket' in message and 'event_id' in message and 'new' in message:
|
||||
await self.dispatch(message['ticket'], message['event_id'], message['new'])
|
||||
else:
|
||||
print("Error: Invalid message format")
|
||||
|
||||
async def dispatch(self, message, event_id):
|
||||
async def dispatch(self, ticket, event_id, new):
|
||||
message = render_notification_new_ticket(ticket) if new else render_notification_reply_ticket(ticket)
|
||||
print("Dispatching message:", message, "with event_id:", event_id)
|
||||
targets = await self.get_notification_targets()
|
||||
await telegram_notify(message, TELEGRAM_GROUP_CHAT_ID)
|
48
core/notifications/migrations/0001_initial.py
Normal file
48
core/notifications/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 4.2.7 on 2024-05-03 21:02
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
from notifications.defauls import auto_reply_body, new_issue_notification, reply_issue_notification
|
||||
from notifications.models import MessageTemplate
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
def create_required_templates(apps, schema_editor):
|
||||
MessageTemplate.objects.create(name='auto_reply', message=auto_reply_body)
|
||||
MessageTemplate.objects.create(name='new_issue_notification', message=new_issue_notification)
|
||||
MessageTemplate.objects.create(name='reply_issue_notification', message=reply_issue_notification)
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MessageTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('message', models.TextField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('marked_confidential', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserNotificationChannel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('channel_type',
|
||||
models.CharField(choices=[('telegram', 'telegram'), ('email', 'email')], max_length=255)),
|
||||
('channel_target', models.CharField(max_length=255)),
|
||||
('event_filter', models.CharField(max_length=255)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(create_required_templates),
|
||||
]
|
0
core/notifications/migrations/__init__.py
Normal file
0
core/notifications/migrations/__init__.py
Normal file
22
core/notifications/models.py
Normal file
22
core/notifications/models.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from django.db import models
|
||||
|
||||
from authentication.models import ExtendedUser
|
||||
|
||||
|
||||
class MessageTemplate(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
message = models.TextField()
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
marked_confidential = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class UserNotificationChannel(models.Model):
|
||||
user = models.ForeignKey(ExtendedUser, models.CASCADE)
|
||||
channel_type = models.CharField(choices=[('telegram', 'telegram'), ('email', 'email')], max_length=255)
|
||||
channel_target = models.CharField(max_length=255)
|
||||
event_filter = models.CharField(max_length=255)
|
||||
active = models.BooleanField(default=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def validate_constraints(self, exclude=None): # TODO: email -> emailaddress, telegram -> chatid
|
||||
return True
|
54
core/notifications/templates.py
Normal file
54
core/notifications/templates.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
import jinja2
|
||||
from core.settings import PRIMARY_HOST
|
||||
|
||||
from notifications.models import MessageTemplate
|
||||
|
||||
# auto_reply_title = f"Re: {{ ticket_name }} [#{{ ticket_uuid }}]"
|
||||
|
||||
|
||||
TEMLATE_VARS = ['ticket_name', 'ticket_uuid', 'ticket_id', 'ticket_url',
|
||||
'event_slug', 'event_name',
|
||||
'username', 'user_nick',
|
||||
'web_host'] # TODO customer_name, tracking_code
|
||||
|
||||
|
||||
def limit_length(s, length=50):
|
||||
if len(s) > length:
|
||||
return s[:(length - 3)] + "..."
|
||||
return s
|
||||
|
||||
|
||||
def ticket_url(ticket):
|
||||
eventslug = ticket.event.slug if ticket.event else "37C3" # TODO 37C3 should not be hardcoded
|
||||
return f"https://{PRIMARY_HOST}/{eventslug}/ticket/{ticket.id}/"
|
||||
|
||||
|
||||
def render_template(template, **kwargs):
|
||||
try:
|
||||
|
||||
environment = jinja2.Environment()
|
||||
environment.filters['limit_length'] = limit_length
|
||||
|
||||
tmpl = MessageTemplate.objects.get(name=template)
|
||||
template = environment.from_string(tmpl.message)
|
||||
return template.render(**kwargs, web_host=PRIMARY_HOST)
|
||||
except MessageTemplate.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def render_auto_reply(ticket):
|
||||
eventslug = ticket.event.slug if ticket.event else "37C3" # TODO 37C3 should not be hardcoded
|
||||
return render_template('auto_reply', ticket_name=ticket.name, ticket_uuid=ticket.short_uuid(),
|
||||
ticket_id=ticket.id, event_slug=eventslug, ticket_url=ticket_url(ticket))
|
||||
|
||||
|
||||
def render_notification_new_ticket(ticket):
|
||||
eventslug = ticket.event.slug if ticket.event else "37C3" # TODO 37C3 should not be hardcoded
|
||||
return render_template('new_issue_notification', ticket_name=ticket.name, ticket_uuid=ticket.short_uuid(),
|
||||
ticket_id=ticket.id, event_slug=eventslug, ticket_url=ticket_url(ticket))
|
||||
|
||||
|
||||
def render_notification_reply_ticket(ticket):
|
||||
eventslug = ticket.event.slug if ticket.event else "37C3" # TODO 37C3 should not be hardcoded
|
||||
return render_template('reply_issue_notification', ticket_name=ticket.name, ticket_uuid=ticket.short_uuid(),
|
||||
ticket_id=ticket.id, event_slug=eventslug, ticket_url=ticket_url(ticket))
|
0
core/notifications/tests/__init__.py
Normal file
0
core/notifications/tests/__init__.py
Normal file
2
core/server.py
Normal file → Executable file
2
core/server.py
Normal file → Executable file
|
@ -12,7 +12,7 @@ django.setup()
|
|||
from helper import init_loop
|
||||
from mail.protocol import LMTPHandler
|
||||
from mail.socket import UnixSocketLMTPController
|
||||
from mail.notifications import NotificationDispatcher
|
||||
from notifications.dispatch import NotificationDispatcher
|
||||
|
||||
|
||||
class UvicornServer(uvicorn.Server):
|
||||
|
|
20
core/tickets/migrations/0009_issuethread_event.py
Normal file
20
core/tickets/migrations/0009_issuethread_event.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.7 on 2024-05-03 21:21
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_alter_event_created_at_alter_item_created_at'),
|
||||
('tickets', '0008_alter_issuethread_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='issuethread',
|
||||
name='event',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_threads', to='inventory.event'),
|
||||
),
|
||||
]
|
|
@ -33,6 +33,7 @@ class IssueThread(SoftDeleteModel):
|
|||
id = models.AutoField(primary_key=True)
|
||||
uuid = models.CharField(max_length=255, unique=True, null=False, blank=False)
|
||||
name = models.CharField(max_length=255)
|
||||
event = models.ForeignKey(Event, null=True, on_delete=models.SET_NULL, related_name='issue_threads')
|
||||
manually_created = models.BooleanField(default=False)
|
||||
|
||||
def short_uuid(self):
|
||||
|
|
Loading…
Reference in a new issue