From 91a48c44ec42704826222b8b3bdfb881ccb5031b Mon Sep 17 00:00:00 2001 From: Artur Czepiel Date: Sat, 8 Mar 2025 14:39:48 +0100 Subject: [PATCH 1/4] v1 of zammad discord router --- Makefile | 2 +- deploy/templates/app/intbot.env.example | 5 + intbot/core/bot/channel_router.py | 28 ++ intbot/core/endpoints/webhooks.py | 8 +- intbot/core/integrations/zammad.py | 160 +++++++++++ intbot/core/tasks.py | 30 +- intbot/intbot/settings.py | 20 ++ intbot/tests/test_bot/test_channel_router.py | 53 +++- intbot/tests/test_integrations/test_zammad.py | 265 ++++++++++++++++++ intbot/tests/test_webhooks.py | 5 + 10 files changed, 569 insertions(+), 7 deletions(-) create mode 100644 intbot/core/integrations/zammad.py create mode 100644 intbot/tests/test_integrations/test_zammad.py diff --git a/Makefile b/Makefile index 77215b0..860e194 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ test: $(TEST_CMD) -s -v test_last_failed: - $(TEST_CMD) -s -v --last-failed + $(TEST_CMD) -s -vv --last-failed test_: test_last_failed diff --git a/deploy/templates/app/intbot.env.example b/deploy/templates/app/intbot.env.example index 2fbdb49..ce35f62 100644 --- a/deploy/templates/app/intbot.env.example +++ b/deploy/templates/app/intbot.env.example @@ -34,3 +34,8 @@ GITHUB_WEBHOOK_SECRET_TOKEN="github-webhook-secret-token" GITHUB_BOARD_PROJECT_ID="GITHUB_BOARD_PROJECT_ID" GITHUB_EP2025_PROJECT_ID="GITHUB_EP2025_PROJECT_ID" GITHUB_EM_PROJECT_ID="GITHUB_EM_PROJECT_ID" + +# Zammad +ZAMMAD_WEBHOOK_SECRET_TOKEN="zammad-shared-secret-goes-here" +ZAMMAD_GROUP_BILLING="zammad-billing-group-name-goes-here" +ZAMMAD_GROUP_HELPDESK="zammad-helpdesk-group-name-goes-here" diff --git a/intbot/core/bot/channel_router.py b/intbot/core/bot/channel_router.py index 66996d5..d9a2b95 100644 --- a/intbot/core/bot/channel_router.py +++ b/intbot/core/bot/channel_router.py @@ -9,6 +9,7 @@ GithubRepositories, parse_github_webhook, ) +from core.integrations.zammad import ZammadGroups from core.models import Webhook from django.conf import settings @@ -23,6 +24,7 @@ class DiscordChannel: class Channels: + # Github test_channel = DiscordChannel( channel_id=settings.DISCORD_TEST_CHANNEL_ID, channel_name=settings.DISCORD_TEST_CHANNEL_NAME, @@ -49,11 +51,24 @@ class Channels: channel_name=settings.DISCORD_BOT_CHANNEL_NAME, ) + # Zammad + billing_channel = DiscordChannel( + channel_id=settings.DISCORD_BILLING_CHANNEL_ID, + channel_name=settings.DISCORD_BILLING_CHANNEL_NAME, + ) + helpdesk_channel = DiscordChannel( + channel_id=settings.DISCORD_HELPDESK_CHANNEL_ID, + channel_name=settings.DISCORD_HELPDESK_CHANNEL_NAME, + ) + def discord_channel_router(wh: Webhook) -> DiscordChannel: if wh.source == "github": return github_router(wh) + elif wh.source == "zammad": + return zammad_router(wh) + elif wh.source == "internal": return internal_router(wh) @@ -91,6 +106,19 @@ def github_router(wh: Webhook) -> DiscordChannel: return dont_send_it +def zammad_router(wh: Webhook) -> DiscordChannel: + groups = { + ZammadGroups.helpdesk: Channels.helpdesk_channel, + ZammadGroups.billing: Channels.billing_channel, + } + + if channel := groups.get(wh.extra["group"]): + return channel + + # If it doesn't match any of the groups, just skip it + return dont_send_it + + def internal_router(wh: Webhook) -> DiscordChannel: # For now just send all the internal messages to a test channel return Channels.test_channel diff --git a/intbot/core/endpoints/webhooks.py b/intbot/core/endpoints/webhooks.py index d7716d0..e0d1fd7 100644 --- a/intbot/core/endpoints/webhooks.py +++ b/intbot/core/endpoints/webhooks.py @@ -107,6 +107,12 @@ def zammad_webhook_endpoint(request): wh = Webhook.objects.create( source="zammad", + # Because the webhooks just send full objects without indication + # what changed, or what triggered the action, we will custom URLs + # for different types of actions. + # In other words – how the webhook is processed on the backend + # depends the Trigger configuration in zammad. + # action=action, meta=zammad_headers, signature=signature, content=json.loads(request.body), @@ -117,7 +123,7 @@ def zammad_webhook_endpoint(request): process_webhook.enqueue(str(wh.uuid)) return JsonResponse({"status": "created", "guid": wh.uuid}) - return HttpResponseNotAllowed("Only POST") + return HttpResponseNotAllowed(permitted_methods=["POST"]) def verify_zammad_signature(request) -> str: diff --git a/intbot/core/integrations/zammad.py b/intbot/core/integrations/zammad.py new file mode 100644 index 0000000..704ac07 --- /dev/null +++ b/intbot/core/integrations/zammad.py @@ -0,0 +1,160 @@ +from datetime import datetime +from django.conf import settings + +from core.models import Webhook +from pydantic import BaseModel + + +class ZammadGroups: + billing = settings.ZAMMAD_GROUP_BILLING + helpdesk = settings.ZAMMAD_GROUP_HELPDESK + + +class ZammadGroup(BaseModel): + id: int + name: str + + +class ZammadUser(BaseModel): + firstname: str + lastname: str + + +class ZammadTicket(BaseModel): + id: int + group: ZammadGroup + title: str + owner: ZammadUser + state: str + number: str + customer: ZammadUser + created_at: datetime + updated_at: datetime + updated_by: ZammadUser + article_ids: list[int] + + +class ZammadArticle(BaseModel): + sender: str + internal: bool + ticket_id: int + created_at: datetime + created_by: ZammadUser + subject: str + + +class ZammadWebhook(BaseModel): + ticket: ZammadTicket + article: ZammadArticle | None + + +JsonType = dict[str, str | int | float | list | dict] + + +class ZammadParser: + + class Actions: + new_ticket_created = "new_ticket_created" + new_message_in_thread = "new_message_in_thread" + replied_in_thread = "replied_in_thread" + new_internal_note = "new_internal_note" + updated_ticket = "updated_ticket" + + def __init__(self, content: JsonType): + self.content = content + # Ticket is always there, article is optional + # Example: change of status of the Ticket doesn't contain article + self.ticket = ZammadTicket.model_validate(self.content["ticket"]) + self.article = ( + ZammadArticle.model_validate(self.content["article"]) + if self.content["article"] + else None + ) + + @property + def action(self): + """ + Zammad doesn't give us an action inside the webhook, so we can either + set custom triggers and URLs for every action, or we can try to infer + the action from the content of the webhook. For simplcity of the + overall setup, we are implementing the latter here. + + "New Ticket created"? -- has article, and len(article_ids) == 1 + -- state change will not have article associated with it. + "New message in the thread" -- article, sender==Customer + "We sent a new reply in the thread" -- article, sender==Agent + "New internal note in the thread" -- article, internal==true + "Updated the ticket ...", -- updated_by.firstname + """ + # Implementing this as cascading if statements here is part of the + # assumptions. + # For example the "sender == Customer" is going to be True also for their + # first message that originally creates the ticket. However first time + # we get a message, we will return "New ticket" and second time "New + # message in the thread". + if self.article and len(self.ticket.article_ids) == 1: + # This means we have an article, and it's a first one, therefore a + # ticket is new. + return self.Actions.new_ticket_created + + elif self.article and self.article.internal is True: + return self.Actions.new_internal_note + + elif self.article and self.article.sender == "Customer": + return self.Actions.new_message_in_thread + + elif self.article and self.article.sender == "Agent": + return self.Actions.replied_in_thread + + elif not self.article: + return self.Actions.updated_ticket + + raise ValueError("Unsupported scenario") + + @property + def updated_by(self): + return self.ticket.updated_by.firstname + + @property + def group(self): + return self.ticket.group.name + + @property + def url(self): + return f"https://servicedesk.europython.eu/#ticket/zoom/{self.ticket.id}" + + def to_discord_message(self): + message = "{group}: {sender} {action} {details}".format + + # Action + actions = { + self.Actions.new_ticket_created: "created new ticket", + self.Actions.new_message_in_thread: "sent a new message", + self.Actions.replied_in_thread: "replied to a ticket", + self.Actions.new_internal_note: "created internal note", + self.Actions.updated_ticket: "updated ticket", + } + + action = actions[self.action] + + return message(group=self.group, sender=self.updated_by, action=action, details=self.url) + + def meta(self): + return { + "group": self.group, + "sender": self.updated_by, + "action": self.action, + "message": self.to_discord_message(), + } + + +def prep_zammad_webhook(wh: Webhook): + """Parse and store some information for later""" + zp = ZammadParser(wh.content) + + wh.event = zp.action + wh.extra = zp.meta() + + wh.save() + + return wh diff --git a/intbot/core/tasks.py b/intbot/core/tasks.py index 8a96e79..7d61684 100644 --- a/intbot/core/tasks.py +++ b/intbot/core/tasks.py @@ -1,6 +1,7 @@ import logging from core.integrations.github import parse_github_webhook, prep_github_webhook +from core.integrations.zammad import prep_zammad_webhook from core.bot.channel_router import discord_channel_router, dont_send_it from core.models import DiscordMessage, Webhook from django.utils import timezone @@ -66,7 +67,7 @@ def process_github_webhook(wh: Webhook): channel_id=channel.channel_id, channel_name=channel.channel_name, content=f"GitHub: {parsed.as_discord_message()}", - # Mark as unsend - to be sent with the next batch + # Mark as unsent - to be sent with the next batch sent_at=None, ) wh.processed_at = timezone.now() @@ -74,6 +75,27 @@ def process_github_webhook(wh: Webhook): def process_zammad_webhook(wh: Webhook): - # NOTE(artcz) Do nothing for now. Just a placeholder. - # Processing will come in the next PR. - return + if wh.source != "zammad": + raise ValueError("Incorrect wh.source = {wh.source}") + + # Unlike in github, the zammad webhook is richer and + # contains much more information, so no extra fetch is needed. + # However, we can extract information and store it in the meta field, that + # way we can reuse it later more easily. + wh = prep_zammad_webhook(wh) + channel = discord_channel_router(wh) + + if channel == dont_send_it: + wh.processed_at = timezone.now() + wh.save() + return + + DiscordMessage.objects.create( + channel_id=channel.channel_id, + channel_name=channel.channel_name, + content=f"Zammad: {wh.meta['message']}", + # Mark as unsent - to be sent with the next batch + sent_at=None, + ) + wh.processed_at = timezone.now() + wh.save() diff --git a/intbot/intbot/settings.py b/intbot/intbot/settings.py index bc596fa..36511fb 100644 --- a/intbot/intbot/settings.py +++ b/intbot/intbot/settings.py @@ -159,6 +159,11 @@ def get(name) -> str: DISCORD_BOT_CHANNEL_ID = get("DISCORD_BOT_CHANNEL_ID") DISCORD_BOT_CHANNEL_NAME = get("DISCORD_BOT_CHANNEL_NAME") +DISCORD_HELPDESK_CHANNEL_ID = get("DISCORD_HELPDESK_CHANNEL_ID") +DISCORD_HELPDESK_CHANNEL_NAME = get("DISCORD_HELPDESK_CHANNEL_NAME") +DISCORD_BILLING_CHANNEL_ID = get("DISCORD_BILLING_CHANNEL_ID") +DISCORD_BILLING_CHANNEL_NAME = get("DISCORD_BILLING_CHANNEL_NAME") + # Github GITHUB_API_TOKEN = get("GITHUB_API_TOKEN") GITHUB_WEBHOOK_SECRET_TOKEN = get("GITHUB_WEBHOOK_SECRET_TOKEN") @@ -170,6 +175,11 @@ def get(name) -> str: # Zammad ZAMMAD_WEBHOOK_SECRET_TOKEN = get("ZAMMAD_WEBHOOK_SECRET_TOKEN") +ZAMMAD_GROUP_BILLING = get("ZAMMAD_GROUP_BILLING") +ZAMMAD_GROUP_HELPDESK = get("ZAMMAD_GROUP_HELPDESK") + + + if DJANGO_ENV == "dev": DEBUG = True ALLOWED_HOSTS = ["127.0.0.1", "localhost"] @@ -246,6 +256,16 @@ def get(name) -> str: DISCORD_EM_CHANNEL_NAME = "em_channel" DISCORD_EM_CHANNEL_ID = "123123" + DISCORD_HELPDESK_CHANNEL_ID = "1237777" + DISCORD_HELPDESK_CHANNEL_NAME = "helpdesk_channel" + DISCORD_BILLING_CHANNEL_ID = "123999" + DISCORD_BILLING_CHANNEL_NAME = "billing_channel" + + ZAMMAD_GROUP_HELPDESK = "TestZammad Helpdesk" + ZAMMAD_GROUP_BILLING = "TestZammad Billing" + + + elif DJANGO_ENV == "local_container": DEBUG = False diff --git a/intbot/tests/test_bot/test_channel_router.py b/intbot/tests/test_bot/test_channel_router.py index 536a6e6..08aae40 100644 --- a/intbot/tests/test_bot/test_channel_router.py +++ b/intbot/tests/test_bot/test_channel_router.py @@ -2,11 +2,16 @@ Integrated tests for the Discord Channel Router """ -from core.bot.channel_router import Channels, discord_channel_router +from core.bot.channel_router import Channels, discord_channel_router, dont_send_it from core.models import Webhook class TestDiscordChannelRouter: + def test_it_doesnt_send_unkown_messages(self): + wh = Webhook(source="unkown") + channel = discord_channel_router(wh) + assert channel == dont_send_it + def test_it_routes_board_project_to_board_channel(self): wh = Webhook( source="github", @@ -66,3 +71,49 @@ def test_it_routes_EM_project_to_EM_channel(self): assert channel == Channels.em_channel assert channel.channel_name == "em_channel" assert channel.channel_id == "123123" + + def test_it_routes_zammad_billing_queue_to_billing_channel(self): + wh = Webhook( + source="zammad", + meta={}, + content={}, + extra={ + "group": "TestZammad Billing", + }, + ) + + channel = discord_channel_router(wh) + + assert channel == Channels.billing_channel + assert channel.channel_name == "billing_channel" + assert channel.channel_id == "123999" + + def test_it_routes_zammad_helpdesk_queue_to_helpdesk_channel(self): + wh = Webhook( + source="zammad", + meta={}, + content={}, + extra={ + "group": "TestZammad Helpdesk", + }, + ) + + channel = discord_channel_router(wh) + + assert channel == Channels.helpdesk_channel + assert channel.channel_name == "helpdesk_channel" + assert channel.channel_id == "1237777" + + def test_it_skips_unkown_zammad_groups(self): + wh = Webhook( + source="zammad", + meta={}, + content={}, + extra={ + "group": "Unkown Random Group", + }, + ) + + channel = discord_channel_router(wh) + + assert channel == dont_send_it diff --git a/intbot/tests/test_integrations/test_zammad.py b/intbot/tests/test_integrations/test_zammad.py new file mode 100644 index 0000000..07010ee --- /dev/null +++ b/intbot/tests/test_integrations/test_zammad.py @@ -0,0 +1,265 @@ + +import pytest +from core.integrations.zammad import ZammadParser, prep_zammad_webhook +from core.models import Webhook +from django.utils import timezone + + +def test_zammad_parser_with_new_ticket(): + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Cookie", "lastname": "Monster"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "open", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1], + }, + "article": { + "sender": "Customer", + "internal": False, + "ticket_id": 123, + "created_at": timezone.now(), + "created_by": {"firstname": "Cookie", "lastname": "Monster"}, + "subject": "New Cookies please", + }, + } + + zp = ZammadParser(js) + + assert zp.meta() == { + "group": "TestHelpdesk", + "sender": "Cookie", + "action": "new_ticket_created", + "message": ( + "TestHelpdesk: Cookie created new ticket " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } + + +def test_zammad_parser_with_new_message(): + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Cookie", "lastname": "Monster"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "open", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1, 2], + }, + "article": { + "sender": "Customer", + "internal": False, + "ticket_id": 123, + "created_at": timezone.now(), + "created_by": {"firstname": "Cookie", "lastname": "Monster"}, + "subject": "New Cookies please", + }, + } + + zp = ZammadParser(js) + + assert zp.meta() == { + "group": "TestHelpdesk", + "sender": "Cookie", + "action": "new_message_in_thread", + "message": ( + "TestHelpdesk: Cookie sent a new message " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } + + +def test_zammad_replied_to_a_ticket(): + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "open", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1, 2], + }, + "article": { + "sender": "Agent", + "internal": False, + "ticket_id": 123, + "created_at": timezone.now(), + "created_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "subject": "New Cookies please", + }, + } + + zp = ZammadParser(js) + + assert zp.meta() == { + "group": "TestHelpdesk", + "sender": "Kermit", + "action": "replied_in_thread", + "message": ( + "TestHelpdesk: Kermit replied to a ticket " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } + + +def test_zammad_create_internal_note(): + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "open", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1, 2], + }, + "article": { + "sender": "Agent", + "internal": True, + "ticket_id": 123, + "created_at": timezone.now(), + "created_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "subject": "New Cookies please", + }, + } + + zp = ZammadParser(js) + + assert zp.meta() == { + "group": "TestHelpdesk", + "sender": "Kermit", + "action": "new_internal_note", + "message": ( + "TestHelpdesk: Kermit created internal note " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } + + +def test_zammad_updated_ticket(): + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "closed", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1, 2], + }, + # No article, just change of status + "article": {}, + } + + zp = ZammadParser(js) + + assert zp.meta() == { + "group": "TestHelpdesk", + "sender": "Kermit", + "action": "updated_ticket", + "message": ( + "TestHelpdesk: Kermit updated ticket " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } + + +def test_zammad_unsupported_scenario(): + """Just for completeness and coverage""" + + js = { + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "closed", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": timezone.now(), + "updated_at": timezone.now(), + "article_ids": [1, 2], + }, + # No article, just change of status + "article": { + "sender": "Unsupported Entity", + "internal": False, + "ticket_id": 123, + "created_at": timezone.now(), + "created_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "subject": "New Cookies please", + }, + } + + zp = ZammadParser(js) + + with pytest.raises(ValueError, match="Unsupported scenario"): + zp.action + + +@pytest.mark.django_db +def test_prep_zammad_webhook(): + wh = Webhook.objects.create( + content={ + "ticket": { + "id": 123, + "group": {"id": "123", "name": "TestHelpdesk"}, + "updated_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "title": "Test Ticket Title", + "owner": {"firstname": "Kermit", "lastname": "TheFrog"}, + "state": "open", + "number": "13374321", + "customer": {"firstname": "Cookie", "lastname": "Monster"}, + "created_at": str(timezone.now()), + "updated_at": str(timezone.now()), + "article_ids": [1, 2], + }, + "article": { + "sender": "Agent", + "internal": True, + "ticket_id": 123, + "created_at": str(timezone.now()), + "created_by": {"firstname": "Kermit", "lastname": "TheFrog"}, + "subject": "New Cookies please", + }, + }, + extra={}, + ) + + wh = prep_zammad_webhook(wh) + + assert wh.extra == { + "group": "TestHelpdesk", + "sender": "Kermit", + "action": "new_internal_note", + "message": ( + "TestHelpdesk: Kermit created internal note " + "https://servicedesk.europython.eu/#ticket/zoom/123" + ), + } diff --git a/intbot/tests/test_webhooks.py b/intbot/tests/test_webhooks.py index 9b8ba59..8c6166a 100644 --- a/intbot/tests/test_webhooks.py +++ b/intbot/tests/test_webhooks.py @@ -210,3 +210,8 @@ def test_zammad_webhook_endpoint_works_with_correct_token(client): assert response.json()["status"] == "created" assert response.json()["guid"] == str(wh.uuid) assert wh.source == "zammad" + + +def test_zammad_webhook_endpoint_fails_if_request_not_post(client): + response = client.get("/webhook/zammad/") + assert response.status_code == 405 From 4fd0c28fadf9ec99e2d806f26d33b1c9a293d8d1 Mon Sep 17 00:00:00 2001 From: Artur Czepiel Date: Sat, 8 Mar 2025 14:48:54 +0100 Subject: [PATCH 2/4] fix comments --- intbot/core/endpoints/webhooks.py | 6 ------ intbot/core/integrations/zammad.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/intbot/core/endpoints/webhooks.py b/intbot/core/endpoints/webhooks.py index e0d1fd7..18cc1c4 100644 --- a/intbot/core/endpoints/webhooks.py +++ b/intbot/core/endpoints/webhooks.py @@ -107,12 +107,6 @@ def zammad_webhook_endpoint(request): wh = Webhook.objects.create( source="zammad", - # Because the webhooks just send full objects without indication - # what changed, or what triggered the action, we will custom URLs - # for different types of actions. - # In other words – how the webhook is processed on the backend - # depends the Trigger configuration in zammad. - # action=action, meta=zammad_headers, signature=signature, content=json.loads(request.body), diff --git a/intbot/core/integrations/zammad.py b/intbot/core/integrations/zammad.py index 704ac07..b76f125 100644 --- a/intbot/core/integrations/zammad.py +++ b/intbot/core/integrations/zammad.py @@ -76,7 +76,7 @@ def action(self): """ Zammad doesn't give us an action inside the webhook, so we can either set custom triggers and URLs for every action, or we can try to infer - the action from the content of the webhook. For simplcity of the + the action from the content of the webhook. For simplicity of the overall setup, we are implementing the latter here. "New Ticket created"? -- has article, and len(article_ids) == 1 From 69bc72d7945dc0360766738c0b52e890490fa78b Mon Sep 17 00:00:00 2001 From: Artur Czepiel Date: Sat, 8 Mar 2025 15:16:39 +0100 Subject: [PATCH 3/4] review feedback --- intbot/core/bot/channel_router.py | 6 +++--- intbot/core/integrations/zammad.py | 31 +++++++++++++++--------------- intbot/intbot/settings.py | 1 + 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/intbot/core/bot/channel_router.py b/intbot/core/bot/channel_router.py index d9a2b95..3f740ce 100644 --- a/intbot/core/bot/channel_router.py +++ b/intbot/core/bot/channel_router.py @@ -9,7 +9,7 @@ GithubRepositories, parse_github_webhook, ) -from core.integrations.zammad import ZammadGroups +from core.integrations.zammad import ZammadConfig from core.models import Webhook from django.conf import settings @@ -108,8 +108,8 @@ def github_router(wh: Webhook) -> DiscordChannel: def zammad_router(wh: Webhook) -> DiscordChannel: groups = { - ZammadGroups.helpdesk: Channels.helpdesk_channel, - ZammadGroups.billing: Channels.billing_channel, + ZammadConfig.helpdesk_group: Channels.helpdesk_channel, + ZammadConfig.billing_group: Channels.billing_channel, } if channel := groups.get(wh.extra["group"]): diff --git a/intbot/core/integrations/zammad.py b/intbot/core/integrations/zammad.py index b76f125..074f263 100644 --- a/intbot/core/integrations/zammad.py +++ b/intbot/core/integrations/zammad.py @@ -5,10 +5,10 @@ from pydantic import BaseModel -class ZammadGroups: - billing = settings.ZAMMAD_GROUP_BILLING - helpdesk = settings.ZAMMAD_GROUP_HELPDESK - +class ZammadConfig: + url = settings.ZAMMAD_URL # servicedesk.europython.eu + billing_group = settings.ZAMMAD_GROUP_BILLING + helpdesk_group = settings.ZAMMAD_GROUP_HELPDESK class ZammadGroup(BaseModel): id: int @@ -92,19 +92,20 @@ def action(self): # first message that originally creates the ticket. However first time # we get a message, we will return "New ticket" and second time "New # message in the thread". - if self.article and len(self.ticket.article_ids) == 1: - # This means we have an article, and it's a first one, therefore a - # ticket is new. - return self.Actions.new_ticket_created + if self.article: + if len(self.ticket.article_ids) == 1: + # This means we have an article, and it's a first one, therefore a + # ticket is new. + return self.Actions.new_ticket_created - elif self.article and self.article.internal is True: - return self.Actions.new_internal_note + elif self.article.internal is True: + return self.Actions.new_internal_note - elif self.article and self.article.sender == "Customer": - return self.Actions.new_message_in_thread + elif self.article.sender == "Customer": + return self.Actions.new_message_in_thread - elif self.article and self.article.sender == "Agent": - return self.Actions.replied_in_thread + elif self.article.sender == "Agent": + return self.Actions.replied_in_thread elif not self.article: return self.Actions.updated_ticket @@ -121,7 +122,7 @@ def group(self): @property def url(self): - return f"https://servicedesk.europython.eu/#ticket/zoom/{self.ticket.id}" + return f"https://{ZammadConfig.url}/#ticket/zoom/{self.ticket.id}" def to_discord_message(self): message = "{group}: {sender} {action} {details}".format diff --git a/intbot/intbot/settings.py b/intbot/intbot/settings.py index 36511fb..3a102e6 100644 --- a/intbot/intbot/settings.py +++ b/intbot/intbot/settings.py @@ -175,6 +175,7 @@ def get(name) -> str: # Zammad ZAMMAD_WEBHOOK_SECRET_TOKEN = get("ZAMMAD_WEBHOOK_SECRET_TOKEN") +ZAMMAD_URL = "servicedesk.europython.eu" ZAMMAD_GROUP_BILLING = get("ZAMMAD_GROUP_BILLING") ZAMMAD_GROUP_HELPDESK = get("ZAMMAD_GROUP_HELPDESK") From afb019ba7f5bcff5c4bf95667811628f1f1cdb2e Mon Sep 17 00:00:00 2001 From: Artur Czepiel Date: Sat, 8 Mar 2025 15:21:07 +0100 Subject: [PATCH 4/4] more review feedback --- intbot/core/tasks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/intbot/core/tasks.py b/intbot/core/tasks.py index 7d61684..70d27f6 100644 --- a/intbot/core/tasks.py +++ b/intbot/core/tasks.py @@ -59,6 +59,8 @@ def process_github_webhook(wh: Webhook): channel = discord_channel_router(wh) if channel == dont_send_it: + # Mark as processed, to avoid re-processing in the future if we + # shouldn't send a message. wh.processed_at = timezone.now() wh.save() return @@ -86,6 +88,8 @@ def process_zammad_webhook(wh: Webhook): channel = discord_channel_router(wh) if channel == dont_send_it: + # Mark as processed, to avoid re-processing in the future if we + # shouldn't send a message. wh.processed_at = timezone.now() wh.save() return