mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2024-12-13 10:21:05 +01:00
commit
c138047128
@ -1,6 +1,3 @@
|
||||
Integrating django-helpdesk into your application
|
||||
=================================================
|
||||
|
||||
Ticket submission with embeded iframe
|
||||
-------------------------------------
|
||||
|
@ -17,7 +17,8 @@ Contents
|
||||
spam
|
||||
custom_fields
|
||||
api
|
||||
integration
|
||||
webhooks
|
||||
iframe_submission
|
||||
teams
|
||||
license
|
||||
|
||||
|
12
docs/webhooks.rst
Normal file
12
docs/webhooks.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Registering webhooks to be notified of helpesk events
|
||||
-----------------------------------------------------
|
||||
|
||||
You can register webhooks to allow third party apps to be notified of helpdesk events. Webhooks can be registered in one of two ways:
|
||||
|
||||
1. Setting the following environement variables to a comma separated list of URLs; ``HELPDESK_NEW_TICKET_WEBHOOK_URLS``& ``HELPDESK_FOLLOWUP_WEBHOOK_URLS``.
|
||||
|
||||
2. Adding getter functions to your ``settings.py``. These should return a list of strings (urls); ``HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS`` & ``HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS``.
|
||||
|
||||
3. You can optionally set ``HELPDESK_WEBHOOK_TIMEOUT`` which defaults to 3 seconds. Warning, however, webhook requests are sent out sychronously on ticket update. If your webhook handling server is too slow, you should fix this rather than causing helpdesk freezes by messing with this variable.
|
||||
|
||||
Once these URLs are configured, a serialized copy of the ticket object will be posted to each of these URLs each time a ticket is created or followed up on respectively.
|
@ -8,3 +8,6 @@ class HelpdeskConfig(AppConfig):
|
||||
# see:
|
||||
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
|
||||
def ready(self):
|
||||
from . import webhooks # noqa: F401
|
||||
|
@ -21,6 +21,7 @@ from email.message import EmailMessage, MIMEPart
|
||||
from email.utils import getaddresses
|
||||
from email_reply_parser import EmailReplyParser
|
||||
from helpdesk import settings
|
||||
from helpdesk import webhooks
|
||||
from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException
|
||||
from helpdesk.lib import process_attachments, safe_template_context
|
||||
from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
|
||||
@ -616,6 +617,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
|
||||
"Message seems to be auto-reply, not sending any emails back to the sender")
|
||||
else:
|
||||
send_info_email(message_id, f, ticket, context, queue, new)
|
||||
if not new:
|
||||
webhooks.notify_followup_webhooks(f)
|
||||
return ticket
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
|
||||
|
||||
@ -277,3 +278,19 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
||||
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
||||
# Override it in your own Django settings.py
|
||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
||||
|
||||
def get_followup_webhook_urls():
|
||||
urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None)
|
||||
if urls:
|
||||
return re.split(r'[\s],[\s]', urls)
|
||||
|
||||
HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls)
|
||||
|
||||
def get_new_ticket_webhook_urls():
|
||||
urls = os.environ.get('HELPDESK_NEW_TICKET_WEBHOOK_URLS', None)
|
||||
if urls:
|
||||
return urls.split(',')
|
||||
|
||||
HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS', get_new_ticket_webhook_urls)
|
||||
|
||||
HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, 'HELPDESK_WEBHOOK_TIMEOUT', 3)
|
||||
|
210
helpdesk/tests/test_webhooks.py
Normal file
210
helpdesk/tests/test_webhooks.py
Normal file
@ -0,0 +1,210 @@
|
||||
from django.contrib.auth.models import User
|
||||
from helpdesk.models import Queue
|
||||
from rest_framework.status import (
|
||||
HTTP_201_CREATED
|
||||
)
|
||||
from rest_framework.test import APITestCase
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
|
||||
# Set up a test weberver listeining on localhost:8123 for webhooks
|
||||
import http.server
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
|
||||
class WebhookRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
server: "WebhookServer"
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
body = self.rfile.read(content_length)
|
||||
self.server.requests.append({
|
||||
'path': self.path,
|
||||
'headers': self.headers,
|
||||
'body': body
|
||||
})
|
||||
if self.path == '/new-ticket':
|
||||
self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8')))
|
||||
if self.path == '/new-ticket-1':
|
||||
self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8')))
|
||||
elif self.path == '/followup':
|
||||
self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8')))
|
||||
elif self.path == '/followup-1':
|
||||
self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8')))
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if not self.path == '/get-past-requests':
|
||||
self.send_response(HTTPStatus.NOT_FOUND)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({
|
||||
'new_ticket_requests': self.server.handled_new_ticket_requests,
|
||||
'new_ticket_requests_1': self.server.handled_new_ticket_requests_1,
|
||||
'follow_up_requests': self.server.handled_follow_up_requests,
|
||||
'follow_up_requests_1': self.server.handled_follow_up_requests_1
|
||||
}).encode('utf-8'))
|
||||
|
||||
|
||||
class WebhookServer(http.server.HTTPServer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.requests = []
|
||||
self.handled_new_ticket_requests = []
|
||||
self.handled_new_ticket_requests_1 = []
|
||||
self.handled_follow_up_requests = []
|
||||
self.handled_follow_up_requests_1 = []
|
||||
|
||||
def start(self):
|
||||
self.thread = threading.Thread(target=self.serve_forever)
|
||||
self.thread.daemon = True # Set as a daemon so it will be killed once the main thread is dead
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.shutdown()
|
||||
self.server_close()
|
||||
self.thread.join()
|
||||
|
||||
|
||||
class WebhookTest(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.queue = Queue.objects.create(
|
||||
title='Test Queue',
|
||||
slug='test-queue',
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
staff_user = User.objects.create_user(username='test', is_staff=True)
|
||||
self.client.force_authenticate(staff_user)
|
||||
|
||||
def test_test_server(self):
|
||||
server = WebhookServer(('localhost', 8123), WebhookRequestHandler)
|
||||
server.start()
|
||||
|
||||
requests.post('http://localhost:8123/new-ticket', json={
|
||||
"foo": "bar"})
|
||||
handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json()
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar")
|
||||
server.stop()
|
||||
|
||||
def test_create_ticket_and_followup_via_api(self):
|
||||
server = WebhookServer(('localhost', 8124), WebhookRequestHandler)
|
||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1'
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1'
|
||||
server.start()
|
||||
|
||||
response = self.client.post('/api/tickets/', {
|
||||
'queue': self.queue.id,
|
||||
'title': 'Test title',
|
||||
'description': 'Test description\nMulti lines',
|
||||
'submitter_email': 'test@mail.com',
|
||||
'priority': 4
|
||||
})
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) == 1)
|
||||
self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title")
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title")
|
||||
self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines")
|
||||
response = self.client.post('/api/followups/', {
|
||||
'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"],
|
||||
"comment": "Test comment",
|
||||
})
|
||||
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||
handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests')
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 1)
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment")
|
||||
server.stop()
|
||||
|
||||
def test_create_ticket_and_followup_via_email(self):
|
||||
from .. import email
|
||||
|
||||
server = WebhookServer(('localhost', 8125), WebhookRequestHandler)
|
||||
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket'
|
||||
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup'
|
||||
server.start()
|
||||
class MockMessage(dict):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get_all(self, key, default=None):
|
||||
return self.__dict__.get(key, default)
|
||||
|
||||
payload = {
|
||||
'body': "hello",
|
||||
'full_body': "hello",
|
||||
'subject': "Test subject",
|
||||
'queue': self.queue,
|
||||
'sender_email': "user@example.com",
|
||||
'priority': "1",
|
||||
'files': [],
|
||||
}
|
||||
|
||||
message = {
|
||||
"To": ["info@example.com"],
|
||||
"Cc": [],
|
||||
"Message-Id": "random1",
|
||||
"In-Reply-To": "",
|
||||
}
|
||||
email.create_object_from_email_message(
|
||||
message=MockMessage(**message),
|
||||
ticket_id=None,
|
||||
payload=payload,
|
||||
files=[],
|
||||
logger=logging.getLogger('helpdesk'),
|
||||
)
|
||||
|
||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1)
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0)
|
||||
|
||||
ticket_id = handled_webhook_requests['new_ticket_requests'][-1]["ticket"]['id']
|
||||
from .. import models
|
||||
ticket = models.Ticket.objects.get(id=ticket_id)
|
||||
|
||||
payload = {
|
||||
'body': "hello",
|
||||
'full_body': "hello",
|
||||
'subject': f"[test-queue-{ticket_id}] Test subject",
|
||||
'queue': self.queue,
|
||||
'sender_email': "user@example.com",
|
||||
'priority': "1",
|
||||
'files': [],
|
||||
}
|
||||
|
||||
message = {
|
||||
"To": ["info@example.com"],
|
||||
"Cc": [],
|
||||
"Message-Id": "random",
|
||||
"In-Reply-To": "123",
|
||||
}
|
||||
email.create_object_from_email_message(
|
||||
message=MockMessage(**message),
|
||||
ticket_id=ticket_id,
|
||||
payload=payload,
|
||||
files=[],
|
||||
logger=logging.getLogger('helpdesk'),
|
||||
)
|
||||
handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests')
|
||||
handled_webhook_requests = handled_webhook_requests.json()
|
||||
self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1)
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello")
|
||||
self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id)
|
||||
|
||||
server.stop()
|
||||
|
||||
|
@ -386,6 +386,9 @@ def update_ticket(
|
||||
))
|
||||
ticket.save()
|
||||
|
||||
from helpdesk.webhooks import notify_followup_webhooks
|
||||
notify_followup_webhooks(f)
|
||||
|
||||
# auto subscribe user if enabled
|
||||
add_staff_subscription(user, ticket)
|
||||
return f
|
||||
|
@ -210,7 +210,7 @@ class ViewTicket(TemplateView):
|
||||
queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req)
|
||||
if request.user.is_authenticated and request.user.email == email:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
||||
elif hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email)
|
||||
else:
|
||||
ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key)
|
||||
|
56
helpdesk/webhooks.py
Normal file
56
helpdesk/webhooks.py
Normal file
@ -0,0 +1,56 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from . import settings
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import logging
|
||||
|
||||
from .models import Ticket
|
||||
from .serializers import TicketSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def notify_followup_webhooks(followup):
|
||||
urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS()
|
||||
if not urls:
|
||||
return
|
||||
# Serialize the ticket associated with the followup
|
||||
ticket = followup.ticket
|
||||
serialized_ticket = TicketSerializer(ticket).data
|
||||
|
||||
# Prepare the data to send
|
||||
data = {
|
||||
'ticket': serialized_ticket,
|
||||
'queue_slug': ticket.queue.slug,
|
||||
'followup_id': followup.id
|
||||
}
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error('Timeout while sending followup webhook to %s', url)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Ticket)
|
||||
def ticket_post_save(sender, instance, created, **kwargs):
|
||||
if not created:
|
||||
return
|
||||
urls = settings.HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS()
|
||||
if not urls:
|
||||
return
|
||||
# Serialize the ticket
|
||||
serialized_ticket = TicketSerializer(instance).data
|
||||
|
||||
# Prepare the data to send
|
||||
data = {
|
||||
'ticket': serialized_ticket,
|
||||
'queue_slug': instance.queue.slug
|
||||
}
|
||||
|
||||
for url in urls:
|
||||
try:
|
||||
requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error('Timeout while sending new ticket webhook to %s', url)
|
@ -13,3 +13,4 @@ django-model-utils
|
||||
django-cleanup
|
||||
oauthlib
|
||||
requests_oauthlib
|
||||
requests
|
||||
|
Loading…
Reference in New Issue
Block a user