mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2024-12-04 22:11:06 +01:00
Implement webhooks. Fixes #264
This commit is contained in:
parent
5e1fb838cb
commit
e708281dcd
@ -1,9 +1,3 @@
|
|||||||
Integrating django-helpdesk into your application
|
|
||||||
=================================================
|
|
||||||
|
|
||||||
Ticket submission with embeded iframe
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
Ticket submission with embeded iframe
|
Ticket submission with embeded iframe
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
@ -17,7 +17,8 @@ Contents
|
|||||||
spam
|
spam
|
||||||
custom_fields
|
custom_fields
|
||||||
api
|
api
|
||||||
integration
|
webhooks
|
||||||
|
iframe_submission
|
||||||
teams
|
teams
|
||||||
license
|
license
|
||||||
|
|
||||||
|
10
docs/webhooks.rst
Normal file
10
docs/webhooks.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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 comma separated environement variables; ``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``.
|
||||||
|
|
||||||
|
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:
|
# see:
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
# https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field
|
||||||
default_auto_field = 'django.db.models.AutoField'
|
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.utils import getaddresses
|
||||||
from email_reply_parser import EmailReplyParser
|
from email_reply_parser import EmailReplyParser
|
||||||
from helpdesk import settings
|
from helpdesk import settings
|
||||||
|
from helpdesk import webhooks
|
||||||
from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException
|
from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException
|
||||||
from helpdesk.lib import process_attachments, safe_template_context
|
from helpdesk.lib import process_attachments, safe_template_context
|
||||||
from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket
|
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")
|
"Message seems to be auto-reply, not sending any emails back to the sender")
|
||||||
else:
|
else:
|
||||||
send_info_email(message_id, f, ticket, context, queue, new)
|
send_info_email(message_id, f, ticket, context, queue, new)
|
||||||
|
if not new:
|
||||||
|
webhooks.notify_followup_webhooks(f)
|
||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
|
@ -277,3 +277,19 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
|||||||
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
# Attachment directories should be created with permission 755 (rwxr-xr-x)
|
||||||
# Override it in your own Django settings.py
|
# Override it in your own Django settings.py
|
||||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
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 urls.split(',')
|
||||||
|
|
||||||
|
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)
|
||||||
|
200
helpdesk/tests/test_webhooks.py
Normal file
200
helpdesk/tests/test_webhooks.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
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')))
|
||||||
|
elif self.path == '/followup':
|
||||||
|
self.server.handled_follow_up_requests.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,
|
||||||
|
'follow_up_requests': self.server.handled_follow_up_requests
|
||||||
|
}).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_follow_up_requests = []
|
||||||
|
|
||||||
|
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)
|
||||||
|
os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8123/new-ticket'
|
||||||
|
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8123/followup'
|
||||||
|
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'
|
||||||
|
os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup'
|
||||||
|
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.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]["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(handled_webhook_requests['follow_up_requests'][-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()
|
ticket.save()
|
||||||
|
|
||||||
|
from helpdesk.webhooks import notify_followup_webhooks
|
||||||
|
notify_followup_webhooks(f)
|
||||||
|
|
||||||
# auto subscribe user if enabled
|
# auto subscribe user if enabled
|
||||||
add_staff_subscription(user, ticket)
|
add_staff_subscription(user, ticket)
|
||||||
return f
|
return f
|
||||||
|
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
|
django-cleanup
|
||||||
oauthlib
|
oauthlib
|
||||||
requests_oauthlib
|
requests_oauthlib
|
||||||
|
requests
|
||||||
|
Loading…
Reference in New Issue
Block a user