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',
'channels',
'authentication',
'notifications',
'files',
'tickets',
'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)
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 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)

View file

@ -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):

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

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 mail.protocol import LMTPHandler
from mail.socket import UnixSocketLMTPController
from mail.notifications import NotificationDispatcher
from notifications.dispatch import NotificationDispatcher
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'),
),
]