This commit is contained in:
j3d1 2024-05-03 23:34:47 +02:00
parent a4d79896da
commit 5ddd8c0b4b
15 changed files with 180 additions and 78 deletions

View file

@ -61,6 +61,7 @@ INSTALLED_APPS = [
'drf_yasg', 'drf_yasg',
'channels', 'channels',
'authentication', 'authentication',
'notifications',
'files', 'files',
'tickets', 'tickets',
'inventory', 'inventory',

View file

@ -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)),
],
),
]

View file

@ -41,13 +41,4 @@ class EmailAttachment(AbstractFile):
name = models.CharField(max_length=255) 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

View file

@ -5,8 +5,8 @@ from channels.layers import get_channel_layer
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from core.settings import PRIMARY_HOST
from mail.models import Email, EventAddress, EmailAttachment from mail.models import Email, EventAddress, EmailAttachment
from notifications.templates import render_auto_reply
from notify_sessions.models import SystemEvent from notify_sessions.models import SystemEvent
from tickets.models import IssueThread from tickets.models import IssueThread
@ -83,14 +83,13 @@ def make_reply(reply_email, references=None, event=None):
return reply 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 email.message import EmailMessage
from core.settings import MAIL_DOMAIN from core.settings import MAIL_DOMAIN
event = event or "mail"
notification = EmailMessage() notification = EmailMessage()
notification["From"] = "notifications@%s" % MAIL_DOMAIN notification["From"] = "notifications@%s" % MAIL_DOMAIN
notification["To"] = to notification["To"] = to
notification["Subject"] = f"System3 Notification" notification["Subject"] = f"[C3LF Notification]%s" % title
# notification["Reply-To"] = f"{event}@{MAIL_DOMAIN}" # notification["Reply-To"] = f"{event}@{MAIL_DOMAIN}"
# notification["In-Reply-To"] = email.reference # notification["In-Reply-To"] = email.reference
# notification["Message-ID"] = email.id + "@" + MAIL_DOMAIN # notification["Message-ID"] = email.id + "@" + MAIL_DOMAIN
@ -231,16 +230,7 @@ def receive_email(envelope, log=None):
references = collect_references(active_issue_thread) references = collect_references(active_issue_thread)
if not sender.startswith('noreply'): if not sender.startswith('noreply'):
subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]" subject = f"Re: {subject} [#{active_issue_thread.short_uuid()}]"
body = '''Your request (#{}) has been received and will be reviewed by our lost&found angels. body = render_auto_reply(active_issue_thread)
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())
reply_email = Email.objects.create( reply_email = Email.objects.create(
sender=recipient, recipient=sender, body=body, subject=subject, sender=recipient, recipient=sender, body=body, subject=subject,
in_reply_to=header_message_id, event=target_event, issue_thread=active_issue_thread) 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.state = 'pending_open'
active_issue_thread.save() active_issue_thread.save()
notification = None return email, new, reply, active_issue_thread
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
class LMTPHandler: class LMTPHandler:
@ -286,7 +263,7 @@ class LMTPHandler:
content = None content = None
try: try:
content = envelope.content 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}") log.info(f"Created email {email.id}")
systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received', systemevent = await database_sync_to_async(SystemEvent.objects.create)(type='email received',
reference=email.id) reference=email.id)
@ -296,11 +273,10 @@ class LMTPHandler:
'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id, 'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id,
"message": "email received"}) "message": "email received"})
log.info(f"Sent message to frontend") log.info(f"Sent message to frontend")
if notification: if thread:
log.info(f"Sending notification {notification}")
await channel_layer.group_send( await channel_layer.group_send(
'general', {"type": "generic.event", "name": "user_notification", "event_id": systemevent.id, 'general', {"type": "generic.event", "name": "user_notification", "event_id": systemevent.id,
"message": notification}) "ticket": thread, "new": new})
if new and reply: if new and reply:
log.info('Sending message to %s' % reply['To']) log.info('Sending message to %s' % reply['To'])
await send_smtp(reply) await send_smtp(reply)

View file

@ -2,7 +2,7 @@ from django.contrib.auth.models import Permission
from django.test import TestCase from django.test import TestCase
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from mail.models import UserNotificationChannel from notifications.models import UserNotificationChannel
class UserNotificationTestCase(TestCase): class UserNotificationTestCase(TestCase):

View file

View 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 }}'''

View file

@ -4,8 +4,9 @@ from channels.db import database_sync_to_async
from urllib.parse import quote as urlencode from urllib.parse import quote as urlencode
from core.settings import TELEGRAM_BOT_TOKEN, TELEGRAM_GROUP_CHAT_ID 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 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): 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 if (message and 'type' in message and message['type'] == 'generic.event' and 'name' in message and
message['name'] == 'user_notification'): message['name'] == 'user_notification'):
if 'message' in message and 'event_id' in message: if 'ticket' in message and 'event_id' in message and 'new' in message:
await self.dispatch(message['message'], message['event_id']) await self.dispatch(message['ticket'], message['event_id'], message['new'])
else: else:
print("Error: Invalid message format") 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) print("Dispatching message:", message, "with event_id:", event_id)
targets = await self.get_notification_targets() targets = await self.get_notification_targets()
await telegram_notify(message, TELEGRAM_GROUP_CHAT_ID) await telegram_notify(message, TELEGRAM_GROUP_CHAT_ID)

View 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),
]

View 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

View 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))

View file

2
core/server.py Normal file → Executable file
View file

@ -12,7 +12,7 @@ django.setup()
from helper import init_loop from helper import init_loop
from mail.protocol import LMTPHandler from mail.protocol import LMTPHandler
from mail.socket import UnixSocketLMTPController from mail.socket import UnixSocketLMTPController
from mail.notifications import NotificationDispatcher from notifications.dispatch import NotificationDispatcher
class UvicornServer(uvicorn.Server): class UvicornServer(uvicorn.Server):

View 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'),
),
]