diff --git a/cg/apps/freshdesk/__init__.py b/cg/apps/freshdesk/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cg/apps/freshdesk/freshdesk_api.py b/cg/apps/freshdesk/freshdesk_api.py new file mode 100644 index 00000000000..869a4d14da9 --- /dev/null +++ b/cg/apps/freshdesk/freshdesk_api.py @@ -0,0 +1,173 @@ +import json +import logging +from typing import Dict, List, Optional + +import requests + +LOG = logging.getLogger(__name__) + + +class Freshdesk: + def __init__(self, api_key: str, domain: str): + """Initialize the Freshdesk API""" + self.api_key = api_key + self.domain = domain + self.base_url = f"https://{domain}/api/v2/" + + def _make_request(self, method: str, endpoint: str, payload: Optional[Dict] = None) -> Dict: + """Make a request to the Freshdesk API""" + url = f"{self.base_url}{endpoint}" + headers = {"Content-Type": "application/json"} + response = requests.request( + method, + url, + auth=(self.api_key, "X"), + headers=headers, + data=json.dumps(payload) if payload else None, + ) + if response.status_code in {200, 201}: + return response.json() + else: + raise Exception( + f"Request failed with status code {response.status_code}: {response.text}" + ) + + def post_ticket_message( + self, ticket_id: int, message: str, private: bool = True, verbose: bool = False + ) -> Dict: + """ + Post a message (note) to a ticket. + :param ticket_id: The ID of the ticket. + :param message: The content of the message. + :param private: Whether the message should be private or visible to the customer. + :param verbose: Whether to print a success message. + :return: Details of the posted message. + """ + + html_message = ( + message.replace("\x85", "
") + .replace("\u2028", "
") + .replace("\u2029", "
") + .replace("\r\n", "
") + .replace("\r", "
") + .replace("\n", "
") + .replace("\\n", "
") + ) + + endpoint = f"/tickets/{ticket_id}/notes" + payload = {"body": html_message, "private": private} + + print("Payload being sent:", json.dumps(payload, indent=2)) + + response = self._make_request("POST", endpoint, payload) + + if "id" in response: + if verbose: + print( + f"Message successfully posted to ticket {ticket_id} (Note ID: {response['id']})." + ) + else: + raise Exception( + "Message posting was successful but the response is missing expected fields." + ) + + return {"body": response.get("body")} + + def get_ticket(self, ticket_id: int, verbose: bool = False) -> Dict: + """Retrieve ticket details by ID""" + response = self._make_request("GET", f"/tickets/{ticket_id}") + if verbose: + print(f"Ticket {ticket_id} details retrieved.") + return response + + def update_ticket(self, ticket_id: int, updates: Dict) -> Dict: + """Update ticket details (e.g., status, priority)""" + return self._make_request("PUT", f"/tickets/{ticket_id}", updates) + + def get_ticket_tags(self, ticket_id: int) -> List[str]: + """ + Retrieve the tags associated with a ticket. + :param ticket_id: The ID of the ticket. + :return: List of tags. + """ + ticket = self.get_ticket(ticket_id) + return ticket.get("tags", []) + + def set_ticket_status(self, ticket_id: int, status: int) -> Dict: + """ + Update the status of a ticket. + :param ticket_id: The ID of the ticket. + :param status: The new status code. + :return: Updated ticket details. + """ + updates = {"status": status} + return self.update_ticket(ticket_id, updates) + + def get_ticket_group(self, ticket_id: int) -> List[str]: + """ + Retrieve the groups associated with a ticket. + :param ticket_id: + :return: List of groups. + """ + ticket = self.get_ticket(ticket_id) + return ticket.get("group_id", []) + + def set_ticket_group(self, ticket_id: int, group_id: int) -> int: + """ + Update the group associated with a ticket. + :param ticket_id: ID of the ticket to update. + :param group_id: ID of the group to assign. + :return: The group_id assigned to the ticket. + """ + updates = {"group_id": group_id} + self.update_ticket(ticket_id, updates) + return self.get_ticket(ticket_id)["group_id"] + + def add_ticket_tag(self, ticket_id: int, tag: str, verbose: bool = False) -> Dict: + """ + Add a tag to a ticket. + :param ticket_id: The ID of the ticket. + :param tag: The tag to add. + :return: Updated ticket details. + """ + ticket = self.get_ticket(ticket_id) + tags = ticket.get("tags", []) + if tag not in tags: + tags.append(tag) + if verbose: + print(f"Tag '{tag}' added to ticket {ticket_id}.") + return self.update_ticket(ticket_id, {"tags": tags}) + + def remove_ticket_tag(self, ticket_id: int, tag: str, verbose: bool = False) -> Dict: + """ + Remove a tag from a ticket. + :param ticket_id: The ID of the ticket. + :param tag: The tag to remove. + :return: Updated ticket details. + """ + ticket = self.get_ticket(ticket_id) + tags = ticket.get("tags", []) + if tag in tags: + tags.remove(tag) + else: + if verbose: + print(f"Tag '{tag}' not found in ticket {ticket_id}. No changes made.") + return ticket + response = self.update_ticket(ticket_id, {"tags": tags}) + if verbose: + print(f"Tag '{tag}' removed from ticket {ticket_id}.") + return response + + def get_ticket_status(self, ticket_id: int, verbose: bool = False) -> str: + """ + Retrieve the status of a ticket. + :param ticket_id: The ID of the ticket. + :param verbose: Whether to return a human-readable status. + :return: Status code or status string. + """ + ticket = self.get_ticket(ticket_id) + status = ticket.get("status") + if verbose: + status_mapping = {2: "Open", 3: "Pending", 4: "Resolved", 5: "Closed"} + return status_mapping.get(status, "Unknown") + return status diff --git a/cg/cli/base.py b/cg/cli/base.py index e510134b4f7..b2555501e84 100644 --- a/cg/cli/base.py +++ b/cg/cli/base.py @@ -4,8 +4,8 @@ import sys from pathlib import Path -import rich_click as click import coloredlogs +import rich_click as click from sqlalchemy.orm import scoped_session import cg @@ -18,6 +18,7 @@ from cg.cli.deliver.base import deliver as deliver_cmd from cg.cli.demultiplex.base import demultiplex_cmd_group as demultiplex_cmd from cg.cli.downsample import downsample +from cg.cli.freshdesk.base import freshdesk_cli as freshdesk_cmd from cg.cli.generate.base import generate as generate_cmd from cg.cli.get import get from cg.cli.post_process.post_process import post_process_group as post_processing @@ -141,6 +142,7 @@ def search(query): base.add_command(compress) base.add_command(decompress) base.add_command(delete) +base.add_command(freshdesk_cmd) base.add_command(get) base.add_command(set_cmd) base.add_command(transfer_group) diff --git a/cg/cli/freshdesk/__init__.py b/cg/cli/freshdesk/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cg/cli/freshdesk/base.py b/cg/cli/freshdesk/base.py new file mode 100644 index 00000000000..7605f146a5d --- /dev/null +++ b/cg/cli/freshdesk/base.py @@ -0,0 +1,181 @@ +import click + +from cg.apps.freshdesk.freshdesk_api import Freshdesk + +# TODO: Add Keys to CG config +# from freshdesk.keys import FRESHDESK_API_KEY, FRESHDESK_DOMAIN + + +API_KEY = "YOUR_API_KEY" +DOMAIN = "scilifelab.freshdesk.com" +freshdesk = Freshdesk(api_key=API_KEY, domain=DOMAIN) + + +@click.group() +def freshdesk_cli(): + """CLI for interacting with Freshdesk.""" + pass + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def get_ticket(ticket_id, verbose): + """Retrieve details of a ticket.""" + try: + ticket = Freshdesk.get_ticket(ticket_id, verbose=verbose) + click.echo(ticket) + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.argument("message", type=str) +@click.option( + "--private/--public", default=True, help="Set the message visibility (default: private)." +) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def post_message(ticket_id, message, private, verbose): + """Post a html message to a ticket.""" + try: + response = Freshdesk.post_ticket_message(ticket_id, message, private, verbose=verbose) + click.echo(f"Message posted: {response}") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.argument("status", type=int) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def set_status(ticket_id, status, verbose): + """ + Update the status of a ticket. + \n + 2: "Open",\n + 3: "Pending",\n + 4: "Resolved",\n + 5: "Closed"\n + """ + try: + # TODO: Use response + Freshdesk.set_ticket_status(ticket_id, status, verbose=verbose) + click.echo(f"Ticket {ticket_id} status updated to {status}.") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def get_tags(ticket_id, verbose): + """Retrieve tags of a ticket.""" + try: + tags = Freshdesk.get_ticket_tags(ticket_id) + click.echo(f"Tags: {tags}") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def get_group(ticket_id, verbose): + """Retrieve group assigned to a ticket.""" + try: + group_id = Freshdesk.get_ticket_group(ticket_id) + if verbose: + # TODO: Add this to a utils function + group_id_mapping = { + None: "Unassigned", + 202000118168: "Production Bioinformatics", + 202000118167: "Production Lab", + 202000118169: "Production Managers", + } + click.echo(f"group_id: {group_id_mapping[group_id]}") + else: + click.echo(f"group_id: {group_id}") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int, required=True) +@click.argument("group_id", type=int, required=True) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def set_group(ticket_id, group_id, verbose): + """Assign a group to a ticket.\n + 202000118168: "Production Bioinformatics",\n + 202000118167: "Production Lab",\n + 202000118169: "Production Managers"\n + + """ + try: + group_assigned = Freshdesk.set_ticket_group(ticket_id, group_id) + if verbose: + # TODO: Add this to a utils function + group_id_mapping = { + None: "Unassigned", + 202000118168: "Production Bioinformatics", + 202000118167: "Production Lab", + 202000118169: "Production Managers", + } + click.echo(f"group_id: {group_id_mapping[group_assigned]}") + else: + click.echo(f"group_id: {group_assigned}") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.argument("tag", type=str) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def add_tag(ticket_id, tag, verbose): + """Add a tag to a ticket.""" + try: + # TODO: Use the response + Freshdesk.add_ticket_tag(ticket_id, tag, verbose=verbose) + click.echo(f"Tag '{tag}' added to ticket {ticket_id}.") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.argument("tag", type=str) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def remove_tag(ticket_id, tag, verbose): + """Remove a tag from a ticket.""" + try: + # TODO: Use the response + Freshdesk.remove_ticket_tag(ticket_id, tag, verbose=verbose) + click.echo(f"Tag '{tag}' removed from ticket {ticket_id}.") + except Exception as e: + click.echo(f"Error: {e}", err=True) + + +@freshdesk_cli.command() +@click.argument("ticket_id", type=int) +@click.option("--verbose", is_flag=True, help="Show detailed output.") +def get_status(ticket_id, verbose): + """Retrieve the status of a ticket. + \n + 2: "Open",\n + 3: "Pending",\n + 4: "Resolved",\n + 5: "Closed"\n + """ + try: + status = Freshdesk.get_ticket_status(ticket_id) + if verbose: + # TODO: Add this to a utils function + status_mapping = {2: "Open", 3: "Pending", 4: "Resolved", 5: "Closed"} + click.echo( + f"Ticket {ticket_id} status: {status_mapping.get(status, 'Unknown')} ({status})" + ) + else: + click.echo(status) + except Exception as e: + click.echo(f"Error: {e}", err=True)